@deepseek-ai/dsh-client-ui-primitives 0.1.5-rc.1 → 0.1.6-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
5
- README.md: 1919e923c6edfae58e38849996f3979723198075
6
- README.zh.md: e0c90fd14a284aac6a2420f5974eef6ad78138c7
5
+ README.md: 631b19de44660c21813bc4f87b7a59dc8dfa6ee3
6
+ README.zh.md: b23c87f7861555625f28a9fd182aa6acb159b21c
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Shared React UI atoms for the dsh web client: controls, icons, markdown and math rendering, and the terminal/read/diff/search/web output cards (zero cordis)."
2
+ description: "Shared React UI atoms for the dsh web client: controls, icons, markdown and math rendering, and the terminal/read/diff/search/web output cards (zero Cordis)."
3
3
  kind: "package-library"
4
4
  ---
5
5
 
@@ -52,7 +52,7 @@ Check this table before writing a control in a feature package. A plugin cannot
52
52
  | `HoverCard` | Hover preview the pointer can rest on and select from; optional copy button. |
53
53
  | `Toast` | Transient top-center banner held for the owner's `holdMs`. |
54
54
  | `JsonTree`, `JsonBlock` | Read-only JSON inspection. |
55
- | `MarkdownText`, `CodeBlock` | Untrusted GFM with TeX math, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. |
55
+ | `MarkdownText`, `CodeBlock` | Untrusted GFM with TeX math, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. Set `showHeader={false}` when the owner supplies its own language and copy toolbar. |
56
56
  | `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. |
57
57
  | `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIcon`, `LinkIcon` | Glyphs and brand marks. Use `LinkIcon` for 14px clickable-link categories. |
58
58
  | `FileTypeIcon`, `classifyFileType`, `fileExtension` | A category-colored 28px file or folder glyph and the shared case-insensitive filename mapping behind it. Code and configuration files use detailed full-color technology glyphs; use `LinkIcon` for link-leading glyphs and image previews for image content. |
@@ -67,12 +67,15 @@ Writing your own component in your own package is fine when the need is genuinel
67
67
 
68
68
  ### Controls and icons
69
69
 
70
- The catalog above lists what each export is for; this section covers the behavior that props alone do not show. The `ic_ds_*` icon set and `FishLogo`/`BrandWordmark` marks fill brand and inline-icon slots. `FileTypeIcon` renders the traditional 28px Excel, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the full-color square technology artwork for recognized code and configuration files. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Office mappings include XLSM/Numbers as Excel, KEY as slides, and RTF/ODT/Pages as documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIcon` remains the smaller leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action, a connecting label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Hover or keyboard focus shows only the reconnect action label, including while the connecting dots animate. Every state reserves the widest supplied label and uses fixed icon and text columns, so copy changes do not move or resize the control. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus.
70
+ The catalog above lists what each export is for; this section covers the behavior that props alone do not show. The `ic_ds_*` icon set and `FishLogo`/`BrandWordmark` marks fill brand and inline-icon slots. `FileTypeIcon` renders the traditional 28px Excel, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Office mappings include XLSM/Numbers as Excel, KEY as slides, and RTF/ODT/Pages as documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIcon` remains the smaller leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, a connecting spinner beside a label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus.
71
71
 
72
72
  ### Rendering agent output
73
73
 
74
- `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows.
74
+ `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. When the owner passes a `pathImages` vocabulary, image destinations that are local media paths rewrite to displayable URLs on settled renders only (the same streaming gate as file mentions); without a vocabulary, local destinations remain inert alt text. A load or decode failure replaces the image with its authored alt text, or the original destination when alt is empty. Changing the image source permits a fresh load. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax. `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `projectUserText` projects sent user text into inline plain runs and reference chips for the message bubble and queue rows. When supplied with `UserTextReferences`, file and skill references become keyboard-accessible preview buttons using the same hover and focus styling as prose file links; the first pointer click can open a preview, while subsequent clicks and existing text selections retain native selection handling. Keyboard activation opens previews even when text is selected.
75
75
 
