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