diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx b/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx index ec4aa00677..71a8b83e53 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx @@ -670,6 +670,8 @@ describe('useSlashCommandProcessor', () => { expect(actionResult).toEqual({ type: 'submit_prompt', content: [{ text: 'The actual prompt from the TOML file.' }], + activeExtensionName: undefined, + clearExtensionMRU: false, }); expect(mockAddItem).toHaveBeenCalledWith( @@ -704,6 +706,8 @@ describe('useSlashCommandProcessor', () => { expect(actionResult).toEqual({ type: 'submit_prompt', content: [{ text: 'The actual prompt from the mcp command.' }], + activeExtensionName: undefined, + clearExtensionMRU: false, }); expect(mockAddItem).toHaveBeenCalledWith( diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index f55503ad25..2dba4a2a6c 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -448,6 +448,8 @@ export const useSlashCommandProcessor = ( toolName: result.toolName, toolArgs: result.toolArgs, postSubmitPrompt: result.postSubmitPrompt, + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', }; case 'message': addItem( @@ -460,7 +462,11 @@ export const useSlashCommandProcessor = ( }, Date.now(), ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'logout': // Show logout confirmation dialog with Login/Exit options setCustomDialog( @@ -476,30 +482,62 @@ export const useSlashCommandProcessor = ( }, }), ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'dialog': switch (result.dialog) { case 'auth': actions.openAuthDialog(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'theme': actions.openThemeDialog(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'editor': actions.openEditorDialog(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'privacy': actions.openPrivacyNotice(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'sessionBrowser': actions.openSessionBrowser(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'settings': actions.openSettingsDialog(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'model': actions.openModelDialog(); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'agentConfig': { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const props = result.props as Record; @@ -522,16 +560,28 @@ export const useSlashCommandProcessor = ( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion props['definition'] as AgentDefinition, ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } case 'permissions': actions.openPermissionsDialog( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion result.props as { targetDirectory?: string }, ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'help': - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; default: { const unhandled: never = result.dialog; throw new Error( @@ -545,16 +595,26 @@ export const useSlashCommandProcessor = ( result.history.forEach((item, index) => { fullCommandContext.ui.addItem(item, index); }); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } case 'quit': actions.quit(result.messages); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; case 'submit_prompt': return { type: 'submit_prompt', content: result.content, + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', }; case 'confirm_shell_commands': { const callId = `expansion-${Date.now()}`; @@ -611,7 +671,11 @@ export const useSlashCommandProcessor = ( }, Date.now(), ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } if (outcome === ToolConfirmationOutcome.ProceedAlways) { @@ -649,7 +713,11 @@ export const useSlashCommandProcessor = ( }, Date.now(), ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } return await handleSlashCommand( @@ -660,7 +728,11 @@ export const useSlashCommandProcessor = ( } case 'custom_dialog': { setCustomDialog(result.component); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } default: { const unhandled: never = result; @@ -671,7 +743,11 @@ export const useSlashCommandProcessor = ( } } - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } else if (commandToExecute.subCommands) { const helpText = `Command '/${commandToExecute.name}' requires a subcommand. Available:\n${commandToExecute.subCommands .map((sc) => ` - ${sc.name}: ${sc.description || ''}`) @@ -681,11 +757,19 @@ export const useSlashCommandProcessor = ( content: helpText, timestamp: new Date(), }); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } } - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } catch (e: unknown) { hasError = true; if (config) { @@ -704,7 +788,11 @@ export const useSlashCommandProcessor = ( }, Date.now(), ); - return { type: 'handled' }; + return { + type: 'handled', + activeExtensionName: commandToExecute?.extensionName, + clearExtensionMRU: commandToExecute?.name === 'clear', + }; } finally { if (config && resolvedCommandPath[0] && !hasError) { const event = makeSlashCommandEvent({ diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index eee0241a58..a2e65b8fca 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -935,6 +935,13 @@ export const useGeminiStream = ( : false; if (slashCommandResult) { + if (slashCommandResult.clearExtensionMRU) { + config.setActiveExtensionName(undefined); + } else if (slashCommandResult.activeExtensionName) { + config.setActiveExtensionName( + slashCommandResult.activeExtensionName, + ); + } switch (slashCommandResult.type) { case 'schedule_tool': { const { toolName, toolArgs, postSubmitPrompt } = @@ -1750,6 +1757,12 @@ export const useGeminiStream = ( const handleApprovalModeChange = useCallback( async (newApprovalMode: ApprovalMode) => { + if ( + previousApprovalModeRef.current === ApprovalMode.PLAN && + newApprovalMode !== ApprovalMode.PLAN + ) { + config.setActiveExtensionName(undefined); + } if ( previousApprovalModeRef.current === ApprovalMode.PLAN && newApprovalMode !== ApprovalMode.PLAN && diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 6fbc3151d8..7c6224b699 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -498,6 +498,8 @@ export interface ConsoleMessageItem { export interface SubmitPromptResult { type: 'submit_prompt'; content: PartListUnion; + activeExtensionName?: string; + clearExtensionMRU?: boolean; } /** @@ -509,9 +511,13 @@ export type SlashCommandProcessorResult = toolName: string; toolArgs: Record; postSubmitPrompt?: PartListUnion; + activeExtensionName?: string; + clearExtensionMRU?: boolean; } | { type: 'handled'; // Indicates the command was processed and no further action is needed. + activeExtensionName?: string; + clearExtensionMRU?: boolean; } | SubmitPromptResult; diff --git a/packages/core/src/tools/exit-plan-mode.ts b/packages/core/src/tools/exit-plan-mode.ts index 4b705306de..b97926d693 100644 --- a/packages/core/src/tools/exit-plan-mode.ts +++ b/packages/core/src/tools/exit-plan-mode.ts @@ -64,7 +64,6 @@ export class ExitPlanModeTool extends BaseDeclarativeTool< } return this.config.storage.getPlansDir(customDir); } - protected override validateToolParamValues( params: ExitPlanModeParams, ): string | null {