From 058b5e31b4354142a6941e260fca4c5af5c2fddc 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 04df366a98..339ee9ff41 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -3796,7 +3796,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 4e7e1db6f2..5ae09a2964 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -610,9 +610,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; @@ -982,9 +985,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 a0d995f323..da63055531 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1340,6 +1340,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 = @@ -1347,10 +1358,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 }; };