74 lines
7.2 KiB
Markdown
74 lines
7.2 KiB
Markdown
# 标题历史定位时间线:发现记录
|
||
|
||
## 用户意图
|
||
|
||
- “定位”弹层当前只列可跳转的消息步骤。
|
||
- 左侧会话标题由 LLM 通过标题 MCP 修改;每次实际修改都要保留历史。
|
||
- 定位弹层需要插入标题变更节点,用于标识主题变化发生的位置。
|
||
- 标题节点只读、不能选中或触发滚动定位,但需要在视觉上明显区别于消息项。
|
||
- 定位弹层还要按消息时间显示日期分隔节点,参考横线中间承载日期的气泡样式。
|
||
- 日期仅显示到天(`YYYY-MM-DD`),不显示截图示例中的时分秒;日期节点同样不可点击、不编号。
|
||
- 计划审查确认日期时区必须明确:采用浏览器当前用户本地时区,与消息时间的前端展示语义保持一致。
|
||
- 日期视觉明确为两侧横线、中间承载 `YYYY-MM-DD` 的气泡;测试覆盖本地 23:59/00:01 边界。
|
||
|
||
## 工作区与流程约束
|
||
|
||
- 当前 Trellis 指向另一个仍在进行的主题任务,不能擅自切换或覆盖。
|
||
- 根目录已有旧的 `task_plan.md`、`findings.md`、`progress.md`,因此本任务使用 `.planning/title-history-locator/` 隔离。
|
||
- 根目录已有他人未跟踪的 `修复会话切换表单不渲染 TO DO list.csv`,必须保留。
|
||
- 项目约定:代码理解先用 `codebase-memory-mcp`,`rg/sed` 仅做行号与文本校验;禁止使用 graphify。
|
||
- `codebase-memory-mcp` 的 `home-cc-web` 索引状态为 `ready`(4093 nodes / 8699 edges)。
|
||
- Trellis 前后端规范入口及相关组件/数据库规范仍是占位模板,没有额外的项目专属实现约束。
|
||
|
||
## 待确认的技术事实
|
||
|
||
- `ccweb_set_title` 的后端实现、标题锁定与会话持久化结构。
|
||
- “定位”按钮和弹层的前端构建函数、消息序号、消息时间和点击逻辑。
|
||
- 会话详情接口是否已有适合承载时间线元数据的字段。
|
||
- 现有回归脚本中标题 MCP 与定位列表相关 target。
|
||
|
||
## 已定位的实现链路
|
||
|
||
### 标题 MCP
|
||
|
||
- 入口是 `server.js` 的 `callInternalMcpTool()`,`ccweb_set_title` 路由到 `setCurrentConversationTitle()`。
|
||
- `setCurrentConversationTitle()` 会加载会话、拒绝空标题、尊重 `titleSource === 'manual'` 锁定,并在实际处理后保存 `session.title/titleSource`、发送 `session_renamed`、广播列表。
|
||
- 当前成功路径即使标题未变化也会保存并广播;历史事件必须只在 `changed === true` 时追加,避免重复标题伪事件。
|
||
- 手动 UI 改名走 `handleRenameSession()`,写入 `titleSource = 'manual'`;本需求只要求记录 LLM 调用实际生效的标题,因此不把手动改名写入该事件历史。
|
||
- 现有集成回归已经覆盖 MCP 成功、空标题、手动锁定和 `updated` 排序时间不变,适合直接扩展标题历史断言。
|
||
|
||
### 定位弹层
|
||
|
||
- `public/app.js` 的 `buildUserOutlineItems()` 当前从已渲染的 `.msg.user[data-message-id]` DOM 构建纯用户消息数组,通过 `userMessageIndex` 取完整内容。
|
||
- `updateUserOutlinePanel()` 当前把所有项渲染成可点击 `<button class="user-outline-item">`,并以数组位置生成 1-based 编号。
|
||
- 新实现需要把数据模型扩展为 `message/title/date` 三种 item;只有 `message` 渲染成按钮并计数,`title/date` 使用非交互 DOM。
|
||
- 标题事件需要带能与对话顺序合并的锚点;仅有时间戳在同毫秒或历史回放时不够稳,宜在后端记录 `messageIndex`(标题调用发生时已有消息数量)并以时间戳作为辅助信息。
|
||
- 日期可直接来自用户消息索引中的原始 `timestamp`,按浏览器本地年月日格式化;不需要后端新增日期字段。
|
||
|
||
### 会话传输与视觉复用
|
||
|
||
- 会话由 `normalizeSession()` 统一归一化、`sanitizeSessionForPersist()` 自动保留受限的顶层字段;新增标题历史需要在 `normalizeSession()` 做类型/长度清洗,避免旧文件或异常数据直接进入前端。
|
||
- `handleLoadSession()` 的 `session_info` 当前显式列出返回字段,因此必须显式附带标题历史;前端 `normalizeSessionSnapshot()` 也要克隆该字段,才能进入 session cache。
|
||
- 实时 `session_renamed` 事件目前只同步当前标题元数据;应同时携带本次标题事件,前端追加到当前/缓存 snapshot 并立即刷新已打开的定位弹层。
|
||
- 每个消息 DOM 已由 `markSessionMessageElement()` 标注全局 session message index,可用于标题事件与用户消息的稳定顺序合并;分批加载旧历史后 `updateUserOutlinePanel()` 会重建列表。
|
||
- 项目已有 `.agent-message-divider` 的“两侧横线 + 中间等宽时间”视觉,日期项可复用其构图语言,但使用定位弹层专属类,且不受“隐藏代理分隔时间”偏好影响。
|
||
- 点击委托只匹配 `.user-outline-item`,因此标题/日期节点只要不使用该 class 和 `<button>`,天然不会触发定位;仍需回归锁定这一契约。
|
||
- 现有 `assertSetTitleMcpContract()` 适合增加静态/纯函数契约,集成主流程可断言标题历史只在 changed 时追加、锁定时不追加、session_info 可恢复。
|
||
|
||
### 并行后端审计结论与取舍
|
||
|
||
- 独立审计确认顶层 `titleHistory` 是最小、兼容性最好的持久化位置;不应塞进 `messages`,也不应改造 `titleSource` 的手动锁语义。
|
||
- 通用持久化 sanitizer 会保留顶层数组,但可能按通用上限从头截断,因此历史 helper 必须主动只保留最近 100 条。
|
||
- 审计建议扩展记录 manual/system/derived、unchanged 和 ignored 尝试;本任务不采纳。用户要看的是“标题在哪一步变了”,因此只写入 `ccweb_set_title` 且 `changed === true` 的实际 LLM 标题变化,避免假主题节点与范围膨胀。
|
||
- MCP 标题历史写入不得修改 `session.updated`,继续保持现有“改标题不改变会话排序”的契约。
|
||
|
||
## 2026-07-29:标题前置展示语义
|
||
|
||
- 用户确认定位列表是“对话大纲”而不是严格事件审计流;标题应作为章节标题出现在触发该主题的用户消息上方。
|
||
- 当前后端事件把 `messageIndex` 写成调用 `ccweb_set_title` 时的 `session.messages.length`;正常一轮中最近用户消息索引为 `length - 1`。
|
||
- 当前前端把消息放在 `messageIndex * 2`,把标题放在 `event.messageIndex * 2 - 1`,所以标题自然落在最近用户消息之后。
|
||
- 不应直接改变既有 `messageIndex` 的含义,否则旧历史和新历史无法区分;新事件应额外保存零基整数 `anchorMessageIndex`,指向调用时 `session.messages` 中最近的用户消息,且必须小于事件 `messageIndex`。
|
||
- 展示层优先按精确 `anchorMessageIndex` 归属;锚点消息不可见或旧事件没有锚点时,从定位列表中选择 `event.messageIndex` 之前最近的用户消息作为兼容锚点;仍无候选才使用事件自身位置。
|
||
- 标题的日期分组也应继承锚点消息时间,避免跨午夜时标题与所属消息被拆到两个日期区段。
|
||
- 现有 `session_renamed.titleEvent`、`session_info.titleHistory` 和前端 snapshot 都会经过标题历史归一化;只要后端与前端 normalizer 显式保留新字段,实时与重载链路即可共用同一排序逻辑,但必须由回归断言锁定。
|