Files
cc-web/.planning/title-history-locator/findings.md

74 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 标题历史定位时间线:发现记录
## 用户意图
- “定位”弹层当前只列可跳转的消息步骤。
- 左侧会话标题由 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 显式保留新字段,实时与重载链路即可共用同一排序逻辑,但必须由回归断言锁定。