From 39a7d59b274ae7268705f8aca159f206ab6ebb2b Mon Sep 17 00:00:00 2001 From: Mahima Shanware Date: Mon, 6 Apr 2026 16:20:05 +0000 Subject: [PATCH] feat(cli): wire active extension context into slash command routing Extracts the extension context from slash commands based on their registered metadata and sets it as the active context in the Config before execution. This enables the backend to dynamically route plan directories based on the extension that owns the invoked command. --- packages/cli/src/config/config.test.ts | 3 ++- packages/cli/src/config/config.ts | 14 ++++++++------ packages/cli/src/ui/AppContainer.tsx | 16 ++++++++++++---- .../cli/src/ui/commands/clearCommand.test.ts | 1 + packages/cli/src/ui/commands/clearCommand.ts | 3 ++- packages/cli/src/utils/commands.ts | 6 +++++- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 6d4a75bbb0..7b11ca5040 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -3711,7 +3711,8 @@ describe('loadCliConfig mcpEnabled', () => { ]); const config = await loadCliConfig(settings, 'test-session', argv); - expect(config.storage.getPlansDir()).toContain('ext-plans-dir'); + config.setActiveExtensionContext('ext-plan'); + expect(config.getPlansDir()).toContain('ext-plans-dir'); }); it('should NOT use plan directory from active extension when user has specified one', async () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 3ee537ac35..51dc87025b 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -609,9 +609,12 @@ export async function loadCliConfig( }); await extensionManager.loadExtensions(); - const extensionPlanSettings = extensionManager - .getExtensions() - .find((ext) => ext.isActive && ext.plan?.directory)?.plan; + const extensionPlanDirs: Record = {}; + for (const ext of extensionManager.getExtensions()) { + if (ext.isActive && ext.plan?.directory) { + extensionPlanDirs[ext.name] = ext.plan.directory; + } + } const experimentalJitContext = settings.experimental.jitContext; @@ -969,9 +972,8 @@ export async function loadCliConfig( plan: settings.general?.plan?.enabled ?? true, tracker: settings.experimental?.taskTracker, directWebFetch: settings.experimental?.directWebFetch, - planSettings: settings.general?.plan?.directory - ? settings.general.plan - : (extensionPlanSettings ?? settings.general?.plan), + planSettings: settings.general?.plan, + extensionPlanDirs, enableEventDrivenScheduler: true, skillsSupport: settings.skills?.enabled ?? true, disabledSkills: settings.skills?.disabled, diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 4da8acfdb7..ee0d49f2be 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1308,6 +1308,17 @@ Logging in with Google... Restarting Gemini CLI to continue. } } + const parsedCommand = parseSlashCommand( + submittedValue, + slashCommands ?? [], + ); + + if (parsedCommand.extensionContext && config) { + if (config.hasExtensionPlanDir(parsedCommand.extensionContext)) { + config.setActiveExtensionContext(parsedCommand.extensionContext); + } + } + const isSlash = isSlashCommand(submittedValue.trim()); const isIdle = streamingState === StreamingState.Idle; const isAgentRunning = @@ -1315,10 +1326,7 @@ Logging in with Google... Restarting Gemini CLI to continue. isToolExecuting(pendingHistoryItems); if (isSlash && isAgentRunning) { - const { commandToExecute } = parseSlashCommand( - submittedValue, - slashCommands ?? [], - ); + const commandToExecute = parsedCommand.commandToExecute; if (commandToExecute?.isSafeConcurrent) { void handleSlashCommand(submittedValue); addInput(submittedValue); diff --git a/packages/cli/src/ui/commands/clearCommand.test.ts b/packages/cli/src/ui/commands/clearCommand.test.ts index 77f6e4854d..0f147cc0d8 100644 --- a/packages/cli/src/ui/commands/clearCommand.test.ts +++ b/packages/cli/src/ui/commands/clearCommand.test.ts @@ -45,6 +45,7 @@ describe('clearCommand', () => { fireSessionEndEvent: vi.fn().mockResolvedValue(undefined), fireSessionStartEvent: vi.fn().mockResolvedValue(undefined), }), + setActiveExtensionContext: vi.fn(), injectionService: { clear: mockHintClear, }, diff --git a/packages/cli/src/ui/commands/clearCommand.ts b/packages/cli/src/ui/commands/clearCommand.ts index fb032da811..e10633294f 100644 --- a/packages/cli/src/ui/commands/clearCommand.ts +++ b/packages/cli/src/ui/commands/clearCommand.ts @@ -30,8 +30,9 @@ export const clearCommand: SlashCommand = { await hookSystem.fireSessionEndEvent(SessionEndReason.Clear); } - // Reset user steering hints + // Reset user steering hints and extension context config?.injectionService.clear(); + config?.setActiveExtensionContext(undefined); // Start a new conversation recording with a new session ID // We MUST do this before calling resetChat() so the new ChatRecordingService diff --git a/packages/cli/src/utils/commands.ts b/packages/cli/src/utils/commands.ts index a96537aadf..ad651248d4 100644 --- a/packages/cli/src/utils/commands.ts +++ b/packages/cli/src/utils/commands.ts @@ -10,6 +10,7 @@ export type ParsedSlashCommand = { commandToExecute: SlashCommand | undefined; args: string; canonicalPath: string[]; + extensionContext?: string; }; /** @@ -69,6 +70,8 @@ export const parseSlashCommand = ( const args = parts.slice(pathIndex).join(' '); + const extensionContext = commandToExecute?.extensionName; + // Backtrack if the matched (sub)command doesn't take arguments but some were provided, // AND the parent command is capable of handling them. if ( @@ -82,8 +85,9 @@ export const parseSlashCommand = ( commandToExecute: parentCommand, args: parts.slice(pathIndex - 1).join(' '), canonicalPath: canonicalPath.slice(0, -1), + extensionContext: parentCommand.extensionName, }; } - return { commandToExecute, args, canonicalPath }; + return { commandToExecute, args, canonicalPath, extensionContext }; };