这页沉淀 Buffin 桌面端「assistant 回复过程区折叠」的最终设计(2026-08-08,经 Deepseek V4 Flash 设计 + Claude Opus 5 独立评审交叉验证,代码证据抽查属实)。核心:无协议层 phase 时用「尾部反向扫」判定正文区,异常/中断路径不折叠,纯前端实现。实现此功能或讨论 session 折叠/分层呈现时先读这页,与 [[Codex App 过程输出与最终回答分层]] 对照。
[!abstract] 当前结论 折叠单位是单条 assistant 消息内部:尾部正文(narrative)始终展开,之前的过程区(reasoning / 中间文本 / 工具调用 / control-receipt)折叠为一行 ToolGroup 风格摘要。正文判定用尾部反向扫(不是「最后一个 text part」)。仅正常完成(status=completed)且开关开启时自动折叠;interrupted / failed / 待响应交互禁止折叠。纯前端,不改 @buffin/api / daemon / provider。
mapTurnStatus(apps/daemon/src/agent/providers/codex/protocol.ts:774-779)对 unknown/inProgress 一律判 failed,中断/失败 run 常以工具收尾 → 「最后一个 text part」是中间文本,语义反了handleControlCompleted(session-store.ts:451-482)无 open message 时新建只含 control-receipt 的 complete message → 正文区为空,整条被藏run.completed 后迟到事件仍被 reduce(session-runtime-store.ts:102-111 无 terminal gate)→ 新建 text part 使原正文塌进折叠区run.completed 的 status,类型为必填 4 值 union(apps/daemon/src/agent/types.ts:589-591;发出点 protocol.ts:283、translator.ts:358、scripted/runner.ts:106/215)status?: string(packages/api/src/session-events.ts:41),协议包不用改handleRunCompleted(state, _event, ...)(session-store.ts:199-219)形参 _event 从未解引用,所有 message 一律标 completeinterrupted 混合用户中断与进程崩溃(drizzle-repository.ts:114-120 把 crashed 和 interrupted 都映到 Interrupted,仅 reason 能区分)input.resolved(service.ts:1196-1199),前端 inputAction 永久停在 pending —— 待响应判定必须用 message 终态短路,而非仅 isRunning(deriveSessionDomain 已有同款先例 session-domain.ts:19-24)ToolRowBase 受控/非受控双支持、懒挂载 + 关闭动画保挂载(ToolRowBase.tsx:55-69),6 例权威测试useAutoOpenDisclosure(use-auto-open-disclosure.ts:8-10)只有 setOpen(true),只能自动开不能自动关,且 userToggledRef 是 per-mount(切 tab 丢 override)PartGroup 的 tool-group 变体没有 partIndex(SessionThread.tsx:669),而 text/reasoning 有(:654/:663)——按 partIndex 切分需先补齐findActiveNarrativePartIndex(thread-presentation.ts:47-60)是从末尾反扫找 narrative part 的现成先例过程区(可折叠) 正文区(始终展开)
reasoning / 中间text / → 尾部连续 narrative
tool-call × N / receipts
判定:从 parts 末尾反扫,遇非 text/reasoning 即停;若尾部第一个就不是 narrative,整个过程区不折叠(安全默认,覆盖 tool-call 结尾、零 text、late delta 三种失效形状)。与 findActiveNarrativePartIndex 同构,零新概念。
自动折叠 ⇔ message 终态 = completed ∧ 开关 on ∧ 无待响应交互 ∧ 尾部为 narrative
interrupted / failed → 不折叠,保留错误上下文(对齐 Codex App 的 isTurnCancelled → shouldAllowCollapse: false,见 [[Codex App 过程输出与最终回答分层]])isPendingInteraction,session-domain.ts:55-59)→ 不折叠;注意渲染侧 ?? 'pending' 兜底(SessionThread.tsx:289/316),undefined 必须同默认,否则待响应表单被折叠ToolRowBase(不包 ToolGroup),header 显示统计摘要(命令数 / 文件数 / 耗时,result 缺失按完成计,过滤 status !== 'completed')usePlaybackBuffer 的 catch-up 会在完成帧硬切(影响限于 run 刚结束一帧)autoCollapseTurns(默认 on):存 device-chrome-store(现成订阅层)或 prefs 补订阅层;关闭时追溯展开已自动折叠的区域| 维度 | Codex App([[Codex App 过程输出与最终回答分层]]) | Buffin 本方案 |
|---|---|---|
| 分层依据 | 协议层 agentMessage.phase = commentary | final_answer | 呈现层「尾部反向扫」位置判定 |
| 折叠时机 | final answer 首个 item 开始 | message 正常完成后 |
| 依赖 | 需要 provider 支持 phase(Claude 等不保证) | 零协议依赖,全 provider 一致 |
| 异常路径 | turn cancelled 不折叠 | interrupted/failed 不折叠(同源语义) |
| 成本 | provider 归一化 + phase 持久化 | 前端一行 status 恢复 + 纯派生 |
两者不冲突:本方案是 phase 缺失时的替代路径;若未来接入 phase,折叠时机可提前到 final answer 开始,判定降级为增强而非必需。
use-auto-scroll-bottom 按 scrollHeight 判 following(:115),折叠使其骤减,需复核纯前端:SessionThread.tsx(折叠容器 + header)、session-store.ts(恢复终态 status)、settings 新条目、i18n(en/zh-Hans/zh-Hant)。对应 GitHub issue:#215(buffin-ai/buffin)。