mirror of
https://github.com/cline/cline.git
synced 2026-08-29 03:52:41 +08:00
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:
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user