---
url: /wave-agent/specs/ui/session-management.md
description: 按项目目录组织的会话存储与聚合列表：跨目录/worktree 聚合、`/resume` 热切换、`/rename` 重命名与子代理会话分离
---

# 功能规格说明：会话管理

**规格文件**：`docs/specs/ui/session-management.md`
**创建日期**：2026-01-21

> **平台边界**：前三个用户故事及会话存储相关功能需求为 CLI/SDK 宿主；会话列表聚合与恢复类用户故事（「跨目录会话聚合列表」「同仓库 worktree 会话聚合」「跨目录/worktree 恢复」「会话内热切换（`/resume`）」）为 CLI 宿主；「插件端聊天头部」与「GUI 会话内热切换（`/resume`）」用户故事适用于 GUI 宿主（VSCE / JB / desktop）；其中远程主机与分屏（pane）语义仅 desktop 适用。「会话自定义标题（重命名）」的**存储与列表语义**为 SDK 层（对所有读会话文件的宿主生效），**改名入口**三端都做：CLI 为 `/rename` 命令与选择器内就地改名，插件端（VSCE / JB）为头部标题就地编辑，桌面端为侧边栏会话行菜单（见 [desktop-sessions.md](../desktop/desktop-sessions.md)）。

## 用户场景与测试 *(必填)*

### 用户故事：基于项目的组织（优先级：P1）

作为处理多个项目的开发者，我希望我的代理会话按项目的工作目录组织，以便我能轻松找到和管理与当前工作相关的会话。

**优先级原因**：面向开发者的代理的核心组织需求。

**独立测试**：在不同目录中创建会话并验证它们存储在 `~/.wave/projects` 下的独立子目录中。

**验收场景**：

1. **假设** 有工作目录，**当** 创建会话时，**则** 它必须存储在以编码路径命名的子目录中。
2. **假设** 有较长或复杂的路径，**当** 编码时，**则** 它必须是文件系统安全的且限制为 200 个字符。

***

### 用户故事：高性能会话列表（优先级：P1）

作为拥有数百个历史会话的用户，我希望会话列表能快速加载，以便在启动或切换会话时不会体验到延迟。

**优先级原因**：随着会话数量增长，这对良好的用户体验至关重要。

**独立测试**：生成 100 个大会话文件并测量列出它们的时间；应该是近乎即时的，因为系统直接扫描会话目录、对每个文件只读取最后一条消息来提取时间戳与 token 数，不维护任何索引文件。

**验收场景**：

1. **假设** 有多个会话文件，**当** 列出会话时，**则** 系统直接扫描项目目录（`~/.wave/projects/<编码路径>/`），按文件名解析会话 ID（跳过 `subagent-` 前缀的子代理会话），对每个文件只读取最后一条消息以提取 `lastActiveAt` 与 token 数，按最后活跃时间倒序返回——不维护或读取任何 `sessions-index.json` 索引。
2. **假设** 会话文件损坏或为空，**当** 列出会话时，**则** 损坏文件跳过（解析失败视为不存在）；空文件以文件修改时间作为 `lastActiveAt` 时间戳，不阻塞整体列表。

***

### 用户故事：会话延迟物化，元数据消息不落盘（优先级：P1）

作为 CLI 用户，我希望仅包含元数据消息（如 SessionStart hook 注入的 `isMeta: true` 上下文）的会话不创建 transcript 文件，以便 `wave -r` 会话列表不会出现「0 tokens / No content」的空壳会话（与 Claude Code 的 lazy materialization 行为一致）。

**优先级原因**：会话列表可见缺陷，且与 Claude Code 行为不一致；修复集中在 `saveSession` 单点，成本低。

**独立测试**：启动 Agent 后不发送任何消息直接 `destroy()`，验证 `~/.wave/projects/<编码路径>/` 下不产生该会话的 JSONL 文件；发送第一条真实消息后再次验证 meta 消息与真实消息一次性写入。

**验收场景**：

1. **假设**新会话仅包含 SessionStart hook 注入的 meta 消息（`isMeta: true`），**当** `saveSession`（如 destroy 时）被调用时，**则**不创建会话文件、不写入任何内容，meta 消息仅保留在内存中
2. **假设**新会话先注入 meta 消息、随后用户发送第一条真实消息，**当** `saveSession` 被调用时，**则** meta 消息与真实消息一次性写入同一文件（meta 消息不丢失、顺序保持）
3. **假设**会话文件已物化（已有已保存消息）后运行时恢复会话并注入 meta 消息，**当** `saveSession` 被调用时，**则**照常追加写入（与 Claude Code 一致：文件物化后所有条目都持久化）
4. **假设** `/clear` 后新会话仅注入 meta 消息，**当** `saveSession` 被调用时，**则**不创建新会话文件
5. **假设**从未落盘的 meta-only 会话，**当**运行 `wave -r` 时，**则**列表不显示该会话
6. **假设**这台机器上已经跑过一次「清理历史 meta-only 会话文件」，**当**之后又出现一个仅含 meta 条目的会话文件（典型来源：`/rewind` 回到第一条用户消息后转录只剩 SessionStart 提示），**则**该文件不得被删除——清理是一次性迁移（跑过即写标记，此后不再扫描），不是每次 agent 初始化都执行的常驻 GC。若把它当常驻 GC，回退到空的会话会被当成「0 tokens 空壳」删掉，而桌面端侧边栏仍留着这条条目，点开只会得到空白页面。

***

### 用户故事：子代理会话分离（优先级：P2）

作为使用子代理的开发者，我希望子代理会话被清楚地标识并与主代理会话分离，以避免污染主会话历史。

**优先级原因**：在处理复杂的多代理工作流时提高清晰度。

**独立测试**：启动子代理并验证其会话文件以 `subagent-` 为前缀。

**验收场景**：

1. **假设** 子代理已创建，**当** 其会话被保存时，**则** 文件名必须以 `subagent-` 开头。

***

### 用户故事：插件端聊天头部（优先级：P2）

作为插件端用户，我希望聊天面板顶部显示当前会话标题和常用操作（新建对话、历史对话、更多菜单），并能在输入框用 `/resume` 打开同一份会话列表，以便快速切换会话和管理当前会话。

**为什么是这个优先级**：会话切换与新建是高频入口，但用户仍可通过清空输入重新开始，属于体验增强。

**独立测试**：发送一条消息后观察标题栏显示会话标题；点击"新建对话"验证当前会话被清空；点击"历史对话"选择一个会话验证消息恢复。

**验收场景**：