76
+ `DiffBlock` compares the old and new content by line. It shows actual additions and deletions with up to three neutral context lines on each side, separates distant changes with `⋯`, and excludes shared context from both summary and footer totals. Search stops beyond 256 line additions/deletions per fragment; those fragments display and count the complete old and new contents as a coarse replacement, including shared lines. Copy includes the full displayed diff with its prefixes. A final newline is treated as a terminator; differences only in the presence of a final newline are not displayed.
77
+
78
+ `JsonTree` clamps collapsed strings to `collapsedStringLines` (three by default). Expanded strings show raw text, retain sibling commas, and fit within the window and outer scrolling containers. Resize and ancestor-scroll events update that limit. Row copy feedback updates independently of JSON value rendering; pending clipboard writes cannot update a different row or an unmounted tree.
76
79
 
77
80
  ### Localizing copy
78
81
 
@@ -86,7 +89,7 @@ The atoms cannot read the application locale, so every piece of user-facing copy
86
89
  <details>
87
90
  <summary>Implementation internals — click to expand</summary>
88
91
 
89
- The package is one separation: presentational React atoms with zero Cordis and zero slot knowledge, styled only through `--dsw-*` tokens, while every feature-specific concern (locale, session data, composition) stays in the composing plugin.
92
+ The package enforces one separation: presentational React atoms with zero Cordis and zero slot knowledge, styled only through `--dsw-*` tokens, while every feature-specific concern (locale, session data, composition) stays in the composing plugin.
90
93
 
91
94
  ### Source map
92
95
 
@@ -141,6 +144,7 @@ None; this package neither assembles nor sends a provider request.
141
144
 
142
145
  These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap.
143
146
 
147
+ - **Diff search is bounded, input processing is linear** — the edit-distance limit trades precise alignment for a coarse replacement on heavily changed fragments. Normalization, fallback rows, and copied output still scale with input size; the height cap limits visible rows, not those allocations.
144
148
  - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it.
145
149
  - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path.
146
150
  - **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "dsh Web 客户端共享的 React UI 原子组件:控件、图标、Markdown 与数学公式渲染,以及终端/读取/差异/搜索/网页输出卡片(零 cordis)。"
2
+ description: "dsh Web 客户端共享的 React UI 原子组件:控件、图标、Markdown 与数学公式渲染,以及终端/读取/差异/搜索/网页输出卡片(零 Cordis)。"
3
3
  kind: "package-library"
4
4
  ---
5
5
 
@@ -32,7 +32,7 @@ kind: "package-library"
32
32
  <a id="component-catalog"></a>
33
33
  ### 组件目录
34
34
 
35
- 在功能包里写控件之前,先查这张表。插件无法 import 另一个插件的组件,因此本包是控件唯一可以共享的地方:合适的就复用,有意的视觉差异提升成 prop,而不是另起一份拷贝。
35
+ 在功能包里写控件之前,先查这张表。插件无法导入另一个插件的组件,因此本包是控件唯一可以共享的地方:合适的就复用,有意的视觉差异提升成 prop,而不是另起一份拷贝。
36
36
 
37
37
  | 导出 | 是什么 |
38
38
  |---|---|
@@ -52,7 +52,7 @@ kind: "package-library"
52
52
  | `HoverCard` | 指针可停留、可选中的悬停预览;可选带复制按钮。 |
53
53
  | `Toast` | 顶部居中的瞬时横幅,保持时长由所有者的 `holdMs` 决定。 |
54
54
  | `JsonTree`、`JsonBlock` | 只读 JSON 查看。 |
