Files
cc-web/.planning/2026-08-16-goal-bubble-rendering/findings.md
2026-08-16 22:20:00 +08:00

6.6 KiB
Raw Blame History

Findings & Decisions: Goal 气泡显示修复

Requirements

  • /goal <目标> 原文应显示为正常用户气泡,而不是居中大系统面板。
  • “正在同步 Goal”只承担进度反馈,不应长期占据消息区。
  • 成功反馈不重复回显完整目标正文。
  • 修复后刷新会话仍能看到 Goal 用户气泡。
  • 不改变 /goal 的 app-server RPC、后台 Goal 执行及其他 Slash 命令语义。

Research Findings

  • home-cc-web 代码索引状态为 ready,当前包含 6208 个节点、14249 条边。
  • sendMessage() 明确跳过所有已知 Slash 命令的用户气泡。
  • handleCodexAppGoalSlashCommand() 将同步与结果统一发送为 system_message。
  • createMsgElement('system') 使用 textContent,不会渲染 Markdown。
  • 通用系统消息 CSS 使用居中、最大宽度 90%、white-space: pre-line,长 Goal 会成为大面板。
  • 暗金荒野主题把 .msg.system 与助手气泡共享磨砂规则,进一步强化面板感。
  • 现有回归明确断言 Goal active 是 system_message,需要随新契约更新。
  • 用户气泡入口是 submitUserMessage();服务端普通消息入口是 handleMessage(),需在这两个边界间设计只用于展示的 Goal 持久化,避免再次调用普通模型链路。
  • submitUserMessage() 会乐观插入用户气泡、增加当前消息计数并启动生成状态,但发送时没有携带前端生成的 messageId。
  • handleMessage() 的普通路径会先把用户消息推入 session.messages 并 saveSession(),随后才进入 Codex App/Claude/Codex 运行时;因此不能直接复用整个普通路径来保存 Goal,否则会把 /goal 再发送给模型。
  • 会话持久化清洗会保留消息对象的额外元数据字段,可为 Goal 展示消息增加明确标记而不改底层序列化框架。
  • WebSocket 在进入 handleMessage() 前先调用 handleSlashCommand();已知 /goal 因此完全绕过普通消息持久化。
  • 前端已经支持服务端 session_message 事件,可把服务端持久化的 Goal 展示消息追加到当前会话与缓存。
  • 代码中已有 ccwebDisplayOnly: true 的持久消息先例,适合标识不应被当作真实模型输入的 UI 展示记录。
  • 目标业务文件 public/app.js、server.js、public/style.css、scripts/regression.js 在本任务开始时没有未提交差异。
  • session_message 会把服务端消息同时追加到会话缓存和当前 DOM,并正常递增消息索引;因此 Goal 路径无需前端乐观插入,可由服务端持久化后立即推送,避免双气泡。
  • 前端将为设置型 /goal 额外发送稳定 clientMessageId,同时保留 Slash 草稿用 requestId;服务端以前者优先作为展示消息 ID。
  • Goal 展示消息正文只保存 command.objective,不重复展示 /goal 前缀、Goal active 标头或状态统计。
  • Slash 草稿在收到首个 system_message 时就会被移除;当前有效 /goal 的“正在同步”已清掉草稿,因此后续 RPC 错误实际上无法再恢复原输入。新契约将把校验失败保留在持久化前,RPC 启动后的失败明确保留气泡且不恢复草稿。
  • 现有集成回归只断言 /goal 不作为普通 /goal... 文本持久化;可替换为“目标正文作为 ccwebDisplayOnly Goal 用户消息持久化,同时后台 Goal 输出仍正常”。
  • 回归入口是 npm run regression,没有现成 Goal 专项参数;执行时需用 60 秒超时保护。
  • Mock app-server 的 thread/goal/set 会生成独立 Goal background output turn,可用“重复命令 ID 后没有第二个 Goal 背景输出”证明去重没有再次调用 Goal RPC。
  • 实现后服务端在 Goal RPC 前持久化并推送 session_message,前端沿用普通 buildMsgElement() 用户角色渲染;成功/同步系统提示均已改为 transient,成功文案不再包含 objective。
  • 同一 clientMessageId 再次提交会返回短暂“Goal 设置请求已处理”,不会追加消息或触发第二个背景 turn。
  • node --check public/app.js server.js scripts/regression.js 与 timeout 60s npm run regression 均通过。
  • PM2 ccweb 当前监听 8002,静态 app.js 已能返回新的 isGoalSetCommandText 与 clientMessageId;按仓库约束未重启,因为另有 /home/cc-web 运行会话。
  • 当前环境没有 playwright、puppeteer 或 Chromium 可执行文件,无法完成真实浏览器截图/多视口像素验收;需在有浏览器的环境补做。
  • 实施期间另一个 /home/cc-web 会话在 public/app.js 与 scripts/regression.js 写入了侧栏折叠改动;这些非 Goal 差异未回滚,当前回归在其共存状态下通过。

Technical Decisions

Decision Rationale
设置型 Goal 显示为用户气泡 这是用户提交的长文本,符合对话视觉语义
Goal 进度和成功仍走系统反馈,但设为 transient 保留操作反馈,不制造持久大块内容
持久化必须由服务端显式记录展示消息 仅前端乐观插入会在刷新后消失
控制型 /goal 命令不显示长用户气泡 pause/resume/clear/show 是控制操作,保持精简
使用独立 Goal 展示消息 helper 直接写入本地历史并推送 session_message,绝不复用会继续进入运行时的 handleMessage()
每条 Goal 展示消息带稳定 ID 与专用元数据 支持传输去重、刷新识别和负向回归断言
有效设置命令在 RPC 前持久化;RPC 失败保留气泡 提供即时可见记录;失败后不恢复已提交正文,避免重试产生重复气泡

Issues Encountered

Issue Resolution
根目录已有其他任务规划与 TODO 文件 使用独立 .planning/2026-08-16-goal-bubble-rendering/,不改 .planning/.active_plan
首轮计划未明确独立持久化、去重和失败边界 按审查意见补充 display-only Goal 消息、稳定命令 ID、禁止普通运行时与分阶段失败语义

Resources

  • public/app.js:Slash 发送、系统消息与用户气泡渲染
  • server.js:Goal RPC、会话消息持久化
  • public/style.css:系统消息与暗金荒野主题样式
  • scripts/regression.js:Codex App Goal 集成回归

Visual/Browser Findings

  • 用户截图中顶部“正在同步 Goal...”为尺寸正常的短系统提示。
  • 完成后 Goal active + 完整目标 被渲染为居中、接近消息区宽度的大块磨砂面板。
  • 长正文包含多行约束与反引号,居中排版和纯文本渲染明显降低可读性。
  • 目标视觉应为右侧普通用户气泡;状态反馈应保持短小,不重复正文。