1. **假设**会话中已有消息，**当**标题栏渲染时，**则**显示由首条消息截断得到的会话标题（最多 30 字符）；无消息时显示"新对话"。
2. **假设**用户点击"新建对话"按钮，**当**当前不在响应流式输出中且当前会话无正在运行的后台任务时，**则**当前会话被清空，标题恢复为"新对话"；流式输出期间或存在正在运行的后台任务期间按钮禁用。
3. **假设**用户点击"历史对话"按钮，**当**操作发生时，**则**弹出历史会话列表；选择其中一个会话后其消息被恢复到聊天面板（当前会话无正在运行的后台任务时；否则选择被忽略）。
4. **假设**用户点击"更多"按钮，**当**操作发生时，**则**弹出更多菜单（设置、企业控制台、登录/退出登录，见 sso-auth.md 的更多菜单条目）。
5. **假设**标题栏右侧显示上下文用量指示，**当**会话进行中时，**则**实时反映当前 token 使用情况（见 context-usage-indicator.md）。
6. **假设**当前会话存在正在运行的后台任务（后台终端工具/后台子代理/后台工作流，类型 `shell`/`subagent`/`workflow` 任一、状态 `running`），**当**用户点击"新建对话"或从"历史对话"列表中选择会话时，**则**两操作均被禁止——"新建对话"按钮与流式输出期间同样处于禁用态，历史会话选择被静默忽略（与流式输出期间的既有守卫行为一致），当前会话内容与后台任务保持原样、不被清空或中断；后台任务全部结束（或停止）后操作恢复可用。
7. **假设**与"新建对话"按钮同语义的 `clearChat` 其它触发路径（如手动输入 `/clear`），**当**当前会话存在正在运行的后台任务时，**则**同样被忽略，守卫与按钮一致（均为原地清空当前会话的操作；桌面端 `/clear` 不在本故事范围）。
8. **假设**用户在输入框输入 `/resume`（宿主本地命令，与其它本地命令一样出现在 `/` 弹窗中），**当**命令执行时，**则**打开与点击"历史对话"按钮完全相同的会话选择器——同一范围（当前工作区目录）、同一份磁盘会话来源（含 CLI 在该目录创建的会话）、同一搜索与键盘操作、同一条恢复路径，不离开面板、不清空当前对话。
9. **假设**用户通过 `/resume` 打开的列表选择会话，**当**当前会话正在流式输出或存在正在运行的后台任务时，**则**与点击"历史对话"列表选择的行为一致：选择被忽略（守卫同场景 6），当前会话与后台任务保持原样。
10. **假设**`/resume` 选择器已打开，**当**用户按 Esc 或点击选择器外部时，**则**关闭选择器，当前会话、消息与输入框草稿保持原样。（`/resume` 不因"统一为全部项目"而扩大范围——插件宿主无法切换工作区，跨项目恢复属于桌面端与 CLI 的能力，见「GUI 会话内热切换」故事。）

***

### 用户故事：跨目录会话聚合列表与移除 7 天窗口（优先级：P1）

作为在多个项目间切换的开发者，我希望 `wave -r` 选择器能列出所有项目的历史会话（`Ctrl+A` 切换，行尾显示项目路径），并且不再把超过 7 天未活跃的会话从列表中过滤掉，以便从任意目录找回并恢复任意项目的旧对话。

**为什么是这个优先级**：这是跨目录恢复的地基——没有全项目列表与完整历史，跨目录恢复无从选择；7 天窗口会隐藏用户以为还在的旧会话，与 Claude Code 行为不一致。

**独立测试**：在项目 A 与项目 B 分别创建会话；从项目 A 运行 `wave -r`，验证默认只见项目 A 的会话；按 `Ctrl+A` 后两个项目的会话都出现且项目 B 的行尾显示其路径；构造一个 8 天前活跃的会话文件，验证它仍出现在列表中（移除 7 天过滤）。

**验收场景**：

1. **假设**当前目录存在历史会话，**当**用户运行 `wave -r`（无参数）时，**则**默认列出当前目录的全部会话，按最后活跃时间倒序，不再按 7 天窗口过滤（超过 7 天未活跃的会话仍显示）。
2. **假设**存在多个项目目录（`~/.wave/projects/` 下多个编码目录），**当**用户在 `wave -r` 选择器中按 `Ctrl+A` 时，**则**切换为全项目列表：遍历全部项目目录，行尾显示每个会话所属的项目路径（尽力解码回原始路径），按最后活跃时间倒序。
3. **假设**同一会话 ID 出现在多个项目目录（如多 worktree 分支），**当**聚合列表渲染时，**则**按 sessionId 去重，仅保留最后活跃时间最新的那条。
4. **假设**用户在 `wave -r` 选择器中再次按 `Ctrl+A`，**当**切换发生时，**则**列表回到"仅当前目录"范围，快捷键为双向切换。
5. **假设**项目目录名因超长路径被截断加哈希后缀、无法可靠解码回原始路径，**当**全项目模式展示项目路径时，**则**显示编码后的目录名作为兜底，不报错。

***

### 用户故事：同仓库 worktree 会话聚合（优先级：P1）

作为在 git 仓库多个 worktree 间工作的开发者，我希望 `wave -r` 选择器能发现同一仓库的其他 worktree 的会话（`Ctrl+W` 展开），以便在其中一个 worktree 里查看并恢复另一个 worktree 的对话。

**为什么是这个优先级**：同一仓库的多个 worktree 是高频并发工作形态，会话按 worktree 目录分散存储，跨 worktree 恢复是跨目录恢复中最常见的一类；与 Claude Code 的 worktree 感知行为对齐。

**独立测试**：在仓库的两个 worktree 中分别创建会话；在其中一个 worktree 里运行 `wave -r`，验证默认只显示当前 worktree 的会话；按 `Ctrl+W` 后两个 worktree 的会话都出现；单 worktree 或非 git 目录下 `Ctrl+W` 不生效。

**验收场景**：

