Web tool-row unified expand and trajectory Inspect
Web 工具行统一展开交互与 trajectory Inspect
The chat view's tool rows had drifted into per-surface interaction dialects: ToolRow expanded through a leading-icon toggle and only for calls with an args body, the bash sample had its own expand affordance, todo/ask-question rows expanded raw args only, single-file tools were not expandable at all, and a call's OUTPUT was reachable only through the details panel. A failing bash command (exit≠0 settles `isError:fals
English
Problem
The chat view's tool rows had drifted into per-surface interaction dialects: ToolRow expanded through a leading-icon toggle and only for calls with an args body, the bash sample had its own expand affordance, todo/ask-question rows expanded raw args only, single-file tools were not expandable at all, and a call's OUTPUT was reachable only through the details panel. A failing bash command (exit≠0 settles isError:false) showed no collapsed-row failure signal. There was also no path from a chat row to its trajectory record, and switching chat → trajectory → chat lost the reader's scroll position because the tab ring unmounts inactive views.
Decision
Every expandable tool row shares one interaction — the whole row toggles (click / Enter / Space) with an icon→chevron hover preview — and one expanded body: an IN/OUT gutter-labeled card with per-section scroll caps; a hover-revealed Inspect pill jumps to the call's trajectory record through a one-shot store handoff; the chat view preserves its semantic reading position across view switches through an in-memory per-session map.
toolRowModelnow derives result material alongside args:output(theresultTextflatten, moved from DetailsPanel into the contract), anderrorSummary(the failure's first line, shown as the collapsed summary in the error color). A row with body, output, or terminal material is expandable; the row itself is the toggle (role="button",aria-expanded), and file-path summaries stay independent links viastopPropagation.- The expanded card (figma 1249:35657) is a column of IN/OUT sections: each section is its own scrollport (max-height 150px) with a sticky gutter label, and the l2 divider spans the full card width. Think prose and the run_code CodeBlock keep their non-card bodies; context injection reuses the row with a label-less
plainBodycard. terminalFailedreads a settled terminal card's exit status so BashRow and GenericToolCard surface a failing command as the row's red state dot — the only failure signal the collapsed row has, since the call itself settlesisError:false.- TerminalBlock's banner joins the same reading model: it shares the card surface (no banner token), an l2 hairline separates it from the body, the command column caps at 150px and scrolls with sticky copy/status controls top-aligned to the first prompt row.
- Inspect:
ToolCallOwnerProps.inspect(absent for rows without a call identity) renders a pill in real flow under the expanded body's bottom-left, revealed by hovering anywhere on the tool call. Clicking writes{ callId }to the chat store's one-shotinspectfield and switches to the trajectory view; TrajectoryTable finds the record, opens its summary, and acknowledges by clearing the field. - Scroll preservation: on every non-bottom scroll, the chat view saves
{ anchorKey, anchorTop, scrollTop }into an apply-scope per-session map exposed aschatScroll; a remount first usesscrollTopto reach the approximate window, then corrects by the stable node/call anchor's rectangle delta so width reflow keeps the same reading row in place. Every pinned path, including Back to bottom, clears the entry synchronously before a tab or session switch. The map remains deliberately unpersisted — a fresh page load keeps the open-jump-to-bottom default.
Alternatives considered
Keeping the leading-icon toggle and per-registrant expand affordances. Rejected: three surfaces had already diverged; the registrant posture (bash sample replicates CSS locally) makes drift permanent unless the interaction contract itself is uniform and small — whole-row toggle plus hover preview.
Routing Inspect through a URL or a trajectory-view prop. Rejected: the view ring renders through the slot registry, so the two views share no parent that could carry a prop; the chat store already crosses that boundary and the one-shot field keeps the handoff replay-safe (persisted snapshots from before the field rehydrate with ?? null).
Persisting the chat scroll offset. Rejected: restoring a days-old offset into a conversation that has since grown reads as a bug; the in-memory map scopes the memory to exactly the view-switch case that loses it.
A per-row expanded OUTPUT fetched from the details panel's material. Unnecessary: the settled result node already rides the snapshot's frozen call slice, so the contract-level resultText flatten serves both the row and the panel from one derivation.
Consequences
Built-in ui-tool views get input and output inspection in place, with the details panel and trajectory remaining the deep-dive surfaces. The shared ToolRow interaction is internal to ui-tool; an external atomic view receives ToolCallViewProps and may expose the supplied inspect callback through its own chrome. The bash view keeps its separate CSS, so future interaction changes still touch it explicitly. --dsw-font-markdown-code-block-small (12/18) is a hand-added token pending a design-platform export. The web-cordis distIndex fix (plain concatenation, not URL.pathname) unblocks preview boots from a cwd with spaces.
中文
问题
聊天视图的工具行交互已经分裂成多种方言:ToolRow 通过前导图标切换展开、且仅限有 args body 的调用,bash 示例有自己的一套展开方式,todo / ask-question 行只能展开原始 args,单文件工具完全不可展开,而调用的 OUTPUT 只能通过详情面板查看。失败的 bash 命令(exit≠0 但结算为 isError:false)在折叠行上没有任何失败信号。此外聊天行没有跳转到 trajectory 记录的入口,且 chat → trajectory → chat 切换会丢失阅读位置(标签环会卸载非活跃视图)。
决定
所有可展开工具行共享同一交互——整行即开关(点击 / Enter / 空格),图标 hover 时渐变为 chevron 预览——以及同一展开体:带 IN/OUT 侧栏标签的卡片,各分区独立滚动上限;hover 显示的 Inspect 胶囊通过 store 的一次性交接跳到该调用的 trajectory 记录;聊天视图用内存态的按会话 Map 在视图切换间保留语义阅读位置。
toolRowModel在 args 之外同时派生结果材料:output(resultText拍平逻辑从 DetailsPanel 移入 contract)和errorSummary(失败首行,作为折叠摘要并以错误色显示)。有 body、output 或 terminal 材料的行即可展开;行本身是开关(role="button"、aria-expanded),文件路径摘要通过stopPropagation保持独立链接。- 展开卡片(figma 1249:35657)是 IN/OUT 分区列:每个分区是独立滚动区(max-height 150px),侧栏标签 sticky 固定,l2 分割线横贯整卡宽度。Think 的推理文本和 run_code 的 CodeBlock 保持非卡片体;上下文注入复用此行并以无标签的
plainBody卡片展开。 terminalFailed读取已结算 terminal 卡片的退出状态,让 BashRow 和 GenericToolCard 把失败命令显示为行的红色状态点——这是折叠行唯一的失败信号,因为调用本身结算为isError:false。- TerminalBlock 的横幅并入同一阅读模型:与卡片共用同一表面(不再用 banner token),与正文之间是 l2 细线,命令列上限 150px 内部滚动,复制/状态控件 sticky 且顶对齐第一行提示符。
- Inspect:
ToolCallOwnerProps.inspect(无调用身份的行不提供)在展开体左下角的正常布局流中渲染胶囊,hover 到工具调用的任意位置时显示。点击将{ callId }写入 chat store 的一次性inspect字段并切换到 trajectory 视图;TrajectoryTable 找到记录、打开其摘要,并通过清空字段确认。 - 滚动保留:每次非贴底滚动时,聊天视图把
{ anchorKey, anchorTop, scrollTop }保存到 apply 作用域的按会话 Map,并以chatScroll暴露;重挂载时先用scrollTop到达近似窗口,再按稳定 node/call 锚点的矩形差值校正,因此宽度重排后仍把同一阅读行保持在原位。包括「回到底部」在内的每条贴底路径都会在切换 tab 或会话前同步清除该项。Map 仍刻意不持久化——新页面加载保持打开即贴底的默认行为。
曾考虑的替代方案
保留前导图标开关和各注册方自有的展开方式。 否决:三个表面已经分化;注册方姿态(bash 示例本地复刻 CSS)意味着除非交互约定本身统一且足够小——整行开关加 hover 预览——否则漂移会永久存在。
通过 URL 或 trajectory 视图 prop 传递 Inspect。 否决:视图环经由 slot 注册表渲染,两个视图没有可携带 prop 的共同父级;chat store 本就跨越该边界,一次性字段让交接具备回放安全性(字段出现之前的持久化快照以 ?? null 复水)。
持久化聊天滚动偏移。 否决:把几天前的偏移恢复到已经增长的会话里读起来像 bug;内存 Map 把记忆精确限定在会丢位置的视图切换场景。
从详情面板的材料为每行单独取展开 OUTPUT。 不必要:已结算结果节点本就在快照的冻结调用切片上,contract 层的 resultText 拍平让行和面板共用一份派生。
后果
ui-tool 内置视图都能就地检查输入与输出,详情面板和 trajectory 仍是深查界面。共享 ToolRow 交互是 ui-tool 内部实现;外部原子视图接收 ToolCallViewProps,可以通过自己的 chrome 暴露其中的 inspect 回调。bash 视图保留独立 CSS,因此未来交互变化仍需显式同步。--dsw-font-markdown-code-block-small(12/18)是手工补充的 token,待设计平台导出后替换。web-cordis 的 distIndex 修复(纯拼接而非 URL.pathname)解除了含空格 cwd 下预览无法启动的问题。