Files
cc-web/.planning/2026-08-11-failed-insert-card-actions/findings.md

104 lines
8.9 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.

# 失败插入卡片重试与删除:发现记录
## 用户需求
- 用户希望“插入失败”的卡片增加“重试”和“删除”按钮。
- 截图中的失败项位于用户消息卡片内,包含图片预览、文件名、尺寸/状态信息和红色“插入失败”状态。
## 视觉观察
- 当前失败状态只有红点和“插入失败”文本,没有恢复或清理入口。
- 操作应靠近失败状态区域,保持卡片宽度和当前主题视觉,不应抢占消息正文。
## 工作区约束
- 当前分支为 `main`,已有另一任务留下的未提交改动。
- 已确认 `.trellis/.current-task` 指向 `08-11-task-board`,本任务不会覆盖该指针。
- 与本任务可能重叠的已修改文件包括 `public/app.js`、`public/index.html`、`scripts/regression.js` 和 `server.js`;后续必须做局部差异审查。
## 待确认的代码事实
- 失败状态的数据结构、稳定标识和持久化位置。
- 原始插入请求的入口、请求参数及重试时可复用的数据。
- 删除应落在草稿状态、消息附件状态还是服务端持久化层。
- 现有测试中与运行时图片插入相关的覆盖位置。
## 初步代码定位
- `codebase-memory-mcp` 项目 `home-cc-web` 索引状态为 `ready`(5737 节点、12312 边)。
- “插入失败”不是普通上传失败文案,而是 Codex App 运行中消息插入(steer)状态:前端入口命中 `codexAppSteerStatusLabel` 与相邻的 `setCodexAppSteerStatusElement`。
- 服务端入口命中 `handleCodexAppSteerMessage`:`turn/steer` 成功发送 `inserted`,失败发送 `failed`,且消息会先以 `persistedUserMessage` 写入会话。
- 下一步需要确认状态事件携带的稳定标识、前端如何找到对应用户消息,以及重试是否可以复用现有 `handleCodexAppSteerMessage` 消息形状。
## 已确认的数据与渲染契约
- 前端 `updateCodexAppSteerMessage(clientMessageId, status, message)` 使用 `userMessageIndex` 或消息元素的 `data-message-id` 精确定位卡片,稳定标识已经存在,无需按文件名匹配。
- `setCodexAppSteerStatusElement` 目前只切换 `pending / inserted / failed` 三种 CSS class,并把状态区域写成纯文本;操作按钮应由这里或紧邻的单一渲染函数统一维护,避免重复绑定。
- 服务端回归 `runCodexAppRuntimeImageSteerRegression` 已覆盖 `clientMessageId`、失败状态和成功插入;失败的“附件不可用”消息明确不会持久化,说明截图里的失败卡片至少有一类是客户端临时项。
- 现有成功分支会持久化一条带附件的用户消息,并最终发送 `inserted`;重试实现必须避免把失败项或成功项重复写入会话。
## 调用链与主要风险
- `handleServerMessage` 仅在 `codex_app_steer_status` 分支调用 `updateCodexAppSteerMessage`;当前状态事件已是最小可扩展协议面。
- `handleCodexAppSteerMessage` 在真正调用 `turn/steer` **之前** 把用户消息写入会话。若 `turn/steer` 异步失败,该持久化消息当前不会回滚,因此“重试”若直接重发原消息会造成重复持久化。
- 失败分为两类:附件解析/空消息等同步前置失败(未持久化)与准备/`turn/steer` 异步失败(可能已持久化)。删除和重试必须能区分或统一修正这两类,否则刷新后的行为会不一致。
- 下一步要确认本地消息对象是否保存 `clientMessageId`、服务端是否已有单条消息删除/替换协议,以及附件引用在失败后是否仍可用。
## 客户端入口补充
- `sendMessage` 为运行中 Codex App 插入创建本地 `messageId`,用该值注册 `userMessageIndex`,并作为 WebSocket 消息的 `clientMessageId` 发送;因此失败卡片已有天然的单项操作键。
- 当前前后端没有通用的“删除单条消息”协议;仅有排队消息、提示卡等其他局部删除实现,不能直接用于持久化的 steer 用户消息。
- 需要在服务端把 `clientMessageId` 与持久化消息关联起来,或在 steer 失败时回滚该消息,才能让删除和重试同时满足“不重复”和“刷新一致”。
## 可行实现方向
- 本地卡片由 `sendMessage` 创建,包含完整 `text` 与克隆后的 `attachments`;扩展 `userMessageIndex` 即可保存重试载荷,不需要从 DOM 反解析。
- `createMsgElement` 是失败状态和附件预览的共同装配点,`setCodexAppSteerStatusElement` 是状态区域的唯一更新点,适合统一创建/移除按钮并维护 `disabled` / `aria-busy`。
- 推荐服务端在真实 `turn/steer` 最终失败时按对象引用回滚刚持久化的用户消息。这样所有失败卡片都保持客户端临时态,删除无需新增持久化删除协议,重试也不会制造历史重复项。
- 重试仍需校验当前 Codex App turn 是否在运行;若运行已结束,应给出明确失败状态,不能让卡片永久停在 `pending`。
## 本地源码校验
- 当前样式集中在 `public/style.css` 的 `.codex-steer-*` 区域,失败卡片已有红色边框/背景和 11px 状态行;新操作可在同一区域追加,避免影响其他消息卡片。
- 项目没有 DOM 测试依赖,仅有 `node scripts/regression.js`;前端交互测试需要使用轻量假 DOM/纯函数抽取,或在现有回归中增加协议和静态契约断言。
- 本任务开始时的重叠文件基线:`public/app.js` 已有 +155 行、`server.js` +532/-7 行、`scripts/regression.js` +73/-11 行、`public/index.html` +14 行,均来自既有“任务看板”工作,后续差异必须按目标函数/样式区段核对。
- `public/style.css` 当前没有未提交改动,是本任务新增样式的低冲突落点。
## 测试与冲突边界
- 现有 `runCodexAppStaleRunningRegression` 已有一个稳定的 `expectedTurnId` 不匹配失败夹具,适合新增“失败后持久化消息已回滚”的服务端断言。
- 现有成功恢复分支已断言用户消息只持久化一次,可继续保护“重试/恢复不重复”。
- 既有未提交 diff 的 hunk 不覆盖 `setCodexAppSteerStatusElement`、`sendMessage` 或 `handleCodexAppSteerMessage` 主体;可以对这些区段做局部补丁,但最终仍需逐 hunk 复核。
- 仓库已有未跟踪的任务看板 MiniDOM 单测脚本,但它属于另一任务;本功能不能修改或依赖该未提交测试文件。若需要 DOM 单测,应建立独立、最小的测试入口。
## 失败夹具与规范结论
- mock app-server 的 mismatch 夹具会保留一个不同的活动 turn,因此每次 `turn/steer` 都稳定返回 `expectedTurnId does not match active turn`;可用于重复验证失败与回滚,但不能作为成功重试夹具。
- 前端 Trellis 规范除“静态资源版本握手”外大部分仍是模板占位,没有额外组件框架约束;本任务应遵循仓库现有原生 DOM + CSS 组织方式。
- 回归脚本已有按 target 执行的分支,可为本功能提供小范围测试入口,避免每次执行完整回归超过 60 秒。
## 资源
- 用户截图:`sessions/_attachments/8da11ed4-5989-4d30-a12a-f27522138799.png`
- Trellis 任务:`.trellis/tasks/08-11-failed-insert-card-actions/`
## 最终实现状态
- 前端新增 `codexAppSteerRecords`,保存失败项的原文本、附件、会话、模式和 agent;重试沿用同一 `clientMessageId`,并通过 `status + inFlight` 阻止重复发送。
- failed 状态统一渲染“重试”“删除”;pending/inserted 移除操作,卡片使用 `aria-busy`,状态行使用 `role=status` 与 `aria-live=polite`。
- 本地运行中插入不再提前增加 `currentSessionMessageCount`,只有收到 `inserted` 才提交消息索引;失败/删除不会制造普通路径的索引漂移。
- 服务端为临时 steer 用户消息写入稳定 `id`,真正的 `turn/steer` 最终失败时先按 id 从最新会话回滚,再发 failed 状态;stale recovery 成功路径保持一次持久化。
- 前端 MiniDOM 单测覆盖失败操作、双击保护、载荷复用、再次失败恢复、删除单项、断线/跨会话/turn 结束保护与成功索引提交。
## 验证结果
- `node scripts/failed-insert-card-unit.js`:通过。
- `node scripts/regression.js --target runtime-image-send`:通过。
- `node --check public/app.js`、`server.js`、`scripts/regression.js`:通过。
- `node scripts/regression.js`:因首次定向命令参数错误而实际执行,完整回归通过(约 46 秒)。
## 独立审查修正
- 检查代理发现 `options.emitUserMessage` 的程序化 `session_message` 虽会显示失败操作,但缺少原始重试载荷;现已由服务端附加临时 steer 元数据,前端据此建立同样的重试记录。
- 主线程进一步将这类 `session_message` 在结果未定时保持为临时项:不提前写入消息缓存、不提前占用 `currentSessionMessageCount`,收到 `inserted` 后才提交,避免失败删除后的缓存幽灵和索引漂移。