1. **假设**当前目录位于 git 仓库内且该仓库有多个 worktree，**当**用户运行 `wave -r` 时，**则**选择器默认显示当前 worktree 的会话。
2. **假设**仓库有多个 worktree，**当**用户在 `wave -r` 选择器中按 `Ctrl+W` 时，**则**展开为同仓库全部 worktree 的会话（经 `git worktree list` 发现并匹配各 worktree 对应的会话目录），再次按 `Ctrl+W` 收回为仅当前 worktree。
3. **假设**仓库只有一个 worktree 或当前目录不在 git 仓库内，**当**用户按 `Ctrl+W` 时，**则**该快捷键不产生任何效果（底部提示不出现该快捷键）。
4. **假设**worktree 路径与普通项目路径可能前缀相同（如 `repo` 与 `repo-foo`），**当**worktree 目录匹配时，**则**按编码前缀长度优先匹配，避免短前缀误吞长路径的会话目录。
5. **假设**用户按 `Ctrl+A` 进入全项目模式后又按 `Ctrl+W`，**当**两个切换叠加时，**则**各自独立生效：全项目模式决定"哪些目录"、worktree 模式决定"是否限定同仓库 worktree"，两者同时开启时列出全部项目（全项目模式优先，不再受 worktree 过滤约束）。

***

### 用户故事：跨目录/worktree 恢复（优先级：P1）

作为在任意目录使用 `wave` 的开发者，我希望从选择器选中会话后能直接恢复：当前目录会话原地恢复；同仓库其他 worktree 的会话自动切换到目标 worktree 后恢复；其他项目的会话得到可执行的 `cd` 命令，以便无需记忆会话 ID 就能继续任何历史对话。

**为什么是这个优先级**：恢复是列表的终点——能列出但恢复不了等于功能缺失；worktree 自动切换与跨项目 `cd` 命令分别覆盖"同仓库高频场景"与"跨项目低频场景"，与 Claude Code 行为一致。

**独立测试**：在仓库 worktree B 创建会话后，在 worktree A 中经 `wave -r` + `Ctrl+W` 选中该会话，验证进程工作目录切换到 worktree B 且历史消息恢复；在另一个项目目录选中其会话，验证输出 `cd <路径> && wave --restore <sessionId>` 并复制到剪贴板。

**验收场景**：

1. **假设**用户选中当前目录的会话，**当**确认选择时，**则**直接恢复该会话（现有行为不变，消息历史载入、不改变工作目录）。
2. **假设**用户选中同仓库其他 worktree 的会话，**当**确认选择时，**则**直接恢复该会话，进程工作目录切换为该 worktree 路径，历史消息完整载入。
3. **假设**用户选中其他项目（非同仓库 worktree）的会话，**当**确认选择时，**则**不在当前进程恢复，而是显示 `cd <项目路径> && wave --restore <sessionId>` 命令并将该命令复制到剪贴板，随后退出选择器。
4. **假设**选中的 worktree 目录已被删除（`git worktree remove` 后），**当**用户确认选择时，**则**按"其他项目"处理：给出 `cd` 命令提示而不是报错崩溃。
5. **假设**用户直接运行 `wave --restore <sessionId>` 且该会话不在当前目录，**当**查找时，**则**扫描全部项目目录（含同仓库 worktree 目录），找到即恢复；全部找不到时给出明确错误（会话不存在），不静默地新建空会话。
6. **假设**用户运行 `wave -c`（`--continue`），**当**执行时，**则**行为保持不变：恢复当前目录最近一次会话，不跨目录。
7. **假设**宿主（桌面端 / 插件端 / daemon 客户端）经 `initialize { restoreSessionId }` 从磁盘重新托管一条会话，而发起这次 attach 的进程 cwd 不是该会话的创建目录（daemon 重启、应用重启、宿主索引过期），**当**会话被重新托管时，**则**以会话**自己**转录元数据头记录的 `workdir` 为工作目录——agent 的 cwd、转录的落点、工具的执行目录都锚定该目录，不随 attach 方的 cwd 漂移；worktree 里的会话因此重启后仍在自己的 worktree 里执行（不会掉进宿主仓库），`destroy --remove-worktree` 也仍解析到同一个 worktree。记录目录已被手工删除时退回 attach 方的 cwd 并记一条 warn（不失败、但降级可观测）。进程内的 `wave --restore <sessionId>`（场景 5）不经这条协议路径，语义不变。

***

### 用户故事：会话内热切换（`/resume`）（优先级：P1）

作为已经在一次对话中的开发者，我希望输入 `/resume` 就能列出历史会话并切换到其中一条，不必退出进程重新启动，以便在不丢失进程状态的前提下继续别处的对话。

**为什么是这个优先级**：换会话是会话功能里唯一仍需"退出重开"的高频动作。列表聚合、范围切换、归属判定与恢复语义都已为 `wave -r` 建好，会话内入口是这条链路的最后一段；缺失会让 CLI 与已支持进程内切换的插件端 / 桌面端体验断层。

**独立测试**：在目录 A 创建两条会话并在第一条里对话若干轮，在同一进程内输入 `/resume` 选中目录 A 的第二条会话，验证消息、用量与任务列表整体替换为目标会话且进程未退出；再切换到同仓库另一 worktree 的会话，验证工作目录随之切换、后续新消息追加到目标会话文件；按 Esc 取消，验证无任何副作用。

**验收场景**：

1. **假设**用户在一次对话中输入 `/resume`，**当**命令执行时，**则**在输入框上方打开会话选择器覆盖视图（不退出进程、不清空当前对话），支持 ↑↓ 选择、Enter 确认、Esc 取消，且范围切换（`Ctrl+W` 同仓库全部 worktree、`Ctrl+A` 全部项目）与快捷键提示同 `wave -r` 的选择器完全一致。
2. **假设**选择器已打开，**当**用户按 Esc 时，**则**关闭选择器，当前会话、消息与输入框内容保持原样。
3. **假设**用户选中当前目录的会话，**当**确认选择时，**则**在进程内切换：显示的消息流整体替换为目标会话的完整历史（含压缩前的显示历史），用量按目标会话重算，任务 / 待办列表替换为目标会话的任务，工作目录不变。
4. **假设**用户选中同仓库其他 worktree 的会话，**当**确认选择时，**则**在进程内切换并同时把工作目录切换到该会话所属目录；切换后涉及工作目录的派生内容（项目记忆、规则、目录相关的系统提示段）必须按新目录重新计算，不得沿用旧目录的缓存。
5. **假设**上一轮 `/resume` 已把会话切到 worktree B，**当**用户再次 `/resume` 并选中 worktree C（或切回原目录）的会话时，**则**切换同样成功——不允许出现"只能切换一次、之后再也切不动"的状态。
6. **假设**用户选中跨项目（非同仓库 worktree）的会话，**当**确认选择时，**则**不在当前进程恢复，而是在选择器内显示 `cd <项目路径> && wave --restore <sessionId>` 提示（可复制到剪贴板），**进程不退出**；关闭提示后原会话及其内容不受影响。（与"跨目录/worktree 恢复"故事场景 3 的唯一差别是入口在会话内，因此不退出进程。）
7. **假设**选中的 worktree 目录已被删除，**当**确认选择时，**则**按"跨项目"处理：给出 `cd` 提示而不是报错崩溃（与 `wave -r` 场景 4 一致）。
8. **假设**正在生成回复（加载中），**当**用户输入 `/resume` 时，**则**命令被忽略并提示，不中止当前回合、不切换会话（与 `/clear` 在运行中的既有口径一致）。
9. **假设**切换成功，**当**切换完成时，**则**会话 ID 与落盘项目目录同步变化：后续新消息必须追加写入目标会话原本所在的会话文件，不得在当前目录另建一份同名会话文件。
10. **假设**切换成功，**当**切换完成时，**则**输入框草稿被清空、加载态复位，并触发会话切换的生命周期钩子（上一会话的 `SessionEnd`、目标会话的 `SessionStart`，来源标记为"恢复"）。
11. **假设**目标会话文件在磁盘上不存在或已损坏，**当**用户确认选择时，**则**切换不发生、给出失败提示，当前会话保持原状且可继续使用。

