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

7.2 KiB
Raw Blame History

标题历史定位时间线:发现记录

用户意图

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