mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-01 12:41:00 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80ff4bd39c |
@@ -1,61 +0,0 @@
|
||||
import { evaluate } from 'mathjs';
|
||||
|
||||
export interface CalculatorState {
|
||||
display: string;
|
||||
equation: string;
|
||||
lastResult: string | null;
|
||||
}
|
||||
|
||||
export const initialState: CalculatorState = {
|
||||
display: '0',
|
||||
equation: '',
|
||||
lastResult: null,
|
||||
};
|
||||
|
||||
export function handleInput(state: CalculatorState, input: string): CalculatorState {
|
||||
if (/[0-9]/.test(input)) {
|
||||
if (state.display === '0' || state.lastResult !== null) {
|
||||
return { ...state, display: input, lastResult: null };
|
||||
} else {
|
||||
return { ...state, display: state.display + input };
|
||||
}
|
||||
} else if (input === '.') {
|
||||
if (!state.display.includes('.')) {
|
||||
return { ...state, display: state.display + '.' };
|
||||
}
|
||||
} else if (['+', '-', '*', '/'].includes(input)) {
|
||||
return {
|
||||
...state,
|
||||
equation: `${state.display} ${input} `,
|
||||
display: '0',
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function calculate(state: CalculatorState): CalculatorState {
|
||||
try {
|
||||
const fullEquation = state.equation + state.display;
|
||||
const result = evaluate(fullEquation);
|
||||
const resultStr = result.toString();
|
||||
return {
|
||||
display: resultStr,
|
||||
equation: '',
|
||||
lastResult: resultStr,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
display: 'Error',
|
||||
equation: '',
|
||||
lastResult: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteLast(state: CalculatorState): CalculatorState {
|
||||
if (state.display.length > 1) {
|
||||
return { ...state, display: state.display.slice(0, -1) };
|
||||
} else {
|
||||
return { ...state, display: '0' };
|
||||
}
|
||||
}
|
||||
@@ -1867,32 +1867,6 @@ describe('loadCliConfig with admin.mcp.config', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig deepValidation', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
it('should enable deepValidation by default', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.isDeepValidationEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow disabling deepValidation via settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
deepValidation: false,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.isDeepValidationEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig model selection', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
|
||||
@@ -900,7 +900,6 @@ export async function loadCliConfig(
|
||||
memoryBoundaryMarkers: settings.context?.memoryBoundaryMarkers,
|
||||
importFormat: settings.context?.importFormat,
|
||||
debugMode,
|
||||
deepValidation: settings.general?.deepValidation ?? true,
|
||||
question,
|
||||
worktreeSettings,
|
||||
|
||||
|
||||
@@ -236,16 +236,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable DevTools inspector on launch.',
|
||||
showInDialog: false,
|
||||
},
|
||||
deepValidation: {
|
||||
type: 'boolean',
|
||||
label: 'Deep Validation',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Run a subagent after the main agent finishes to perform final validation.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enableAutoUpdate: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Auto Update',
|
||||
|
||||
@@ -534,6 +534,7 @@ export const mockAppState: AppState = {
|
||||
};
|
||||
|
||||
const mockUIActions: UIActions = {
|
||||
toggleAlternateBuffer: vi.fn(),
|
||||
handleThemeSelect: vi.fn(),
|
||||
closeThemeDialog: vi.fn(),
|
||||
handleThemeHighlight: vi.fn(),
|
||||
|
||||
@@ -68,8 +68,10 @@ import {
|
||||
writeToStdout,
|
||||
disableMouseEvents,
|
||||
enterAlternateScreen,
|
||||
exitAlternateScreen,
|
||||
enableMouseEvents,
|
||||
disableLineWrapping,
|
||||
enableLineWrapping,
|
||||
shouldEnterAlternateScreen,
|
||||
startupProfiler,
|
||||
SessionStartSource,
|
||||
@@ -213,7 +215,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
});
|
||||
|
||||
useMemoryMonitor(historyManager);
|
||||
const isAlternateBuffer = config.getUseAlternateBuffer();
|
||||
const [isAlternateBuffer, setIsAlternateBuffer] = useState(config.getUseAlternateBuffer());
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [forceRerenderKey, setForceRerenderKey] = useState(0);
|
||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||
@@ -1550,6 +1552,23 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
type: TransientMessageType;
|
||||
}>(WARNING_PROMPT_DURATION_MS);
|
||||
|
||||
const [shownBufferToggleHint, setShownBufferToggleHint] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAlternateBuffer) return;
|
||||
|
||||
const isLongHistory = historyManager.history.length > 15;
|
||||
const isComplexPrompt = buffer.text.length > 200 || buffer.text.includes('\n');
|
||||
|
||||
if ((isLongHistory || isComplexPrompt) && !shownBufferToggleHint) {
|
||||
showTransientMessage({
|
||||
text: 'Tip: Press Alt+T to toggle full-screen mode for better scrolling/editing',
|
||||
type: TransientMessageType.Hint
|
||||
});
|
||||
setShownBufferToggleHint(true);
|
||||
}
|
||||
}, [historyManager.history.length, buffer.text, isAlternateBuffer, shownBufferToggleHint, showTransientMessage]);
|
||||
|
||||
const {
|
||||
isFolderTrustDialogOpen,
|
||||
discoveryResults: folderDiscoveryResults,
|
||||
@@ -1700,6 +1719,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_BUFFER_MODE](key)) {
|
||||
toggleAlternateBuffer();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.QUIT](key)) {
|
||||
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
|
||||
// This should happen regardless of the count.
|
||||
@@ -2204,8 +2228,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
};
|
||||
}, [config, refreshStatic]);
|
||||
|
||||
const showIsAlternateBufferHint = (historyManager.history.length > 15 || buffer.text.length > 200 || buffer.text.includes('\n')) && !isAlternateBuffer;
|
||||
|
||||
const uiState: UIState = useMemo(
|
||||
() => ({
|
||||
isAlternateBuffer,
|
||||
history: historyManager.history,
|
||||
historyManager,
|
||||
isThemeDialogOpen,
|
||||
@@ -2331,6 +2358,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
showIsAlternateBufferHint,
|
||||
hintMode:
|
||||
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
|
||||
hintBuffer: '',
|
||||
@@ -2457,6 +2485,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
showIsAlternateBufferHint,
|
||||
isAlternateBuffer,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2465,6 +2495,31 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
[setShowPrivacyNotice],
|
||||
);
|
||||
|
||||
const toggleAlternateBuffer = useCallback(() => {
|
||||
setIsAlternateBuffer(prev => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
enterAlternateScreen();
|
||||
enableMouseEvents();
|
||||
disableLineWrapping();
|
||||
} else {
|
||||
exitAlternateScreen();
|
||||
disableMouseEvents();
|
||||
enableLineWrapping();
|
||||
writeToStdout('\x1b[2J\x1b[H');
|
||||
}
|
||||
process.stdout.emit('resize');
|
||||
|
||||
// Give a tick for resize to process, then trigger remount to force full redraw
|
||||
setImmediate(() => {
|
||||
refreshStatic();
|
||||
setForceRerenderKey((prev) => prev + 1);
|
||||
});
|
||||
|
||||
return next;
|
||||
});
|
||||
}, [setIsAlternateBuffer, refreshStatic, setForceRerenderKey]);
|
||||
|
||||
const uiActions: UIActions = useMemo(
|
||||
() => ({
|
||||
handleThemeSelect,
|
||||
@@ -2476,6 +2531,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleEditorSelect,
|
||||
exitEditorDialog,
|
||||
exitPrivacyNotice,
|
||||
toggleAlternateBuffer,
|
||||
closeSettingsDialog,
|
||||
closeModelDialog,
|
||||
openAgentConfigDialog,
|
||||
@@ -2614,6 +2670,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
config,
|
||||
historyManager,
|
||||
getPreferredEditor,
|
||||
toggleAlternateBuffer,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -142,4 +142,38 @@ describe('<StatusRow />', () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Tip: Test Tip');
|
||||
});
|
||||
|
||||
it('renders buffer toggle hint when showIsAlternateBufferHint is true', async () => {
|
||||
(useComposerStatus as Mock).mockReturnValue({
|
||||
isInteractiveShellWaiting: false,
|
||||
showLoadingIndicator: false,
|
||||
showTips: false,
|
||||
showWit: false,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
...defaultUiState,
|
||||
showIsAlternateBufferHint: true,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<StatusRow
|
||||
showUiDetails={false}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
width: 100,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('[Alt+T] Switch to Full Screen');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -206,7 +206,12 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
return uiState.currentTip;
|
||||
}
|
||||
|
||||
// 2. Shortcut Hint (Fallback)
|
||||
// 2. Buffer Toggle Hint
|
||||
if (uiState.showIsAlternateBufferHint) {
|
||||
return '[Alt+T] Switch to Full Screen';
|
||||
}
|
||||
|
||||
// 3. Shortcut Hint (Fallback)
|
||||
if (
|
||||
settings.merged.ui.showShortcutsHint &&
|
||||
!hideUiDetailsForSuggestions &&
|
||||
|
||||
@@ -205,6 +205,8 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
||||
'\u03A9': 'z', // "Ω" Option+z
|
||||
'\u00B8': 'Z', // "¸" Option+Shift+z
|
||||
'\u2202': 'd', // "∂" delete word forward
|
||||
'\u2020': 't', // "†" toggle full screen buffer
|
||||
'\u00E5': 'a', // "å" Option+a for alternate buffer
|
||||
};
|
||||
|
||||
function nonKeyboardEventFilter(
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface UIActions {
|
||||
setShortcutsHelpVisible: (visible: boolean) => void;
|
||||
setCleanUiDetailsVisible: (visible: boolean) => void;
|
||||
toggleCleanUiDetailsVisible: () => void;
|
||||
toggleAlternateBuffer: () => void;
|
||||
revealCleanUiDetailsTemporarily: (durationMs?: number) => void;
|
||||
handleWarning: (message: string) => void;
|
||||
setEmbeddedShellFocused: (value: boolean) => void;
|
||||
|
||||
@@ -118,6 +118,8 @@ export interface UIState {
|
||||
isEditorDialogOpen: boolean;
|
||||
showPrivacyNotice: boolean;
|
||||
corgiMode: boolean;
|
||||
isAlternateBuffer: boolean;
|
||||
showIsAlternateBufferHint: boolean;
|
||||
debugMessage: string;
|
||||
quittingMessages: HistoryItem[] | null;
|
||||
isSettingsDialogOpen: boolean;
|
||||
|
||||
@@ -11,49 +11,47 @@ import {
|
||||
isAlternateBufferEnabled,
|
||||
} from './useAlternateBuffer.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
vi.mock('../contexts/ConfigContext.js', () => ({
|
||||
useConfig: vi.fn(),
|
||||
}));
|
||||
vi.mock('../contexts/UIStateContext.js');
|
||||
|
||||
const mockUseConfig = vi.mocked(
|
||||
await import('../contexts/ConfigContext.js').then((m) => m.useConfig),
|
||||
);
|
||||
const mockUseUIState = vi.mocked(useUIState);
|
||||
|
||||
describe('useAlternateBuffer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return false when config.getUseAlternateBuffer returns false', async () => {
|
||||
mockUseConfig.mockReturnValue({
|
||||
getUseAlternateBuffer: () => false,
|
||||
} as unknown as ReturnType<typeof mockUseConfig>);
|
||||
it('should return false when uiState.isAlternateBuffer is false', async () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
isAlternateBuffer: false,
|
||||
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||
|
||||
const { result } = await renderHook(() => useAlternateBuffer());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when config.getUseAlternateBuffer returns true', async () => {
|
||||
mockUseConfig.mockReturnValue({
|
||||
getUseAlternateBuffer: () => true,
|
||||
} as unknown as ReturnType<typeof mockUseConfig>);
|
||||
it('should return true when uiState.isAlternateBuffer is true', async () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
isAlternateBuffer: true,
|
||||
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||
|
||||
const { result } = await renderHook(() => useAlternateBuffer());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the immutable config value, not react to settings changes', async () => {
|
||||
const mockConfig = {
|
||||
getUseAlternateBuffer: () => true,
|
||||
} as unknown as ReturnType<typeof mockUseConfig>;
|
||||
|
||||
mockUseConfig.mockReturnValue(mockConfig);
|
||||
it('should react to state changes', async () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
isAlternateBuffer: false,
|
||||
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||
|
||||
const { result, rerender } = await renderHook(() => useAlternateBuffer());
|
||||
|
||||
// Value should remain true even after rerender
|
||||
expect(result.current).toBe(true);
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
mockUseUIState.mockReturnValue({
|
||||
isAlternateBuffer: true,
|
||||
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||
|
||||
rerender();
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
export const isAlternateBufferEnabled = (config: Config): boolean =>
|
||||
config.getUseAlternateBuffer();
|
||||
|
||||
// This is read from Config so that the UI reads the same value per application session
|
||||
// This is read from UIState so that the UI can toggle dynamically
|
||||
export const useAlternateBuffer = (): boolean => {
|
||||
const config = useConfig();
|
||||
return isAlternateBufferEnabled(config);
|
||||
const uiState = useUIState();
|
||||
return uiState.isAlternateBuffer;
|
||||
};
|
||||
|
||||
@@ -95,6 +95,7 @@ export enum Command {
|
||||
RESTART_APP = 'app.restart',
|
||||
SUSPEND_APP = 'app.suspend',
|
||||
SHOW_SHELL_INPUT_UNFOCUS_WARNING = 'app.showShellUnfocusWarning',
|
||||
TOGGLE_BUFFER_MODE = 'app.toggleBufferMode',
|
||||
|
||||
// Background Shell Controls
|
||||
BACKGROUND_SHELL_ESCAPE = 'background.escape',
|
||||
@@ -392,6 +393,7 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
|
||||
[Command.RESTART_APP, [new KeyBinding('r'), new KeyBinding('shift+r')]],
|
||||
[Command.SUSPEND_APP, [new KeyBinding('ctrl+z')]],
|
||||
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING, [new KeyBinding('tab')]],
|
||||
[Command.TOGGLE_BUFFER_MODE, [new KeyBinding('alt+a')]],
|
||||
|
||||
// Background Shell Controls
|
||||
[Command.BACKGROUND_SHELL_ESCAPE, [new KeyBinding('escape')]],
|
||||
@@ -609,6 +611,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.',
|
||||
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
|
||||
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
|
||||
[Command.TOGGLE_BUFFER_MODE]: 'Toggle between regular and full screen (alternate buffer) mode.',
|
||||
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
|
||||
[Command.CYCLE_APPROVAL_MODE]:
|
||||
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy.',
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import type { LocalAgentDefinition } from './types.js';
|
||||
|
||||
const DeepValidationAgentSchema = z.object({
|
||||
report: z.string().describe('The final validation report.'),
|
||||
isSatisfied: z.boolean().describe('Whether the original prompt was fully satisfied.'),
|
||||
});
|
||||
|
||||
/**
|
||||
* A specialized subagent that performs final validation after the main agent finishes.
|
||||
* It reflects on the original prompt and determines if it was satisfied by reviewing
|
||||
* the changes and running the application and tests.
|
||||
*/
|
||||
export const DeepValidationAgent = (
|
||||
context: AgentLoopContext,
|
||||
originalPrompt: string,
|
||||
): LocalAgentDefinition<typeof DeepValidationAgentSchema> => ({
|
||||
kind: 'local',
|
||||
name: 'deep-validation',
|
||||
displayName: 'Deep Validation Agent',
|
||||
internal: true,
|
||||
description:
|
||||
'A specialized subagent that performs final validation. It reflects on the original prompt and determines if it was satisfied by reviewing the changes and running the application and tests, if applicable.',
|
||||
inputConfig: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
originalPrompt: {
|
||||
type: 'string',
|
||||
description: 'The original user request or prompt that was executed.',
|
||||
},
|
||||
},
|
||||
required: ['originalPrompt'],
|
||||
},
|
||||
},
|
||||
outputConfig: {
|
||||
outputName: 'validationResult',
|
||||
description: 'The final validation report and satisfaction status.',
|
||||
schema: DeepValidationAgentSchema,
|
||||
},
|
||||
modelConfig: {
|
||||
model: 'inherit',
|
||||
},
|
||||
get toolConfig() {
|
||||
const tools = context.toolRegistry?.getAllToolNames() ?? [];
|
||||
return {
|
||||
tools,
|
||||
};
|
||||
},
|
||||
get promptConfig() {
|
||||
return {
|
||||
systemPrompt: `You are the Deep Validation subagent. Your job is to perform final validation after the main agent finishes.
|
||||
The user's original request was: ${originalPrompt}
|
||||
You must deterministically determine if the request was satisfied.
|
||||
Review the changes using git, check the code, and run the application, any existing linters, formatters, and tests if applicable.
|
||||
Make a comprehensive and prioritized list of any validation failures and then fix them in priority order. Take care not to leave the code worse than you found it.
|
||||
|
||||
Use the following order to guide your prioritization of fixes:
|
||||
- Unfulfilled aspects of the original request.
|
||||
- Issues blocking correct operation of the solution, such as runtime failures, test failures, etc.
|
||||
- Conventions
|
||||
- Linters
|
||||
- Best practices for the ecosystem.
|
||||
|
||||
Try to keep your changes targeted and as minimal as possible while still resolving the validation issue.
|
||||
|
||||
Return a final validation report detailing your findings and a boolean indicating if the request was fully satisfied.`,
|
||||
query: `Please validate the original request: ${originalPrompt}`,
|
||||
};
|
||||
},
|
||||
runConfig: {
|
||||
maxTimeMinutes: 10,
|
||||
maxTurns: 10,
|
||||
},
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
|
||||
* @param messageBus Message bus for policy enforcement.
|
||||
*/
|
||||
constructor(
|
||||
private readonly definition: LocalAgentDefinition<any>,
|
||||
private readonly definition: LocalAgentDefinition,
|
||||
private readonly context: AgentLoopContext,
|
||||
params: AgentInputs,
|
||||
messageBus: MessageBus,
|
||||
|
||||
@@ -272,7 +272,6 @@ describe('AgentRegistry', () => {
|
||||
codebase_investigator: { enabled: false },
|
||||
cli_help: { enabled: false },
|
||||
generalist: { enabled: false },
|
||||
'deep-validation': { enabled: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import { loadAgentsFromDirectory } from './agentLoader.js';
|
||||
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
|
||||
import { CliHelpAgent } from './cli-help-agent.js';
|
||||
import { GeneralistAgent } from './generalist-agent.js';
|
||||
import { DeepValidationAgent } from './deep-validation-agent.js';
|
||||
import { BrowserAgentDefinition } from './browser/browserAgentDefinition.js';
|
||||
import { MemoryManagerAgent } from './memory-manager-agent.js';
|
||||
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
|
||||
@@ -244,7 +243,7 @@ export class AgentRegistry {
|
||||
|
||||
if (this.config.getDebugMode()) {
|
||||
debugLogger.log(
|
||||
`[AgentRegistry] Loaded with ${this.getAllDefinitions().length} agents.`,
|
||||
`[AgentRegistry] Loaded with ${this.agents.size} agents.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -253,7 +252,6 @@ export class AgentRegistry {
|
||||
this.registerLocalAgent(CodebaseInvestigatorAgent(this.config));
|
||||
this.registerLocalAgent(CliHelpAgent(this.config));
|
||||
this.registerLocalAgent(GeneralistAgent(this.config));
|
||||
this.registerLocalAgent(DeepValidationAgent(this.config, '${originalPrompt}'));
|
||||
|
||||
// Register the browser agent if enabled in settings.
|
||||
// Tools are configured dynamically at invocation time via browserAgentFactory.
|
||||
@@ -671,7 +669,7 @@ export class AgentRegistry {
|
||||
* Returns all active agent definitions.
|
||||
*/
|
||||
getAllDefinitions(): AgentDefinition[] {
|
||||
return Array.from(this.agents.values()).filter((d) => !d.internal);
|
||||
return Array.from(this.agents.values());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -193,7 +193,6 @@ export interface BaseAgentDefinition<
|
||||
displayName?: string;
|
||||
description: string;
|
||||
experimental?: boolean;
|
||||
internal?: boolean;
|
||||
inputConfig: InputConfig;
|
||||
outputConfig?: OutputConfig<TOutput>;
|
||||
metadata?: {
|
||||
|
||||
@@ -43,15 +43,7 @@ import { EnterPlanModeTool } from '../tools/enter-plan-mode.js';
|
||||
import { GeminiClient } from '../core/client.js';
|
||||
import { BaseLlmClient } from '../core/baseLlmClient.js';
|
||||
import { LocalLiteRtLmClient } from '../core/localLiteRtLmClient.js';
|
||||
import {
|
||||
type HookDefinition,
|
||||
HookEventName,
|
||||
HookType,
|
||||
type AfterAgentInput,
|
||||
type HookInput,
|
||||
} from '../hooks/types.js';
|
||||
import { DeepValidationAgent } from '../agents/deep-validation-agent.js';
|
||||
import { LocalSubagentInvocation } from '../agents/local-invocation.js';
|
||||
import type { HookDefinition, HookEventName } from '../hooks/types.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { GitService } from '../services/gitService.js';
|
||||
import {
|
||||
@@ -583,7 +575,6 @@ export interface ConfigParameters {
|
||||
toolSandboxing?: boolean;
|
||||
targetDir: string;
|
||||
debugMode: boolean;
|
||||
deepValidation?: boolean;
|
||||
question?: string;
|
||||
|
||||
coreTools?: string[];
|
||||
@@ -749,7 +740,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly targetDir: string;
|
||||
private workspaceContext: WorkspaceContext;
|
||||
private readonly debugMode: boolean;
|
||||
private readonly deepValidation: boolean;
|
||||
private readonly question: string | undefined;
|
||||
private readonly worktreeSettings: WorktreeSettings | undefined;
|
||||
readonly enableConseca: boolean;
|
||||
@@ -1019,7 +1009,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.workspaceContext = new WorkspaceContext(this.targetDir, []);
|
||||
this.pendingIncludeDirectories = params.includeDirectories ?? [];
|
||||
this.debugMode = params.debugMode;
|
||||
this.deepValidation = params.deepValidation ?? false;
|
||||
this.question = params.question;
|
||||
this.worktreeSettings = params.worktreeSettings;
|
||||
|
||||
@@ -1454,53 +1443,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize hook system if enabled or if deepValidation is enabled
|
||||
if (this.getEnableHooks() || this.deepValidation) {
|
||||
// Initialize hook system if enabled
|
||||
if (this.getEnableHooks()) {
|
||||
this.hookSystem = new HookSystem(this);
|
||||
await this.hookSystem.initialize();
|
||||
|
||||
// Register Deep Validation hook if enabled
|
||||
if (this.deepValidation) {
|
||||
this.hookSystem.registerHook(
|
||||
{
|
||||
type: HookType.Runtime,
|
||||
name: 'deep-validation-hook',
|
||||
action: async (input: HookInput) => {
|
||||
const afterAgentInput = input as AfterAgentInput;
|
||||
const invocation = new LocalSubagentInvocation(
|
||||
DeepValidationAgent(this, afterAgentInput.prompt),
|
||||
this,
|
||||
{ originalPrompt: afterAgentInput.prompt },
|
||||
this.messageBus,
|
||||
);
|
||||
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
const progress = result.returnDisplay as any;
|
||||
const subagentResult = progress?.result;
|
||||
|
||||
let validationResult: { report: string; isSatisfied: boolean };
|
||||
try {
|
||||
validationResult = JSON.parse(subagentResult);
|
||||
} catch {
|
||||
validationResult = {
|
||||
report: subagentResult || 'Failed to parse validation report.',
|
||||
isSatisfied: false,
|
||||
};
|
||||
}
|
||||
|
||||
const satisfactionMessage = validationResult.isSatisfied
|
||||
? '✅ Deep validation satisfied.'
|
||||
: '⚠️ Deep validation NOT satisfied.';
|
||||
|
||||
return {
|
||||
continue: true,
|
||||
systemMessage: `${satisfactionMessage}\n\n${validationResult.report}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
HookEventName.AfterAgent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.experimentalJitContext) {
|
||||
@@ -2051,11 +1997,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
getDebugMode(): boolean {
|
||||
return this.debugMode;
|
||||
}
|
||||
|
||||
isDeepValidationEnabled(): boolean {
|
||||
return this.deepValidation;
|
||||
}
|
||||
|
||||
getQuestion(): string | undefined {
|
||||
return this.question;
|
||||
}
|
||||
@@ -3567,7 +3508,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
const discoveredNames = this.agentRegistry.getAllDiscoveredAgentNames();
|
||||
for (const agentName of discoveredNames) {
|
||||
const definition = this.agentRegistry.getDiscoveredDefinition(agentName);
|
||||
if (!definition || definition.internal) {
|
||||
if (!definition) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Config } from './config.js';
|
||||
import { HookEventName } from '../hooks/types.js';
|
||||
import { LocalSubagentInvocation } from '../agents/local-invocation.js';
|
||||
|
||||
vi.mock('../agents/local-invocation.js', () => ({
|
||||
LocalSubagentInvocation: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../agents/deep-validation-agent.js', () => ({
|
||||
DeepValidationAgent: vi.fn().mockReturnValue({
|
||||
kind: 'local',
|
||||
name: 'deep-validation',
|
||||
displayName: 'Deep Validation Agent',
|
||||
description: 'test description',
|
||||
inputConfig: { inputSchema: {} },
|
||||
outputConfig: { outputName: 'validationResult', schema: { safeParse: vi.fn() } },
|
||||
promptConfig: { systemPrompt: 'test prompt', query: 'test query' },
|
||||
modelConfig: { model: 'inherit' },
|
||||
runConfig: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Deep Validation Integration', () => {
|
||||
let config: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should register AfterAgent hook when deepValidation is enabled', async () => {
|
||||
config = new Config({
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp',
|
||||
debugMode: false,
|
||||
cwd: '/tmp',
|
||||
model: 'test-model',
|
||||
enableHooks: true,
|
||||
deepValidation: true,
|
||||
} as any);
|
||||
|
||||
await config.initialize();
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
expect(hookSystem).toBeDefined();
|
||||
|
||||
const hooks = hookSystem?.getAllHooks();
|
||||
expect(hooks).toBeDefined();
|
||||
expect(hooks?.some(h => h.config.name === 'deep-validation-hook' && h.eventName === HookEventName.AfterAgent)).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT register AfterAgent hook when deepValidation is disabled', async () => {
|
||||
config = new Config({
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp',
|
||||
debugMode: false,
|
||||
cwd: '/tmp',
|
||||
model: 'test-model',
|
||||
enableHooks: true,
|
||||
deepValidation: false,
|
||||
} as any);
|
||||
|
||||
await config.initialize();
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
const hooks = hookSystem?.getAllHooks();
|
||||
expect(hooks?.some(h => h.config.name === 'deep-validation-hook')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute DeepValidationAgent when hook is triggered', async () => {
|
||||
config = new Config({
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/tmp',
|
||||
debugMode: false,
|
||||
cwd: '/tmp',
|
||||
model: 'test-model',
|
||||
enableHooks: true,
|
||||
deepValidation: true,
|
||||
} as any);
|
||||
|
||||
const mockExecute = vi.fn().mockResolvedValue({
|
||||
returnDisplay: {
|
||||
result: JSON.stringify({
|
||||
report: 'All good',
|
||||
isSatisfied: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
(LocalSubagentInvocation as any).mockImplementation(() => ({
|
||||
execute: mockExecute,
|
||||
}));
|
||||
|
||||
await config.initialize();
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
const deepValidationEntry = hookSystem?.getAllHooks()?.find(h => h.config.name === 'deep-validation-hook');
|
||||
|
||||
expect(deepValidationEntry).toBeDefined();
|
||||
|
||||
if (deepValidationEntry && 'action' in deepValidationEntry.config) {
|
||||
const result = await (deepValidationEntry.config as any).action({ prompt: 'original prompt' } as any);
|
||||
|
||||
expect(LocalSubagentInvocation).toHaveBeenCalled();
|
||||
expect(mockExecute).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({
|
||||
continue: true,
|
||||
systemMessage: expect.stringContaining('✅ Deep validation satisfied.'),
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
systemMessage: expect.stringContaining('All good'),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -809,7 +809,7 @@ export class GeminiClient {
|
||||
|
||||
// Update cumulative response in hook state
|
||||
// We do this immediately after the stream finishes for THIS turn.
|
||||
const hooksEnabled = this.config.getHookSystem() !== undefined;
|
||||
const hooksEnabled = this.config.getEnableHooks();
|
||||
if (hooksEnabled) {
|
||||
const responseText = turn.getResponseText() || '';
|
||||
const hookState = this.hookStateMap.get(prompt_id);
|
||||
@@ -900,7 +900,7 @@ export class GeminiClient {
|
||||
this.config.resetTurn();
|
||||
}
|
||||
|
||||
const hooksEnabled = this.config.getHookSystem() !== undefined;
|
||||
const hooksEnabled = this.config.getEnableHooks();
|
||||
const messageBus = this.context.messageBus;
|
||||
|
||||
if (this.lastPromptId !== prompt_id) {
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('HookRegistry', () => {
|
||||
mockConfig = {
|
||||
storage: mockStorage,
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
getHooks: vi.fn(), getEnableHooks: vi.fn().mockReturnValue(true).mockReturnValue({}),
|
||||
getHooks: vi.fn().mockReturnValue({}),
|
||||
getProjectHooks: vi.fn().mockReturnValue({}),
|
||||
getDisabledHooks: vi.fn().mockReturnValue([]),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
|
||||
@@ -73,10 +73,7 @@ export class HookRegistry {
|
||||
(entry) => entry.source === ConfigSource.Runtime,
|
||||
);
|
||||
this.entries = [...runtimeHooks];
|
||||
|
||||
if (this.config.getEnableHooks()) {
|
||||
this.processHooksFromConfig();
|
||||
}
|
||||
this.processHooksFromConfig();
|
||||
|
||||
debugLogger.debug(
|
||||
`Hook registry initialized with ${this.entries.length} hook entries`,
|
||||
|
||||
@@ -77,13 +77,6 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"deepValidation": {
|
||||
"title": "Deep Validation",
|
||||
"description": "Run a subagent after the main agent finishes to perform final validation.",
|
||||
"markdownDescription": "Run a subagent after the main agent finishes to perform final validation. This subagent will reflect on the original prompt, review changes, and run tests to ensure satisfaction.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `false`",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"enableAutoUpdate": {
|
||||
"title": "Enable Auto Update",
|
||||
"description": "Enable automatic updates.",
|
||||
|
||||
Reference in New Issue
Block a user