***

### 用户故事：GUI 会话内热切换（`/resume`）（优先级：P1）

作为在插件端或桌面端界面里的开发者，我希望在输入框里输入 `/resume` 就能打开会话选择器并切换到其中一条，以便不必离开当前界面、也不必重启应用，就能继续另一段对话；其中桌面端还应能列出命令行与其它客户端创建的会话（含远程主机上的），插件端则与当前宿主既有的历史会话入口保持同一范围。

**为什么是这个优先级**：GUI 里切换会话已有入口，但桌面端的会话树只收录本应用创建的会话——命令行那边开的对话完全不可见；插件端的历史弹窗已按工作区目录从磁盘读取，缺的只是输入框里的入口。`/resume` 是 GUI 与 CLI 行为对齐的最后一段，也是"跨客户端接着同一段对话"的唯一入口。

**为什么插件端不扩到全部项目**：插件宿主由绑定的工作区决定一切（会话落盘目录、工具作用域、打开的文件），列出其它项目的会话后既无法切换工作区、也无从恢复，只会与头部「历史对话」按钮的范围产生不一致。跨项目恢复是桌面端（可切换工作目录与主机的宿主）与 CLI（跨项目给 `cd` 提示）的能力。

**独立测试**：用 CLI 在某个项目目录创建一段会话（界面从未创建过它），在桌面端输入 `/resume`，验证列表中同时出现该项目与该会话（每行带项目路径），选中后当前分屏切换到该会话且消息历史与 CLI 中所见一致；在插件端（VSCE / JB）输入 `/resume`，验证打开的列表与头部「历史对话」按钮完全一致（当前工作区目录，含 CLI 在该目录创建的会话）；再选中一条所属目录已被删除的会话，验证给出目录不存在的提示、且当前会话不受影响。

**验收场景**：

1. **假设**用户在 GUI（插件端或桌面端）输入 `/resume`，**当**命令执行时，**则**打开可搜索的会话选择器——插件端为面板头部下方的下拉弹窗（与"历史对话"按钮同一个），桌面端为居中模态对话框（渲染在应用根层，分屏状态下同样可用）——不离开当前界面、不清空当前对话，支持 ↑↓ 选择、Enter 确认、Esc 取消。
2. **假设**选择器打开，**当**列表加载时，**则**插件端（VSCE / JB）与该宿主头部「历史对话」按钮的范围完全一致（当前工作区目录的会话，按最后活跃时间倒序）；桌面端列出全部项目（`~/.wave/projects` 下全部项目目录，跨目录去重后按最后活跃时间倒序），每行显示会话标题与该会话所属项目路径。
3. **假设**某条会话不是当前界面创建的（由 CLI、另一客户端、或同一台机器上另一个 wave 进程写入），**当**选择器列表加载时，**则**该会话在其所属目录范围内同样出现并可选——列表来自磁盘上的会话文件，而不是界面自己的会话索引（插件端即"CLI 在同一工作区目录开的会话"，桌面端即"任意目录的会话"）。
4. **假设**当前对话属于某台远程主机（桌面端），**当**选择器列表加载时，**则**列表为该主机磁盘上的会话（每行标注项目路径），不混入本地或其它主机的会话——范围随"当前对话所属主机"变化，切换焦点分屏后再次打开即换成该主机的会话；当前主机不可达时给出可见的失败提示（不得静默显示空列表），当前会话不受影响。
5. **假设**用户选中一条会话，**当**确认选择时，**则**在当前位置切换：消息流整体替换为该会话的完整历史，界面不重开、进程不退出；桌面端复用既有分屏切换路径（该会话已显示在其它分屏时聚焦那个分屏，而不是再开一份）。
6. **假设**选中的会话所属目录在对应主机上已不存在（插件端即工作区目录已被删除），**当**确认选择时，**则**不切换，给出「该会话的目录已不存在」提示；当前会话及其内容保持原状、可继续使用。
7. **假设**目标会话记录在磁盘上不存在或已损坏，**当**确认选择时，**则**切换不发生、给出失败提示，当前会话保持原状（桌面端按既有规则同时从会话索引移除该条目）。
8. **假设**当前会话正在流式输出、或存在正在运行的后台任务，**当**用户输入 `/resume` 时，**则**沿用该宿主对既有切换入口的守卫口径：插件端与「历史对话」按钮一致（忽略选择并提示、不切换），桌面端维持其多会话并行语义（可切走，后台会话不被中断）。
9. **假设**选择器已打开，**当**用户按 Esc 或点击选择器外部时，**则**关闭选择器，当前会话、消息与输入框草稿保持原样。
10. **假设**插件端（VSCE / JB）的「历史对话」按钮与 `/resume` 命令，**当**任一入口打开列表时，**则**两者打开同一个选择器、同一份列表（同一范围、同一搜索与选中行为）；`/resume` 不得把列表范围扩大到工作区之外，也不允许出现两份范围不同的历史列表。（桌面端的选择器与侧边栏会话树采用两条刻意的来源，不受本条约束，见 `desktop/desktop-sessions.md`。）
11. **假设**磁盘上存在数百条会话，**当**选择器打开时，**则**列表在打开时按需读取并显示加载态，不阻塞输入框与当前对话；不因数量设隐藏上限（搜索用于缩小范围，而不是靠截断列表控制规模）。

***

### 用户故事：会话元数据头：真实创建时间与 git 分支（优先级：P1）

