From 58d20415903c91bf0cfa411de3aff665e4de5b7a Mon Sep 17 00:00:00 2001 From: Mahima Shanware Date: Fri, 10 Apr 2026 20:04:31 +0000 Subject: [PATCH] fix(core): trigger JIT directory provisioning synchronously in enter-plan-mode to restore sandbox compatibility This fixes a regression where sandboxed environments (like Windows bwrap) would crash when attempting to bind the plans directory if it was lazily created after the sandbox was initialized. Additionally, documented the sticky extension context logic in AppContainer to address code review feedback. --- packages/cli/src/ui/AppContainer.tsx | 7 +++++++ packages/core/src/tools/enter-plan-mode.test.ts | 11 +++++++++++ packages/core/src/tools/enter-plan-mode.ts | 13 +++++++++++++ 3 files changed, 31 insertions(+) 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