Fix hidden plan/act mode-switch prompts reappearing when resuming a task from history (#12769)

* fix(vscode): hide synthetic mode-switch and resumption prompts when rehydrating chat from history

* chore: add changeset
This commit is contained in:
Saoud Rizwan
2026-07-30 17:04:32 -07:00
committed by GitHub
parent 49c7a89882
commit 56fd6bb1ce
6 changed files with 82 additions and 19 deletions
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix hidden plan/act mode-switch and task-resumption prompts reappearing as user messages when a task is reopened from history
+23 -16
View File
@@ -45,6 +45,7 @@ import type {
} from "@shared/ExtensionMessage"
import { Logger } from "@shared/services/Logger"
import { MessageIdMinter } from "./message-id-minter"
import { isSyntheticSdkUserMessage } from "./sdk-user-message-mapping"
import { isDeniedToolApprovalMistake, isKnownToolApprovalDenial } from "./tool-approval-denial"
// ---------------------------------------------------------------------------
@@ -2238,18 +2239,22 @@ export function sdkMessagesToClineMessages(
if (typeof message.content === "string") {
const text = message.content.trim()
if (text) {
// Visible user text marks a turn boundary: drop the preceding turn's outcome
// User text marks a turn boundary: drop the preceding turn's outcome
// signals (its text is NOT retagged — see endFinalTurn) and pick up the mode
// of the NEW turn from this message's wrapper.
// of the NEW turn from this message's wrapper. Synthetic runtime prompts
// (task resumption, plan -> act auto-continue) still advance the turn/mode
// state but never had a visible bubble live, so don't emit one here either.
state.clearTurnOutcome()
currentMode = message.uiMode ?? currentMode
clineMessages.push({
ts: state.nextTs(),
type: "say",
say: clineMessages.length === 0 ? "task" : "user_feedback",
text,
partial: false,
})
if (!isSyntheticSdkUserMessage(message)) {
clineMessages.push({
ts: state.nextTs(),
type: "say",
say: clineMessages.length === 0 ? "task" : "user_feedback",
text,
partial: false,
})
}
}
continue
}
@@ -2258,13 +2263,15 @@ export function sdkMessagesToClineMessages(
if (userText) {
state.clearTurnOutcome()
currentMode = message.uiMode ?? currentMode
clineMessages.push({
ts: state.nextTs(),
type: "say",
say: clineMessages.length === 0 ? "task" : "user_feedback",
text: userText,
partial: false,
})
if (!isSyntheticSdkUserMessage(message)) {
clineMessages.push({
ts: state.nextTs(),
type: "say",
say: clineMessages.length === 0 ? "task" : "user_feedback",
text: userText,
partial: false,
})
}
}
for (const block of message.content) {
+2 -1
View File
@@ -10,6 +10,7 @@ import type { SdkMessageCoordinator } from "./sdk-message-coordinator"
import type { SdkSessionConfigBuilder } from "./sdk-session-config-builder"
import { isAbortError, type SdkSessionLifecycle } from "./sdk-session-lifecycle"
import type { SdkSessionRebuildScheduler } from "./sdk-session-rebuild-scheduler"
import { ACT_MODE_CONTINUATION_PROMPT } from "./sdk-user-message-mapping"
import type { SdkSessionHost } from "./session-host"
import type { TaskProxy } from "./task-proxy"
import type { VscodeSessionHost } from "./vscode-session-host"
@@ -22,7 +23,7 @@ function usesClineAccountAuth(providerId: string): boolean {
return getProviderAuthStorageId(providerId) === "cline"
}
export const ACT_MODE_CONTINUATION_PROMPT = "The user approved switching to act mode. Continue with the approved plan now."
export { ACT_MODE_CONTINUATION_PROMPT }
export interface SdkModeCoordinatorOptions {
stateManager: StateManager
@@ -259,6 +259,49 @@ describe("SdkTaskHistory", () => {
expect(result).toContainEqual(expect.objectContaining({ type: "say", say: "text", text: "Built." }))
})
it("hides the plan -> act auto-continuation prompt when rehydrating from history", async () => {
// Live, the canned continuation is sent with fireAndForgetSend and never echoed
// as user_feedback; reopening the task from history must not resurface it.
const { history, readMessages } = makeHistory([makeSessionRecord("task-1")])
readMessages.mockResolvedValueOnce([
{ role: "user", content: '<user_input mode="plan">plan the feature</user_input>' },
{ role: "assistant", content: [{ type: "text", text: "Here is the plan." }] },
{
role: "user",
content:
'<user_input mode="act"><mode_notice>The user switched from plan mode to act mode before sending this message.</mode_notice>The user approved switching to act mode. Continue with the approved plan now.</user_input>',
},
{ role: "assistant", content: [{ type: "text", text: "Implemented." }] },
] as never)
const result = await history.getClineMessages("task-1")
expect(result.filter((m) => m.say === "user_feedback")).toHaveLength(0)
expect(result.map((m) => m.text).join("\n")).not.toContain("The user approved switching to act mode")
// The hidden prompt still carries the turn's mode: the final completion row
// must render as an act-mode completion, not a plan box.
expect(result).toContainEqual(expect.objectContaining({ type: "say", say: "completion_result", text: "Implemented." }))
})
it("hides [TASK RESUMPTION] prompts when rehydrating from history", async () => {
const { history, readMessages } = makeHistory([makeSessionRecord("task-1")])
readMessages.mockResolvedValueOnce([
{ role: "user", content: '<user_input mode="act">build the feature</user_input>' },
{ role: "assistant", content: [{ type: "text", text: "Partway done." }] },
{
role: "user",
content: '<user_input mode="act">[TASK RESUMPTION] Please continue where you left off.</user_input>',
},
{ role: "assistant", content: [{ type: "text", text: "Finished." }] },
] as never)
const result = await history.getClineMessages("task-1")
expect(result.filter((m) => m.say === "user_feedback")).toHaveLength(0)
expect(result.map((m) => m.text).join("\n")).not.toContain("[TASK RESUMPTION]")
expect(result).toContainEqual(expect.objectContaining({ type: "say", say: "completion_result", text: "Finished." }))
})
it("retags the terminal text of a session whose record says it completed", async () => {
// "completed" is written by the runtime host when the session is released after a
// clean final turn (task switch / clear / extension dispose).
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"
import { ACT_MODE_CONTINUATION_PROMPT } from "./sdk-mode-coordinator"
import {
ACT_MODE_CONTINUATION_PROMPT,
extractSdkUserText,
findSdkUserMessageIndexByOrdinal,
getSdkCheckpointRunCountForMessageIndex,
@@ -1,5 +1,12 @@
import { normalizeUserInput, stripModeNotices } from "@cline/shared"
import { ACT_MODE_CONTINUATION_PROMPT } from "./sdk-mode-coordinator"
/**
* Canned prompt SdkModeCoordinator sends to drive the plan -> act
* auto-continuation. Defined here (a leaf module) rather than in the
* coordinator so display-layer consumers (message-translator, ordinal
* mapping) don't pull the coordinator's heavy import graph into their tests.
*/
export const ACT_MODE_CONTINUATION_PROMPT = "The user approved switching to act mode. Continue with the approved plan now."
export type SdkUserMessage = {
role?: unknown