55
- | `MarkdownText`、`CodeBlock` | 不可信 GFM 与 TeX 数学,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。 |
55
+ | `MarkdownText`、`CodeBlock` | 不可信 GFM 与 TeX 数学,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。调用方提供自己的语言与复制工具栏时,设置 `showHeader={false}`。 |
56
56
  | `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 |
57
57
  | `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIcon`、`LinkIcon` | 字形与品牌标识。`LinkIcon` 用于 14px 的可点击链接分类。 |
58
58
  | `FileTypeIcon`、`classifyFileType`、`fileExtension` | 按类别着色的 28px 文件或文件夹图形,以及它背后共享的不区分大小写文件名映射。代码与配置文件使用细分的全彩技术图形;链接前置图形使用 `LinkIcon`,图片内容使用图片预览。 |
@@ -67,12 +67,15 @@ kind: "package-library"
67
67
 
68
68
  ### 控件与图标
69
69
 
70
- 上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。`ic_ds_*` 图标集与 `FishLogo`/`BrandWordmark` 标记填充品牌与行内图标 slot。`FileTypeIcon` 渲染传统的 28px Excel、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为已识别的代码和配置文件使用全彩方形技术图形。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。办公文件映射包含 XLSM/Numbers 的表格图标、KEY 的幻灯片图标,以及 RTF/ODT/Pages 的文档图标。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIcon` 仍是可点击产物链接较小的前置分类图形——地球、文件夹、代码、图片、文档或纸张——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作、以独立于 retry 时序的 500ms 节奏推进一至三个点的连接中状态,或成功色的恢复状态。悬停或键盘聚焦时只显示重连操作文案,连接中的圆点动画也保持隐藏。所有状态都为最长的输入 label 预留空间,并使用固定的图标列和文字列,因此文案变化不会移动控件或改变其宽度。它的 owner 提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。 `Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。 `rankByName` 是 `/` 菜单命令源与 skill 源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 `Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。
70
+ 上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。`ic_ds_*` 图标集与 `FishLogo`/`BrandWordmark` 标记填充品牌与行内图标 slot。`FileTypeIcon` 渲染传统的 28px Excel、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。办公文件映射包含 XLSM/Numbers 的表格图标、KEY 的幻灯片图标,以及 RTF/ODT/Pages 的文档图标。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIcon` 仍是可点击产物链接较小的前置分类图形——地球、文件夹、代码、图片、文档或纸张——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、spinner 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。`Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 `Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。
71
71
 
72
72
  ### 渲染 agent 输出
73
73
 
74
- `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。
74
+ `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。当 owner 传入 `pathImages` 词表时,本地媒体路径的图片目标只在落定渲染阶段重写为可展示 URL(与 file mentions 相同的流式门);不传词表时本地目标保持惰性 alt 文本。加载或解码失败后,图片替换为作者的 alt 文本;alt 为空时显示原始目标路径。图片源变化后可重新加载。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`projectUserText` 把已发送的用户文本投影为行内普通文本段与引用 chip,供消息气泡和排队行使用。 传入 `UserTextReferences` 时,文件和 skill 引用成为支持键盘操作的预览按钮,复用正文文件链接的悬停和聚焦样式;第一次指针点击可以打开预览,后续点击和已有选区保留原生选择行为。键盘激活在存在选区时仍可打开预览。
75
75
 
76
+ `DiffBlock` 按行比较新旧内容。它显示实际增删行及两侧最多三行中性上下文,用 `⋯` 分隔远距离改动,摘要和底部统计都不计入共享上下文。若一个片段需要超过 256 次行新增或删除,则停止精确比较;该片段按完整新旧内容显示和统计为粗粒度替换,包含共享行。复制包含完整显示 diff 及其前缀。末尾换行视为行终止符;仅末尾换行不同不会显示为改动。
77
+
78
+ `JsonTree` 把折叠字符串限制为 `collapsedStringLines` 行(默认三行)。展开后显示原始文本、保留同级逗号,并限制在窗口与外层滚动容器内;尺寸变化和祖先滚动事件会更新此限制。行复制反馈独立于 JSON 值渲染更新;尚未完成的剪贴板写入不会更新另一行或已卸载的树。
76
79
 
77
80
  ### 本地化文案
78
81
 
@@ -100,7 +103,7 @@ kind: "package-library"
100
103
  | [`src/icons/`](src/icons/) | `ic_ds_*` 字形组件与品牌标记 |
101
104
  | [`src/useAnchoredPosition.ts`](src/useAnchoredPosition.ts) / [`src/useAnchoredMaxHeight.ts`](src/useAnchoredMaxHeight.ts) | 浮动面板与浮层几何钩子 |
102
105
 
103
- ### 流式 markdown
106
+ ### 流式 Markdown
104
107
 
105
108
  回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复。末尾的顶层未闭合 fence 会保留已解析的 code node,只把最后一个已完成行与当前未完成行交给同一套 GFM grammar;闭合 fence 或有歧义的解析会回到普通尾部路径。高亮同样从保存的 Shiki grammar state 续接,并只发布新完成行与可变尾部。`CodeBlock` 把已完成行封入固定大小的 React 分组、复用更早的分组,并在代码与语言未变化时跨定稿保留整棵高亮树。定稿时的全量解析仍会解析跨过冻结边界的引用。
106
109
 
@@ -119,7 +122,7 @@ kind: "package-library"
119
122
 
120
123
  - [ui-renderer](../ui-renderer/README.zh.md)——挂载组装后应用并绑定 slot 数据的 React 渲染器。
121
124
  - [ui-tool](../ui-tool/README.zh.md)——拼装这些输出卡片的工具调用展示层。
122
- - [ui-conversation](../ui-conversation/README.zh.md)——渲染 markdown 回复与工具卡片的聊天界面。
125
+ - [ui-conversation](../ui-conversation/README.zh.md)——渲染 Markdown 回复与工具卡片的聊天界面。
123
126
  - [ui-theme](../ui-theme/README.zh.md)——这些原子组件样式所依赖的 `--dsw-*` token 体系。
124
127
  - [Web 样式](../../../docs/web-styling.zh.md)——Web 客户端组件的权威样式规则。
125
128
 
@@ -141,12 +144,13 @@ kind: "package-library"
141
144
 
142
145
  这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。
143
146
 
147
+ - **Diff 搜索有上限,输入处理仍为线性**:编辑距离上限使大量改动的片段采用粗粒度替换,不再精确对齐。规范化、回退行及复制内容仍随输入大小增长;高度限制只约束可见行数,不限制这些分配。
144
148
  - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。
145
149
  - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。
146
150
  - **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
147
151
  - **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。
148
152
  - **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing、error 和 idle。
149
- - **面向用户的文案必须由渲染点提供**:这些原子组件是 zero-Cordis 的,拿不到 `ctx.locale`;各功能必须通过 primitive 的带类型 prop 提供完整本地化 label(见[决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。
153
+ - **面向用户的文案必须由渲染点提供**:这些原子组件是 zero-Cordis 的,拿不到 `ctx.locale`;各功能必须通过原子组件的带类型 prop 提供完整本地化 label(见[决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。
150
154
  - **`TerminalBlock` 不是终端模拟器**:它渲染已结束或仍在运行的命令输出,而不是交互式会话:SGR 颜色、回车、退格、行内擦除、制表位与字符宽度会被遵循;绝对光标定位、清屏与备用屏幕序列会被剥离。
151
155
 
152
156
  <a id="dev-note"></a>
@@ -4,25 +4,49 @@
4
4
  grid-template-columns: 14px max-content;
5
5
  align-items: center;
6
6
  column-gap: 4px;
7
- height: 32px;
8
- padding: 0 10px;
7
+ height: 28px;
8
+ padding: 0 8px;
9
9
  box-sizing: border-box;
10
- border: none;
11
- border-radius: 8px;
10
+ border: 1px solid transparent;
11
+ border-radius: 13px;
12
12
  font-family: inherit;
13
13
  font-size: 12px;
14
14
  font-weight: 500;
15
15
  line-height: 18px;
16
16
  white-space: nowrap;
17
- transition: background-color 160ms ease-out, color 160ms ease-out;
17
+ transition:
18
+ background-color 160ms ease-out,
19
+ color 160ms ease-out,
20
+ border-color 160ms ease-out,
21
+ opacity 150ms ease-out;
22
+ animation: indicator-enter 150ms ease-out;
23
+ }
24
+
25
+ .leaving {
26
+ opacity: 0;
27
+ }
28
+
29
+ @keyframes indicator-enter {
30
+ from {
31
+ opacity: 0;
32
+ }
18
33
  }
19
34
 
20
35
  .warning {
21
36
  background: var(--dsw-alias-state-warn-tertiary);
22
37
  color: var(--dsw-alias-state-warn-label);
38
+ border-color: color-mix(in srgb, var(--dsw-alias-state-warn-label) 20%, transparent);
23
39
  cursor: pointer;
24
40
  }
25
41
 
42
+ .warning:hover {
43
+ background: color-mix(
44
+ in srgb,
45
+ var(--dsw-alias-state-warn-tertiary),
46
+ var(--dsw-alias-state-warn-primary) 6%
47
+ );
48
+ }
49
+
26
50
  .warning:active {
27
51
  background: color-mix(
28
52
  in srgb,
@@ -39,6 +63,7 @@
39
63
  .success {
40
64
  background: var(--dsw-alias-state-success-tertiary);
41
65
  color: var(--dsw-alias-state-success-primary);
66
+ border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 20%, transparent);
42
67
  }
43
68
 
44
69
  .icon {
@@ -49,35 +74,20 @@
49
74
  }
50
75
 
51
76
  .label {
52
- display: grid;
53
77
  text-align: left;
54
78
  }
55
79
 
56
- .stateLabel,
57
- .hoverLabel,
58
- .sizeLabel {
59
- grid-area: 1 / 1;
60
- }
61
-
62
- .sizeLabel {
63
- visibility: hidden;
64
- }
65
-
66
- .warning:is(:hover, :focus-visible) .stateLabel {
67
- visibility: hidden;
68
- }
69
-
70
- .hoverLabel {
71
- visibility: hidden;
80
+ .spinner {
81
+ animation: spinner-rotate 0.9s linear infinite;
72
82
  }
73
83
 
74
- .warning:is(:hover, :focus-visible) .hoverLabel {
75
- visibility: visible;
84
+ @keyframes spinner-rotate {
85
+ to { transform: rotate(360deg); }
76
86
  }
77
87
 
78
88
  .dots {
79
89
  display: inline-block;
80
- width: 1.5em;
90
+ width: 1em;
81
91
  text-align: left;
82
92
  }
83
93
 
@@ -100,8 +110,14 @@
100
110
  }
101
111
 
102
112
  @media (prefers-reduced-motion: reduce) {
113
+ .indicator,
103
114
  .secondDot,
104
- .thirdDot {
115
+ .thirdDot,
116
+ .spinner {
105
117
  animation: none;
106
118
  }
119
+
120
+ .indicator {
121
+ transition: none;
122
+ }
107
123
  }
@@ -76,6 +76,14 @@
76
76
  color: var(--dsw-alias-state-success-primary);
77
77
  }
78
78
 
79
+ .context {
80
+ color: var(--dsw-alias-label-secondary);
81
+ }
82
+
83
+ .context::before {
84
+ content: ' ';
85
+ }
86
+
79
87
  .expand {
80
88
  display: block;
81
89
  width: 100%;
@@ -29,17 +29,17 @@
29
29
 
30
30
  .container {
31
31
  box-sizing: border-box;
32
- width: max-content;
33
- min-width: 100%;
32
+ width: 100%;
33
+ min-width: 0;
34
34
  margin: 0;
35
- padding: 6px 8px 8px;
35
+ padding: 6px 8px 8px 8px;
36
36
  white-space: pre;
37
37
  }
38
38
 
39
39
  .expandedTopLevel {
40
40
  box-sizing: border-box;
41
- width: max-content;
42
- min-width: 100%;
41
+ width: 100%;
42
+ min-width: 0;
43
43
  padding: 6px 8px 8px 14px;
44
44
  }
45
45
 
@@ -66,11 +66,13 @@
66
66
  .row {
67
67
  position: relative;
68
68
  box-sizing: border-box;
69
- min-width: 100%;
69
+ min-width: 0;
70
70
  min-height: 16px;
71
71
  margin: 0;
72
72
  padding: 0 0 0 10px;
73
73
  list-style: none;
74
+ overflow-wrap: anywhere;
75
+ white-space: pre-wrap;
74
76
  }
75
77
 
76
78
  .row:not(.topLevelBracket):hover:not(:has(.row:hover))::after,
@@ -81,13 +83,13 @@
81
83
  top: 0;
82
84
  right: 0;
83
85
  left: 0;
84
- height: 16px;
86
+ height: var(--json-tree-row-height, 16px);
85
87
  background: var(--json-tree-hover);
86
88
  content: '';
87
89
  pointer-events: none;
88
90
  }
89
91
 
90
- .row > span:not(.expander) {
92
+ .row > span:not(.expander, .copySlot) {
91
93
  position: relative;
92
94
  z-index: 1;
93
95
  }
@@ -106,6 +108,100 @@
106
108
  color: var(--json-tree-string);
107
109
  }
108
110
 
111
+ .stringField {
112
+ position: relative;
113
+ z-index: 1;
114
+ display: flow-root;
115
+ }
116
+
117
+ .stringText {
118
+ display: block;
119
+ max-height: calc(var(--json-tree-collapsed-lines) * 16px);
120
+ padding-left: 12px;
121
+ overflow: hidden;
122
+ text-indent: -12px;
123
+ word-break: break-all;
124
+ }
125
+
126
+ .stringRaw {
127
+ margin: 4px 0 0 12px;
128
+ overflow: auto;
129
+ color: var(--dsw-alias-label-primary);
130
+ font: inherit;
131
+ overflow-wrap: normal;
132
+ white-space: pre;
133
+ word-break: normal;
134
+ }
135
+
136
+ .stringActions {
137
+ position: absolute;
138
+ top: 1px;
139
+ right: 4px;
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: flex-end;
143
+ gap: 4px;
144
+ width: fit-content;
145
+ height: 16px;
146
+ }
147
+
148
+ .stringRaw[data-wrap='true'] {
149
+ overflow-wrap: anywhere;
150
+ white-space: pre-wrap;
151
+ word-break: break-all;
152
+ }
153
+
154
+ .stringActions > .copySlot {
155
+ opacity: 1;
156
+ pointer-events: auto;
157
+ }
158
+
159
+ .stringToggleSlot {
160
+ display: flex;
161
+ float: right;
162
+ align-items: flex-end;
163
+ height: calc(var(--json-tree-collapsed-lines) * 16px);
164
+ margin-left: 2px;
165
+ shape-outside: inset(calc((var(--json-tree-collapsed-lines) - 1) * 16px) 0 0);
166
+ pointer-events: none;
167
+ }
168
+
169
+ .stringToggle {
170
+ display: inline-flex;
171
+ align-items: baseline;
172
+ gap: 2px;
173
+ margin: 0;
174
+ padding: 0;
175
+ border: 0;
176
+ color: var(--dsw-alias-label-secondary);
177
+ background: transparent;
178
+ font: inherit;
179
+ text-indent: 0;
180
+ white-space: nowrap;
181
+ cursor: pointer;
182
+ pointer-events: auto;
183
+ }
184
+
185
+ .stringToggle:hover {
186
+ color: var(--dsw-alias-label-primary);
187
+ }
188
+
189
+ .stringToggle:focus-visible {
190
+ outline: 1px solid var(--dsw-alias-state-business-primary);
191
+ outline-offset: 1px;
192
+ }
193
+
194
+ .row:has(> .stringField) {
195
+ --json-tree-row-height: 100%;
196
+ }
197
+
198
+ .summary {
199
+ display: block;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
202
+ white-space: nowrap;
203
+ }
204
+
109
205
  .numberValue {
110
206
  color: var(--json-tree-number);
111
207
  }
@@ -134,13 +230,38 @@
134
230
  color: var(--dsw-alias-label-tertiary);
135
231
  }
136
232
 
137
- .copyAnchor {
138
- position: fixed;
139
- z-index: 3;
233
+ .copySlot {
140
234
  display: inline-flex;
235
+ width: 20px;
236
+ height: 16px;
237
+ opacity: 0;
238
+ pointer-events: none;
141
239
  }
142
240
 
143
- .copyButton {
241
+ .row > .copySlot {
242
+ position: absolute;
243
+ z-index: 2;
244
+ top: 0;
245
+ right: 0;
246
+ }
247
+
248
+ .row:has(> .stringField) > .copySlot {
249
+ top: auto;
250
+ bottom: 0;
251
+ }
252
+
253
+ .row:has(> .copySlot) > .stringField .stringToggleSlot {
254
+ padding-right: 24px;
255
+ }
256
+
257
+ .row:hover:not(:has(.row:hover)) > .copySlot,
258
+ .row[data-json-copy-active] > .copySlot,
259
+ .copySlot:focus-within {
260
+ opacity: 1;
261
+ pointer-events: auto;
262
+ }
263
+
264
+ .actionButton {
144
265
  box-sizing: border-box;
145
266
  display: inline-flex;
146
267
  align-items: center;
@@ -152,22 +273,24 @@
152
273
  border: 0;
153
274
  border-radius: 3px;
154
275
  color: var(--dsw-alias-label-secondary);
155
- background: var(--dsw-alias-bg-layer-1);
156
- box-shadow: -5px 0 5px var(--dsw-alias-bg-layer-1);
276
+ background: transparent;
157
277
  cursor: pointer;
158
278
  }
159
279
 
160
- .copyButton:hover {
280
+ .actionButton:hover {
161
281
  color: var(--dsw-alias-label-primary);
162
- background: var(--dsw-alias-interactive-bg-hover);
163
282
  }
164
283
 
165
- .copyButton:focus-visible {
284
+ .actionButton[aria-pressed='true'] {
285
+ color: var(--dsw-alias-state-business-primary);
286
+ }
287
+
288
+ .actionButton:focus-visible {
166
289
  outline: 1px solid var(--dsw-alias-state-business-primary);
167
290
  outline-offset: -1px;
168
291
  }
169
292
 
170
- .copyButton[data-state='failed'] {
293
+ .actionButton[data-state='failed'] {
171
294
  color: var(--dsw-alias-state-error-primary);
172
295
  }
173
296