111 lines
14 KiB
Markdown
111 lines
14 KiB
Markdown
# 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 + 完整目标` 被渲染为居中、接近消息区宽度的大块磨砂面板。
|
||
- 长正文包含多行约束与反引号,居中排版和纯文本渲染明显降低可读性。
|
||
- 目标视觉应为右侧普通用户气泡;状态反馈应保持短小,不重复正文。
|
||
- 用户在真实桌面界面确认 Goal 已显示为普通用户气泡,但缺少明确的“目标模式”身份标签。
|
||
- `/goal` 由服务端 Slash/RPC 旁路处理,不会先进入模型回合,因此不能依赖模型主动调用 `ccweb_set_title`。
|
||
- 当前会话中直接调用 `ccweb_set_title` 成功,证明 MCP 本身可用;缺口位于 `/goal` 旁路没有标题联动,而不是标题 MCP 故障。
|
||
- 本轮开始时 `public/app.js`、`public/style.css` 与 `server.js` 没有未提交差异;工作树中的 Codex App 重试改动属于其他任务,必须避开。
|
||
- `.planning/.active_plan` 指向另一任务,本轮继续显式维护 `.planning/2026-08-16-goal-bubble-rendering/`,不改全局 active plan。
|
||
- 主题架构要求视觉修复只命中用户指出的消息层级;Goal 标签应使用专用 selector,不能扩大到普通用户气泡或其他卡片。
|
||
- CSS 接入需注意共享暗色 `:is()` 的 specificity,并在最终验收中核对真实 computed style;自动化结构断言不能替代浏览器视觉确认。
|
||
- 修改静态 CSS/JS 后需检查 cache bust 与服务实际加载版本;本次不新增主题资产,也不改 welcome 双路径。
|
||
- Trellis 当前没有 `.trellis/.current-task`,现有任务列表也没有 Goal 标记/标题修复任务;进入代码阶段前需建立任务资料并提供给实现与检查代理。
|
||
- Trellis 要求 Codex 的实现与质量检查由独立子代理执行;当前运行时没有名为 `trellis-implement`/`trellis-check` 的自定义角色,因此后续使用通用 worker,并在任务提示中显式要求读取 Trellis PRD、上下文清单和相关规范。
|
||
- `home-cc-web` 索引为 `ready`,当前 6444 个节点、14582 条边,可直接用于符号级定位。
|
||
- 精确命中 `createMsgElement()`、`buildMsgElement()`、`persistCodexAppGoalDisplayMessage()`、`handleCodexAppGoalSlashCommand()`、`isTitleLockedByUser()` 与 `setCurrentConversationTitle()`。
|
||
- `setCurrentConversationTitle()` 已具备标题历史、持久化、`session_renamed` 推送和 `broadcastSessionList()`,但它将来源写为 `llm`;Goal 自动派生标题需要复用相同广播契约并保留 `derived` 来源语义。
|
||
- 手动重命名路径会设置 `titleSource: 'manual'`,已有 `isTitleLockedByUser()` 可作为禁止 Goal 覆盖用户标题的第一道保护。
|
||
- `buildMsgElement()` 会把完整消息对象作为 `meta` 传给 `createMsgElement()`;因此在 `createMsgElement()` 读取 `meta.ccwebGoalCommand.action === 'set'`,即可让实时与历史 Goal 消息共用同一标签渲染,无需额外历史迁移。
|
||
- `persistCodexAppGoalDisplayMessage()` 当前在消息入栈后统一 `saveSession()`;最小写入方案是在同一保存前尝试派生默认标题,并把 `titleChanged` 返回给 Slash handler 负责发送 `session_renamed` 与列表广播。
|
||
- 普通消息自动标题只在标题严格等于 `New Chat`/`Untitled` 时生效,取用户正文前 60 字并把换行替为空格;Goal 应沿用同一默认标题判定和长度上限,同时压平连续空白,避免多行 objective 生成杂乱标题。
|
||
- `normalizeTitleHistory()` 只接受 `source: 'llm'`;现有普通消息的 `derived` 标题也不写 title history。因此 Goal 自动标题应保持 `titleSource: 'derived'` 并不伪造 LLM 标题历史事件,只推送 `session_renamed` 元数据和会话列表。
|
||
- Trellis 相关上下文应覆盖前端组件/质量规范、后端质量规范和跨层契约指南;本次改动同时涉及 DOM/CSS、WebSocket 事件、会话持久化与集成回归。
|
||
- 暗金荒野主题的真实 ID 是 `wasteland`,不是 `gilded-wasteland`;Goal 标签的主题适配必须限定 `html[data-theme='wasteland']`。
|
||
- `index.html` 的 `app.js` 使用动态 `__CC_WEB_FRONTEND_ASSET_VERSION__`,JS 无需手工版本号;`style.css` 仍使用固定 `20260805-usage-loading-inline`,新增标签样式后应推进样式 cache bust,并同步既有回归契约中的精确版本断言。
|
||
- 现有 Goal 集成回归集中在 `scripts/regression.js` 约 7135 行,已覆盖 display-only 元数据、稳定 request ID、重复请求和刷新持久化,适合在同一场景追加标题持久化/广播断言;前端 DOM 标签需要增加静态契约测试。
|
||
- Goal thread 的 MCP 注入链路已确认:`ensureCodexAppGoalThread()` 调用 `codexAppThreadParams()`,后者通过 `codexAppThreadConfig()` 和 `listRuntimeMcpServerConfigs()` 写入 `mcp_servers.ccweb`;内置配置携带当前 `sourceSessionId`。
|
||
- 用户怀疑“Goal 未注册 ccweb MCP”的根因不是注册缺失:`thread/goal/set` 是 app-server RPC,不是模型 turn/MCP 调用。工具可用性不会强制模型调用 `ccweb_set_title`,所以可靠标题仍应由服务端旁路直接派生。
|
||
- 第一轮独立计划审查发现:仅广播 `session_list` 只会刷新侧栏,当前聊天顶部标题依赖 `session_renamed`。计划已补成“先向 viewers 发送 `session_renamed`,再广播列表”,回归也必须分别断言。
|
||
- 第二轮独立计划复审已通过,无实施阻断项;建议将默认标题判定收敛为 helper,并把自动标题严格限定在 `/goal set` 专用路径。
|
||
- 现有 Goal 集成场景复用了已经发送过多条普通消息的 `codexAppSession`,其标题早已不是默认值;自动标题正向回归需要另建一个尚为 `New Chat` 的 Codex App 会话,不能在原场景上做错误断言。
|
||
- 当前 `wasteland` 用户气泡最大宽度为 `min(42%, 480px)`;标签必须是内容流内的紧凑元素,正文继续依赖现有 `word-break`/`pre-wrap`,不能增加固定宽度或绝对定位造成窄屏溢出。
|
||
- 当前目标文件中只有 `scripts/regression.js` 带并发 Codex App 重试差异;实现必须只在 Goal/缓存断言附近追加,保留约 2446 和 main target 注册处的现有重试代码。
|
||
- 本机可用 `/usr/bin/firefox`,虽未发现 Playwright/Puppeteer;可在服务重启条件满足后用 Firefox headless 做基础桌面/窄屏截图验收。
|
||
- 当前 `127.0.0.1:8002` 返回 200 且 `Cache-Control: no-store`;在线 HTML 仍引用 `style.css?v=20260805-usage-loading-inline` 与动态 app.js 版本,CSS cache bust 更新后需重新检查实际响应。
|
||
- 已完整读取主题验收清单;本次不新增主题/素材,因此资产、主题注册和 welcome 双路径条目不适用,但仍需执行消息 DOM 隔离、`wasteland` computed style、桌面/窄屏、缓存 URL、JS 回归与真实浏览器记录。
|
||
- `sendSessionEventToViewers()` 原样转发 payload,不自动补 `sessionId`;Goal 自动标题事件必须显式携带 `type`、`sessionId`、`title` 和 `publicTitleMetadata()`。
|
||
- `normalizeSession()` 已接受 `titleSource: 'derived'`;`VALID_TITLE_SOURCES` 包含 derived,因此 Goal 旁路可安全复用该来源而不修改清洗逻辑。
|
||
- `truncateTextValue()` 超长时会追加持久化截断标记,不适合标题;Goal 标题应使用独立的 60 个 Unicode code point 截取,避免标题中出现换行标记。
|
||
- 现有 mock 在 `thread/goal/set` 后会发 Goal 后台 turn;包含 `dynamic` 的普通 turn 会执行 `completeMcpToolTurn()`,其结果带 `hasCcwebMcpConfig`,可作为 Goal thread 注册 ccweb MCP 的集成证据。
|
||
- 现有 Codex App 回归会话在发送多条普通消息后才进入旧 Goal 场景;新增默认标题测试应在同一服务器内创建独立的 `codexapp-goal-title` 会话,避免旧会话标题已被普通消息派生。
|