作为 `wave -r` 用户，我希望会话文件的 metadata header 持久化真实创建时间（`createdAt`）与创建时的 git 分支（`gitBranch`），以便列表能使用真实创建时间，并在多 worktree 场景区分同一仓库不同分支的会话。

**为什么是这个优先级**：`SessionMetadata.createdAt` 目前是列表扫描时即时生成的 `new Date()`（恒等于"当前时间"，完全失真）；多 worktree 场景的会话无法显示所属分支，只能靠目录名猜测。

**独立测试**：创建会话后检查 JSONL 首行为 metadata header 且含 `workdir`、`createdAt`、`gitBranch` 字段；列表返回的 `createdAt` 与 header 一致；非 git 目录创建会话时 header 不含 `gitBranch`。

**验收场景**：

1. **假设**创建新会话，**当** session 文件写入时，**则**首行为 metadata header，含 `workdir` 与 `createdAt`（ISO 8601，会话创建时刻）；目录为 git 仓库时含 `gitBranch`（`git branch --show-current` 结果），非 git 目录或执行失败时省略 `gitBranch`。
2. **假设**会话文件带 metadata header，**当**会话列表（当前目录 / 全项目 / worktree 模式）返回 `SessionMetadata` 时，**则** `createdAt` 取 header 中的真实创建时间，而非每次扫描生成当前时间。
3. **假设**会话文件带 `gitBranch`，**当** `wave -r` 选择器渲染会话行时，**则**行尾显示该分支标签（如 `[main]`）；无分支信息时不显示。
4. **假设**旧版会话文件（无 header 或 header 缺 `createdAt`），**当**列表返回时，**则** `createdAt` 回退为列表生成时的当前时间（legacy 文件无创建时间记录，行为不劣化）。
5. **假设**git 命令执行失败或超时（非 git 目录、git 不可用、目录被删除），**当**创建会话时，**则** header 省略 `gitBranch`，创建流程不中断、不报错。

***

### 用户故事：会话自定义标题（重命名）（优先级：P2）

作为用户，我希望给会话起一个自己的标题，而不是永远显示首条用户消息截断出的那 30 个字符，以便在会话列表里一眼认出某段对话。

**为什么是这个优先级**：标题目前完全不可改（全仓无重命名入口），想换个说法只能重开会话；而它是会话列表里唯一的辨识信息。属于体验增强而非阻塞性缺陷，故 P2。存储侧参照 Claude Code：标题作为**保留条目追加进会话自己的 JSONL 文件**（`custom-title`），而不是另建索引文件——这样标题随会话文件走，任何读该文件的宿主（CLI `wave -r`、GUI `/resume`、插件端头部）都能拿到，无需额外的同步通道。

**独立测试**：对一个已有会话重命名后，检查该会话 JSONL 末尾多出一条 `{"type":"custom-title","customTitle":"…"}` 保留条目，而 metadata header 与既有消息行逐字节不变；列表返回该会话的 `customTitle`；继续追加超过 64KB 的消息，分别验证「正常退出」「压缩」「重新恢复该会话」三条路径之后再次列表，标题仍为自定义标题而非首条消息；再对已有标题的会话执行 `/rewind`，验证重写后的文件末尾仍带 `custom-title` 条目。

**验收场景**：

1. **假设**用户对某会话设置自定义标题，**当**宿主持久化时，**则**必须向该会话自己的 JSONL 文件**追加**一条保留条目 `{"type":"custom-title","customTitle":"<标题>","sessionId":"<会话 ID>"}`——不得改写 metadata header（保持「创建时写入一次、永不重写」的既有契约）、不得改动任何既有消息行、不得为此新建旁路索引文件。

2. **假设**会话文件带 `custom-title` 条目，**当** `listSessions` / `listAllSessions`（当前目录 / 全项目 / worktree 三种模式）返回 `SessionMetadata` 时，**则**必须带上 `customTitle` 字段。

3. **假设**会话既有自定义标题又有首条用户消息，**当**渲染会话标签时，**则**显示优先级必须为「自定义标题 > 首条用户消息截断 30 字符 > `新对话`」；只有自定义标题缺失时才回退到截断规则。

4. **假设**会话文件在末尾带 `custom-title` 保留条目，**当**加载该会话（恢复 / `/resume` / 全量线程读取）时，**则**该条目不得作为消息出现在对话流中，也不得参与 `lastActiveAt`（末条消息时间）、`latestTotalTokens`（尾窗 token 扫描）与首条消息提取——读取侧的每个入口都必须显式跳过全部保留条目类型（`metadata`、`custom-title`）。

5. **假设**会话已重命名，**当**出现下列任一「会话收尾 / 会话接续」时机时，**则**若该会话存在自定义标题，必须把它重新追加到文件末尾（重追加前必须先做一次尾窗读刷新，吸收其它进程——CLI 选择器、SDK 宿主——刚写入的更新值，再落内存持有值）：

   1. **正常退出**：`Agent.destroy()` 收尾时，且必须在 `saveSession()` 之后独立成步（`saveSession()` 无新消息会提前 return，挂在其内部会被跳过）。对应 Claude Code 的 `registerCleanup`（`sessionStorage.ts:458`）。
   2. **压缩结束**：`compactMessagesAndUpdateSession` 追加完压缩块之后。一个挂点同时覆盖手动 `/compact` 与请求前自动压缩两条入口（二者共用 `aiManager.compactConversation`）。对应 Claude Code 的 `compact.ts:711` 与 `:1057` 两处。
   3. **接管恢复**：`restoreSession` 成功加载目标会话之后。对应 Claude Code 的 `adoptResumedSessionFile`（`sessionStorage.ts:1533`）——让标题在恢复的当下就回到 EOF，而不是等这次会话再退出；没有这一步，「恢复一个标题已滑出尾窗的旧会话」在列表里仍看不到标题。
   4. **切换会话（切走）**：`restoreSession` 保存并结束当前会话的那一步（`interactionService.ts` 发 `SessionEnd("resume")` 的同点）——语义等同该会话的收尾。
   5. **`/rewind` 之后**：`rewriteSessionFile` 是整文件重写、只序列化消息，会**物理丢掉**末尾的 `custom-title` 保留条目，重写后必须补回标题。wave 独有——Claude Code 的 rewind 是 checkpoint 式、不重写转录文件。（同一处还会丢掉 metadata header，那是与标题无关的既有缺陷，本轮只补标题，header 的丢失另行跟踪，见边界情况。）

   不做「首次物化」时机（Claude Code 的 `sessionStorage.ts:983`）：CC 在该点补写的是 `mode` / `agentSetting` 这类「启动期已确定、文件尚未创建」的字段，而 wave 的自定义标题在 `renameSession` 时就立即写盘，不存在「内存有、盘上无」的窗口；将来新增启动期字段时再补。也不做 `/clear`：它生成新的会话 ID，标题属于旧会话、不应继承。

   函数命名对齐 Claude Code 的 `reAppendSessionMetadata`；字段本轮只含 `custom-title`，形状按「进程持有的保留条目集合」预留扩展，不提前引入无生产者的字段。改名的即时可见由「改名即追加到当时 EOF」保证；活会话期间标题被消息推出尾窗的窗口期是刻意取舍，见边界情况。

