---
url: /wave-agent/specs/commands/rewind-command.md
description: '`/rewind` 从检查点列表回退对话与文件变更，保留会话标题与文件级元数据'
---

# 功能规格说明：Rewind 命令

**创建日期**：2026-02-02

## 用户场景与测试 *（必填）*

### 用户故事：基本消息回退（优先级：P1）

作为用户，我希望能够通过选择特定的用户消息将对话恢复到之前的状态，以便纠正错误或探索不同的路径，而不会使历史记录变得混乱。

**为什么是这个优先级**：这是该功能的核心功能。它允许用户管理对话历史并撤消不需要的 AI 响应。

**独立测试**：可以通过发送多条消息、触发 `/rewind`、选择一条之前的用户消息，并验证所选消息及其后的所有消息已从历史记录中删除来进行完整测试。

**验收场景**：

1. **假设**对话中有 3 条用户消息和 3 条 AI 响应，**当**用户输入 `/rewind` 并选择第 2 条用户消息时，**则**第 2 条用户消息、其对应的 AI 响应、第 3 条用户消息及其 AI 响应全部被删除。
2. **假设**存在一个对话，**当**用户输入 `/rewind` 但取消选择时，**则**不删除任何消息，对话保持不变。

***

### 用户故事：带文件回退的回退（优先级：P2）

作为用户，我希望在删除消息中由 agent 所做的文件更改能够自动回退，以便我的本地环境与对话状态保持同步。

**为什么是这个优先级**：这确保了对话历史与磁盘上文件的实际状态之间的一致性，这对于编码助手至关重要。

**独立测试**：可以通过让 agent 创建或修改文件，然后回退到该文件操作之前的某个点，并验证文件已恢复到之前的状态，或者如果是新创建的文件则被删除来进行测试。

**验收场景**：

1. **假设** agent 在最近一轮中修改了 `src/main.ts`，**当**用户回退到该轮之前的用户消息时，**则** `src/main.ts` 恢复到该修改之前的内容。
2. **假设** agent 创建了新文件 `tests/new_test.ts`，**当**用户回退到文件创建之前的某个点时，**则** `tests/new_test.ts` 从文件系统中删除。

***

### 用户故事：支持子 agent 的回退（优先级：P2）

作为用户，我希望回退主对话时子 agent 所做的文件更改能够自动回退，并且任何正在运行的后台子 agent 任务在其上下文被移除时能够被终止。

**为什么是这个优先级**：这确保了回退功能在整个 agent 生态系统中保持一致，包括模块化子 agent 和后台任务。

**验收场景**：

1. **假设**子 agent 修改了一个文件，**当**用户将主对话回退到子 agent 被调用之前的某个点时，**则**子 agent 所做的文件更改被回退。
2. **假设**子 agent 在后台运行一个长时间任务，**当**用户将主对话回退到子 agent 任务启动之前的某个点时，**则**后台子 agent 任务自动终止。

***

### 用户故事：Webview 端 /rewind 检查点列表（优先级：P1）

作为插件端与桌面端用户，我希望输入 `/rewind` 后看到由所有用户消息组成的检查点列表（完整展示、可键盘导航），而不是只能在消息气泡上逐条寻找回滚按钮，以便快速回退到任意历史节点（包括压缩前的消息）。

**为什么是这个优先级**：目前 webview 端只能通过 hover 每条用户消息上的回滚按钮来回退，无法一览所有检查点，且压缩前的消息完全不可达。这与 CLI 的 `/rewind` 体验差距很大。

**独立测试**：可以通过发送多条消息、输入 `/rewind`、键盘导航选择一条检查点并确认，验证所选消息及其后的所有消息被删除来进行完整测试。

**验收场景**：

