diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 23e7ca073e..968d61109d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1384,12 +1384,19 @@ Logging in with Google... Restarting Gemini CLI to continue. if (config) { if (parsedCommand.extensionContext) { + // Explicit extension invocation sets the "sticky" context to that extension, + // allowing subsequent multi-turn conversational replies (e.g. "yes, do that") + // to continue interacting with the extension's isolated plan directory. if (config.hasExtensionPlanDir(parsedCommand.extensionContext)) { config.setActiveExtensionContext(parsedCommand.extensionContext); } else { + // Extension doesn't have a plan dir registered, so fallback to default workspace context config.setActiveExtensionContext(undefined); } } else if (parsedCommand.commandToExecute?.name === 'plan') { + // If the user explicitly runs the native global /plan command (e.g., "/plan copy"), + // they are signaling an intent to manage their standard workspace plans, + // so we clear the sticky extension context to avoid misdirecting the operation. config.setActiveExtensionContext(undefined); } } diff --git a/packages/core/src/tools/enter-plan-mode.test.ts b/packages/core/src/tools/enter-plan-mode.test.ts index 8d44f313d3..a0a5676aa9 100644 --- a/packages/core/src/tools/enter-plan-mode.test.ts +++ b/packages/core/src/tools/enter-plan-mode.test.ts @@ -131,6 +131,17 @@ describe('EnterPlanModeTool', () => { expect(result.returnDisplay).toBe('Switching to Plan mode'); }); + it('should call getPlansDir immediately after setting ApprovalMode.PLAN to ensure JIT directory creation', async () => { + const invocation = tool.build({}); + + await invocation.execute({ abortSignal: new AbortController().signal }); + + expect(mockConfig.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.PLAN, + ); + expect(mockConfig.getPlansDir).toHaveBeenCalled(); + }); + it('should include optional reason in output display but not in llmContent', async () => { const reason = 'Design new database schema'; const invocation = tool.build({ reason }); diff --git a/packages/core/src/tools/enter-plan-mode.ts b/packages/core/src/tools/enter-plan-mode.ts index ca5ef465a9..9c43eb2b47 100644 --- a/packages/core/src/tools/enter-plan-mode.ts +++ b/packages/core/src/tools/enter-plan-mode.ts @@ -19,6 +19,7 @@ import { ENTER_PLAN_MODE_TOOL_NAME } from './tool-names.js'; import { ApprovalMode } from '../policy/types.js'; import { ENTER_PLAN_MODE_DEFINITION } from './definitions/coreTools.js'; import { resolveToolDeclaration } from './definitions/resolver.js'; +import { debugLogger } from '../utils/debugLogger.js'; export interface EnterPlanModeParams { reason?: string; @@ -123,6 +124,18 @@ export class EnterPlanModeInvocation extends BaseToolInvocation< this.config.setApprovalMode(ApprovalMode.PLAN); + // Trigger JIT provisioning immediately. In sandboxed environments, + // the plans directory must exist on the host before it can be bound/allowed. + try { + this.config.getPlansDir(); + } catch (e) { + // Log error but don't fail; write_file will try again later if possible + debugLogger.error( + 'Failed to create plans directory during initialization.', + e, + ); + } + return { llmContent: 'Switching to Plan mode.', returnDisplay: this.params.reason