6. **假设**会话尚无 transcript 文件（仅有 meta 消息、尚未物化），**当**用户重命名时，**则**必须先物化该会话文件再写入标题（重命名是用户显式动作，不受「meta-only 会话不落盘」约束）；后续消息写入同一文件。

7. **假设**用户提交的标题 trim 后为空，**当**保存时，**则**必须什么都不做——不发请求、不写条目、不改变已有标题（空串不是「清除标题」的信号）。

8. **假设**写盘失败（权限不足、磁盘错误、远端不可达），**当**重命名结束时，**则**必须让调用方感知失败，以便界面把已乐观上屏的新标题回滚为原标题——不得静默留在「看起来成功了」的状态。

9. **假设**旧会话文件不含 `custom-title` 条目，**当**列出与渲染时，**则**行为必须与现状完全一致（回退首条用户消息截断），不报错、不补写。

10. **假设**会话文件不存在或中段损坏，**当**重命名或列出时，**则**沿用既有容错口径（缺失视为不存在、损坏跳过），不得因自定义标题引入新的崩溃路径。

***

### 用户故事：会话重命名入口：CLI 与插件端（优先级：P2）

作为 CLI / 插件端用户，我希望就地给当前会话或列表里看中的那条会话改标题，以便不必重开会话就能把某段对话标成自己认得出的名字。

**为什么是这个优先级**：与桌面端侧边栏同时做（三端共用同一份存储与显示优先级）。CLI 侧照 Claude Code 的两条腿：`/rename` 命令 + 选择器内就地改名（`Ctrl+R`）；插件端入口只能是头部标题本身（面板没有会话列表 UI，唯一的标题展示点就是头部）。桌面端的侧边栏行菜单见 [desktop/desktop-sessions.md](../desktop/desktop-sessions.md)。

**独立测试**：CLI 内输入 `/rename 我的标题` 后检查会话文件末尾多出 `custom-title` 条目、头部与 `wave -r` 列表都显示新标题；在 `wave -r` 选择器里对某条会话按 `Ctrl+R` 改名后按 Enter，验证该行立即显示新标题；在插件端点击头部标题改成新名字，验证标题即时变化、重启 IDE 后仍是新标题。

**验收场景**：

1. **假设**用户在 CLI 输入 `/rename <标题>`，**当**命令执行时，**则**当前会话的自定义标题被写为 `<标题>`（trim 后），并给出成功提示；命令不得要求先退出进程、不得改变会话 ID 或消息历史。
2. **假设**用户只输入 `/rename`（不带参数），**当**命令执行时，**则**给出一条用法提示（如 `Usage: /rename <title>`）并保持原标题不变——本轮**不做**「无参数则用模型自动生成标题」（与 [desktop/desktop-sessions.md](../desktop/desktop-sessions.md) 非目标「AI 自动生成标题」一致）。
3. **假设**当前会话正在生成回复（加载中），**当**用户输入 `/rename <标题>`，**则**命令照常执行（改名与生成互不影响），不中止当前回合。
4. **假设** `wave -r` 或会话内 `/resume` 的选择器已打开，**当**用户对聚焦的那条会话按 `Ctrl+R`，**则**该行的预览区变为标题输入框（初值为该会话当前标题，见 [desktop/desktop-sessions.md](../desktop/desktop-sessions.md) 的显示优先级），列表本身不关闭、不丢失范围与选中位置。
5. **假设**用户正在选择器内改名，**当**按 Enter，**则**保存新标题并退出编辑（回到普通列表模式，该行立即显示新标题）；**当**按 Esc，**则**退出编辑并回滚为原标题（不提交、不关闭选择器）。
6. **假设**选择器内提交的标题 trim 后为空，**当**按 Enter 时，**则**什么都不做（不发请求、标题不变、退出编辑）；重命名失败（文件缺失/不可写/远端不可达）时给出可见失败提示、标题保持原样。
7. **假设**选择器列出的是**非当前会话**（其它目录/其它 worktree/其它项目的会话），**当**用户对它改名，**则**写入该会话自己的会话文件（按其所属项目目录定位），并同步更新该行的显示——不得因为「不是当前会话」而拒绝，也不得误写到当前会话的文件。
8. **假设**插件端（VSCE / JB）聊天面板头部显示会话标题，**当**用户点击标题，**则**标题就地变为输入框（自动聚焦、内容为当前标题并**全选**），不弹出模态对话框、不改变面板布局、不清空对话。
9. **假设**用户正在头部行内编辑，**当**按 Enter 或点击输入框之外（blur），**则**保存新标题；**当**按 Esc，**则**回滚为原标题；**当**提交内容 trim 后为空，**则**什么都不做。行内形态**不提供 Save 按钮**，也不在保存期间禁用对话。
10. **假设**用户使用中文/日文等输入法，**当**处于组合输入中（`isComposing`），**则**不得把 Enter / Esc 当作保存 / 取消处理；**假设**焦点在标题输入框，**当**用户按键，**则**按键不得冒泡触发宿主快捷键。
11. **假设**标题保存成功，**当**插件端面板重载或 IDE 重启，**则**头部必须仍显示自定义标题（标题随会话文件持久化，不依赖界面内存状态）。
12. **假设**同一条会话先由 CLI 改名、再在插件端或桌面端打开，**当**任一宿主显示其标题时，**则**必须显示同一个自定义标题——三个宿主共用同一份存储（会话文件里的 `custom-title` 保留条目）与同一套显示优先级，不得各存一份。标题被消息推出尾窗的窗口期内允许列表侧暂时回退为首条消息（见边界情况的刻意取舍），该会话被恢复或其宿主正常退出后必须回到自定义标题。

***

### 边界情况