1. **假设**对话中有 5 条用户消息，**当**用户在输入框选择 `/rewind` 命令时，**则**界面以列表形式完整展示全部 5 条用户消息的文本预览（非输入补全下拉），列表可滚动，最新一条默认选中。
2. **假设**检查点列表已打开，**当**用户按 ↑/↓ 键时，**则**选中项在列表中移动并自动滚动到可见区域；**当**用户按 Enter 时，**则**弹出与现有"回滚到此消息"一致的二次确认框；**当**用户按 Esc 时，**则**关闭列表且不产生任何改动。
3. **假设**检查点列表已打开，**当**用户用鼠标点击某条检查点时，**则**与按 Enter 效果一致（弹出二次确认框）。
4. **假设**对话经历过压缩，**当**用户打开 `/rewind` 列表时，**则**压缩前的用户消息也出现在列表中，选中确认后可正常回退。
5. **假设** agent 正在流式输出，**当**用户触发 `/rewind` 时，**则**命令被忽略（与 `/clear` 行为一致）。
6. **假设**对话中没有任何用户消息，**当**用户打开 `/rewind` 列表时，**则**列表展示空状态提示，按 Esc 关闭。
7. **假设**对话中存在后台任务通知（`task_notification`）和 hook 注入的 user 消息（`isMeta: true`），**当**用户打开 `/rewind` 列表（CLI 或 Webview 端）时，**则**这些系统生成、用户不可见的消息不显示在检查点列表中，列表仅包含用户实际输入的可见消息。
8. **假设**对话经历过多次压缩（如 `u1 a1 u2 a2 u3 a3 c1 u2 a2 u3 a3 u4 a4 c2 u3 a3 u4 a4 u5 a5`），**当**用户回退到压缩边界之后的用户消息（如回退到 `u5`）时，**则** webview 显示与回退时刻发给 LLM 的消息一致——从最后一条压缩摘要开始展示（如 `c2 u3 a3 u4 a4`），压缩前的重复原始历史不再显示。
9. **假设**用户确认回滚（消息气泡上的回滚按钮与 `/rewind` 列表两条入口同此一条），**则**回滚成功后**那条被回滚的用户消息的完整正文回到输入框**、光标落在末尾并聚焦，用户可以直接改完再发；`ok === false` 时输入框保持原样。回填的是**那条消息的原始草稿**（引用标签按标记还原成标签，不是标签的可见文字），不是检查点列表里那份归一化过的预览文本。

**回填怎么来**：桌面端 / VS Code / JetBrains 三端的草稿本来就在宿主手里——宿主把 `rewindToMessage` 回执里的 `inputContent` 存成该会话的草稿（`inputDrafts` / `inputContent`），随后的快照把它交给 webview，输入框按 `renderDraftContent` 还原。**远控页没有宿主草稿通道**（shim 对 `updateInputContent` 是本地应答「草稿只留在本机浏览器里」，页面自己持有输入框），所以这条只能由它自己那次请求的回执带回：`rewindResult` 除 `{requestId, messageId, ok}` 外再带 `inputContent`，页面收到后按同一条 `renderDraftContent` 路径填进输入框并聚焦。回执**不带消息列表**这条既有口径不变（列表由页面自己截断）。

**列表怎么来**：检查点表扫转录当场投影成 `{id, 文本, 在完整线程里的位置}`，不把整份线程读进内存——列表要的是「哪几条能回滚到」，不是每条消息的完整内容（工具结果里的图片是 base64）。位置是**完整线程里的绝对下标**（压缩前的消息也算在内），因为回滚就是「按这个位置截断」。同一份投影三端共用（CLI 选择器、远控页、桌面端/VS Code 弹窗），过滤与同 id 折叠只写一处；选中之后按 id 再定位一次，不读整份线程。**执行截断也只扫一遍**：一趟扫描同时切出「留下的一段」（压缩后、回滚点之前）与「裁掉的一段」（回滚点之后，文件快照从它们身上取），并带出回滚之后要用的两条计数（完整条数、首个用户输入），不为截断把整条线程的消息对象攒起来。

***

### 用户故事：回退不破坏会话文件级元信息（优先级：P1）

作为用户，我希望回退对话后会话的标题、创建时间与 git 分支保持不变，且回退中途被打断也不会损坏会话文件，以便回退之后仍能在会话列表里认出这条对话并正常恢复它。

**为什么是这个优先级**：`/rewind` 是转录文件唯一的整文件重写路径。头部 metadata 与 `custom-title` 都是文件级保留条目、不在消息数组里，重写时若一并丢弃，会话会静默退化成「legacy 无头文件」——`createdAt` 变成当前时间、`gitBranch` 消失、自定义标题再也读不回来，且不会有任何报错。这条与回退本身同等重要，因为它决定回退是否可逆。

