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

74 lines
6.6 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.

# 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 + 完整目标` 被渲染为居中、接近消息区宽度的大块磨砂面板。
- 长正文包含多行约束与反引号,居中排版和纯文本渲染明显降低可读性。
- 目标视觉应为右侧普通用户气泡;状态反馈应保持短小,不重复正文。