Cline VS Code 扩展:修复历史重开任务时隐藏的模式切换/续跑提示"复活"成用户消息的问题
本篇围绕 Cline 仓库中一条变更说明 hidden-mode-prompts-history.md 展开:它修复了 VS Code 扩展(changeset 指向 claude-dev 包,patch 级修订)中的一个历史回放缺陷——当任务从历史记录中被重新打开时,Plan/Act 模式切换自动续跑的隐藏提示和 [TASK RESUMPTION] 任务续跑提示会再次以"用户消息"的身份出现在会话界面里。读懂这篇文章后,你将理解 Cline SDK 架构下"持久化对话历史"与"可视化转写记录"之间的映射机制、合成(synthetic)用户消息的判定规则,以及消息重新水合(rehydration)时如何正确隐藏这些提示并保留其模式语义。
一、缺陷本身:哪些提示是"隐藏"的,为什么会重新出现
Cline 的 Plan/Act 双模式与断点续跑能力依赖两类由运行时自动发送的提示词,它们会进入 SDK 的持久化消息历史,但在实时会话中从不作为用户气泡回显:
| 提示 | 触发时机 | 发送方式 |
|---|---|---|
Act 模式续跑提示(常量 ACT_MODE_CONTINUATION_PROMPT:"The user approved switching to act mode. Continue with the approved plan now.") |
用户在 Plan 模式下产出计划后批准切换 Act 模式 | 以 fire-and-forget 方式发出,实时界面不产生 user_feedback 行 |
| 任务续跑提示 | 任务中断后恢复执行时 | 内容以 [TASK RESUMPTION] 前缀开头,同样无可见气泡 |
这两类提示的共同特征是:存在于 SDK 历史中,但不在可视转写(visible transcript)里。问题出在从历史记录重开任务的路径上:旧实现把持久化的 SDK 消息逐条翻译成 ClineMessage 时,没有识别并跳过这些合成提示,于是重开任务后它们会"复活",渲染成一条看似用户发送过的消息,既误导用户,也会打乱后续消息的序号映射。
二、提示的注入源头:模式协调器与合成判定层
2.1 续跑提示从哪来
sdk-mode-coordinator.ts 中的 SdkModeCoordinator 负责用户发起的模式切换:当用户在下一条消息前切换了模式,协调器会把 <mode_notice> 元素(例如 "The user switched from plan mode to act mode before sending this message.")戳到下一条出站消息上;批准 Plan → Act 切换时,它再以 fire-and-forget 方式发送固定文案的续跑提示。测试 sdk-mode-coordinator.test.ts 多处断言了该文案的出现(约 L154、L189、L214、L312),可据此确认提示的确切措辞。
[TASK RESUMPTION] 提示则由任务控制/会话生命周期路径在恢复运行前注入,其文本前缀是固定的判定依据。
2.2 合成用户消息的判定规则
核心判定集中在 sdk-user-message-mapping.ts:
ACT_MODE_CONTINUATION_PROMPT(第 9 行):续跑提示的常量定义。特意放在这个"叶子模块"而非协调器中,注释说明原因:展示层消费者(message-translator、ordinal 映射)不必为了一个字符串把协调器的重依赖图拉进测试。isSyntheticUserPrompt(text)(第 50-66 行):先经normalizeUserInput剥掉<user_input mode="...">包装、再经stripModeNotices剥掉可能前置的<mode_notice>,然后匹配三种情形——[TASK RESUMPTION]前缀、与续跑提示完全相等、或<hook_context前缀(Hook 注入的上下文,仅面向模型)。剥除<mode_notice>是细节中的关键:用户主动切换模式时,<mode_notice>会拼在固定续跑文案前面,若不去除,合成提示会被误判为可见用户消息,导致其后的编辑/重生成(edit/regenerate)序号整体偏移一格。isSyntheticSdkUserMessage(message)(第 129-139 行):在文本判定之上再加两层——metadata.displayRole为system/status的运行时消息(Hook 上下文、压缩摘要)直接判定为合成;以及"仅附件不算用户输入"规则(hasAttachmentBlocks区分tool_result、image、file块,携带图片/文件的续跑消息仍计为可见用户消息)。
2.3 为什么"跳过"它们会影响全局:序号映射
sdk-user-message-mapping.ts 中的 findSdkUserMessageIndexByOrdinal(第 146-160 行)把"N 条可见用户消息"(1-based 序号,对应 task/user_feedback 行)映射到持久化 SDK 历史中的下标。如果隐藏提示没有被跳过,其后的每一条用户消息映射都会提前一格,Edit / Regenerate / 断点选择等依赖该映射的功能会全部错位。getSdkCheckpointRunCountForMessageIndex(第 168-188 行)则体现了另一面:隐藏的模式/续跑提示虽然没有 webview 行,但仍推进 checkpoint 运行计数器(仅 recovery_notice 除外)——"不显示"与"不参与计数"是两件独立的事。
三、修复的落点:历史重新水合时跳过合成提示
3.1 历史重开任务的调用链
重开任务时,sdk-task-history.ts 中的 SdkTaskHistory.getClineMessages(taskId) 从 SDK 会话宿主读取持久化消息(host.readMessages),经 message-translator.ts 的 sdkMessagesToClineMessages(第 2306 行导出)翻译成 webview 消费的 ClineMessage[]。该翻译器头部的映射表(第 7-27 行)说明了 SDK 事件到 say/ask 类型的规则,例如 chunk → say:"text"(partial)、attempt_completion → say:"completion_result" 等。修复正是发生在这一"从磁盘历史到可视消息"的翻译路径上:对 isSyntheticSdkUserMessage 判定为真的用户消息,不再生成 say:"user_feedback" 行(也不产生 say:"task" 行),从而让这些提示在重开后保持"隐藏"。
3.2 隐藏提示的模式语义仍要保留
隐藏的代价是:这一轮真正发生的模式(plan 还是 act)不再能从可见的用户消息上读出。翻译器因此保留了提示携带的模式信息,用于样式化该轮的"推断完成行":当一轮以纯文本收尾而非完成工具时,最后一条文本行会被就地重标(retag)为 say:"completion_result"(act/yolo,绿色完成框)或 say:"plan_completion_result"(plan,计划框),随后追加一条 ask:"completion_result",使重开的任务显示完成/续跑入口而不是卡住的转圈。
测试 sdk-task-history.test.ts 中的用例 hides the plan -> act auto-continuation prompt when rehydrating from history(第 263-285 行)覆盖了完整场景:持久化历史为「plan 模式提问 → 计划文本 → 带 <mode_notice> 的合成续跑提示(act 模式)→ "Implemented."」。断言有三层:
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")
// 隐藏提示仍携带该轮模式:完成行必须是 act 模式完成框,而不是计划框
expect(result).toContainEqual(expect.objectContaining({ type: "say", say: "completion_result", text: "Implemented." }))
即:续跑提示不出现、全文不再包含其文案、但完成行仍按 act 模式渲染(而非沿用上一轮的 plan 样式)。hides [TASK RESUMPTION] prompts when rehydrating from history(第 287-304 行)对 [TASK RESUMPTION] Please continue where you left off. 做了同构断言。
3.3 配套的边界规则(同组测试中的可验证事实)
同文件中的相邻用例还界定了"推断完成行"的触发边界,它们与隐藏提示逻辑共享同一套重水合规则:
- 只有当会话记录
status === "completed"时才重标终止文本(第 306-318 行用例);failed/cancelled/running/pending/idle都不视为干净收尾(第 320-339 行,注释指出idle也可能是被中止回合的状态,不能信任)。 - 以悬空
tool_use结尾的转写不做重标(第 354-369 行)——中止回合的文本保持普通say:"text"。 - 无会话记录(结局未知)时同样不重标(第 341-352 行)。
四、验证路径与延伸阅读
- 行为回归:apps/vscode/src/sdk/sdk-task-history.test.ts(隐藏提示、模式样式化、状态边界)
- 合成判定与序号映射:apps/vscode/src/sdk/sdk-user-message-mapping.ts 及其测试 sdk-user-message-mapping.test.ts(含
<mode_notice>前置场景,约 L38-L42) - 翻译层:apps/vscode/src/sdk/message-translator.ts 与 message-translator.test.ts(
<mode_notice>展示层剥离,约 L207-L216) - 提示注入侧:apps/vscode/src/sdk/sdk-mode-coordinator.ts、sdk-session-lifecycle.ts(出站回合的统一漏斗,可戳
<mode_notice>) - 模式行为的用户文档:docs/core-workflows/plan-and-act.mdx
五、小结
这条 patch 级变更的本质,是在"持久化历史"与"可视转写"之间补上了对合成提示的正确处理:isSyntheticUserPrompt / isSyntheticSdkUserMessage 提供统一的判定(剥掉 <user_input> 与 <mode_notice> 包装后再匹配 [TASK RESUMPTION]、续跑提示全文、<hook_context 前缀与 displayRole 元数据),SdkTaskHistory.getClineMessages 在重水合时据此跳过这些消息,同时保留其携带的模式信息用于完成框样式。修复同时保住了两个不变量——重开的任务界面不再出现"假用户消息",以及可见用户消息与 SDK 历史下标之间的 ordinal 映射不发生偏移。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00