* **权限问题**：如果无法创建或写入会话目录，系统应优雅地失败并给出清晰的错误提示。
* **损坏的文件**：如果 JSONL 文件包含无效 JSON，在列出时应跳过，在加载时应视为不存在。
* **空会话**：没有消息的文件应使用文件的修改时间作为 `lastActiveAt` 时间戳。
* **元数据消息**：当会话文件尚未物化（无已保存消息）且所有未保存消息均为 `isMeta: true`（如 SessionStart hook 上下文）时，跳过持久化；meta 消息保留在内存中，随第一条真实 user/assistant 消息一并写入。
* **路径编码冲突**：使用哈希后缀确保长路径即使被截断也具有唯一性。
* **会话标题为空**：插件端会话首条消息缺失或为空时，标题回退为"新对话"。
* **流式期间新建对话**：插件端在 AI 响应流式输出期间禁用"新建对话"按钮，防止中断中的会话被意外清空。
* **后台任务运行期间新建对话/加载历史**：插件端（VSCE/JB）在当前会话存在正在运行的后台任务（类型 `shell`/`subagent`/`workflow`、状态 `running`，消息实时推送至 webview）时，与流式输出期间同等对待——"新建对话"按钮禁用、历史会话选择被忽略，防止原地清空/替换会话使正在运行的后台终端工具、后台子代理或后台工作流被意外中断或与 UI 脱离；任务结束后守卫自动解除。桌面端为多会话并行模型（"新建对话"创建独立会话、切换会话不中止后台会话），不适用本守卫。**远控 Web 页同样不适用**：它复用这份 ChatApp，但顶栏「新建对话」在页面上**重绑**为"在远端机器上另起一条会话"（`initialize`，见 [remote-web.md](../remote/remote-web.md)「Web 端也能发起新会话」故事），「会话」切换也只改手机自己那份视图——两者都不触动电脑侧正在跑的那条会话，那三种忙闲状态在页面上因此都不构成禁用或忽略的理由（完整口径与依据见该故事场景 10）。
* **会话 ID 重复**：同一 sessionId 出现在多个项目目录时，聚合列表按最后活跃时间取最新（见"跨目录会话聚合列表"故事）。
* **非 git 目录**：`git worktree list` 失败时按"无 worktree"处理，`Ctrl+W` 不生效。
* **长路径解码兜底**：项目目录名被哈希截断时，全项目模式展示编码名兜底；恢复仍按目录内文件定位，不受展示影响。
* **剪贴板不可用**：跨项目提示的 `cd` 命令复制失败时仍显示命令文本，用户可手动复制。
* **worktree 目录已删除**：选中的会话属于已删除的 worktree 时按"其他项目"处理，给出 `cd` 命令提示而非报错。
* **选择器语义单一来源**：同一份会话在 `/resume` 与 `wave -r` 两处入口必须落在同一个恢复类别（原地 / 同仓库 worktree / 跨项目），范围切换后的列表内容与快捷键提示也必须一致；不允许两处各自实现导致行为漂移。
* **会话内切换的落盘目录**：切换会话必须使会话 ID 与落盘项目目录同步变化。只换 ID 不换目录会把目标会话的后续消息写进当前目录，造成同一 sessionId 在两个项目目录各有一份文件。
* **连续切换**：会话内 `/resume` 可重复执行，含 worktree ↔ 原目录、worktree ↔ 另一 worktree；不得因为"当前已处于上一次切换后的目录"而拒绝再次切换。
* **切换后的目录派生缓存**：项目记忆、规则与目录相关的系统提示段在切换后必须失效重建。沿用旧目录的缓存会让模型读到另一个项目的上下文，且不会有任何报错。
* **worktree 会话状态不随会话恢复**：会话记录只持久化创建时的工作目录，未持久化 worktree 会话状态（分支、是否由 hook 创建等）。因此切换后只对齐工作目录，worktree 会话状态不恢复——这是刻意取舍，不是缺陷。
* **重托管的工作目录取自会话记录，不取自客户端 cwd**：协议层的 `initialize { restoreSessionId }` 是「把磁盘上的会话挂到本进程」的入口，它带的 `workdir` 是**客户端**的目录（daemon 重启后 `status`/`send` 重托管时就是客户端恰好在哪）。因此重托管必须在建 agent 之前读回转录首行的 `workdir` 并用它——否则会话会在客户端目录里执行：`create --worktree` 的会话掉回宿主仓库（隔离静默失效、改动写错位置），转录也会被追加到客户端目录的项目目录下造成同一 sessionId 两份文件。判定只有一条：**有记录且目录还在 → 用记录；记录目录已不在 → 退回客户端 cwd 并记 warn**。显式指定目录的进程内入口（`/resume` 选择器传入的 `workdir`、`wave --restore`）不走这条推导，保持各自既有语义。
* **读文件缓存不随会话恢复**：切换后不重建"本会话已读文件"的缓存（既有 gap，本轮不做），模型可能重复读取已读文件。
* **无消息的当前会话**：切换前若当前会话尚无任何消息，不因切换时的自动保存而产生空的会话文件。
* **metadata header 向后兼容**：`createSession` 不传 metadata 时仍写空文件（旧调用方不变）；读取侧缺 header 的 legacy 文件走 decodeSync/当前时间回退。
* **metadata header 只读**：header 是 append-only 文件的首行，创建后不重写；`gitBranch` 为创建时刻快照，不随后续分支切换更新。唯一会整文件重写转录的是 `/rewind`：它不修改 header 内容，但必须原样保留首行（连同 `custom-title` 条目），见 `ui/rewind-command.md`。
* **gitBranch 为 per-session 数据**：同一 workdir 不同时间创建的会话可能在不同分支，`gitBranch` 按会话文件逐一读取，不做目录级缓存。
* **GUI 列表单一来源**：GUI 的 `/resume`（以及插件端「历史对话」）与 CLI 的 `wave -r` 必须读同一份磁盘会话来源——不允许 GUI 依赖界面自己的会话索引，否则 CLI 或其它客户端创建的会话不可见（桌面端现状即为此问题）。"同一来源"不等于"同一范围"：范围为各宿主的既有语义（插件端＝工作区目录、桌面端＝全部项目与主机、CLI＝当前目录及其 `Ctrl+W`/`Ctrl+A` 展开）。
* **插件端范围不扩大**：插件宿主无法切换工作区，因此 `/resume` 的列表范围必须与头部「历史对话」按钮一致——不得因"统一为全部项目"而列出工作区之外的会话（跨项目恢复属于桌面端与 CLI 的能力）。
* **桌面端侧边栏索引语义不变**：`/resume` 的磁盘来源只服务选择器本身，不改动侧边栏会话树与状态看板沿用会话索引的既有语义（被 `/resume` 恢复的索引外会话登记进索引，见 `desktop/desktop-sessions.md`）；两条来源并存是刻意设计，不是待清理的重复。
* **选择器形态按宿主既有语言**：插件端沿用面板头部下拉弹窗（与"历史对话"按钮同一组件、同一位置）；桌面端用居中模态对话框，且必须渲染在应用根层——桌面端分屏时根 ChatApp 不挂载 `chatContainer`，挂在其中的浮层在分屏下会打不开（既有教训）。
* **桌面端会话范围随当前主机**：桌面端 `/resume` 只列"当前对话所属主机"磁盘上的会话（本地对话列本地、远程对话列那台主机），与登录态、最近目录、会话索引的既有"当前主机"语义一致；不做跨主机聚合。切换主机/焦点分屏后再次打开选择器即为该主机的列表。
* **桌面端主机的两种失败要区分**：当前主机不可达时给出可见失败提示（不得静默显示空列表，否则用户会误以为没有历史会话）；"目录确实不存在"（`ssh test -d` 判定）才走目录缺失提示，探测失败不得据此判定（见 `desktop/desktop-remote.md` 的「远端主机不可达时不得删除会话」）。
* **不做会话存活检测（本轮不做）**：会话在 wave 中没有锁，同一段会话可能正被终端或另一个进程使用；GUI 选择器不检测「该会话仍在别处打开」，也不因此拒绝切换，多点同时追加写入的风险留待独立议题（`/resume` 只负责切换）。
* **不做外部会话的信任确认（本轮不做）**：从磁盘发现的会话不按"外部导入"要求二次确认，选中即切换。
* **GUI 选择器不提供 fork**：选中即继续该会话本身，不复制为新会话（`--fork-session` 类语义不在本轮）。
* **保留条目是 SDK 私有契约，读取侧必须逐个入口跳过**：会话 JSONL 里除「带 `timestamp` 的真实消息」之外的行都是保留条目。全量读取、末条消息、尾窗 token 扫描、首条消息提取每个入口都必须跳过全部保留类型——漏一处就会把条目当成消息（`lastActiveAt` 变成 Invalid Date、列表排序错乱，且不会有任何报错）。
* **重追加是稀疏触发，活会话窗口期内列表可回退（刻意取舍）**：自定义标题落到文件末尾的时机只有五个——①改名时（追加到当时 EOF）；②正常退出（`Agent.destroy()`）；③压缩结束；④接管恢复（`restoreSession` 加载完成后）；⑤`/rewind` 重写后（完整清单与理由见验收场景 5，对齐 Claude Code 的 `reAppendSessionMetadata`）。其余时间继续追加消息会把标题行逐渐推出 64KB 尾窗（一轮超大工具输出即可直接推出）：窗口期内**其它**宿主（另一终端 `wave -r`、GUI `/resume`）列出该会话会回退为「首条用户消息截断」，持有该会话的宿主不受影响（标题在内存）；进程被硬杀（未走退出收尾）同样落在这个窗口里。这是对齐 Claude Code 稀疏触发的刻意取舍，换取每次保存不再多付一次 64KB 尾窗读 + 一行标题追加；该会话下次正常退出 / 压缩 / 被恢复后重新追加，标题回到 EOF 自愈。
* **不做列表侧 head 兜底（刻意取舍）**：不照抄 Claude Code 的 head 64KB 扫描（`listSessionsImpl.ts:97-103` 的 `tail || head` 双窗）——head 兜底只对「改名发生在文件尚小于 64KB 时」的会话有效，却要给列表里每个长会话文件多一次头部读。代价：「早期改名 + 标题已滑出尾窗 + 尚未自愈」的会话在列表里也回退首条消息（Claude Code 靠 head 兜底能救这一部分）。
* **恢复会话必须读回标题并在当场追加回末尾（自愈的来源）**：恢复路径整读会话文件时必须收集 `custom-title` 作为内存值，并在加载完成后立即重追加到 EOF（验收场景 5 的第 3 项，对齐 Claude Code 的 `adoptResumedSessionFile`）——否则标题一旦滑出尾窗，退出时既没有内存值、尾窗也读不到，标题将永久退回首条消息；而且在本次会话退出之前，其它宿主在列表里也一直看不到标题。有这一环，任何「滑出」都只是窗口期而非数据丢失（文件里的条目 append-only 永不删；`/rewind` 是唯一物理删除内容的路径，删的是被回退的消息、保留条目原样留下，它仍按验收场景 5 第 5 项把标题重追加到 EOF，见 `ui/rewind-command.md`）。
* **`/rewind` 重写保留文件级保留条目（含 metadata header）**：重写不重建文件，而是逐行筛选原文件（见 `ui/rewind-command.md`），首行的 `{"type":"metadata",...}` 与既有 `custom-title` 原样保留、按 `RESERVED_ENTRY_TYPES` 注册表搬运，故重写后 `readMetadata()` 仍返回创建时的 `workdir` / `createdAt` / `gitBranch`、`readCustomTitle()` 仍读得到；标题另按验收场景 5 第 5 项重追加到 EOF，保证列表在其尾窗里看得到。
* **重命名不改变会话身份**：会话 ID、文件路径、创建时间、列表排序（按最后活跃时间）与 worktree 归属均不受重命名影响。
* **标题长度上限 200 字符**：自定义标题在写入边界（UI 输入 + 宿主校验）截断/拒绝到 200 字符，与同仓其它对象（项目/头部会话名 200）一致——不照抄 Claude Code 的「会话标题不设限」。
* **不做 AI 自动标题（本轮不做）**：不引入 `ai-title` 保留条目、不额外调用模型；没有自定义标题时继续用首条用户消息截断兜底。因此 CLI `/rename` 不带参数时不生成名字，只提示用法。
* **不做全局快捷键**：不照抄 Claude Code 的 `⌘/Ctrl+⌥+R`——CLI 只认选择器内 `Ctrl+R`（与既有 `Ctrl+A`/`Ctrl+W` 同族），GUI 只认鼠标入口。
* **插件端历史弹窗列表不提供改名入口**：插件端的改名入口只有聊天面板头部的标题本身（面板没有常驻会话列表 UI）；桌面端的入口是侧边栏会话行菜单 + 头部标题。`/resume` 与「历史对话」列表内的改名只在 CLI 侧提供。
* **改名不引入派生能力**：不做按标题搜索排序、批量改名、标题历史/撤销。