**独立测试**：对一条既有自定义标题、又有多轮对话的会话执行 `/rewind`，验证文件首行仍为创建时的 metadata header、`custom-title` 条目仍在、会话列表显示的自定义标题与创建时间未变；再在一次 `/rewind` 写入过程中杀死进程，验证文件要么是回退前内容、要么是回退后内容，不存在半截状态。

**验收场景**：

1. **假设**会话已设置自定义标题且包含多轮对话，**当**用户回退到其中某条历史消息时，**则**重写后的文件仍以 metadata header 开头（`workdir` / `createdAt` / `gitBranch` 与创建时逐字一致），且 `custom-title` 条目仍在，会话列表与恢复流程仍能读出该自定义标题。
2. **假设**会话没有自定义标题，**当**用户回退时，**则**文件仍保留首行 metadata header，且不得凭空补写 `custom-title` 条目。
3. **假设**会话文件本身是 legacy 形态（无 metadata header），**当**用户回退时，**则**重写后同样保持无 header 的既有形态，不补造、不报错。
4. **假设**回退的整文件写入被中断（进程被杀、磁盘写满、断电），**当**再次读取该会话时，**则**必须读到回退前或回退后的完整内容之一——不得出现首行丢失、文件被截断或半写状态。
5. **假设**用户对同一会话连续多次回退，**当**每次回退完成时，**则** metadata header 始终只有一行且内容不变；`custom-title` 不丢失、也不会回退成首条消息——保留条目 append-only，回退原样保留既有条目，并按会话管理的稀疏重追加口径（`ui/session-management.md` 验收场景 5 第 5 项）把标题再写一份到 EOF，列表读最后一个即用户设置的最新标题。

***

### 边界情况

* **用户回退到第一条消息时会发生什么？** 整个对话历史应被清除，会话期间所做的任何文件更改（包括子 agent 所做的更改）都应被回退。此时界面会回到空列表（桌面端显示欢迎页），但**会话本身没有消失**：它仍是原来那条会话（同一个 sessionId、侧边栏条目与标题都在）。因此回退后再次发送消息必须在同一会话里继续——不得当作新会话另开一条、不得按新会话规则创建 worktree（见 `desktop/desktop-sessions.md`「基于分支的 worktree 隔离会话」验收场景 9）。
* **对话经过压缩（compact）后还能回退到压缩前的消息吗？** 可以。`/rewind` 的检查点列表必须包含压缩前的用户消息；回退到压缩边界之前的消息时，压缩摘要及其后的所有消息一并删除，对话恢复到该检查点的原始（未压缩）状态。
* **回退到压缩边界之后的消息时，webview 会显示什么？** 显示与回退时刻发给 LLM 的消息完全一致：从最近一条压缩摘要开始，仅展示该压缩摘要及其后的消息（如回退到 `u5` 时显示 `c2 u3 a3 u4 a4`）。回退后 agent 的内存消息直接折叠到最近一条压缩摘要（与 compact、resume session 的行为一致），压缩前的重复原始历史不再显示；但会话文件仍保留 rewind 截断后的完整历史（含压缩前消息），因此检查点列表仍包含压缩前的用户消息、仍可继续回退到更早的检查点。
* **如果触发回退时子 agent 任务正在运行会怎样？** 如果启动子 agent 的消息被移除，该任务必须立即停止，以防止不一致的状态。
* **如果要回退的文件在 agent 修改后被外部修改了会怎样？** 系统必须覆盖任何外部更改，并将文件恢复到 agent 在该检查点操作之前的确切状态。
* **如果要回退的文件已被用户手动删除会怎样？** 系统应在之前的内容可用时尝试恢复它，或者优雅地处理缺失的文件而不崩溃。
* **回退到第一条消息时文件级元信息怎么办？** 历史被清空，但 metadata header 与已有的自定义标题必须保留——标题是用户显式设置、与会话内容无关的会话身份信息。
* **回退是转录 append-only 契约的唯一例外**：被回退的消息按用户意图从文件中物理删除，这与 Claude Code「只追加、把被回退的历史留成孤儿死分支」的做法不同，是刻意取舍而非缺陷。转录其余时刻仍是纯追加；多写入方同时改写同一会话文件的覆盖风险不在本轮范围内（与会话管理既有的「无锁」取舍一致，见 `ui/session-management.md`）。
