fix(client): address tool presentation review
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-diff-card.md
|
||||
2026-07-30-web-diff-card.md: c8aed2aa59d82520a2a52523edb9b66d0bb34bf0
|
||||
2026-07-30-web-diff-card.zh.md: c2127844165e5f5c162eb3707fa5c86aa3a536c0
|
||||
2026-07-30-web-diff-card.md: b078da8d0e688d705683599467bd98b5c6a0be48
|
||||
2026-07-30-web-diff-card.zh.md: a9c710df99c1060bbf50b591b2f62120dc7dcbef
|
||||
|
||||
@@ -14,7 +14,7 @@ This is the [terminal card](2026-07-28-web-terminal-card.md) done for the `diff`
|
||||
|
||||
## Decision
|
||||
|
||||
`DiffBlock` is a `ui-primitives` component that renders a file mutation as an inline diff surface, and both Web render sites for a write/edit call consume the diff render intent through it: the chat tool row's body and the details panel's Output section. `ui-tool/src/client/models/diff-card-model.ts` is the single place that turns the snapshot's `callView`/`resultView` pair into the component's props, so the two sites cannot disagree about a change. It returns null — the generic path — whenever neither side declares `card: 'diff'`, including a `card` value this client version does not know, and whenever a settled call's result view is generic, which is how write/edit keep their execution errors on the generic path. The result side is authoritative once the call settles: the applied hunks replace the call-time diff derived from the arguments alone. A paging window that drops the call head still renders, because the result view carries the whole change.
|
||||
`DiffBlock` is a `ui-primitives` component that renders a file mutation as an inline diff surface, and both Web render sites for a write/edit call consume the diff render intent through it: the chat tool row's body and the details panel's Output section. `ui-tool/src/client/tool/models/diff-card-model.ts` is the single place that turns the snapshot's `callView`/`resultView` pair into the component's props, so the two sites cannot disagree about a change. It returns null — the generic path — whenever neither side declares `card: 'diff'`, including a `card` value this client version does not know, and whenever a settled call's result view is generic, which is how write/edit keep their execution errors on the generic path. The result side is authoritative once the call settles: the applied hunks replace the call-time diff derived from the arguments alone. A paging window that drops the call head still renders, because the result view carries the whole change.
|
||||
|
||||
The component shares the TUI's single-column framing, line-terminator rule, and distinct-path file count. Line classification differs: Web renders the complete old and new sides, while the TUI derives neutral context and exact changed rows when its bounded comparison completes and labels its whole-side fallback approximate.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Web 客户端忽略了它。write/edit 调用落到 `GenericToolCard`,其行
|
||||
|
||||
## Decision
|
||||
|
||||
`DiffBlock` 是一个 `ui-primitives` 组件,把文件改动渲染为内联 diff 表面,write/edit 调用的两个 Web 渲染点都通过它消费 diff 渲染意图:chat 工具行的行体和详情面板的 Output 区。`ui-tool/src/client/models/diff-card-model.ts` 是唯一把快照的 `callView`/`resultView` 对转成组件 props 的地方,因此两个渲染点不会对一次改动产生分歧。当两侧都未声明 `card: 'diff'` 时它返回 null —— 走通用路径 —— 包括本客户端版本不认识的 `card` 值,以及已结算调用的 result view 是 generic 的情况(write/edit 的执行错误正是这样留在通用路径上的)。调用结算后 result 侧是权威:已应用的 hunk 替换仅从参数推导的 call 时 diff。分页窗口丢弃了 call 头也仍能渲染,因为 result view 携带完整改动。
|
||||
`DiffBlock` 是一个 `ui-primitives` 组件,把文件改动渲染为内联 diff 表面,write/edit 调用的两个 Web 渲染点都通过它消费 diff 渲染意图:chat 工具行的行体和详情面板的 Output 区。`ui-tool/src/client/tool/models/diff-card-model.ts` 是唯一把快照的 `callView`/`resultView` 对转成组件 props 的地方,因此两个渲染点不会对一次改动产生分歧。当两侧都未声明 `card: 'diff'` 时它返回 null —— 走通用路径 —— 包括本客户端版本不认识的 `card` 值,以及已结算调用的 result view 是 generic 的情况(write/edit 的执行错误正是这样留在通用路径上的)。调用结算后 result 侧是权威:已应用的 hunk 替换仅从参数推导的 call 时 diff。分页窗口丢弃了 call 头也仍能渲染,因为 result view 携带完整改动。
|
||||
|
||||
该组件与 TUI 共用单栏框架、行终止符规则和去重路径计数。两者的行分类不同:Web 渲染完整的变更前后两侧,而 TUI 会在有界比较完成时派生中性上下文和精确变更行,并把整侧回退标记为近似结果。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.md
|
||||
2026-07-30-web-read-card-frontend.md: 10ed9d3eaa54c2440cf3fbd00a7e44b76c1acfe9
|
||||
2026-07-30-web-read-card-frontend.zh.md: 931316135470b7e7257f7f9d016b24fe741fdbf5
|
||||
2026-07-30-web-read-card-frontend.md: e659645066b706f35709fead4c11023eb4fe9554
|
||||
2026-07-30-web-read-card-frontend.zh.md: 4fc02e23b45399c01b5c99d388f5ae1726b026c0
|
||||
|
||||
@@ -10,7 +10,7 @@ The [read backend](2026-07-30-web-read-card.md) added a fourth render-intent car
|
||||
|
||||
## Decision
|
||||
|
||||
`ReadBlock` is a `ui-primitives` component that renders a read result as a line-numbered, optionally syntax-highlighted file view, and both Web render sites for a read consume the read render intent through it: the chat tool row (resident under the summary line) and the details panel's Output section. `ui-tool/src/client/models/read-card-model.ts` is the single place that turns the snapshot's `resultView` into the component's props, so the two sites cannot disagree.
|
||||
`ReadBlock` is a `ui-primitives` component that renders a read result as a line-numbered, optionally syntax-highlighted file view, and both Web render sites for a read consume the read render intent through it: the chat tool row (resident under the summary line) and the details panel's Output section. `ui-tool/src/client/tool/models/read-card-model.ts` is the single place that turns the snapshot's `resultView` into the component's props, so the two sites cannot disagree.
|
||||
|
||||
**A new `ReadBlock` primitive, not an extension of `CodeBlock`.** `CodeBlock` already does shiki highlighting with a language banner and a copy control, but a read view needs a per-line gutter carrying each line's own file number, which `CodeBlock` renders as a single `<pre>` tree with no per-line structure. Extending `CodeBlock` with an optional gutter would push a read-specific concern (windowed line numbers, a "showing N of M" note, a height cap) onto every markdown fence and every `run_code` body that shares that component. Instead `ReadBlock` reuses the part that is genuinely shared: the shiki grammar singleton in `markdown/highlight.ts`. A new `highlightLines(code, lang)` there tokenizes into shiki's own per-line token arrays (`codeToTokens`) rather than the single-`<pre>` HTML `highlightToHtml` produces, so the block can place one gutter number per line and still color the content through the same `--shiki-*` custom properties on the same grammar allowlist. The height cap and its head/tail expand arithmetic are copied from `TerminalBlock` (`ceil(max/2)` head plus the remaining tail), so a long read and a long command output collapse at the same place. The copy control writes the window's raw text (the lines joined by newlines), never the gutter numbers or the banner.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
`ReadBlock` 是一个 `ui-primitives` 组件,把一次读取结果渲染成带行号、可选语法高亮的文件视图,读取的两个 Web 渲染点都通过它消费读取渲染意图:聊天工具行(常驻在摘要行之下)与详情面板的 Output 区段。`ui-tool/src/client/models/read-card-model.ts` 是把快照的 `resultView` 转成组件 props 的唯一位置,因此两个渲染点不会产生分歧。
|
||||
`ReadBlock` 是一个 `ui-primitives` 组件,把一次读取结果渲染成带行号、可选语法高亮的文件视图,读取的两个 Web 渲染点都通过它消费读取渲染意图:聊天工具行(常驻在摘要行之下)与详情面板的 Output 区段。`ui-tool/src/client/tool/models/read-card-model.ts` 是把快照的 `resultView` 转成组件 props 的唯一位置,因此两个渲染点不会产生分歧。
|
||||
|
||||
**新建一个 `ReadBlock` primitive,而不是扩展 `CodeBlock`。** `CodeBlock` 已经带语言横幅和复制控件做 shiki 高亮,但读取视图需要一个每行带该行自身文件行号的行号栏,而 `CodeBlock` 把内容渲染为单个 `<pre>` 树、没有逐行结构。给 `CodeBlock` 加一个可选行号栏会把读取专属的关切(窗口行号、"显示 N / M"提示、高度上限)强加给共享该组件的每个 markdown 代码围栏和每个 `run_code` 程序体。`ReadBlock` 转而复用真正共享的部分:`markdown/highlight.ts` 里的 shiki 语法单例。那里新增的 `highlightLines(code, lang)` 把代码切成 shiki 自己的逐行 token 数组(`codeToTokens`),而不是 `highlightToHtml` 产出的单 `<pre>` HTML,于是该 block 能每行放一个行号、同时用同一套 `--shiki-*` 自定义属性、同一份语法白名单给内容上色。高度上限及其头/尾展开算法照抄自 `TerminalBlock`(`ceil(max/2)` 行头部加剩余的尾部),因此长读取和长命令输出在同一处折叠。复制控件写入窗口的原始文本(各行以换行拼接),绝不含行号栏或横幅。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md
|
||||
2026-07-30-web-result-card-frontend.md: 1d58710dbce3ed1aa9337e1841940195f71db40a
|
||||
2026-07-30-web-result-card-frontend.zh.md: 8705aa05557c1fdf642f498939bc2ceddde91f1f
|
||||
2026-07-30-web-result-card-frontend.md: c7e63220d824cf0bd3ac3536c528707d0db379bc
|
||||
2026-07-30-web-result-card-frontend.zh.md: 4cec11371e8d0848f2f03ded0b093d80cc25a7a5
|
||||
|
||||
@@ -10,7 +10,7 @@ The `web_search` and `web_fetch` tools declare a `card: 'web'` result view ([web
|
||||
|
||||
## Decision
|
||||
|
||||
`WebBlock` is a `ui-primitives` component that renders a completed web retrieval, and every Web render site for a web call consumes the `web` render intent through it: the keyed chat tool rows (`web_search`/`web_fetch`), the `GenericToolCard` render-site fallback, and the details panel's Output section. `ui-tool/src/client/models/web-card-model.ts` is the single place that turns the snapshot's `resultView` into the component's props, mirroring `terminal-card-model.ts`, so no two sites disagree about what a web call shows. It returns null — the generic path — for a running call (the web card is result-only, since the tools keep a generic pending view), for a settled call whose result view is not a web card including a `card` value this client version does not know (which arrives over the wire and so cannot be trusted to be a compiled variant), for a generic result view (a web tool's error path returns the generic card, whose text the generic path preserves), and for a web card whose `kind` this client version does not know (a newer host's value off the wire, which reading as a fetch would draw as an empty URL and `HTTP undefined`).
|
||||
`WebBlock` is a `ui-primitives` component that renders a completed web retrieval, and every Web render site for a web call consumes the `web` render intent through it: the keyed chat tool rows (`web_search`/`web_fetch`), the `GenericToolCard` render-site fallback, and the details panel's Output section. `ui-tool/src/client/tool/models/web-card-model.ts` is the single place that turns the snapshot's `resultView` into the component's props, mirroring `terminal-card-model.ts`, so no two sites disagree about what a web call shows. It returns null — the generic path — for a running call (the web card is result-only, since the tools keep a generic pending view), for a settled call whose result view is not a web card including a `card` value this client version does not know (which arrives over the wire and so cannot be trusted to be a compiled variant), for a generic result view (a web tool's error path returns the generic card, whose text the generic path preserves), and for a web card whose `kind` this client version does not know (a newer host's value off the wire, which reading as a fetch would draw as an empty URL and `HTTP undefined`).
|
||||
|
||||
One component draws both kinds, discriminated by `kind`. A `search` shows the answer as markdown above a citation list; each source is a safe external link labelled by its title, or its hostname when the provider gave none, with the snippet and publication date below it, and a `来源列表已截断` indicator when the tool capped the list. A `fetch` shows a compact summary: the linked final URL, its HTTP status, and a `内容已截断` indicator. One component rather than two because both are web retrieval rendered as one card family, which is exactly the reason the contract carries them under one `card` tag with a `kind` discriminant.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
`WebBlock` 是一个 `ui-primitives` 组件,渲染一次已完成的 web 检索,web 调用的每个 Web 渲染点都通过它消费 `web` 渲染意图:键控的 chat 工具行(`web_search`/`web_fetch`)、`GenericToolCard` 渲染点兜底,以及详情面板的 Output 区。`ui-tool/src/client/models/web-card-model.ts` 是唯一把快照的 `resultView` 转成组件 props 的地方,镜像 `terminal-card-model.ts`,因此没有两个渲染点会对一次 web 调用的显示产生分歧。它返回 null —— 走通用路径 —— 对运行中的调用(web 卡片是 result-only 的,因为工具保留 generic pending 视图)、对 result view 不是 web 卡片的已结算调用(包括本客户端版本不认识的 `card` 值,它经 wire 抵达因而不能被信任为已编译的变体)、对 generic result view(web 工具的错误路径返回 generic 卡片,其文本由通用路径保留)、以及对本客户端版本不认识 `kind` 的 web 卡片(更新的 host 经 wire 发来的值,读作 fetch 会画出空 URL 和 `HTTP undefined`)。
|
||||
`WebBlock` 是一个 `ui-primitives` 组件,渲染一次已完成的 web 检索,web 调用的每个 Web 渲染点都通过它消费 `web` 渲染意图:键控的 chat 工具行(`web_search`/`web_fetch`)、`GenericToolCard` 渲染点兜底,以及详情面板的 Output 区。`ui-tool/src/client/tool/models/web-card-model.ts` 是唯一把快照的 `resultView` 转成组件 props 的地方,镜像 `terminal-card-model.ts`,因此没有两个渲染点会对一次 web 调用的显示产生分歧。它返回 null —— 走通用路径 —— 对运行中的调用(web 卡片是 result-only 的,因为工具保留 generic pending 视图)、对 result view 不是 web 卡片的已结算调用(包括本客户端版本不认识的 `card` 值,它经 wire 抵达因而不能被信任为已编译的变体)、对 generic result view(web 工具的错误路径返回 generic 卡片,其文本由通用路径保留)、以及对本客户端版本不认识 `kind` 的 web 卡片(更新的 host 经 wire 发来的值,读作 fetch 会画出空 URL 和 `HTTP undefined`)。
|
||||
|
||||
一个组件绘制两种 kind,由 `kind` 判别。`search` 把 answer 作为 markdown 显示在引用列表上方;每个 source 是一个安全外链,以其标题为标签,provider 未给标题时以其主机名为标签,下方是 snippet 与发布日期,工具截断列表时显示 `来源列表已截断` 提示。`fetch` 显示一个紧凑摘要:带链接的最终 URL、其 HTTP 状态、以及 `内容已截断` 提示。用一个组件而非两个,因为两者都是渲染为同一卡片族的 web 检索 —— 这正是契约把它们放在一个 `card` 标签下、用 `kind` 判别的原因。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-search-card.md
|
||||
2026-07-30-web-search-card.md: a350756a22d2ba5da8a0ff5d3a4cb3f257ac566b
|
||||
2026-07-30-web-search-card.zh.md: d6ec2a08eb02a92a50d4742c3d87c22f42c095d4
|
||||
2026-07-30-web-search-card.md: e0be857df69dfd46b6e936c775c6bae1476c26dc
|
||||
2026-07-30-web-search-card.zh.md: 1704b8f04c519511d5afa32ae6683c1e48ce7992
|
||||
|
||||
@@ -12,7 +12,7 @@ This is the follow-up the search render card note names: that PR was the backend
|
||||
|
||||
## Decision
|
||||
|
||||
`SearchBlock` is a `ui-primitives` component that renders a completed search as either shape, and the Web render sites for a `grep`/`glob` call consume the search render intent through it. `ui-tool/src/client/models/search-card-model.ts` is the single place that turns the snapshot's `resultView` into the component's props, so no render site re-derives the shape. It returns null — the generic path — whenever the result view is not a search card, including a still-running call (a search card is result-time only, so there is nothing before `execute`), a generic result a `grep`/`glob` failure or a nested `run_code` dispatch produces, a terminal result view, a `card` value this client version does not know, a `card: 'search'` view whose `shape` this version does not compile, and — because `shape` and the grouped/flat contents ride the same untrusted wire frame the host schema only string-checks — a known `shape` whose `files`/`paths` is missing or malformed (which would otherwise crash `SearchBlock` at `.reduce`/`.map`). The result-view discriminant is `shape` (not `kind`, which the backend reserves for the call view's icon-picking tag); `SearchBlock`'s own prop stays `kind`, mapped from `shape` in this derivation.
|
||||
`SearchBlock` is a `ui-primitives` component that renders a completed search as either shape, and the Web render sites for a `grep`/`glob` call consume the search render intent through it. `ui-tool/src/client/tool/models/search-card-model.ts` is the single place that turns the snapshot's `resultView` into the component's props, so no render site re-derives the shape. It returns null — the generic path — whenever the result view is not a search card, including a still-running call (a search card is result-time only, so there is nothing before `execute`), a generic result a `grep`/`glob` failure or a nested `run_code` dispatch produces, a terminal result view, a `card` value this client version does not know, a `card: 'search'` view whose `shape` this version does not compile, and — because `shape` and the grouped/flat contents ride the same untrusted wire frame the host schema only string-checks — a known `shape` whose `files`/`paths` is missing or malformed (which would otherwise crash `SearchBlock` at `.reduce`/`.map`). The result-view discriminant is `shape` (not `kind`, which the backend reserves for the call view's icon-picking tag); `SearchBlock`'s own prop stays `kind`, mapped from `shape` in this derivation.
|
||||
|
||||
The asymmetry with the terminal card is deliberate and inherited from the backend contract: `terminalCardModel` reads both `callView` and `resultView` because a command, cwd, and description exist at call time; `searchCardModel` reads only `resultView` because a search's matches or paths exist only after execution. A running search row therefore shows its summary alone, with no card.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
`SearchBlock` 是一个 `ui-primitives` 组件,把一次已完成的搜索渲染成两种形态之一,`grep`/`glob` 调用的 Web 渲染点都通过它消费搜索 render intent。`ui-tool/src/client/models/search-card-model.ts` 是把 snapshot 的 `resultView` 转成组件 props 的唯一位置,因此没有渲染点重新推导形态。当结果视图不是搜索卡片时它返回 null(走 generic 路径),包括仍在运行的调用(搜索卡片仅在结果阶段存在,`execute` 前无内容)、`grep`/`glob` 失败或嵌套 `run_code` dispatch 产生的 generic 结果、terminal 结果视图、本客户端版本不认识的 `card` 值、`shape` 是本版本无法编译的 `card: 'search'` 视图,以及 —— 因为 `shape` 和分组/扁平内容与 host schema 只做字符串校验的那同一个不可信 wire 帧同行 —— 一个 `shape` 已知但 `files`/`paths` 缺失或格式错误的视图(否则会让 `SearchBlock` 在 `.reduce`/`.map` 处崩溃)。结果视图的判别键是 `shape`(不是 `kind` —— 后端把 `kind` 留给 call view 的选图标签);`SearchBlock` 自身的 prop 仍是 `kind`,由本推导从 `shape` 映射得到。
|
||||
`SearchBlock` 是一个 `ui-primitives` 组件,把一次已完成的搜索渲染成两种形态之一,`grep`/`glob` 调用的 Web 渲染点都通过它消费搜索 render intent。`ui-tool/src/client/tool/models/search-card-model.ts` 是把 snapshot 的 `resultView` 转成组件 props 的唯一位置,因此没有渲染点重新推导形态。当结果视图不是搜索卡片时它返回 null(走 generic 路径),包括仍在运行的调用(搜索卡片仅在结果阶段存在,`execute` 前无内容)、`grep`/`glob` 失败或嵌套 `run_code` dispatch 产生的 generic 结果、terminal 结果视图、本客户端版本不认识的 `card` 值、`shape` 是本版本无法编译的 `card: 'search'` 视图,以及 —— 因为 `shape` 和分组/扁平内容与 host schema 只做字符串校验的那同一个不可信 wire 帧同行 —— 一个 `shape` 已知但 `files`/`paths` 缺失或格式错误的视图(否则会让 `SearchBlock` 在 `.reduce`/`.map` 处崩溃)。结果视图的判别键是 `shape`(不是 `kind` —— 后端把 `kind` 留给 call view 的选图标签);`SearchBlock` 自身的 prop 仍是 `kind`,由本推导从 `shape` 映射得到。
|
||||
|
||||
与终端卡片的不对称是刻意的,继承自后端契约:`terminalCardModel` 同时读 `callView` 和 `resultView`,因为命令、cwd、description 在调用时就存在;`searchCardModel` 只读 `resultView`,因为搜索的匹配或路径只在执行后存在。因此运行中的搜索行只显示摘要,没有卡片。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user