From 36e6ccd6c7f4d1ba2c7aca44d845d181626d6082 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:04:07 +0200 Subject: [PATCH] fix(claude): show plan mode when Claude enters it on its own (#6074) When Claude Code calls its EnterPlanMode tool, it reports the new mode through a system status message with permissionMode "plan". The Claude provider only read the compacting status from those messages, so the session kept the previous mode and never emitted mode_changed: the app, `paseo inspect`, and the stored agent record all kept showing the old mode. The provider now records the mode carried by a status message and emits mode_changed when it differs, sharing the bookkeeping with the init message so the plan-approval resume mode stays the mode Claude left. Closes #6063 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../agent/providers/claude/agent.test.ts | 38 +++++++++++++++++++ .../server/agent/providers/claude/agent.ts | 28 ++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index d28ac910ac..d88d8105cf 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -3069,6 +3069,44 @@ describe("ClaudeAgentSession context window usage", () => { } }); + test("reports the plan mode Claude enters on its own with EnterPlanMode", async () => { + // Claude Code announces a mode it switched to itself as a status message + // carrying the new permissionMode, right after the EnterPlanMode tool call. + const client = new ClaudeAgentClient({ + logger, + queryFactory: createQueryFactoryForTurns([ + [ + { ...createInitMessage(), permissionMode: "acceptEdits" }, + { + type: "system", + subtype: "status", + status: null, + permissionMode: "plan", + session_id: "session-1", + }, + createSuccessResult(), + ], + ]), + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + modeId: "acceptEdits", + }); + + try { + const events = await collectStreamEvents(session, "enter plan mode"); + + expect(events).toContainEqual( + expect.objectContaining({ type: "mode_changed", currentModeId: "plan" }), + ); + expect(await session.getCurrentMode()).toBe("plan"); + } finally { + await session.close(); + } + }); + test("a compaction abandoned mid-turn does not suppress the next compaction marker", async () => { // The first turn starts compacting and then ends without ever reaching a // compact_boundary, so the marker it opened is never resolved. diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 10048356b1..7b16c030a4 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -4321,6 +4321,19 @@ class ClaudeAgentSession implements AgentSession { return; } if (message.subtype === "status") { + // Claude Code reports a mode it switched to on its own, such as entering + // plan mode with the EnterPlanMode tool, through the status message. + if ( + isPermissionMode(message.permissionMode) && + this.observePermissionMode(message.permissionMode) + ) { + events.push({ + type: "mode_changed", + provider: "claude", + currentModeId: this.currentMode, + availableModes: this.availableModes, + }); + } const status = toObjectRecord(message)?.status; if (status === "compacting") { this.compacting = true; @@ -4632,10 +4645,7 @@ class ClaudeAgentSession implements AgentSession { notice = this.createClaudeSessionChangedNotice(existingSessionId, newSessionId); } this.availableModes = DEFAULT_MODES; - this.currentMode = message.permissionMode; - if (this.currentMode !== "plan") { - this.planResumeMode = this.currentMode; - } + this.observePermissionMode(message.permissionMode); this.persistence = null; if (message.model) { const normalizedRuntimeModel = normalizeClaudeRuntimeModelId(message.model); @@ -4654,6 +4664,16 @@ class ClaudeAgentSession implements AgentSession { return { threadStartedSessionId, notice }; } + /** Records a mode Claude Code reports, returning whether it differs from the current one. */ + private observePermissionMode(mode: PermissionMode): boolean { + const changed = this.currentMode !== mode; + this.currentMode = mode; + if (mode !== "plan") { + this.planResumeMode = mode; + } + return changed; + } + private readMissingResumedConversationError(message: SDKMessage): string | null { if (message.type !== "result" || message.subtype !== "error_during_execution") { return null;