mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 14:04:41 -07:00
refactor(cli): finalize event-driven transition and remove interaction bridge (#18569)
This commit is contained in:
@@ -182,7 +182,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Confirm Tool',
|
title: 'Confirm Tool',
|
||||||
prompt: 'Confirm this action?',
|
prompt: 'Confirm this action?',
|
||||||
onConfirm: async () => {},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ describe('<HistoryItemDisplay />', () => {
|
|||||||
command: 'echo "\u001b[31mhello\u001b[0m"',
|
command: 'echo "\u001b[31mhello\u001b[0m"',
|
||||||
rootCommand: 'echo',
|
rootCommand: 'echo',
|
||||||
rootCommands: ['echo'],
|
rootCommands: ['echo'],
|
||||||
onConfirm: async () => {},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||||
import { ToolCallStatus, StreamingState } from '../types.js';
|
import { ToolCallStatus, StreamingState } from '../types.js';
|
||||||
@@ -70,7 +70,6 @@ describe('ToolConfirmationQueue', () => {
|
|||||||
command: 'ls',
|
command: 'ls',
|
||||||
rootCommand: 'ls',
|
rootCommand: 'ls',
|
||||||
rootCommands: ['ls'],
|
rootCommands: ['ls'],
|
||||||
onConfirm: vi.fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -144,7 +143,6 @@ describe('ToolConfirmationQueue', () => {
|
|||||||
fileDiff: longDiff,
|
fileDiff: longDiff,
|
||||||
originalContent: 'old',
|
originalContent: 'old',
|
||||||
newContent: 'new',
|
newContent: 'new',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -192,7 +190,6 @@ describe('ToolConfirmationQueue', () => {
|
|||||||
fileDiff: longDiff,
|
fileDiff: longDiff,
|
||||||
originalContent: 'old',
|
originalContent: 'old',
|
||||||
newContent: 'new',
|
newContent: 'new',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -242,7 +239,6 @@ describe('ToolConfirmationQueue', () => {
|
|||||||
fileDiff: longDiff,
|
fileDiff: longDiff,
|
||||||
originalContent: 'old',
|
originalContent: 'old',
|
||||||
newContent: 'new',
|
newContent: 'new',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
index: 1,
|
index: 1,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||||
import type {
|
import type {
|
||||||
ToolCallConfirmationDetails,
|
SerializableConfirmationDetails,
|
||||||
Config,
|
Config,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { initializeShellParsers } from '@google/gemini-cli-core';
|
import { initializeShellParsers } from '@google/gemini-cli-core';
|
||||||
@@ -24,13 +24,12 @@ describe('ToolConfirmationMessage Redirection', () => {
|
|||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
it('should display redirection warning and tip for redirected commands', () => {
|
it('should display redirection warning and tip for redirected commands', () => {
|
||||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
const confirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'exec',
|
type: 'exec',
|
||||||
title: 'Confirm Shell Command',
|
title: 'Confirm Shell Command',
|
||||||
command: 'echo "hello" > test.txt',
|
command: 'echo "hello" > test.txt',
|
||||||
rootCommand: 'echo, redirection (>)',
|
rootCommand: 'echo, redirection (>)',
|
||||||
rootCommands: ['echo'],
|
rootCommands: ['echo'],
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
const { lastFrame } = renderWithProviders(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||||
import type {
|
import type {
|
||||||
ToolCallConfirmationDetails,
|
SerializableConfirmationDetails,
|
||||||
Config,
|
Config,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
@@ -39,12 +39,11 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
it('should not display urls if prompt and url are the same', () => {
|
it('should not display urls if prompt and url are the same', () => {
|
||||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
const confirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Confirm Web Fetch',
|
title: 'Confirm Web Fetch',
|
||||||
prompt: 'https://example.com',
|
prompt: 'https://example.com',
|
||||||
urls: ['https://example.com'],
|
urls: ['https://example.com'],
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
const { lastFrame } = renderWithProviders(
|
||||||
@@ -61,7 +60,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display urls if prompt and url are different', () => {
|
it('should display urls if prompt and url are different', () => {
|
||||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
const confirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Confirm Web Fetch',
|
title: 'Confirm Web Fetch',
|
||||||
prompt:
|
prompt:
|
||||||
@@ -69,7 +68,6 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
urls: [
|
urls: [
|
||||||
'https://raw.githubusercontent.com/google/gemini-react/main/README.md',
|
'https://raw.githubusercontent.com/google/gemini-react/main/README.md',
|
||||||
],
|
],
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
const { lastFrame } = renderWithProviders(
|
||||||
@@ -86,14 +84,13 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display multiple commands for exec type when provided', () => {
|
it('should display multiple commands for exec type when provided', () => {
|
||||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
const confirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'exec',
|
type: 'exec',
|
||||||
title: 'Confirm Multiple Commands',
|
title: 'Confirm Multiple Commands',
|
||||||
command: 'echo "hello"', // Primary command
|
command: 'echo "hello"', // Primary command
|
||||||
rootCommand: 'echo',
|
rootCommand: 'echo',
|
||||||
rootCommands: ['echo'],
|
rootCommands: ['echo'],
|
||||||
commands: ['echo "hello"', 'ls -la', 'whoami'], // Multi-command list
|
commands: ['echo "hello"', 'ls -la', 'whoami'], // Multi-command list
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
const { lastFrame } = renderWithProviders(
|
||||||
@@ -114,7 +111,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('with folder trust', () => {
|
describe('with folder trust', () => {
|
||||||
const editConfirmationDetails: ToolCallConfirmationDetails = {
|
const editConfirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: 'Confirm Edit',
|
title: 'Confirm Edit',
|
||||||
fileName: 'test.txt',
|
fileName: 'test.txt',
|
||||||
@@ -122,33 +119,29 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
fileDiff: '...diff...',
|
fileDiff: '...diff...',
|
||||||
originalContent: 'a',
|
originalContent: 'a',
|
||||||
newContent: 'b',
|
newContent: 'b',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const execConfirmationDetails: ToolCallConfirmationDetails = {
|
const execConfirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'exec',
|
type: 'exec',
|
||||||
title: 'Confirm Execution',
|
title: 'Confirm Execution',
|
||||||
command: 'echo "hello"',
|
command: 'echo "hello"',
|
||||||
rootCommand: 'echo',
|
rootCommand: 'echo',
|
||||||
rootCommands: ['echo'],
|
rootCommands: ['echo'],
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoConfirmationDetails: ToolCallConfirmationDetails = {
|
const infoConfirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Confirm Web Fetch',
|
title: 'Confirm Web Fetch',
|
||||||
prompt: 'https://example.com',
|
prompt: 'https://example.com',
|
||||||
urls: ['https://example.com'],
|
urls: ['https://example.com'],
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mcpConfirmationDetails: ToolCallConfirmationDetails = {
|
const mcpConfirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'mcp',
|
type: 'mcp',
|
||||||
title: 'Confirm MCP Tool',
|
title: 'Confirm MCP Tool',
|
||||||
serverName: 'test-server',
|
serverName: 'test-server',
|
||||||
toolName: 'test-tool',
|
toolName: 'test-tool',
|
||||||
toolDisplayName: 'Test Tool',
|
toolDisplayName: 'Test Tool',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
@@ -214,7 +207,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('enablePermanentToolApproval setting', () => {
|
describe('enablePermanentToolApproval setting', () => {
|
||||||
const editConfirmationDetails: ToolCallConfirmationDetails = {
|
const editConfirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: 'Confirm Edit',
|
title: 'Confirm Edit',
|
||||||
fileName: 'test.txt',
|
fileName: 'test.txt',
|
||||||
@@ -222,7 +215,6 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
fileDiff: '...diff...',
|
fileDiff: '...diff...',
|
||||||
originalContent: 'a',
|
originalContent: 'a',
|
||||||
newContent: 'b',
|
newContent: 'b',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should NOT show "Allow for all future sessions" when setting is false (default)', () => {
|
it('should NOT show "Allow for all future sessions" when setting is false (default)', () => {
|
||||||
@@ -275,7 +267,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Modify with external editor option', () => {
|
describe('Modify with external editor option', () => {
|
||||||
const editConfirmationDetails: ToolCallConfirmationDetails = {
|
const editConfirmationDetails: SerializableConfirmationDetails = {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: 'Confirm Edit',
|
title: 'Confirm Edit',
|
||||||
fileName: 'test.txt',
|
fileName: 'test.txt',
|
||||||
@@ -283,7 +275,6 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
fileDiff: '...diff...',
|
fileDiff: '...diff...',
|
||||||
originalContent: 'a',
|
originalContent: 'a',
|
||||||
newContent: 'b',
|
newContent: 'b',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should show "Modify with external editor" when NOT in IDE mode', () => {
|
it('should show "Modify with external editor" when NOT in IDE mode', () => {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { DiffRenderer } from './DiffRenderer.js';
|
|||||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||||
import {
|
import {
|
||||||
type SerializableConfirmationDetails,
|
type SerializableConfirmationDetails,
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
type Config,
|
type Config,
|
||||||
type ToolConfirmationPayload,
|
type ToolConfirmationPayload,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
@@ -38,9 +37,7 @@ import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js';
|
|||||||
|
|
||||||
export interface ToolConfirmationMessageProps {
|
export interface ToolConfirmationMessageProps {
|
||||||
callId: string;
|
callId: string;
|
||||||
confirmationDetails:
|
confirmationDetails: SerializableConfirmationDetails;
|
||||||
| ToolCallConfirmationDetails
|
|
||||||
| SerializableConfirmationDetails;
|
|
||||||
config: Config;
|
config: Config;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
availableTerminalHeight?: number;
|
availableTerminalHeight?: number;
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Confirm tool',
|
title: 'Confirm tool',
|
||||||
prompt: 'Do you want to proceed?',
|
prompt: 'Do you want to proceed?',
|
||||||
onConfirm: vi.fn(),
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
MessageBusType,
|
MessageBusType,
|
||||||
IdeClient,
|
IdeClient,
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { ToolCallStatus, type IndividualToolCallDisplay } from '../types.js';
|
import { ToolCallStatus, type IndividualToolCallDisplay } from '../types.js';
|
||||||
|
|
||||||
@@ -50,21 +49,9 @@ describe('ToolActionsContext', () => {
|
|||||||
resultDisplay: undefined,
|
resultDisplay: undefined,
|
||||||
confirmationDetails: { type: 'info', title: 'title', prompt: 'prompt' },
|
confirmationDetails: { type: 'info', title: 'title', prompt: 'prompt' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
callId: 'legacy-call',
|
|
||||||
name: 'legacy-tool',
|
|
||||||
description: 'desc',
|
|
||||||
status: ToolCallStatus.Confirming,
|
|
||||||
resultDisplay: undefined,
|
|
||||||
confirmationDetails: {
|
|
||||||
type: 'info',
|
|
||||||
title: 'legacy',
|
|
||||||
prompt: 'prompt',
|
|
||||||
onConfirm: vi.fn(),
|
|
||||||
} as ToolCallConfirmationDetails,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
callId: 'edit-call',
|
callId: 'edit-call',
|
||||||
|
correlationId: 'corr-edit',
|
||||||
name: 'edit-tool',
|
name: 'edit-tool',
|
||||||
description: 'desc',
|
description: 'desc',
|
||||||
status: ToolCallStatus.Confirming,
|
status: ToolCallStatus.Confirming,
|
||||||
@@ -77,8 +64,7 @@ describe('ToolActionsContext', () => {
|
|||||||
fileDiff: 'diff',
|
fileDiff: 'diff',
|
||||||
originalContent: 'old',
|
originalContent: 'old',
|
||||||
newContent: 'new',
|
newContent: 'new',
|
||||||
onConfirm: vi.fn(),
|
},
|
||||||
} as ToolCallConfirmationDetails,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -92,7 +78,7 @@ describe('ToolActionsContext', () => {
|
|||||||
</ToolActionsProvider>
|
</ToolActionsProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
it('publishes to MessageBus for tools with correlationId (Modern Path)', async () => {
|
it('publishes to MessageBus for tools with correlationId', async () => {
|
||||||
const { result } = renderHook(() => useToolActions(), { wrapper });
|
const { result } = renderHook(() => useToolActions(), { wrapper });
|
||||||
|
|
||||||
await result.current.confirm(
|
await result.current.confirm(
|
||||||
@@ -110,27 +96,6 @@ describe('ToolActionsContext', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onConfirm for legacy tools (Legacy Path)', async () => {
|
|
||||||
const { result } = renderHook(() => useToolActions(), { wrapper });
|
|
||||||
const legacyDetails = mockToolCalls[1]
|
|
||||||
.confirmationDetails as ToolCallConfirmationDetails;
|
|
||||||
|
|
||||||
await result.current.confirm(
|
|
||||||
'legacy-call',
|
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (legacyDetails && 'onConfirm' in legacyDetails) {
|
|
||||||
expect(legacyDetails.onConfirm).toHaveBeenCalledWith(
|
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected onConfirm to be present');
|
|
||||||
}
|
|
||||||
expect(mockMessageBus.publish).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles cancel by calling confirm with Cancel outcome', async () => {
|
it('handles cancel by calling confirm with Cancel outcome', async () => {
|
||||||
const { result } = renderHook(() => useToolActions(), { wrapper });
|
const { result } = renderHook(() => useToolActions(), { wrapper });
|
||||||
|
|
||||||
@@ -170,13 +135,11 @@ describe('ToolActionsContext', () => {
|
|||||||
'/f.txt',
|
'/f.txt',
|
||||||
'accepted',
|
'accepted',
|
||||||
);
|
);
|
||||||
const editDetails = mockToolCalls[2]
|
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||||
.confirmationDetails as ToolCallConfirmationDetails;
|
expect.objectContaining({
|
||||||
if (editDetails && 'onConfirm' in editDetails) {
|
correlationId: 'corr-edit',
|
||||||
expect(editDetails.onConfirm).toHaveBeenCalled();
|
}),
|
||||||
} else {
|
);
|
||||||
throw new Error('Expected onConfirm to be present');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates isDiffingEnabled when IdeClient status changes', async () => {
|
it('updates isDiffingEnabled when IdeClient status changes', async () => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
MessageBusType,
|
MessageBusType,
|
||||||
type Config,
|
type Config,
|
||||||
type ToolConfirmationPayload,
|
type ToolConfirmationPayload,
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
debugLogger,
|
debugLogger,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { IndividualToolCallDisplay } from '../types.js';
|
import type { IndividualToolCallDisplay } from '../types.js';
|
||||||
@@ -113,8 +112,7 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
|
|||||||
await ideClient?.resolveDiffFromCli(details.filePath, cliOutcome);
|
await ideClient?.resolveDiffFromCli(details.filePath, cliOutcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Dispatch
|
// 2. Dispatch via Event Bus
|
||||||
// PATH A: Event Bus (Modern)
|
|
||||||
if (tool.correlationId) {
|
if (tool.correlationId) {
|
||||||
await config.getMessageBus().publish({
|
await config.getMessageBus().publish({
|
||||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||||
@@ -127,20 +125,7 @@ export const ToolActionsProvider: React.FC<ToolActionsProviderProps> = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATH B: Legacy Callback (Adapter or Old Scheduler)
|
debugLogger.warn(`ToolActions: No correlationId for ${callId}`);
|
||||||
if (
|
|
||||||
details &&
|
|
||||||
'onConfirm' in details &&
|
|
||||||
typeof details.onConfirm === 'function'
|
|
||||||
) {
|
|
||||||
await (details as ToolCallConfirmationDetails).onConfirm(
|
|
||||||
outcome,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLogger.warn(`ToolActions: No confirmation mechanism for ${callId}`);
|
|
||||||
},
|
},
|
||||||
[config, ideClient, toolCalls, isDiffingEnabled],
|
[config, ideClient, toolCalls, isDiffingEnabled],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import {
|
import {
|
||||||
type ToolCall,
|
type ToolCall,
|
||||||
type Status as CoreStatus,
|
type Status as CoreStatus,
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
type SerializableConfirmationDetails,
|
type SerializableConfirmationDetails,
|
||||||
type ToolResultDisplay,
|
type ToolResultDisplay,
|
||||||
debugLogger,
|
debugLogger,
|
||||||
@@ -76,10 +75,8 @@ export function mapToDisplay(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let resultDisplay: ToolResultDisplay | undefined = undefined;
|
let resultDisplay: ToolResultDisplay | undefined = undefined;
|
||||||
let confirmationDetails:
|
let confirmationDetails: SerializableConfirmationDetails | undefined =
|
||||||
| ToolCallConfirmationDetails
|
undefined;
|
||||||
| SerializableConfirmationDetails
|
|
||||||
| undefined = undefined;
|
|
||||||
let outputFile: string | undefined = undefined;
|
let outputFile: string | undefined = undefined;
|
||||||
let ptyId: number | undefined = undefined;
|
let ptyId: number | undefined = undefined;
|
||||||
let correlationId: string | undefined = undefined;
|
let correlationId: string | undefined = undefined;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
GeminiEventType as ServerGeminiEventType,
|
GeminiEventType as ServerGeminiEventType,
|
||||||
ToolErrorType,
|
ToolErrorType,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
|
MessageBusType,
|
||||||
tokenLimit,
|
tokenLimit,
|
||||||
debugLogger,
|
debugLogger,
|
||||||
coreEvents,
|
coreEvents,
|
||||||
@@ -49,6 +50,11 @@ const mockSendMessageStream = vi
|
|||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue((async function* () {})());
|
.mockReturnValue((async function* () {})());
|
||||||
const mockStartChat = vi.fn();
|
const mockStartChat = vi.fn();
|
||||||
|
const mockMessageBus = {
|
||||||
|
publish: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
unsubscribe: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
const MockedGeminiClientClass = vi.hoisted(() =>
|
const MockedGeminiClientClass = vi.hoisted(() =>
|
||||||
vi.fn().mockImplementation(function (this: any, _config: any) {
|
vi.fn().mockImplementation(function (this: any, _config: any) {
|
||||||
@@ -250,6 +256,7 @@ describe('useGeminiStream', () => {
|
|||||||
isJitContextEnabled: vi.fn(() => false),
|
isJitContextEnabled: vi.fn(() => false),
|
||||||
getGlobalMemory: vi.fn(() => ''),
|
getGlobalMemory: vi.fn(() => ''),
|
||||||
getUserMemory: vi.fn(() => ''),
|
getUserMemory: vi.fn(() => ''),
|
||||||
|
getMessageBus: vi.fn(() => mockMessageBus),
|
||||||
getIdeMode: vi.fn(() => false),
|
getIdeMode: vi.fn(() => false),
|
||||||
getEnableHooks: vi.fn(() => false),
|
getEnableHooks: vi.fn(() => false),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
@@ -399,7 +406,6 @@ describe('useGeminiStream', () => {
|
|||||||
toolName: string,
|
toolName: string,
|
||||||
callId: string,
|
callId: string,
|
||||||
confirmationType: 'edit' | 'info',
|
confirmationType: 'edit' | 'info',
|
||||||
mockOnConfirm: Mock,
|
|
||||||
status: TrackedToolCall['status'] = 'awaiting_approval',
|
status: TrackedToolCall['status'] = 'awaiting_approval',
|
||||||
): TrackedWaitingToolCall => ({
|
): TrackedWaitingToolCall => ({
|
||||||
request: {
|
request: {
|
||||||
@@ -416,7 +422,6 @@ describe('useGeminiStream', () => {
|
|||||||
? {
|
? {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: 'Confirm Edit',
|
title: 'Confirm Edit',
|
||||||
onConfirm: mockOnConfirm,
|
|
||||||
fileName: 'file.txt',
|
fileName: 'file.txt',
|
||||||
filePath: '/test/file.txt',
|
filePath: '/test/file.txt',
|
||||||
fileDiff: 'fake diff',
|
fileDiff: 'fake diff',
|
||||||
@@ -426,7 +431,6 @@ describe('useGeminiStream', () => {
|
|||||||
: {
|
: {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: `${toolName} confirmation`,
|
title: `${toolName} confirmation`,
|
||||||
onConfirm: mockOnConfirm,
|
|
||||||
prompt: `Execute ${toolName}?`,
|
prompt: `Execute ${toolName}?`,
|
||||||
},
|
},
|
||||||
tool: {
|
tool: {
|
||||||
@@ -438,6 +442,7 @@ describe('useGeminiStream', () => {
|
|||||||
invocation: {
|
invocation: {
|
||||||
getDescription: () => 'Mock description',
|
getDescription: () => 'Mock description',
|
||||||
} as unknown as AnyToolInvocation,
|
} as unknown as AnyToolInvocation,
|
||||||
|
correlationId: `corr-${callId}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to render hook with default parameters - reduces boilerplate
|
// Helper to render hook with default parameters - reduces boilerplate
|
||||||
@@ -1763,10 +1768,9 @@ describe('useGeminiStream', () => {
|
|||||||
|
|
||||||
describe('handleApprovalModeChange', () => {
|
describe('handleApprovalModeChange', () => {
|
||||||
it('should auto-approve all pending tool calls when switching to YOLO mode', async () => {
|
it('should auto-approve all pending tool calls when switching to YOLO mode', async () => {
|
||||||
const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
|
|
||||||
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
||||||
createMockToolCall('replace', 'call1', 'edit', mockOnConfirm),
|
createMockToolCall('replace', 'call1', 'edit'),
|
||||||
createMockToolCall('read_file', 'call2', 'info', mockOnConfirm),
|
createMockToolCall('read_file', 'call2', 'info'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
||||||
@@ -1776,21 +1780,27 @@ describe('useGeminiStream', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Both tool calls should be auto-approved
|
// Both tool calls should be auto-approved
|
||||||
expect(mockOnConfirm).toHaveBeenCalledTimes(2);
|
expect(mockMessageBus.publish).toHaveBeenCalledTimes(2);
|
||||||
expect(mockOnConfirm).toHaveBeenCalledWith(
|
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
expect.objectContaining({
|
||||||
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||||
|
correlationId: 'corr-call1',
|
||||||
|
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
correlationId: 'corr-call2',
|
||||||
|
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only auto-approve edit tools when switching to AUTO_EDIT mode', async () => {
|
it('should only auto-approve edit tools when switching to AUTO_EDIT mode', async () => {
|
||||||
const mockOnConfirmReplace = vi.fn().mockResolvedValue(undefined);
|
|
||||||
const mockOnConfirmWrite = vi.fn().mockResolvedValue(undefined);
|
|
||||||
const mockOnConfirmRead = vi.fn().mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
||||||
createMockToolCall('replace', 'call1', 'edit', mockOnConfirmReplace),
|
createMockToolCall('replace', 'call1', 'edit'),
|
||||||
createMockToolCall('write_file', 'call2', 'edit', mockOnConfirmWrite),
|
createMockToolCall('write_file', 'call2', 'edit'),
|
||||||
createMockToolCall('read_file', 'call3', 'info', mockOnConfirmRead),
|
createMockToolCall('read_file', 'call3', 'info'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
||||||
@@ -1800,21 +1810,21 @@ describe('useGeminiStream', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Only replace and write_file should be auto-approved
|
// Only replace and write_file should be auto-approved
|
||||||
expect(mockOnConfirmReplace).toHaveBeenCalledWith(
|
expect(mockMessageBus.publish).toHaveBeenCalledTimes(2);
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ correlationId: 'corr-call1' }),
|
||||||
);
|
);
|
||||||
expect(mockOnConfirmWrite).toHaveBeenCalledWith(
|
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
expect.objectContaining({ correlationId: 'corr-call2' }),
|
||||||
|
);
|
||||||
|
expect(mockMessageBus.publish).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ correlationId: 'corr-call3' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
// read_file should not be auto-approved
|
|
||||||
expect(mockOnConfirmRead).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not auto-approve any tools when switching to REQUIRE_CONFIRMATION mode', async () => {
|
it('should not auto-approve any tools when switching to REQUIRE_CONFIRMATION mode', async () => {
|
||||||
const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
|
|
||||||
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
||||||
createMockToolCall('replace', 'call1', 'edit', mockOnConfirm),
|
createMockToolCall('replace', 'call1', 'edit'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
||||||
@@ -1824,21 +1834,19 @@ describe('useGeminiStream', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// No tools should be auto-approved
|
// No tools should be auto-approved
|
||||||
expect(mockOnConfirm).not.toHaveBeenCalled();
|
expect(mockMessageBus.publish).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors gracefully when auto-approving tool calls', async () => {
|
it('should handle errors gracefully when auto-approving tool calls', async () => {
|
||||||
const debuggerSpy = vi
|
const debuggerSpy = vi
|
||||||
.spyOn(debugLogger, 'warn')
|
.spyOn(debugLogger, 'warn')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
const mockOnConfirmSuccess = vi.fn().mockResolvedValue(undefined);
|
|
||||||
const mockOnConfirmError = vi
|
mockMessageBus.publish.mockRejectedValueOnce(new Error('Bus error'));
|
||||||
.fn()
|
|
||||||
.mockRejectedValue(new Error('Approval failed'));
|
|
||||||
|
|
||||||
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
||||||
createMockToolCall('replace', 'call1', 'edit', mockOnConfirmSuccess),
|
createMockToolCall('replace', 'call1', 'edit'),
|
||||||
createMockToolCall('write_file', 'call2', 'edit', mockOnConfirmError),
|
createMockToolCall('write_file', 'call2', 'edit'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
||||||
@@ -1847,13 +1855,10 @@ describe('useGeminiStream', () => {
|
|||||||
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
|
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Both confirmation methods should be called
|
// Both should be attempted despite first error
|
||||||
expect(mockOnConfirmSuccess).toHaveBeenCalled();
|
expect(mockMessageBus.publish).toHaveBeenCalledTimes(2);
|
||||||
expect(mockOnConfirmError).toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Error should be logged
|
|
||||||
expect(debuggerSpy).toHaveBeenCalledWith(
|
expect(debuggerSpy).toHaveBeenCalledWith(
|
||||||
'Failed to auto-approve tool call call2:',
|
'Failed to auto-approve tool call call1:',
|
||||||
expect.any(Error),
|
expect.any(Error),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1882,6 +1887,7 @@ describe('useGeminiStream', () => {
|
|||||||
invocation: {
|
invocation: {
|
||||||
getDescription: () => 'Mock description',
|
getDescription: () => 'Mock description',
|
||||||
} as unknown as AnyToolInvocation,
|
} as unknown as AnyToolInvocation,
|
||||||
|
correlationId: 'corr-1',
|
||||||
} as unknown as TrackedWaitingToolCall,
|
} as unknown as TrackedWaitingToolCall,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1893,83 +1899,9 @@ describe('useGeminiStream', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip tool calls without onConfirm method in confirmationDetails', async () => {
|
|
||||||
const awaitingApprovalToolCalls: TrackedToolCall[] = [
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
callId: 'call1',
|
|
||||||
name: 'replace',
|
|
||||||
args: { old_string: 'old', new_string: 'new' },
|
|
||||||
isClientInitiated: false,
|
|
||||||
prompt_id: 'prompt-id-1',
|
|
||||||
},
|
|
||||||
status: 'awaiting_approval',
|
|
||||||
responseSubmittedToGemini: false,
|
|
||||||
confirmationDetails: {
|
|
||||||
type: 'edit',
|
|
||||||
title: 'Confirm Edit',
|
|
||||||
// No onConfirm method
|
|
||||||
fileName: 'file.txt',
|
|
||||||
filePath: '/test/file.txt',
|
|
||||||
fileDiff: 'fake diff',
|
|
||||||
originalContent: 'old',
|
|
||||||
newContent: 'new',
|
|
||||||
} as any,
|
|
||||||
tool: {
|
|
||||||
name: 'replace',
|
|
||||||
displayName: 'replace',
|
|
||||||
description: 'Replace text',
|
|
||||||
build: vi.fn(),
|
|
||||||
} as any,
|
|
||||||
invocation: {
|
|
||||||
getDescription: () => 'Mock description',
|
|
||||||
} as unknown as AnyToolInvocation,
|
|
||||||
} as TrackedWaitingToolCall,
|
|
||||||
];
|
|
||||||
|
|
||||||
const { result } = renderTestHook(awaitingApprovalToolCalls);
|
|
||||||
|
|
||||||
// Should not throw an error
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only process tool calls with awaiting_approval status', async () => {
|
it('should only process tool calls with awaiting_approval status', async () => {
|
||||||
const mockOnConfirmAwaiting = vi.fn().mockResolvedValue(undefined);
|
|
||||||
const mockOnConfirmExecuting = vi.fn().mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
const mixedStatusToolCalls: TrackedToolCall[] = [
|
const mixedStatusToolCalls: TrackedToolCall[] = [
|
||||||
{
|
createMockToolCall('replace', 'call1', 'edit'),
|
||||||
request: {
|
|
||||||
callId: 'call1',
|
|
||||||
name: 'replace',
|
|
||||||
args: { old_string: 'old', new_string: 'new' },
|
|
||||||
isClientInitiated: false,
|
|
||||||
prompt_id: 'prompt-id-1',
|
|
||||||
},
|
|
||||||
status: 'awaiting_approval',
|
|
||||||
responseSubmittedToGemini: false,
|
|
||||||
confirmationDetails: {
|
|
||||||
type: 'edit',
|
|
||||||
title: 'Confirm Edit',
|
|
||||||
onConfirm: mockOnConfirmAwaiting,
|
|
||||||
fileName: 'file.txt',
|
|
||||||
filePath: '/test/file.txt',
|
|
||||||
fileDiff: 'fake diff',
|
|
||||||
originalContent: 'old',
|
|
||||||
newContent: 'new',
|
|
||||||
},
|
|
||||||
tool: {
|
|
||||||
name: 'replace',
|
|
||||||
displayName: 'replace',
|
|
||||||
description: 'Replace text',
|
|
||||||
build: vi.fn(),
|
|
||||||
} as any,
|
|
||||||
invocation: {
|
|
||||||
getDescription: () => 'Mock description',
|
|
||||||
} as unknown as AnyToolInvocation,
|
|
||||||
} as TrackedWaitingToolCall,
|
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
callId: 'call2',
|
callId: 'call2',
|
||||||
@@ -1991,6 +1923,7 @@ describe('useGeminiStream', () => {
|
|||||||
} as unknown as AnyToolInvocation,
|
} as unknown as AnyToolInvocation,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
liveOutput: 'Writing...',
|
liveOutput: 'Writing...',
|
||||||
|
correlationId: 'corr-call2',
|
||||||
} as TrackedExecutingToolCall,
|
} as TrackedExecutingToolCall,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2000,9 +1933,14 @@ describe('useGeminiStream', () => {
|
|||||||
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
|
await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only the awaiting_approval tool should be processed
|
// Only the awaiting_approval tool should be processed.
|
||||||
expect(mockOnConfirmAwaiting).toHaveBeenCalledTimes(1);
|
expect(mockMessageBus.publish).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOnConfirmExecuting).not.toHaveBeenCalled();
|
expect(mockMessageBus.publish).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ correlationId: 'corr-call1' }),
|
||||||
|
);
|
||||||
|
expect(mockMessageBus.publish).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ correlationId: 'corr-call2' }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
parseAndFormatApiError,
|
parseAndFormatApiError,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
|
MessageBusType,
|
||||||
promptIdContext,
|
promptIdContext,
|
||||||
tokenLimit,
|
tokenLimit,
|
||||||
debugLogger,
|
debugLogger,
|
||||||
@@ -1408,10 +1409,15 @@ export const useGeminiStream = (
|
|||||||
|
|
||||||
// Process pending tool calls sequentially to reduce UI chaos
|
// Process pending tool calls sequentially to reduce UI chaos
|
||||||
for (const call of awaitingApprovalCalls) {
|
for (const call of awaitingApprovalCalls) {
|
||||||
const details = call.confirmationDetails;
|
if (call.correlationId) {
|
||||||
if (details && 'onConfirm' in details) {
|
|
||||||
try {
|
try {
|
||||||
await details.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
await config.getMessageBus().publish({
|
||||||
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||||
|
correlationId: call.correlationId,
|
||||||
|
confirmed: true,
|
||||||
|
requiresUserConfirmation: false,
|
||||||
|
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLogger.warn(
|
debugLogger.warn(
|
||||||
`Failed to auto-approve tool call ${call.request.callId}:`,
|
`Failed to auto-approve tool call ${call.request.callId}:`,
|
||||||
@@ -1422,7 +1428,7 @@ export const useGeminiStream = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toolCalls],
|
[config, toolCalls],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCompletedTools = useCallback(
|
const handleCompletedTools = useCallback(
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import { renderHook } from '../../test-utils/render.js';
|
|||||||
import { useToolScheduler } from './useToolScheduler.js';
|
import { useToolScheduler } from './useToolScheduler.js';
|
||||||
import {
|
import {
|
||||||
MessageBusType,
|
MessageBusType,
|
||||||
ToolConfirmationOutcome,
|
|
||||||
Scheduler,
|
Scheduler,
|
||||||
type Config,
|
type Config,
|
||||||
type MessageBus,
|
type MessageBus,
|
||||||
type CompletedToolCall,
|
type CompletedToolCall,
|
||||||
type ToolCallConfirmationDetails,
|
|
||||||
type ToolCallsUpdateMessage,
|
type ToolCallsUpdateMessage,
|
||||||
type AnyDeclarativeTool,
|
type AnyDeclarativeTool,
|
||||||
type AnyToolInvocation,
|
type AnyToolInvocation,
|
||||||
@@ -132,122 +130,6 @@ describe('useToolScheduler', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('injects onConfirm callback for awaiting_approval tools (Adapter Pattern)', async () => {
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useToolScheduler(
|
|
||||||
vi.fn().mockResolvedValue(undefined),
|
|
||||||
mockConfig,
|
|
||||||
() => undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockToolCall = {
|
|
||||||
status: 'awaiting_approval' as const,
|
|
||||||
request: {
|
|
||||||
callId: 'call-1',
|
|
||||||
name: 'test_tool',
|
|
||||||
args: {},
|
|
||||||
isClientInitiated: false,
|
|
||||||
prompt_id: 'p1',
|
|
||||||
},
|
|
||||||
tool: createMockTool(),
|
|
||||||
invocation: createMockInvocation({
|
|
||||||
getDescription: () => 'Confirming test tool',
|
|
||||||
}),
|
|
||||||
confirmationDetails: { type: 'info', title: 'Confirm', prompt: 'Sure?' },
|
|
||||||
correlationId: 'corr-123',
|
|
||||||
};
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
void mockMessageBus.publish({
|
|
||||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
||||||
toolCalls: [mockToolCall],
|
|
||||||
schedulerId: ROOT_SCHEDULER_ID,
|
|
||||||
} as ToolCallsUpdateMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [toolCalls] = result.current;
|
|
||||||
const call = toolCalls[0];
|
|
||||||
if (call.status !== 'awaiting_approval') {
|
|
||||||
throw new Error('Expected status to be awaiting_approval');
|
|
||||||
}
|
|
||||||
const confirmationDetails =
|
|
||||||
call.confirmationDetails as ToolCallConfirmationDetails;
|
|
||||||
|
|
||||||
expect(confirmationDetails).toBeDefined();
|
|
||||||
expect(typeof confirmationDetails.onConfirm).toBe('function');
|
|
||||||
|
|
||||||
// Test that onConfirm publishes to MessageBus
|
|
||||||
const publishSpy = vi.spyOn(mockMessageBus, 'publish');
|
|
||||||
await confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
|
||||||
|
|
||||||
expect(publishSpy).toHaveBeenCalledWith({
|
|
||||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
||||||
correlationId: 'corr-123',
|
|
||||||
confirmed: true,
|
|
||||||
requiresUserConfirmation: false,
|
|
||||||
outcome: ToolConfirmationOutcome.ProceedOnce,
|
|
||||||
payload: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('injects onConfirm with payload (Inline Edit support)', async () => {
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useToolScheduler(
|
|
||||||
vi.fn().mockResolvedValue(undefined),
|
|
||||||
mockConfig,
|
|
||||||
() => undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockToolCall = {
|
|
||||||
status: 'awaiting_approval' as const,
|
|
||||||
request: {
|
|
||||||
callId: 'call-1',
|
|
||||||
name: 'test_tool',
|
|
||||||
args: {},
|
|
||||||
isClientInitiated: false,
|
|
||||||
prompt_id: 'p1',
|
|
||||||
},
|
|
||||||
tool: createMockTool(),
|
|
||||||
invocation: createMockInvocation(),
|
|
||||||
confirmationDetails: { type: 'edit', title: 'Edit', filePath: 'test.ts' },
|
|
||||||
correlationId: 'corr-edit',
|
|
||||||
};
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
void mockMessageBus.publish({
|
|
||||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
||||||
toolCalls: [mockToolCall],
|
|
||||||
schedulerId: ROOT_SCHEDULER_ID,
|
|
||||||
} as ToolCallsUpdateMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [toolCalls] = result.current;
|
|
||||||
const call = toolCalls[0];
|
|
||||||
if (call.status !== 'awaiting_approval') {
|
|
||||||
throw new Error('Expected awaiting_approval');
|
|
||||||
}
|
|
||||||
const confirmationDetails =
|
|
||||||
call.confirmationDetails as ToolCallConfirmationDetails;
|
|
||||||
|
|
||||||
const publishSpy = vi.spyOn(mockMessageBus, 'publish');
|
|
||||||
const mockPayload = { newContent: 'updated code' };
|
|
||||||
await confirmationDetails.onConfirm(
|
|
||||||
ToolConfirmationOutcome.ProceedOnce,
|
|
||||||
mockPayload,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(publishSpy).toHaveBeenCalledWith({
|
|
||||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
||||||
correlationId: 'corr-edit',
|
|
||||||
confirmed: true,
|
|
||||||
requiresUserConfirmation: false,
|
|
||||||
outcome: ToolConfirmationOutcome.ProceedOnce,
|
|
||||||
payload: mockPayload,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves responseSubmittedToGemini flag across updates', () => {
|
it('preserves responseSubmittedToGemini flag across updates', () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useToolScheduler(
|
useToolScheduler(
|
||||||
|
|||||||
@@ -6,21 +6,18 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type Config,
|
type Config,
|
||||||
type MessageBus,
|
|
||||||
type ToolCallRequestInfo,
|
type ToolCallRequestInfo,
|
||||||
type ToolCall,
|
type ToolCall,
|
||||||
type CompletedToolCall,
|
type CompletedToolCall,
|
||||||
type ToolConfirmationPayload,
|
|
||||||
MessageBusType,
|
MessageBusType,
|
||||||
ToolConfirmationOutcome,
|
ROOT_SCHEDULER_ID,
|
||||||
Scheduler,
|
Scheduler,
|
||||||
type EditorType,
|
type EditorType,
|
||||||
type ToolCallsUpdateMessage,
|
type ToolCallsUpdateMessage,
|
||||||
ROOT_SCHEDULER_ID,
|
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
// Re-exporting types compatible with legacy hook expectations
|
// Re-exporting types compatible with hook expectations
|
||||||
export type ScheduleFn = (
|
export type ScheduleFn = (
|
||||||
request: ToolCallRequestInfo | ToolCallRequestInfo[],
|
request: ToolCallRequestInfo | ToolCallRequestInfo[],
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
@@ -109,8 +106,8 @@ export function useToolScheduler(
|
|||||||
|
|
||||||
const internalAdaptToolCalls = useCallback(
|
const internalAdaptToolCalls = useCallback(
|
||||||
(coreCalls: ToolCall[], prevTracked: TrackedToolCall[]) =>
|
(coreCalls: ToolCall[], prevTracked: TrackedToolCall[]) =>
|
||||||
adaptToolCalls(coreCalls, prevTracked, messageBus),
|
adaptToolCalls(coreCalls, prevTracked),
|
||||||
[messageBus],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -227,12 +224,11 @@ export function useToolScheduler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ADAPTER: Merges UI metadata (submitted flag) and injects legacy callbacks.
|
* ADAPTER: Merges UI metadata (submitted flag).
|
||||||
*/
|
*/
|
||||||
function adaptToolCalls(
|
function adaptToolCalls(
|
||||||
coreCalls: ToolCall[],
|
coreCalls: ToolCall[],
|
||||||
prevTracked: TrackedToolCall[],
|
prevTracked: TrackedToolCall[],
|
||||||
messageBus: MessageBus,
|
|
||||||
): TrackedToolCall[] {
|
): TrackedToolCall[] {
|
||||||
const prevMap = new Map(prevTracked.map((t) => [t.request.callId, t]));
|
const prevMap = new Map(prevTracked.map((t) => [t.request.callId, t]));
|
||||||
|
|
||||||
@@ -240,34 +236,6 @@ function adaptToolCalls(
|
|||||||
const prev = prevMap.get(coreCall.request.callId);
|
const prev = prevMap.get(coreCall.request.callId);
|
||||||
const responseSubmittedToGemini = prev?.responseSubmittedToGemini ?? false;
|
const responseSubmittedToGemini = prev?.responseSubmittedToGemini ?? false;
|
||||||
|
|
||||||
// Inject onConfirm adapter for tools awaiting approval.
|
|
||||||
// The Core provides data-only (serializable) confirmationDetails. We must
|
|
||||||
// inject the legacy callback function that proxies responses back to the
|
|
||||||
// MessageBus.
|
|
||||||
if (coreCall.status === 'awaiting_approval' && coreCall.correlationId) {
|
|
||||||
const correlationId = coreCall.correlationId;
|
|
||||||
return {
|
|
||||||
...coreCall,
|
|
||||||
confirmationDetails: {
|
|
||||||
...coreCall.confirmationDetails,
|
|
||||||
onConfirm: async (
|
|
||||||
outcome: ToolConfirmationOutcome,
|
|
||||||
payload?: ToolConfirmationPayload,
|
|
||||||
) => {
|
|
||||||
await messageBus.publish({
|
|
||||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
||||||
correlationId,
|
|
||||||
confirmed: outcome !== ToolConfirmationOutcome.Cancel,
|
|
||||||
requiresUserConfirmation: false,
|
|
||||||
outcome,
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
responseSubmittedToGemini,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...coreCall,
|
...coreCall,
|
||||||
responseSubmittedToGemini,
|
responseSubmittedToGemini,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
GeminiCLIExtension,
|
GeminiCLIExtension,
|
||||||
MCPServerConfig,
|
MCPServerConfig,
|
||||||
ThoughtSummary,
|
ThoughtSummary,
|
||||||
ToolCallConfirmationDetails,
|
|
||||||
SerializableConfirmationDetails,
|
SerializableConfirmationDetails,
|
||||||
ToolResultDisplay,
|
ToolResultDisplay,
|
||||||
RetrieveUserQuotaResponse,
|
RetrieveUserQuotaResponse,
|
||||||
@@ -64,10 +63,7 @@ export interface ToolCallEvent {
|
|||||||
name: string;
|
name: string;
|
||||||
args: Record<string, never>;
|
args: Record<string, never>;
|
||||||
resultDisplay: ToolResultDisplay | undefined;
|
resultDisplay: ToolResultDisplay | undefined;
|
||||||
confirmationDetails:
|
confirmationDetails: SerializableConfirmationDetails | undefined;
|
||||||
| ToolCallConfirmationDetails
|
|
||||||
| SerializableConfirmationDetails
|
|
||||||
| undefined;
|
|
||||||
correlationId?: string;
|
correlationId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +73,7 @@ export interface IndividualToolCallDisplay {
|
|||||||
description: string;
|
description: string;
|
||||||
resultDisplay: ToolResultDisplay | undefined;
|
resultDisplay: ToolResultDisplay | undefined;
|
||||||
status: ToolCallStatus;
|
status: ToolCallStatus;
|
||||||
confirmationDetails:
|
confirmationDetails: SerializableConfirmationDetails | undefined;
|
||||||
| ToolCallConfirmationDetails
|
|
||||||
| SerializableConfirmationDetails
|
|
||||||
| undefined;
|
|
||||||
renderOutputAsMarkdown?: boolean;
|
renderOutputAsMarkdown?: boolean;
|
||||||
ptyId?: number;
|
ptyId?: number;
|
||||||
outputFile?: string;
|
outputFile?: string;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type {
|
import type {
|
||||||
ToolCallConfirmationDetails,
|
SerializableConfirmationDetails,
|
||||||
ToolEditConfirmationDetails,
|
ToolEditConfirmationDetails,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
@@ -366,13 +366,12 @@ describe('textUtils', () => {
|
|||||||
|
|
||||||
describe('toolConfirmationDetails case study', () => {
|
describe('toolConfirmationDetails case study', () => {
|
||||||
it('should sanitize command and rootCommand for exec type', () => {
|
it('should sanitize command and rootCommand for exec type', () => {
|
||||||
const details: ToolCallConfirmationDetails = {
|
const details: SerializableConfirmationDetails = {
|
||||||
title: '\u001b[34mfake-title\u001b[0m',
|
title: '\u001b[34mfake-title\u001b[0m',
|
||||||
type: 'exec',
|
type: 'exec',
|
||||||
command: '\u001b[31mmls -l\u001b[0m',
|
command: '\u001b[31mmls -l\u001b[0m',
|
||||||
rootCommand: '\u001b[32msudo apt-get update\u001b[0m',
|
rootCommand: '\u001b[32msudo apt-get update\u001b[0m',
|
||||||
rootCommands: ['sudo'],
|
rootCommands: ['sudo'],
|
||||||
onConfirm: async () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitized = escapeAnsiCtrlCodes(details);
|
const sanitized = escapeAnsiCtrlCodes(details);
|
||||||
@@ -387,14 +386,13 @@ describe('textUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize properties for edit type', () => {
|
it('should sanitize properties for edit type', () => {
|
||||||
const details: ToolCallConfirmationDetails = {
|
const details: SerializableConfirmationDetails = {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
title: '\u001b[34mEdit File\u001b[0m',
|
title: '\u001b[34mEdit File\u001b[0m',
|
||||||
fileName: '\u001b[31mfile.txt\u001b[0m',
|
fileName: '\u001b[31mfile.txt\u001b[0m',
|
||||||
filePath: '/path/to/\u001b[32mfile.txt\u001b[0m',
|
filePath: '/path/to/\u001b[32mfile.txt\u001b[0m',
|
||||||
fileDiff:
|
fileDiff:
|
||||||
'diff --git a/file.txt b/file.txt\n--- a/\u001b[33mfile.txt\u001b[0m\n+++ b/file.txt',
|
'diff --git a/file.txt b/file.txt\n--- a/\u001b[33mfile.txt\u001b[0m\n+++ b/file.txt',
|
||||||
onConfirm: async () => {},
|
|
||||||
} as unknown as ToolEditConfirmationDetails;
|
} as unknown as ToolEditConfirmationDetails;
|
||||||
|
|
||||||
const sanitized = escapeAnsiCtrlCodes(details);
|
const sanitized = escapeAnsiCtrlCodes(details);
|
||||||
@@ -412,13 +410,12 @@ describe('textUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize properties for mcp type', () => {
|
it('should sanitize properties for mcp type', () => {
|
||||||
const details: ToolCallConfirmationDetails = {
|
const details: SerializableConfirmationDetails = {
|
||||||
type: 'mcp',
|
type: 'mcp',
|
||||||
title: '\u001b[34mCloud Run\u001b[0m',
|
title: '\u001b[34mCloud Run\u001b[0m',
|
||||||
serverName: '\u001b[31mmy-server\u001b[0m',
|
serverName: '\u001b[31mmy-server\u001b[0m',
|
||||||
toolName: '\u001b[32mdeploy\u001b[0m',
|
toolName: '\u001b[32mdeploy\u001b[0m',
|
||||||
toolDisplayName: '\u001b[33mDeploy Service\u001b[0m',
|
toolDisplayName: '\u001b[33mDeploy Service\u001b[0m',
|
||||||
onConfirm: async () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitized = escapeAnsiCtrlCodes(details);
|
const sanitized = escapeAnsiCtrlCodes(details);
|
||||||
@@ -434,12 +431,11 @@ describe('textUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize properties for info type', () => {
|
it('should sanitize properties for info type', () => {
|
||||||
const details: ToolCallConfirmationDetails = {
|
const details: SerializableConfirmationDetails = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '\u001b[34mWeb Search\u001b[0m',
|
title: '\u001b[34mWeb Search\u001b[0m',
|
||||||
prompt: '\u001b[31mSearch for cats\u001b[0m',
|
prompt: '\u001b[31mSearch for cats\u001b[0m',
|
||||||
urls: ['https://\u001b[32mgoogle.com\u001b[0m'],
|
urls: ['https://\u001b[32mgoogle.com\u001b[0m'],
|
||||||
onConfirm: async () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitized = escapeAnsiCtrlCodes(details);
|
const sanitized = escapeAnsiCtrlCodes(details);
|
||||||
@@ -457,12 +453,11 @@ describe('textUtils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not change the object if no sanitization is needed', () => {
|
it('should not change the object if no sanitization is needed', () => {
|
||||||
const details: ToolCallConfirmationDetails = {
|
const details: SerializableConfirmationDetails = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Web Search',
|
title: 'Web Search',
|
||||||
prompt: 'Search for cats',
|
prompt: 'Search for cats',
|
||||||
urls: ['https://google.com'],
|
urls: ['https://google.com'],
|
||||||
onConfirm: async () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitized = escapeAnsiCtrlCodes(details);
|
const sanitized = escapeAnsiCtrlCodes(details);
|
||||||
|
|||||||
Reference in New Issue
Block a user