Files
Cyrene-For-Android/app
AskaEth 316cd27049 feat(chat): 工具卡接上 resultSummary 结果区(缺席时与今天逐字节一致)
后端新契约切片给 `tool_execution.end` 的 `data` 补了一个**可选** `resultSummary`
(与回看面 `toolCalls[].resultSummary` 同一份预算:已脱敏、已截断 ≤500 字符)。
它的语义是「拿到就带、没拿到就缺席」——失败 / 取消 / 超时 / 未落图时**整个键缺席**,
且契约明说**别把缺席读成「没有输出」**。在那之前工具卡只能显示工具名与状态。

改了什么:
- wire:`WsEvent.ToolExecutionEnd` 新增 `resultSummary: String? = null`;共用的
  `ToolExecutionData` 跟着加这个字段(只由 `.end` 读,`.start` / `.update` 的领域事件
  仍然没有它)。缺键 ⇒ `null`,空串 ⇒ `""`,都在解码层照原样保留、不编结论。
- 领域:`ToolCallState` 新增 `resultSummary`;`applyToolExecution` 多一个同参数
  (非空白时顶替、缺席 / 空白时保留原值——三帧里只有 `.end` 带它,一次落不到不该抹掉旧的);
  VM 的 `onToolExecution` 与 `ToolExecutionEnd` 分支透传。
- 判据:新增纯函数 `toolResultTextOf(tool)`:摘要非空白 ⇒ 用它;否则回落 `output`
  (今天唯一的来源,口径不变);两者都没有 ⇒ `null`。
- UI:`ToolCard` 折叠区里「结果」那一块(参数之后)改由 `toolResultTextOf` 决定要不要画。
  `null` 时**连标签与它上方那道 Spacer 都不画** ⇒ 摘要缺席时这一屏与后端补这个键之前
  逐字节相同。空串同理(服务端承诺不发空串,但真发了也绝不渲染出一个空的结果区块)。
  摘要只展示:不再截断第二次、不解析、不当 markdown;正文沿用与参数同一个 `FoldCardBody`
  (`SelectionContainer` + 不设 `maxLines`),长文本整段可见,没有新增任何滚动 / 限高 modifier。

为什么这么做:一句话的判据、纯函数、单测可钉;渲染层只做「画 / 不画」这一个决定,
于是「老服务端(键恒缺席)」与「新服务端(键在场)」共用同一套 UI,不用等部署。

测试:新增 14 条(`WsEventToolExecutionTest` +5、`ToolExecutionCorrelationTest` +9),
覆盖 ①键在场 ⇒ 解出该值(含 500 字符原样透传)②键缺席 ⇒ `null` 且不报错
③空串 ⇒ 解出 `""` 且判据按「不画」处理 ④`toolResultTextOf` 在缺席 / 空串 / 有值三档下的输出
(含摘要优先于 `output`、结果落卡、缺席不抹旧值、落不到卡时不影响任何卡)。
`assembleDebug` 通过;全量单测 114 suites / 1388 tests / 0 failures / 0 errors(基线 1374)。
2026-10-07 10:14:10 +08:00
..