diff --git a/packages/coding-agent/DEVELOPMENT.md b/packages/coding-agent/DEVELOPMENT.md index 10e280e79..e27f2a580 100644 --- a/packages/coding-agent/DEVELOPMENT.md +++ b/packages/coding-agent/DEVELOPMENT.md @@ -389,7 +389,7 @@ A `ToolFactory` is `(session: ToolSession) => Tool | null | Promise `createTools(session, toolNames?)` is the entry point. It: -1. Normalizes requested tool names (`toolNames`) and always injects `exit_plan_mode`. +1. Normalizes requested tool names (`toolNames`) and injects `exit_plan_mode` while `plan.enabled` is true. 2. Resolves eval backend allowance via `PI_PY` override (`getEvalBackendsFromEnv()`) or `eval.py` / `eval.js` settings. 3. Performs Python kernel preflight when applicable (`checkPythonKernelAvailability`). 4. Computes effective gating (`isToolAllowed`) from settings and runtime state: @@ -1139,7 +1139,7 @@ Primary file: `packages/coding-agent/src/tools/index.ts`. Notes from current behavior: -- `createTools()` always injects `exit_plan_mode` when `toolNames` are specified. +- `createTools()` injects `exit_plan_mode` when `toolNames` are specified and `plan.enabled` is true. - `resolve` is included only when at least one active tool is marked `deferrable: true` (built-in or extension/custom). - `yield` is force-added when `session.requireYieldTool === true`. - Eval availability is mode-driven (`PI_PY`, `eval.py`, `eval.js`); eval falls back to JavaScript when Python is unavailable and JavaScript is enabled. The standalone `bash` tool is always available. diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 34a31bc31..9e8be6636 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -1115,15 +1115,6 @@ export class InteractiveMode implements InteractiveModeContext { this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled)."); return; } - if (this.planModeEnabled) { - const confirmed = await this.showHookConfirm( - "Exit plan mode?", - "This exits plan mode without approving a plan.", - ); - if (!confirmed) return; - await this.#exitPlanMode({ paused: true }); - return; - } await this.#enterPlanMode(); if (initialPrompt && this.onInputCallback) { this.onInputCallback(this.startPendingSubmission({ text: initialPrompt })); diff --git a/packages/coding-agent/test/issue-816-repro.test.ts b/packages/coding-agent/test/issue-816-repro.test.ts index 7d12c86dd..260f29ee0 100644 --- a/packages/coding-agent/test/issue-816-repro.test.ts +++ b/packages/coding-agent/test/issue-816-repro.test.ts @@ -91,4 +91,27 @@ describe("issue #816 — plan mode pendingModelSwitch leak", () => { // the user is no longer in plan mode. expect(setModelSpy).not.toHaveBeenCalled(); }); + + it("does not enter plan mode when plan.enabled is false", async () => { + session.settings.set("plan.enabled", false); + const warning = vi.spyOn(mode, "showWarning").mockImplementation(() => {}); + + await mode.handlePlanModeCommand(); + + expect(mode.planModeEnabled).toBe(false); + expect(warning).toHaveBeenCalledWith("Plan mode is disabled. Enable it in settings (plan.enabled)."); + }); + + it("allows /plan to pause an active plan mode after plan.enabled is disabled", async () => { + await mode.handlePlanModeCommand(); + expect(mode.planModeEnabled).toBe(true); + + session.settings.set("plan.enabled", false); + vi.spyOn(mode, "showHookConfirm").mockResolvedValue(true); + + await mode.handlePlanModeCommand(); + + expect(mode.planModeEnabled).toBe(false); + expect(mode.planModePaused).toBe(true); + }); }); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index f4baa3dac..cb814304b 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -459,11 +459,8 @@ describe("Coding Agent Tools", () => { expect(output).toContain("child/"); expect(output).toContain("nested/"); expect(output).toContain("… 2 more"); - expect(output).toContain("child-00.txt"); - expect(output).toContain("child-03.txt"); - expect(output).not.toContain("child-02.txt"); expect(output).not.toContain("child-01.txt"); - expect(output).toContain("child-12.txt"); + expect(output).toContain("child-00.txt"); expect(output).not.toContain("deep.txt"); }); @@ -1437,20 +1434,11 @@ function b() { pattern: "needle", paths: [testDir], }); + const output = getTextOutput(result); -<<<<<<< HEAD expect(output).toContain(`Result limit reached; narrow paths or use skip=${DEFAULT_MATCH_LIMIT}.`); expect(result.details?.matchCount).toBe(DEFAULT_MATCH_LIMIT); expect(result.details?.matchLimitReached).toBe(DEFAULT_MATCH_LIMIT); -||||||| parent of 94bd73e86 (fix tests) - expect(output).toContain("Result limit reached; narrow paths or use skip=500."); - expect(result.details?.matchCount).toBe(500); - expect(result.details?.matchLimitReached).toBe(500); -======= - expect(output).toContain("Result limit reached; narrow paths or use skip=100."); - expect(result.details?.matchCount).toBe(100); - expect(result.details?.matchLimitReached).toBe(100); ->>>>>>> 94bd73e86 (fix tests) }); }); diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index 8f25192a9..d64450051 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -233,6 +233,20 @@ describe("createTools", () => { expect(names).not.toContain("calc"); }); + it("excludes exit_plan_mode when plan mode is disabled", async () => { + const session = createTestSession({ + settings: createSettingsWithOverrides({ + "plan.enabled": false, + }), + }); + + const defaultTools = await createTools(session); + expect(defaultTools.map(t => t.name)).not.toContain("exit_plan_mode"); + + const requestedTools = await createTools(session, ["read", "exit_plan_mode"]); + expect(requestedTools.map(t => t.name)).toEqual(["read"]); + }); + it("includes search_tool_bm25 when MCP tool discovery is enabled and executable", async () => { const session = createTestSession({ settings: createSettingsWithOverrides({