@deepseek-ai/dsh-client-ui-primitives 0.1.1-rc.2 → 0.1.2-alpha.3

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.
Files changed (47) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +91 -24
  3. package/README.zh.md +92 -25
  4. package/lib/ConnectionIndicator.module.css +107 -0
  5. package/lib/DiffBlock.module.css +0 -6
  6. package/lib/DisclosureRow.module.css +21 -6
  7. package/lib/ReadBlock.module.css +0 -6
  8. package/lib/SearchBlock.module.css +0 -8
  9. package/lib/TerminalBlock.module.css +0 -6
  10. package/lib/Toast.module.css +7 -10
  11. package/lib/Tooltip.module.css +0 -7
  12. package/lib/WebBlock.module.css +0 -6
  13. package/lib/index.js +1243 -352
  14. package/lib/markdown/CodeBlock.module.css +4 -2
  15. package/lib/markdown/MarkdownText.module.css +2 -2
  16. package/lib/types/ConnectionIndicator.d.ts +25 -0
  17. package/lib/types/DiffBlock.d.ts +28 -9
  18. package/lib/types/FishLogo.d.ts +7 -0
  19. package/lib/types/FoldToggle.d.ts +20 -0
  20. package/lib/types/HoverCard.d.ts +4 -4
  21. package/lib/types/JsonTree.d.ts +4 -6
  22. package/lib/types/Modal.d.ts +22 -18
  23. package/lib/types/OnboardingSurface.d.ts +2 -2
  24. package/lib/types/ReadBlock.d.ts +13 -1
  25. package/lib/types/ReferenceIcon.d.ts +16 -0
  26. package/lib/types/RiskConfirmation.d.ts +2 -1
  27. package/lib/types/SearchBlock.d.ts +14 -0
  28. package/lib/types/TerminalBlock.d.ts +4 -10
  29. package/lib/types/Toast.d.ts +9 -1
  30. package/lib/types/WebBlock.d.ts +13 -0
  31. package/lib/types/icons/index.d.ts +8 -0
  32. package/lib/types/index.d.ts +14 -8
  33. package/lib/types/markdown/CodeBlock.d.ts +12 -3
  34. package/lib/types/markdown/JsonBlock.d.ts +1 -1
  35. package/lib/types/markdown/MarkdownText.d.ts +8 -7
  36. package/lib/types/markdown/highlight.d.ts +69 -2
  37. package/lib/types/markdown/incremental.d.ts +23 -12
  38. package/lib/types/markdown/render.d.ts +9 -4
  39. package/lib/types/markdown/useViewportHighlighting.d.ts +11 -0
  40. package/lib/types/relative-time.d.ts +23 -0
  41. package/lib/types/useAnchoredPosition.d.ts +8 -6
  42. package/lib/types/useDismissOnOutsidePointer.d.ts +3 -1
  43. package/lib/types/user-text.d.ts +19 -0
  44. package/lib/user-text.module.css +35 -0
  45. package/package.json +4 -5
  46. package/lib/ConnectionBanner.module.css +0 -13
  47. package/lib/types/ConnectionBanner.d.ts +0 -12
package/lib/index.js CHANGED
@@ -12,20 +12,21 @@ import css$6 from "./HoverCard.module.css";
12
12
  import css$7 from "./Modal.module.css";
13
13
  import css$8 from "./OnboardingSurface.module.css";
14
14
  import css$9 from "./RiskConfirmation.module.css";
15
- import css$10 from "./ConnectionBanner.module.css";
16
- import css$11 from "./Tooltip.module.css";
17
- import css$12 from "./Toast.module.css";
18
- import css$13 from "./JsonTree.module.css";
15
+ import css$10 from "./ConnectionIndicator.module.css";
16
+ import css$11 from "./user-text.module.css";
17
+ import css$12 from "./Tooltip.module.css";
18
+ import css$13 from "./Toast.module.css";
19
+ import css$14 from "./JsonTree.module.css";
19
20
  import Anser from "anser";
20
- import css$14 from "./TerminalBlock.module.css";
21
+ import css$15 from "./TerminalBlock.module.css";
21
22
  import { createCssVariablesTheme, createHighlighterCoreSync } from "shiki/core";
22
23
  import { createJavaScriptRegexEngine, defaultJavaScriptRegexConstructor } from "shiki/engine/javascript";
23
24
  import langTs from "@shikijs/langs/typescript";
24
25
  import langBash from "@shikijs/langs/shellscript";
25
26
  import langJson from "@shikijs/langs/json";
26
- import css$15 from "./ReadBlock.module.css";
27
- import css$16 from "./DiffBlock.module.css";
28
- import css$17 from "./SearchBlock.module.css";
27
+ import css$16 from "./ReadBlock.module.css";
28
+ import css$17 from "./DiffBlock.module.css";
29
+ import css$18 from "./SearchBlock.module.css";
29
30
  import { fromMarkdown } from "mdast-util-from-markdown";
30
31
  import { gfmFromMarkdown } from "mdast-util-gfm";
31
32
  import { mathFromMarkdown } from "mdast-util-math";
@@ -37,13 +38,13 @@ import { classifyCharacter } from "micromark-util-classify-character";
37
38
  import { codes, constants, types } from "micromark-util-symbol";
38
39
  import { factorySpace } from "micromark-factory-space";
39
40
  import { normalizeUri } from "micromark-util-sanitize-uri";
40
- import css$18 from "./markdown/CodeBlock.module.css";
41
+ import css$19 from "./markdown/CodeBlock.module.css";
41
42
  import katex from "katex";
42
- import css$19 from "./markdown/MarkdownText.module.css";
43
+ import css$20 from "./markdown/MarkdownText.module.css";
43
44
  import "katex/dist/katex.min.css";
44
- import css$20 from "./WebBlock.module.css";
45
- import css$21 from "./markdown/JsonBlock.module.css";
46
- import css$22 from "./markdown/MessageText.module.css";
45
+ import css$21 from "./WebBlock.module.css";
46
+ import css$22 from "./markdown/JsonBlock.module.css";
47
+ import css$23 from "./markdown/MessageText.module.css";
47
48
  //#region lib/types/StateDot.js
48
49
  /** Outer 3x3 matrix cells (2px pixels on a 10px grid), clockwise from top-left. */
49
50
  const MATRIX_CELLS = [
@@ -618,6 +619,33 @@ const IconBrowseOutline16 = ({ size = 16, className }) => jsxs("svg", {
618
619
  })
619
620
  ]
620
621
  });
622
+ /** ic_ds_context_injection_outline_16 (figma extract): browse document frame with an open top and an arrow dropping in. */
623
+ const IconContextInjectionOutline16 = ({ size = 16, className }) => jsxs("svg", {
624
+ width: size,
625
+ height: size,
626
+ className,
627
+ viewBox: "0 0 16 16",
628
+ fill: "none",
629
+ xmlns: "http://www.w3.org/2000/svg",
630
+ children: [
631
+ jsx("path", {
632
+ d: "M11.9512 1.13281C12.401 1.20666 12.8093 1.34164 13.1738 1.60645C13.4282 1.79137 13.6521 2.01609 13.8369 2.27051C14.1574 2.71187 14.2892 3.21614 14.3506 3.78223C14.4105 4.33532 14.4102 5.02658 14.4102 5.87305V10.0273C14.4102 10.8738 14.4105 11.5651 14.3506 12.1182C14.2892 12.6843 14.1574 13.1885 13.8369 13.6299C13.652 13.8843 13.4282 14.109 13.1738 14.2939C12.7324 14.6146 12.2273 14.7462 11.6611 14.8076C11.1081 14.8675 10.4166 14.8672 9.57031 14.8672H6.43164C5.58533 14.8672 4.89387 14.8675 4.34082 14.8076C3.77474 14.7463 3.27046 14.6144 2.8291 14.2939C2.57453 14.109 2.35003 13.8844 2.16504 13.6299C1.84444 13.1885 1.71272 12.6844 1.65137 12.1182C1.59147 11.5651 1.5918 10.8738 1.5918 10.0273V5.87305C1.5918 5.02655 1.59146 4.33533 1.65137 3.78223C1.71272 3.21606 1.84443 2.71191 2.16504 2.27051C2.35003 2.01596 2.57453 1.79141 2.8291 1.60645C3.19332 1.34202 3.60062 1.20669 4.0498 1.13281V2.56445C3.87191 2.61154 3.74906 2.66836 3.65137 2.73926C3.51583 2.83777 3.3964 2.95726 3.29785 3.09277C3.1794 3.25581 3.09143 3.4856 3.04297 3.93262C2.9931 4.39287 2.99219 4.99529 2.99219 5.87305V10.0273C2.99219 10.905 2.99312 11.5075 3.04297 11.9678C3.09142 12.4147 3.17943 12.6446 3.29785 12.8076C3.3964 12.9431 3.51583 13.0626 3.65137 13.1611C3.81441 13.2795 4.04437 13.3676 4.49121 13.416C4.95142 13.4658 5.55411 13.4668 6.43164 13.4668H9.57031C10.4479 13.4668 11.0505 13.4659 11.5107 13.416C11.9576 13.3675 12.1876 13.2796 12.3506 13.1611C12.4861 13.0626 12.6056 12.9431 12.7041 12.8076C12.8224 12.6446 12.9106 12.4146 12.959 11.9678C13.0088 11.5075 13.0098 10.905 13.0098 10.0273V5.87305C13.0098 4.99532 13.0088 4.39286 12.959 3.93262C12.9105 3.48579 12.8225 3.2558 12.7041 3.09277C12.6056 2.95727 12.4861 2.83778 12.3506 2.73926C12.2527 2.66816 12.1296 2.61064 11.9512 2.56348V1.13281Z",
633
+ fill: "currentColor"
634
+ }),
635
+ jsx("path", {
636
+ d: "M9.32227 11.4141H4.95508V10.2148H9.32227V11.4141Z",
637
+ fill: "currentColor"
638
+ }),
639
+ jsx("path", {
640
+ d: "M11.0439 8.90039H4.95508V7.70117H11.0439V8.90039Z",
641
+ fill: "currentColor"
642
+ }),
643
+ jsx("path", {
644
+ d: "M8.59961 3.75781L9.70996 2.64746L10.5586 3.49609L8.49512 5.55957C8.22173 5.83266 7.77816 5.83285 7.50488 5.55957L5.44141 3.49512L6.28906 2.64746L7.40039 3.75781V1.09668H8.59961V3.75781Z",
645
+ fill: "currentColor"
646
+ })
647
+ ]
648
+ });
621
649
  /** ic_ds_link_outline_14 */
622
650
  const IconLinkOutline14 = ({ size = 14, className }) => jsxs("svg", {
623
651
  width: size,
@@ -1127,6 +1155,55 @@ const IconDataOutline16 = ({ size = 16, className }) => jsxs("svg", {
1127
1155
  fill: "currentColor"
1128
1156
  })]
1129
1157
  });
1158
+ /** IconDataOutline16 without its gear: a three-tier database cylinder. */
1159
+ const IconDatabaseOutline16 = ({ size = 16, className }) => jsxs("svg", {
1160
+ width: size,
1161
+ height: size,
1162
+ className,
1163
+ viewBox: "0 0 16 16",
1164
+ fill: "none",
1165
+ xmlns: "http://www.w3.org/2000/svg",
1166
+ children: [
1167
+ jsx("ellipse", {
1168
+ cx: "8",
1169
+ cy: "3.6",
1170
+ rx: "5.75",
1171
+ ry: "2.4",
1172
+ stroke: "currentColor",
1173
+ strokeWidth: "1.25"
1174
+ }),
1175
+ jsx("path", {
1176
+ d: "M2.25 3.6V12.3A5.75 2.4 0 0 0 13.75 12.3V3.6",
1177
+ stroke: "currentColor",
1178
+ strokeWidth: "1.25"
1179
+ }),
1180
+ jsx("path", {
1181
+ d: "M2.25 7.95A5.75 2.4 0 0 0 13.75 7.95",
1182
+ stroke: "currentColor",
1183
+ strokeWidth: "1.25"
1184
+ })
1185
+ ]
1186
+ });
1187
+ /** Thin-stroke clock: outlined dial with square-cut hour and minute hands. */
1188
+ const IconClockOutline16 = ({ size = 16, className }) => jsxs("svg", {
1189
+ width: size,
1190
+ height: size,
1191
+ className,
1192
+ viewBox: "0 0 16 16",
1193
+ fill: "none",
1194
+ xmlns: "http://www.w3.org/2000/svg",
1195
+ children: [jsx("circle", {
1196
+ cx: "8",
1197
+ cy: "8",
1198
+ r: "6.375",
1199
+ stroke: "currentColor",
1200
+ strokeWidth: "1.25"
1201
+ }), jsx("path", {
1202
+ d: "M8 4.4V8.3L10.7 9.85",
1203
+ stroke: "currentColor",
1204
+ strokeWidth: "1.25"
1205
+ })]
1206
+ });
1130
1207
  /** ic_send_outline_14 (figma extract): thin-stroke upward send arrow. */
1131
1208
  const IconSendOutline14 = ({ size = 14, className }) => jsx("svg", {
1132
1209
  width: size,
@@ -1318,6 +1395,56 @@ const IconQuestionOutline14 = ({ size = 14, className }) => jsxs("svg", {
1318
1395
  })
1319
1396
  ]
1320
1397
  });
1398
+ /** Alarm clock outline for active scheduled-task indicators. */
1399
+ const IconAlarmClockOutline16 = ({ size = 16, className }) => jsxs("svg", {
1400
+ "aria-hidden": "true",
1401
+ width: size,
1402
+ height: size,
1403
+ className,
1404
+ viewBox: "0 0 16 16",
1405
+ fill: "none",
1406
+ xmlns: "http://www.w3.org/2000/svg",
1407
+ children: [
1408
+ jsx("path", {
1409
+ d: "M3.5 2.5 1.75 4",
1410
+ stroke: "currentColor",
1411
+ strokeWidth: "1.25",
1412
+ strokeLinecap: "round"
1413
+ }),
1414
+ jsx("path", {
1415
+ d: "M12.5 2.5 14.25 4",
1416
+ stroke: "currentColor",
1417
+ strokeWidth: "1.25",
1418
+ strokeLinecap: "round"
1419
+ }),
1420
+ jsx("circle", {
1421
+ cx: "8",
1422
+ cy: "8.5",
1423
+ r: "4.75",
1424
+ stroke: "currentColor",
1425
+ strokeWidth: "1.25"
1426
+ }),
1427
+ jsx("path", {
1428
+ d: "M8 5.75V8.5L10 9.75",
1429
+ stroke: "currentColor",
1430
+ strokeWidth: "1.25",
1431
+ strokeLinecap: "round",
1432
+ strokeLinejoin: "round"
1433
+ }),
1434
+ jsx("path", {
1435
+ d: "m4.75 12.25-1 1.5",
1436
+ stroke: "currentColor",
1437
+ strokeWidth: "1.25",
1438
+ strokeLinecap: "round"
1439
+ }),
1440
+ jsx("path", {
1441
+ d: "m11.25 12.25 1 1.5",
1442
+ stroke: "currentColor",
1443
+ strokeWidth: "1.25",
1444
+ strokeLinecap: "round"
1445
+ })
1446
+ ]
1447
+ });
1321
1448
  /** ic_ds_archive_outline_20 (figma extract): lidded box + label slot. The export's
1322
1449
  * 0.11px stroke ring around the box contour is dropped — it restates the same
1323
1450
  * contour in the same ink, which currentColor already carries. */
@@ -1755,19 +1882,19 @@ function useAnchoredMaxHeight(ref, cap, signal) {
1755
1882
  *
1756
1883
  * A portaled panel is positioned from its anchor's viewport rect, which stops
1757
1884
  * being true the moment anything scrolls or the window resizes. This owns that
1758
- * one concern: measure the anchor, offset the panel below it, clamp the result
1759
- * inside the viewport, and re-run on scroll (capture phase, so scrollers nested
1760
- * inside the page are caught too), on resize, and on the panel's own size
1761
- * changes while the element is open.
1885
+ * one concern: measure the anchor, offset the panel below or above it, clamp
1886
+ * the result inside the viewport, and re-run on scroll (capture phase, so
1887
+ * scrollers nested inside the page are caught too), on resize, and on the
1888
+ * panel's own size changes while the element is open.
1762
1889
  * @module @deepseek-ai/dsh-client-ui-primitives/useAnchoredPosition
1763
1890
  */
1764
1891
  /**
1765
1892
  * Track an anchor and return the panel's fixed coordinates.
1766
- * @param options - the open state, the two refs, and the gap/margin distances.
1893
+ * @param options - the open state, the two refs, the placement side, and the gap/margin distances.
1767
1894
  * @returns `left`/`top` for the panel, or `null` before the first measurement.
1768
1895
  */
1769
1896
  function useAnchoredPosition(options) {
1770
- const { open, anchorRef, panelRef, gap, margin } = options;
1897
+ const { open, anchorRef, panelRef, side = "bottom", gap, margin } = options;
1771
1898
  const [position, setPosition] = useState(null);
1772
1899
  useLayoutEffect(() => {
1773
1900
  if (!open) {
@@ -1784,7 +1911,7 @@ function useAnchoredPosition(options) {
1784
1911
  const width = panel?.offsetWidth ?? 0;
1785
1912
  const height = panel?.offsetHeight ?? 0;
1786
1913
  let left = rect.left;
1787
- let top = rect.bottom + gap;
1914
+ let top = side === "top" ? rect.top - gap - height : rect.bottom + gap;
1788
1915
  if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
1789
1916
  if (height > 0) top = Math.min(Math.max(top, margin), window.innerHeight - height - margin);
1790
1917
  /* v8 ignore stop */
@@ -1811,6 +1938,7 @@ function useAnchoredPosition(options) {
1811
1938
  open,
1812
1939
  anchorRef,
1813
1940
  panelRef,
1941
+ side,
1814
1942
  gap,
1815
1943
  margin
1816
1944
  ]);
@@ -1827,12 +1955,14 @@ function useAnchoredPosition(options) {
1827
1955
  * @param root - element containing both the trigger and the open surface.
1828
1956
  * @param open - whether the surface is showing; false detaches the listener.
1829
1957
  * @param setOpen - state setter invoked with false on an outside pointerdown.
1958
+ * @param portal - surface portaled outside the root (a `document.body` dialog)
1959
+ * that also counts as inside; omit when the root contains the whole popover.
1830
1960
  */
1831
- function useDismissOnOutsidePointer(root, open, setOpen) {
1961
+ function useDismissOnOutsidePointer(root, open, setOpen, portal) {
1832
1962
  useEffect(() => {
1833
1963
  if (!open) return;
1834
1964
  const closeOutside = (event) => {
1835
- if (event.target instanceof Node && !root.current?.contains(event.target)) setOpen(false);
1965
+ if (event.target instanceof Node && root.current?.contains(event.target) !== true && portal?.current?.contains(event.target) !== true) setOpen(false);
1836
1966
  };
1837
1967
  document.addEventListener("pointerdown", closeOutside);
1838
1968
  return () => {
@@ -1841,7 +1971,8 @@ function useDismissOnOutsidePointer(root, open, setOpen) {
1841
1971
  }, [
1842
1972
  root,
1843
1973
  open,
1844
- setOpen
1974
+ setOpen,
1975
+ portal
1845
1976
  ]);
1846
1977
  }
1847
1978
  //#endregion
@@ -1888,11 +2019,11 @@ async function writeClipboard(text) {
1888
2019
  * @param props.disabled - suppress opening; turning true closes an open card.
1889
2020
  * @param props.copyText - optional primary value copied by activation and
1890
2021
  * included in the card's accessible name.
1891
- * @param props.copyLabel - accessible activation-label prefix (default "复制").
1892
- * @param props.copiedLabel - visible success label (default "复制成功").
2022
+ * @param props.copyLabel - localized accessible activation-label prefix.
2023
+ * @param props.copiedLabel - localized visible success label.
1893
2024
  * @returns anchor wrapper with the conditional portaled card.
1894
2025
  */
1895
- function HoverCard({ anchor, content, openDelayMs = 500, disabled = false, copyText, copyLabel = "复制", copiedLabel = "复制成功" }) {
2026
+ function HoverCard({ anchor, content, openDelayMs = 500, disabled = false, copyText, copyLabel, copiedLabel }) {
1896
2027
  const rootRef = useRef(null);
1897
2028
  const cardRef = useRef(null);
1898
2029
  const timerRef = useRef(null);
@@ -2058,23 +2189,20 @@ function HoverCard({ anchor, content, openDelayMs = 500, disabled = false, copyT
2058
2189
  //#endregion
2059
2190
  //#region lib/types/Modal.js
2060
2191
  /**
2061
- * Render a centered modal over a blurred page mask.
2192
+ * Render a centered, body-portaled modal over a blurred page mask.
2062
2193
  * @param props.open - whether the dialog is showing.
2063
2194
  * @param props.onClose - Escape or mask click.
2064
2195
  * @param props.title - dialog heading (aria-label in every mode).
2065
- * @param props.closeLabel - accessible close-button label.
2196
+ * @param props.closeLabel - localized accessible close-button label.
2066
2197
  * @param props.description - optional supporting sentence under the title.
2067
2198
  * @param props.children - body (inputs, etc.).
2068
2199
  * @param props.footer - action row (Cancel / Create).
2069
2200
  * @param props.contentClassName - optional class for a scrollable content region.
2070
2201
  * @param props.headless - render children directly in the card (no default
2071
- * header/close/body chrome) for dialogs whose figma frame owns its own
2072
- * header structure; mask, card, Escape, and aria-label remain.
2073
- * @param props.closeLabel - close-button aria label; the owner passes
2074
- * localized copy (this package is cordis-free, so copy arrives via props).
2202
+ * header/close/body chrome); mask, card, Escape, and aria-label remain.
2075
2203
  * @returns null when closed; otherwise the overlay tree.
2076
2204
  */
2077
- function Modal({ open, onClose, title, closeLabel = "Close", description, children, footer, className, contentClassName, headless = false }) {
2205
+ function Modal({ open, onClose, title, closeLabel, description, children, footer, className, contentClassName, headless = false }) {
2078
2206
  useEffect(() => {
2079
2207
  if (!open) return;
2080
2208
  const onKeyDown = (e) => {
@@ -2133,8 +2261,8 @@ function Modal({ open, onClose, title, closeLabel = "Close", description, childr
2133
2261
  //#endregion
2134
2262
  //#region lib/types/OnboardingSurface.js
2135
2263
  /**
2136
- * Render the onboarding takeover chrome (mask + opaque stage) around one
2137
- * step's content and keep the application root inert while mounted.
2264
+ * Render a body-portaled onboarding stage and keep the application root inert
2265
+ * while mounted.
2138
2266
  * @param props.children - the step's page content, centered on the stage.
2139
2267
  * @returns the body-portaled overlay tree.
2140
2268
  */
@@ -2169,11 +2297,12 @@ function OnboardingSurface({ children }) {
2169
2297
  * Render one in-page confirmation whose primary action is unavailable until
2170
2298
  * the caller-controlled acknowledgement is checked.
2171
2299
  */
2172
- function RiskConfirmation({ open, title, description, acknowledgeLabel, cancelLabel, confirmLabel, acknowledged, disabled = false, onAcknowledgedChange, onCancel, onConfirm }) {
2300
+ function RiskConfirmation({ open, title, description, acknowledgeLabel, cancelLabel, closeLabel, confirmLabel, acknowledged, disabled = false, onAcknowledgedChange, onCancel, onConfirm }) {
2173
2301
  return jsxs(Modal, {
2174
2302
  open,
2175
2303
  onClose: onCancel,
2176
2304
  title,
2305
+ closeLabel,
2177
2306
  className: css$9.confirmation ?? "",
2178
2307
  contentClassName: css$9.confirmationContent ?? "",
2179
2308
  footer: jsxs(Fragment, { children: [jsx(Button, {
@@ -2209,23 +2338,112 @@ function RiskConfirmation({ open, title, description, acknowledgeLabel, cancelLa
2209
2338
  });
2210
2339
  }
2211
2340
  //#endregion
2212
- //#region lib/types/ConnectionBanner.js
2341
+ //#region lib/types/ConnectionIndicator.js
2213
2342
  /**
2214
- * Render the reconnecting banner.
2215
- * @param props.reconnecting - true while the connection is in backoff/retry.
2216
- * @param props.label - banner text; the owner passes localized copy (this
2217
- * package is cordis-free, so copy arrives via props).
2218
- * @returns the banner, or null when connected.
2343
+ * Render an inline connection-recovery control.
2344
+ * @param props.state - visible outage, retry-attempt, or recovered state.
2345
+ * @param props.disconnectedLabel - localized outage text.
2346
+ * @param props.reconnectLabel - localized action text shown on hover or focus.
2347
+ * @param props.connectingLabel - localized retry text followed by the attempt dots.
2348
+ * @param props.recoveredLabel - localized recovery confirmation.
2349
+ * @param props.reconnectActionLabel - accessible label for the outage action.
2350
+ * @param props.restartActionLabel - accessible label for replacing an active attempt.
2351
+ * @param props.onReconnect - request an immediate reconnect attempt.
2352
+ * @returns the indicator, or null when no connection feedback is active.
2219
2353
  */
2220
- function ConnectionBanner({ reconnecting, label = "连接已断开,正在重连…" }) {
2221
- if (!reconnecting) return null;
2222
- return jsx("div", {
2223
- className: css$10.banner,
2224
- children: label
2354
+ function ConnectionIndicator({ state, disconnectedLabel, reconnectLabel, connectingLabel, recoveredLabel, reconnectActionLabel, restartActionLabel, onReconnect }) {
2355
+ if (state === void 0) return null;
2356
+ const sizeLabels = jsxs(Fragment, { children: [
2357
+ jsx("span", {
2358
+ className: css$10.sizeLabel,
2359
+ "aria-hidden": "true",
2360
+ children: disconnectedLabel
2361
+ }),
2362
+ jsx("span", {
2363
+ className: css$10.sizeLabel,
2364
+ "aria-hidden": "true",
2365
+ children: reconnectLabel
2366
+ }),
2367
+ jsxs("span", {
2368
+ className: css$10.sizeLabel,
2369
+ "aria-hidden": "true",
2370
+ children: [connectingLabel, jsx("span", {
2371
+ className: css$10.dots,
2372
+ children: "..."
2373
+ })]
2374
+ }),
2375
+ jsx("span", {
2376
+ className: css$10.sizeLabel,
2377
+ "aria-hidden": "true",
2378
+ children: recoveredLabel
2379
+ })
2380
+ ] });
2381
+ if (state === "recovered") return jsxs("div", {
2382
+ className: `${css$10.indicator} ${css$10.success}`,
2383
+ role: "status",
2384
+ "aria-label": recoveredLabel,
2385
+ children: [jsx("span", {
2386
+ className: css$10.icon,
2387
+ "aria-hidden": "true",
2388
+ children: jsx(IconCheckOutline16, { size: 14 })
2389
+ }), jsxs("span", {
2390
+ className: css$10.label,
2391
+ children: [sizeLabels, jsx("span", {
2392
+ className: css$10.stateLabel,
2393
+ children: recoveredLabel
2394
+ })]
2395
+ })]
2396
+ });
2397
+ const connecting = state === "connecting";
2398
+ return jsxs("button", {
2399
+ type: "button",
2400
+ className: `${css$10.indicator} ${css$10.warning}`,
2401
+ "data-phase": state,
2402
+ "aria-label": connecting ? restartActionLabel : reconnectActionLabel,
2403
+ onClick: onReconnect,
2404
+ children: [jsx("span", {
2405
+ className: css$10.icon,
2406
+ "aria-hidden": "true",
2407
+ children: jsx(IconWarningOutline16, { size: 14 })
2408
+ }), jsxs("span", {
2409
+ className: css$10.label,
2410
+ children: [
2411
+ sizeLabels,
2412
+ jsx("span", {
2413
+ className: css$10.stateLabel,
2414
+ children: connecting ? jsxs(Fragment, { children: [connectingLabel, jsxs("span", {
2415
+ className: css$10.dots,
2416
+ "aria-hidden": "true",
2417
+ children: [
2418
+ jsx("span", { children: "." }),
2419
+ jsx("span", {
2420
+ className: css$10.secondDot,
2421
+ children: "."
2422
+ }),
2423
+ jsx("span", {
2424
+ className: css$10.thirdDot,
2425
+ children: "."
2426
+ })
2427
+ ]
2428
+ })] }) : disconnectedLabel
2429
+ }),
2430
+ jsx("span", {
2431
+ className: css$10.hoverLabel,
2432
+ children: reconnectLabel
2433
+ })
2434
+ ]
2435
+ })]
2225
2436
  });
2226
2437
  }
2227
2438
  //#endregion
2228
2439
  //#region lib/types/FishLogo.js
2440
+ /** Native viewBox of {@link FISH_LOGO_PATH} (width and height in user units). */
2441
+ const FISH_LOGO_VIEWBOX = {
2442
+ width: 23.16,
2443
+ height: 17.04
2444
+ };
2445
+ /** The fish silhouette path data, exported for consumers that compose their own svg (entrance effects, masks) around the same geometry. */
2446
+ const FISH_LOGO_PATH = "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z";
2229
2447
  /**
2230
2448
  * Render the fish logo.
2231
2449
  * @param props.size - width in px (default 24; height keeps the 23.16:17.04 ratio).
@@ -2235,13 +2453,13 @@ function ConnectionBanner({ reconnecting, label = "连接已断开,正在重
2235
2453
  function FishLogo({ size = 24, className }) {
2236
2454
  return jsx("svg", {
2237
2455
  width: size,
2238
- height: size * 17.04 / 23.16,
2456
+ height: size * FISH_LOGO_VIEWBOX.height / FISH_LOGO_VIEWBOX.width,
2239
2457
  className,
2240
- viewBox: "0 0 23.16 17.04",
2458
+ viewBox: `0 0 ${FISH_LOGO_VIEWBOX.width} ${FISH_LOGO_VIEWBOX.height}`,
2241
2459
  fill: "none",
2242
2460
  "aria-hidden": "true",
2243
2461
  children: jsx("path", {
2244
- d: "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z",
2462
+ d: FISH_LOGO_PATH,
2245
2463
  fill: "currentColor"
2246
2464
  })
2247
2465
  });
@@ -2369,6 +2587,120 @@ function BrandWordmark({ size = 24, className, includeMark = true }) {
2369
2587
  });
2370
2588
  }
2371
2589
  //#endregion
2590
+ //#region lib/types/ReferenceIcon.js
2591
+ /**
2592
+ * Render the icon that identifies one inline reference domain.
2593
+ * @param props - Reference kind, optional size, and optional CSS class.
2594
+ * @returns The corresponding current-color SVG glyph.
2595
+ */
2596
+ function ReferenceIcon({ kind, size = 16, className }) {
2597
+ switch (kind) {
2598
+ case "session": return jsx("svg", {
2599
+ width: size,
2600
+ height: size,
2601
+ className,
2602
+ viewBox: "0 0 16 16",
2603
+ fill: "none",
2604
+ "aria-hidden": true,
2605
+ children: jsx("path", {
2606
+ d: "M8 0.597656C3.91296 0.597656 0.599716 3.91103 0.599609 7.99805C0.599609 9.13171 0.854567 10.2079 1.31152 11.1699L1.59277 11.7607L2.77441 11.1992L2.49414 10.6084L2.36035 10.3076C2.06865 9.59612 1.90723 8.81645 1.90723 7.99805C1.90733 4.63362 4.63554 1.90625 8 1.90625C11.3644 1.90635 14.0917 4.63368 14.0918 7.99805C14.0918 11.3625 11.3644 14.0907 8 14.0908C7.311 14.0908 6.80642 14.0414 6.35938 13.918C5.919 13.7963 5.50105 13.5929 5.00098 13.2441C4.26805 12.7329 3.21756 12.5526 2.35156 13.0996L2.33789 13.1084L2.32422 13.1182L1.74805 13.5234L2.18164 14.8184L3.05957 14.2002C3.37505 14.0068 3.84248 14.0319 4.25195 14.3174C4.84447 14.7307 5.39718 15.009 6.01172 15.1787C6.61963 15.3465 7.25579 15.3984 8 15.3984C12.087 15.3983 15.4004 12.0851 15.4004 7.99805C15.4003 3.9111 12.087 0.59776 8 0.597656ZM4.56836 8.50977V9.80371H8.12402V8.50977H4.56836ZM4.56836 7.30078H11.4619V6.00684H4.56836V7.30078Z",
2607
+ fill: "currentColor"
2608
+ })
2609
+ });
2610
+ case "file": return jsx(IconBrowseOutline16, {
2611
+ size,
2612
+ className
2613
+ });
2614
+ case "folder": return jsx(IconFolderClose16, {
2615
+ size,
2616
+ className
2617
+ });
2618
+ }
2619
+ }
2620
+ //#endregion
2621
+ //#region lib/types/user-text.js
2622
+ /** The wire form a session chip serializes to; label is the display text. */
2623
+ const SESSION_WIRE_RE = /@\[([^\]\n]+)\]\(dsh-session:[^)\s]+\)/gu;
2624
+ /**
2625
+ * Split one sent text into inline plain runs and reference chips.
2626
+ * @param text - the logged model text of the message or queue row.
2627
+ * @param sessionLabels - exact session mention labels associated by an adjacent recall.
2628
+ * @returns inline nodes covering the whole text.
2629
+ */
2630
+ function projectUserText(text, sessionLabels) {
2631
+ const ranges = [];
2632
+ SESSION_WIRE_RE.lastIndex = 0;
2633
+ let wire;
2634
+ while ((wire = SESSION_WIRE_RE.exec(text)) !== null) ranges.push({
2635
+ start: wire.index,
2636
+ end: wire.index + wire[0].length,
2637
+ label: wire[0],
2638
+ kind: "session",
2639
+ display: wire[1]
2640
+ });
2641
+ for (const rawLabel of [...new Set(sessionLabels)].sort((a, b) => b.length - a.length)) {
2642
+ const label = `@${rawLabel}`;
2643
+ let start = text.indexOf(label);
2644
+ while (start >= 0) {
2645
+ ranges.push({
2646
+ start,
2647
+ end: start + label.length,
2648
+ label,
2649
+ kind: "session"
2650
+ });
2651
+ start = text.indexOf(label, start + label.length);
2652
+ }
2653
+ }
2654
+ const re = /(^|\s)(\/[\w-]+|@"[^"\n]+"|@[^\s]+)/gu;
2655
+ let m;
2656
+ while ((m = re.exec(text)) !== null) {
2657
+ const tokenStart = m.index + m[1].length;
2658
+ const rawLabel = m[2];
2659
+ const label = rawLabel.startsWith("@\"") ? rawLabel : rawLabel.replace(/[.,;:!?,。;:!?]+$/gu, "");
2660
+ if (label.length <= 1) continue;
2661
+ ranges.push({
2662
+ start: tokenStart,
2663
+ end: tokenStart + label.length,
2664
+ label,
2665
+ kind: "plain"
2666
+ });
2667
+ }
2668
+ const rankOf = (range) => range.kind === "session" ? 0 : 1;
2669
+ ranges.sort((a, b) => a.start - b.start || rankOf(a) - rankOf(b) || b.end - a.end);
2670
+ const parts = [];
2671
+ let cursor = 0;
2672
+ const pushPlain = (from, to) => {
2673
+ parts.push(jsx("span", {
2674
+ className: css$11.plainRun,
2675
+ children: text.slice(from, to)
2676
+ }, `t${from}`));
2677
+ };
2678
+ for (const range of ranges) {
2679
+ if (range.start < cursor) continue;
2680
+ const { start: tokenStart, end, label, kind } = range;
2681
+ if (tokenStart > cursor) pushPlain(cursor, tokenStart);
2682
+ const referenceKind = kind === "session" ? "session" : label.startsWith("@") ? label.endsWith("/") ? "folder" : "file" : void 0;
2683
+ const displayLabel = range.display ?? (referenceKind === void 0 ? label : referenceKind === "session" ? label.slice(1) : label.slice(1).replace(/^"|"$/gu, "").split(/[\\/]/u).filter(Boolean).at(-1) ?? label.slice(1));
2684
+ parts.push(jsxs("span", {
2685
+ className: css$11.refChip,
2686
+ "data-ref-chip": referenceKind ?? "skill",
2687
+ title: label,
2688
+ children: [referenceKind !== void 0 && jsx(ReferenceIcon, {
2689
+ kind: referenceKind,
2690
+ size: 16,
2691
+ className: css$11.refIcon
2692
+ }), displayLabel]
2693
+ }, tokenStart));
2694
+ cursor = end;
2695
+ }
2696
+ if (parts.length === 0) return jsx("span", {
2697
+ className: css$11.plainRun,
2698
+ children: text
2699
+ });
2700
+ if (cursor < text.length) pushPlain(cursor, text.length);
2701
+ return jsx(Fragment, { children: parts });
2702
+ }
2703
+ //#endregion
2372
2704
  //#region lib/types/Tooltip.js
2373
2705
  /**
2374
2706
  * Attach a hover/focus tooltip to an anchor element.
@@ -2500,7 +2832,7 @@ function Tooltip({ label, side = "right", delayMs = 0, disabled = false, maxWidt
2500
2832
  }
2501
2833
  }), pos !== null && jsx("span", {
2502
2834
  ref: bubble,
2503
- className: css$11.bubble,
2835
+ className: css$12.bubble,
2504
2836
  "data-side": placement,
2505
2837
  style: {
2506
2838
  left: pos.x,
@@ -2513,6 +2845,10 @@ function Tooltip({ label, side = "right", delayMs = 0, disabled = false, maxWidt
2513
2845
  }
2514
2846
  //#endregion
2515
2847
  //#region lib/types/Toast.js
2848
+ /** Full-opacity hold before the fade starts, when the owner names none. */
2849
+ const HOLD_MS = 3e3;
2850
+ /** Fade duration. Must agree with the stylesheet's toast-fade duration. */
2851
+ const FADE_MS = 1e3;
2516
2852
  /**
2517
2853
  * Transient top-center banner: slides in, holds at full opacity, fades out,
2518
2854
  * then reports done so the owner can unmount it. Re-showing the same text
@@ -2521,21 +2857,28 @@ function Tooltip({ label, side = "right", delayMs = 0, disabled = false, maxWidt
2521
2857
  * transformed or filtered ancestor cannot trap the fixed banner in that
2522
2858
  * ancestor's box.
2523
2859
  *
2860
+ * The hold is the owner's to set, because how long a banner has to stay
2861
+ * depends on how much there is to read: a one-line limit lands in the default
2862
+ * window, while a failure that names what broke does not. One value drives
2863
+ * both the unmount timer and the stylesheet's fade delay — the stylesheet
2864
+ * reads it as a custom property — so the two can no longer disagree and leave
2865
+ * the banner unmounting mid-fade.
2524
2866
  * @param props.text - resolved banner copy; the owner passes localized text.
2525
2867
  * @param props.icon - optional leading glyph (e.g. a warning icon).
2868
+ * @param props.holdMs - full-opacity hold before the fade; defaults to 3000.
2526
2869
  * @param props.anchor - optional element whose horizontal center the banner
2527
2870
  * follows (e.g. the composer card, so the banner centers over the chat column
2528
2871
  * rather than the whole window); omitted, it centers on the viewport.
2529
2872
  * @param props.onDone - called once the fade completes; unmount the toast here.
2530
2873
  * @returns the floating banner.
2531
2874
  */
2532
- function Toast({ text, icon, anchor, onDone }) {
2875
+ function Toast({ text, icon, anchor, holdMs = HOLD_MS, onDone }) {
2533
2876
  useEffect(() => {
2534
- const timer = setTimeout(onDone, 4e3);
2877
+ const timer = setTimeout(onDone, holdMs + FADE_MS);
2535
2878
  return () => {
2536
2879
  clearTimeout(timer);
2537
2880
  };
2538
- }, [onDone]);
2881
+ }, [holdMs, onDone]);
2539
2882
  const [left, setLeft] = useState(null);
2540
2883
  useLayoutEffect(() => {
2541
2884
  if (anchor == null) return;
@@ -2550,36 +2893,73 @@ function Toast({ text, icon, anchor, onDone }) {
2550
2893
  };
2551
2894
  }, [anchor]);
2552
2895
  return createPortal(jsxs("div", {
2553
- className: css$12.toast,
2896
+ className: css$13.toast,
2554
2897
  role: "alert",
2555
- style: left === null ? void 0 : { left },
2898
+ style: {
2899
+ ...left === null ? {} : { left },
2900
+ "--dsh-toast-hold": `${String(holdMs)}ms`
2901
+ },
2556
2902
  children: [icon !== void 0 && jsx("span", {
2557
- className: css$12.icon,
2903
+ className: css$13.icon,
2558
2904
  "aria-hidden": true,
2559
2905
  children: icon
2560
2906
  }), jsx("span", {
2561
- className: css$12.text,
2907
+ className: css$13.text,
2562
2908
  children: text
2563
2909
  })]
2564
2910
  }), document.body);
2565
2911
  }
2566
2912
  //#endregion
2913
+ //#region lib/types/relative-time.js
2914
+ /**
2915
+ * Compact relative-time bucketing shared by every surface that dates a
2916
+ * session. Bucketing is here so two surfaces naming the same session agree;
2917
+ * the words stay in each plugin's own dictionary, per locale-owned copy.
2918
+ *
2919
+ * @module @deepseek-ai/dsh-client-ui-primitives/relative-time
2920
+ */
2921
+ /**
2922
+ * Compact relative time, as a structured bucket the renderer localizes
2923
+ * ("now"/"5min"/"3h"/"2d"/"4mo"/"1y" in en).
2924
+ * @param at - epoch ms of the dated moment.
2925
+ * @param now - current epoch ms (injected for pure rendering).
2926
+ * @returns the row's trailing time bucket and magnitude.
2927
+ */
2928
+ function relativeTime(at, now) {
2929
+ const MIN = 6e4;
2930
+ const HOUR = 36e5;
2931
+ const DAY = 864e5;
2932
+ const diff = Math.max(0, now - at);
2933
+ if (diff < MIN) return {
2934
+ unit: "now",
2935
+ n: 0
2936
+ };
2937
+ if (diff < HOUR) return {
2938
+ unit: "minutes",
2939
+ n: Math.floor(diff / MIN)
2940
+ };
2941
+ if (diff < DAY) return {
2942
+ unit: "hours",
2943
+ n: Math.floor(diff / HOUR)
2944
+ };
2945
+ if (diff < 30 * DAY) return {
2946
+ unit: "days",
2947
+ n: Math.floor(diff / DAY)
2948
+ };
2949
+ if (diff < 365 * DAY) return {
2950
+ unit: "months",
2951
+ n: Math.floor(diff / (30 * DAY))
2952
+ };
2953
+ return {
2954
+ unit: "years",
2955
+ n: Math.floor(diff / (365 * DAY))
2956
+ };
2957
+ }
2958
+ //#endregion
2567
2959
  //#region lib/types/JsonTree.js
2568
2960
  const OBJECT_PREVIEW_LIMIT = 4;
2569
2961
  const ARRAY_PREVIEW_LIMIT = 5;
2570
2962
  const PREVIEW_DEPTH_LIMIT = 2;
2571
- const DEFAULT_LABELS$1 = {
2572
- copyValue: "Copy value",
2573
- copyJson: "Copy JSON",
2574
- copyPath: "Copy property path",
2575
- copyPrettyJson: "Copy pretty JSON",
2576
- copyCompactJson: "Copy compact JSON",
2577
- copied: "Copied",
2578
- copyFailed: "Copy failed",
2579
- collapseNode: "Collapse JSON node",
2580
- expandNode: "Expand JSON node",
2581
- copyButtonTitle: (action) => `${action}; right-click for copy options`
2582
- };
2583
2963
  function valueCopyMenuItems(labels) {
2584
2964
  return [
2585
2965
  {
@@ -2624,35 +3004,35 @@ function bracketOf(value) {
2624
3004
  }
2625
3005
  function previewPrimitive(value) {
2626
3006
  if (value === null) return jsx("span", {
2627
- className: css$13.keywordValue,
3007
+ className: css$14.keywordValue,
2628
3008
  children: "null"
2629
3009
  });
2630
3010
  if (typeof value === "string") return jsx("span", {
2631
- className: css$13.stringValue,
3011
+ className: css$14.stringValue,
2632
3012
  children: JSON.stringify(value)
2633
3013
  });
2634
3014
  if (typeof value === "number") return jsx("span", {
2635
- className: css$13.numberValue,
3015
+ className: css$14.numberValue,
2636
3016
  children: String(value)
2637
3017
  });
2638
3018
  if (typeof value === "boolean") return jsx("span", {
2639
- className: css$13.keywordValue,
3019
+ className: css$14.keywordValue,
2640
3020
  children: String(value)
2641
3021
  });
2642
3022
  if (typeof value === "bigint") return jsx("span", {
2643
- className: css$13.otherValue,
3023
+ className: css$14.otherValue,
2644
3024
  children: value.toString()
2645
3025
  });
2646
3026
  if (typeof value === "undefined") return jsx("span", {
2647
- className: css$13.otherValue,
3027
+ className: css$14.otherValue,
2648
3028
  children: "undefined"
2649
3029
  });
2650
3030
  if (typeof value === "symbol") return jsx("span", {
2651
- className: css$13.otherValue,
3031
+ className: css$14.otherValue,
2652
3032
  children: value.description ?? "Symbol"
2653
3033
  });
2654
3034
  if (typeof value === "function") return jsx("span", {
2655
- className: css$13.otherValue,
3035
+ className: css$14.otherValue,
2656
3036
  children: value.name || "Function"
2657
3037
  });
2658
3038
  return null;
@@ -2666,71 +3046,71 @@ function previewValue(value, depth) {
2666
3046
  const [open, close] = bracketOf(value);
2667
3047
  return jsxs(Fragment, { children: [
2668
3048
  jsx("span", {
2669
- className: css$13.punctuation,
3049
+ className: css$14.punctuation,
2670
3050
  children: open
2671
3051
  }),
2672
3052
  depth >= PREVIEW_DEPTH_LIMIT ? jsx("span", {
2673
- className: css$13.previewEllipsis,
3053
+ className: css$14.previewEllipsis,
2674
3054
  children: "…"
2675
3055
  }) : visible.map(([key, item], index) => jsxs("span", { children: [
2676
3056
  index > 0 && jsx("span", {
2677
- className: css$13.punctuation,
3057
+ className: css$14.punctuation,
2678
3058
  children: ", "
2679
3059
  }),
2680
3060
  !array && jsxs(Fragment, { children: [jsx("span", {
2681
- className: css$13.previewProperty,
3061
+ className: css$14.previewProperty,
2682
3062
  children: key
2683
3063
  }), jsx("span", {
2684
- className: css$13.punctuation,
3064
+ className: css$14.punctuation,
2685
3065
  children: ": "
2686
3066
  })] }),
2687
3067
  previewValue(item, depth + 1)
2688
3068
  ] }, key)),
2689
3069
  depth < PREVIEW_DEPTH_LIMIT && entries.length > limit && jsx("span", {
2690
- className: css$13.previewEllipsis,
3070
+ className: css$14.previewEllipsis,
2691
3071
  children: ", …"
2692
3072
  }),
2693
3073
  jsx("span", {
2694
- className: css$13.punctuation,
3074
+ className: css$14.punctuation,
2695
3075
  children: close
2696
3076
  })
2697
3077
  ] });
2698
3078
  }
2699
3079
  function primitiveValue(value) {
2700
3080
  if (value === null) return jsx("span", {
2701
- className: css$13.keywordValue,
3081
+ className: css$14.keywordValue,
2702
3082
  children: "null"
2703
3083
  });
2704
3084
  if (typeof value === "string") return jsx("span", {
2705
- className: css$13.stringValue,
3085
+ className: css$14.stringValue,
2706
3086
  children: JSON.stringify(value)
2707
3087
  });
2708
3088
  if (typeof value === "boolean") return jsx("span", {
2709
- className: css$13.keywordValue,
3089
+ className: css$14.keywordValue,
2710
3090
  children: String(value)
2711
3091
  });
2712
3092
  if (typeof value === "number") return jsx("span", {
2713
- className: css$13.numberValue,
3093
+ className: css$14.numberValue,
2714
3094
  children: String(value)
2715
3095
  });
2716
3096
  if (typeof value === "bigint") return jsx("span", {
2717
- className: css$13.numberValue,
3097
+ className: css$14.numberValue,
2718
3098
  children: `${value.toString()}n`
2719
3099
  });
2720
3100
  if (value instanceof Date) return jsx("span", {
2721
- className: css$13.otherValue,
3101
+ className: css$14.otherValue,
2722
3102
  children: value.toISOString()
2723
3103
  });
2724
3104
  if (typeof value === "function") return jsxs("span", {
2725
- className: css$13.otherValue,
3105
+ className: css$14.otherValue,
2726
3106
  children: ["function() ", "{ }"]
2727
3107
  });
2728
3108
  if (typeof value === "undefined") return jsx("span", {
2729
- className: css$13.otherValue,
3109
+ className: css$14.otherValue,
2730
3110
  children: "undefined"
2731
3111
  });
2732
3112
  return jsx("span", {
2733
- className: css$13.otherValue,
3113
+ className: css$14.otherValue,
2734
3114
  children: value.toString()
2735
3115
  });
2736
3116
  }
@@ -2758,7 +3138,7 @@ function moveFocus(button, direction) {
2758
3138
  function NodeField({ field, expandable, onToggle }) {
2759
3139
  if (field === void 0) return null;
2760
3140
  return jsxs("span", {
2761
- className: clsx(css$13.label, expandable && css$13.clickableLabel),
3141
+ className: clsx(css$14.label, expandable && css$14.clickableLabel),
2762
3142
  onClick: expandable ? onToggle : void 0,
2763
3143
  children: [fieldText(field), ":"]
2764
3144
  });
@@ -2787,7 +3167,7 @@ function JsonTreeNode({ field, initialExpanded, labels, lastElement, onClaimTabS
2787
3167
  }
2788
3168
  };
2789
3169
  const row = (children, ariaExpanded) => jsx("div", {
2790
- className: css$13.row,
3170
+ className: css$14.row,
2791
3171
  role: "treeitem",
2792
3172
  "aria-expanded": ariaExpanded,
2793
3173
  onMouseOver: (event) => {
@@ -2807,7 +3187,7 @@ function JsonTreeNode({ field, initialExpanded, labels, lastElement, onClaimTabS
2807
3187
  }),
2808
3188
  primitiveValue(value),
2809
3189
  !lastElement && jsx("span", {
2810
- className: css$13.punctuation,
3190
+ className: css$14.punctuation,
2811
3191
  children: ","
2812
3192
  })
2813
3193
  ] }));
@@ -2819,22 +3199,22 @@ function JsonTreeNode({ field, initialExpanded, labels, lastElement, onClaimTabS
2819
3199
  onToggle: toggle
2820
3200
  }),
2821
3201
  jsx("span", {
2822
- className: css$13.punctuation,
3202
+ className: css$14.punctuation,
2823
3203
  children: open
2824
3204
  }),
2825
3205
  jsx("span", {
2826
- className: css$13.punctuation,
3206
+ className: css$14.punctuation,
2827
3207
  children: close
2828
3208
  }),
2829
3209
  !lastElement && jsx("span", {
2830
- className: css$13.punctuation,
3210
+ className: css$14.punctuation,
2831
3211
  children: ","
2832
3212
  })
2833
3213
  ] }));
2834
3214
  return row(jsxs(Fragment, { children: [
2835
3215
  jsx("span", {
2836
3216
  ref: expanderRef,
2837
- className: clsx(css$13.expander, expanded ? css$13.collapseIcon : css$13.expandIcon),
3217
+ className: clsx(css$14.expander, expanded ? css$14.collapseIcon : css$14.expandIcon),
2838
3218
  "data-json-expander": true,
2839
3219
  role: "button",
2840
3220
  "aria-label": expanded ? labels.collapseNode : labels.expandNode,
@@ -2853,17 +3233,17 @@ function JsonTreeNode({ field, initialExpanded, labels, lastElement, onClaimTabS
2853
3233
  onToggle: toggle
2854
3234
  }),
2855
3235
  jsx("span", {
2856
- className: css$13.preview,
3236
+ className: css$14.preview,
2857
3237
  children: previewValue(value, 0)
2858
3238
  }),
2859
3239
  !lastElement && jsx("span", {
2860
- className: css$13.punctuation,
3240
+ className: css$14.punctuation,
2861
3241
  children: ","
2862
3242
  }),
2863
3243
  expanded && jsx("ul", {
2864
3244
  id: contentsId,
2865
3245
  role: "group",
2866
- className: css$13.children,
3246
+ className: css$14.children,
2867
3247
  children: entries.map(([key, item], index) => jsx(JsonTreeNode, {
2868
3248
  field: key,
2869
3249
  value: item,
@@ -2900,11 +3280,7 @@ function copyText$2(target, mode) {
2900
3280
  * @param props - Parsed data, accessible label, and display options.
2901
3281
  * @returns A read-only JSON tree with an optionally fixed-open top level.
2902
3282
  */
2903
- function JsonTree({ data, label = "JSON", className, copyable = true, expandTopLevel = true, labels }) {
2904
- const copyLabels = useMemo(() => labels === void 0 ? DEFAULT_LABELS$1 : {
2905
- ...DEFAULT_LABELS$1,
2906
- ...labels
2907
- }, [labels]);
3283
+ function JsonTree({ data, label, className, copyable = true, expandTopLevel = true, labels }) {
2908
3284
  const rootEntries = entriesOf(data);
2909
3285
  const firstExpandableIndex = rootEntries.findIndex(([, value]) => isExpandableValue(value) && entriesOf(value).length > 0);
2910
3286
  const firstExpandableEntry = rootEntries[firstExpandableIndex];
@@ -3025,20 +3401,20 @@ function JsonTree({ data, label = "JSON", className, copyable = true, expandTopL
3025
3401
  const [rootOpen, rootClose] = bracketOf(data);
3026
3402
  const copyTargetIsObject = typeof copyTarget?.value === "object" && copyTarget.value !== null;
3027
3403
  const defaultCopyMode = copyTargetIsObject ? "prettyJson" : "value";
3028
- const copyTitle = copyState === "copied" ? copyLabels.copied : copyState === "failed" ? copyLabels.copyFailed : copyTargetIsObject ? copyLabels.copyPrettyJson : copyLabels.copyValue;
3404
+ const copyTitle = copyState === "copied" ? labels.copied : copyState === "failed" ? labels.copyFailed : copyTargetIsObject ? labels.copyPrettyJson : labels.copyValue;
3029
3405
  return jsxs("div", {
3030
3406
  ref: rootRef,
3031
- className: clsx(css$13.root, className),
3407
+ className: clsx(css$14.root, className),
3032
3408
  onMouseOver: handleRootMouseOver,
3033
3409
  onMouseLeave: () => {
3034
3410
  if (!copyMenuOpenRef.current) clearCopyTarget();
3035
3411
  },
3036
3412
  onScroll: handleScroll,
3037
3413
  children: [expandTopLevel ? jsxs("div", {
3038
- className: css$13.expandedTopLevel,
3414
+ className: css$14.expandedTopLevel,
3039
3415
  children: [
3040
3416
  jsx("div", {
3041
- className: clsx(css$13.row, css$13.topLevelBracket),
3417
+ className: clsx(css$14.row, css$14.topLevelBracket),
3042
3418
  "data-json-root-row": true,
3043
3419
  onMouseOver: (event) => {
3044
3420
  event.stopPropagation();
@@ -3048,19 +3424,19 @@ function JsonTree({ data, label = "JSON", className, copyable = true, expandTopL
3048
3424
  });
3049
3425
  },
3050
3426
  children: jsx("span", {
3051
- className: css$13.punctuation,
3427
+ className: css$14.punctuation,
3052
3428
  children: rootOpen
3053
3429
  })
3054
3430
  }),
3055
3431
  jsx("div", {
3056
3432
  "aria-label": label,
3057
- className: clsx(css$13.container, css$13.expandedTopLevelContainer),
3433
+ className: clsx(css$14.container, css$14.expandedTopLevelContainer),
3058
3434
  role: "tree",
3059
3435
  children: rootEntries.map(([key, value], index) => jsx(JsonTreeNode, {
3060
3436
  field: key,
3061
3437
  value,
3062
3438
  path: [Array.isArray(data) ? index : key],
3063
- labels: copyLabels,
3439
+ labels,
3064
3440
  lastElement: index === rootEntries.length - 1,
3065
3441
  initialExpanded: false,
3066
3442
  tabStopId,
@@ -3069,21 +3445,21 @@ function JsonTree({ data, label = "JSON", className, copyable = true, expandTopL
3069
3445
  }, key))
3070
3446
  }),
3071
3447
  jsx("div", {
3072
- className: clsx(css$13.row, css$13.topLevelBracket),
3448
+ className: clsx(css$14.row, css$14.topLevelBracket),
3073
3449
  children: jsx("span", {
3074
- className: css$13.punctuation,
3450
+ className: css$14.punctuation,
3075
3451
  children: rootClose
3076
3452
  })
3077
3453
  })
3078
3454
  ]
3079
3455
  }) : jsx("div", {
3080
3456
  "aria-label": label,
3081
- className: css$13.container,
3457
+ className: css$14.container,
3082
3458
  role: "tree",
3083
3459
  children: jsx(JsonTreeNode, {
3084
3460
  value: data,
3085
3461
  path: [],
3086
- labels: copyLabels,
3462
+ labels,
3087
3463
  lastElement: true,
3088
3464
  initialExpanded: true,
3089
3465
  tabStopId,
@@ -3091,7 +3467,7 @@ function JsonTree({ data, label = "JSON", className, copyable = true, expandTopL
3091
3467
  onRowHover: handleRowHover
3092
3468
  })
3093
3469
  }), copyTarget !== void 0 && jsx("span", {
3094
- className: css$13.copyAnchor,
3470
+ className: css$14.copyAnchor,
3095
3471
  style: {
3096
3472
  left: copyTarget.left,
3097
3473
  top: copyTarget.top
@@ -3105,11 +3481,11 @@ function JsonTree({ data, label = "JSON", className, copyable = true, expandTopL
3105
3481
  anchor: jsx("button", {
3106
3482
  ref: copyButtonRef,
3107
3483
  type: "button",
3108
- className: css$13.copyButton,
3484
+ className: css$14.copyButton,
3109
3485
  "data-json-copy-button": true,
3110
3486
  "data-state": copyState,
3111
3487
  "aria-label": copyTitle,
3112
- title: copyLabels.copyButtonTitle(copyTitle),
3488
+ title: labels.copyButtonTitle(copyTitle),
3113
3489
  onClick: () => void copy(defaultCopyMode),
3114
3490
  onContextMenu: (event) => {
3115
3491
  event.preventDefault();
@@ -3119,7 +3495,7 @@ function JsonTree({ data, label = "JSON", className, copyable = true, expandTopL
3119
3495
  },
3120
3496
  children: copyState === "copied" ? jsx(IconCheckOutline16, { size: 12 }) : jsx(IconCopyOutline16, { size: 12 })
3121
3497
  }),
3122
- items: copyTargetIsObject ? objectCopyMenuItems(copyLabels) : valueCopyMenuItems(copyLabels),
3498
+ items: copyTargetIsObject ? objectCopyMenuItems(labels) : valueCopyMenuItems(labels),
3123
3499
  onSelect: (id) => {
3124
3500
  copy(id);
3125
3501
  copyMenuOpenRef.current = false;
@@ -3572,26 +3948,8 @@ function useCopyFeedback(text) {
3572
3948
  }
3573
3949
  //#endregion
3574
3950
  //#region lib/types/TerminalBlock.js
3575
- /**
3576
- * Output lines shown before the height cap collapses the middle. Matches the
3577
- * TUI transcript's default tool-output budget so both front ends cut a long
3578
- * command's output at the same place.
3579
- */
3951
+ /** Output lines shown before the height cap collapses the middle. */
3580
3952
  const DEFAULT_TERMINAL_MAX_LINES = 16;
3581
- const DEFAULT_LABELS = {
3582
- signal: (signal) => `信号 ${signal}`,
3583
- exitCode: (exitCode) => `退出码 ${exitCode}`,
3584
- running: "运行中",
3585
- failed: "失败",
3586
- done: "已完成",
3587
- copy: "复制",
3588
- copied: "复制成功",
3589
- noOutput: "无输出",
3590
- collapseAria: "收起输出",
3591
- collapse: "收起",
3592
- expandAria: (hidden) => `展开其余 ${hidden} 行输出`,
3593
- expand: (hidden) => `… 其余 ${hidden} 行`
3594
- };
3595
3953
  /**
3596
3954
  * Prompt label for a working directory: `~` for the home directory itself,
3597
3955
  * otherwise the path's last segment (both separators accepted, trailing
@@ -3656,7 +4014,7 @@ function runState(running, exitCode, signal, labels) {
3656
4014
  * @param line - the line's styled runs.
3657
4015
  * @returns the line's children.
3658
4016
  */
3659
- function renderLine(line) {
4017
+ function renderLine$1(line) {
3660
4018
  return line.map((span, index) => span.style === void 0 ? span.text : jsx("span", {
3661
4019
  style: span.style,
3662
4020
  children: span.text
@@ -3668,10 +4026,7 @@ function renderLine(line) {
3668
4026
  * @returns the terminal block element.
3669
4027
  */
3670
4028
  function TerminalBlock({ command, cwd, home, output, exitCode, signal, running = false, maxLines = 16, className, labels }) {
3671
- const copy = useMemo(() => labels === void 0 ? DEFAULT_LABELS : {
3672
- ...DEFAULT_LABELS,
3673
- ...labels
3674
- }, [labels]);
4029
+ const copy = labels;
3675
4030
  const text = output ?? "";
3676
4031
  const lines = useMemo(() => {
3677
4032
  const parsed = parseAnsiLines(text);
@@ -3691,73 +4046,90 @@ function TerminalBlock({ command, cwd, home, output, exitCode, signal, running =
3691
4046
  const empty = lines.every((line) => line.every((span) => span.text.trim() === ""));
3692
4047
  const { hidden, capped, headLines, tailLines } = headTailCap(lines.length, maxLines, expanded);
3693
4048
  return jsxs("div", {
3694
- className: clsx(css$14.block, className),
4049
+ className: clsx(css$15.block, className),
3695
4050
  "data-terminal": "",
3696
4051
  "data-running": running ? "" : void 0,
3697
4052
  children: [jsxs("div", {
3698
- className: css$14.header,
4053
+ className: css$15.header,
3699
4054
  children: [
3700
4055
  jsxs("div", {
3701
- className: css$14.prompt,
4056
+ className: css$15.prompt,
3702
4057
  children: [jsx("span", {
3703
- className: css$14.runStateLabel,
4058
+ className: css$15.runStateLabel,
3704
4059
  children: state.label
3705
4060
  }), commandLines.map((line, index) => jsxs("div", {
3706
- className: css$14.promptLine,
4061
+ className: css$15.promptLine,
3707
4062
  children: [
3708
4063
  index === 0 && jsx(StateDot, {
3709
4064
  state: state.state,
3710
- className: css$14.runState
4065
+ className: css$15.runState
3711
4066
  }),
3712
4067
  jsx("span", {
3713
- className: css$14.cwd,
4068
+ className: css$15.cwd,
3714
4069
  children: index > 0 || cwd === void 0 ? "$" : promptLabel(cwd, home)
3715
4070
  }),
3716
4071
  jsx("span", {
3717
- className: css$14.command,
4072
+ className: css$15.command,
3718
4073
  children: line
3719
4074
  })
3720
4075
  ]
3721
4076
  }, index))]
3722
4077
  }),
3723
4078
  status !== void 0 && jsx(Pill, {
3724
- className: css$14.status,
4079
+ className: css$15.status,
3725
4080
  children: status
3726
4081
  }),
3727
4082
  !running && !empty && jsx("button", {
3728
4083
  type: "button",
3729
- className: css$14.copyButton,
4084
+ className: css$15.copyButton,
3730
4085
  onClick: onCopy,
3731
4086
  children: copied ? copy.copied : copy.copy
3732
4087
  })
3733
4088
  ]
3734
4089
  }), !running && (empty ? jsx("div", {
3735
- className: css$14.empty,
4090
+ className: css$15.empty,
3736
4091
  children: copy.noOutput
3737
4092
  }) : jsxs("div", {
3738
- className: css$14.output,
4093
+ className: css$15.output,
3739
4094
  children: [
3740
4095
  (capped ? lines.slice(0, headLines) : lines).map((line, index) => jsx("div", {
3741
- className: css$14.line,
3742
- children: renderLine(line)
4096
+ className: css$15.line,
4097
+ children: renderLine$1(line)
3743
4098
  }, index)),
3744
4099
  hidden > 0 && jsx("button", {
3745
4100
  type: "button",
3746
- className: css$14.expand,
4101
+ className: css$15.expand,
3747
4102
  "aria-expanded": expanded,
3748
4103
  "aria-label": expanded ? copy.collapseAria : copy.expandAria(hidden),
3749
4104
  onClick: onToggle,
3750
4105
  children: expanded ? copy.collapse : copy.expand(hidden)
3751
4106
  }),
3752
4107
  capped && lines.slice(lines.length - tailLines).map((line, index) => jsx("div", {
3753
- className: css$14.line,
3754
- children: renderLine(line)
4108
+ className: css$15.line,
4109
+ children: renderLine$1(line)
3755
4110
  }, index))
3756
4111
  ]
3757
4112
  }))]
3758
4113
  });
3759
4114
  }
3760
4115
  //#endregion
4116
+ //#region lib/types/FoldToggle.js
4117
+ /**
4118
+ * Render the shared head-tail fold control with caller-owned localized copy.
4119
+ * @param props - Fold state, localized labels, and toggle callback.
4120
+ * @returns The accessible expand or collapse button.
4121
+ */
4122
+ function FoldToggle({ className, expanded, hidden, labels, onToggle }) {
4123
+ return jsx("button", {
4124
+ type: "button",
4125
+ className,
4126
+ "aria-expanded": expanded,
4127
+ "aria-label": expanded ? labels.collapseAria : labels.expandAria(hidden),
4128
+ onClick: onToggle,
4129
+ children: expanded ? labels.collapse : labels.expand(hidden)
4130
+ });
4131
+ }
4132
+ //#endregion
3761
4133
  //#region lib/types/markdown/highlight.js
3762
4134
  /**
3763
4135
  * The client's ONE syntax highlighter: a synchronous fine-grained shiki core
@@ -3881,6 +4253,14 @@ const LANG_ALIASES = new Map([
3881
4253
  ["xml", "xml"],
3882
4254
  ["lua", "lua"]
3883
4255
  ]);
4256
+ /**
4257
+ * Whether a language hint can use the shared syntax highlighter.
4258
+ * @param lang - Language hint from a code surface.
4259
+ * @returns Whether the hint resolves to a supported grammar.
4260
+ */
4261
+ function supportsHighlighting(lang) {
4262
+ return lang !== void 0 && LANG_ALIASES.has(lang.toLowerCase());
4263
+ }
3884
4264
  /** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
3885
4265
  const cssVariablesTheme = createCssVariablesTheme({
3886
4266
  name: "css-variables",
@@ -4008,6 +4388,153 @@ function highlightToHtml(code, lang) {
4008
4388
  theme: "css-variables"
4009
4389
  });
4010
4390
  }
4391
+ /** vscode-textmate FontStyle bits shiki folds into `text-decoration` values. */
4392
+ const DECORATION_BITS = [[4, "underline"], [8, "line-through"]];
4393
+ /**
4394
+ * The inline style shiki's HTML arm assigns one token (`getTokenStyleObject`
4395
+ * mirrored onto React style keys): the css-variables color plus the
4396
+ * vscode-textmate font-style bits the theme lets through — italic (1), bold
4397
+ * (2), and the {@link DECORATION_BITS} decorations (the theme injects bold,
4398
+ * italic, and underline rules for markup scopes, so markdown fences carry
4399
+ * them). The theme has no per-scope backgrounds, so `background-color` never
4400
+ * occurs; the arm-parity tests fail loud if a shiki upgrade changes that.
4401
+ */
4402
+ function spanStyle(token) {
4403
+ const style = { color: token.color };
4404
+ /* v8 ignore next -- fontStyle is optional in ThemedToken's type; tokenizeWithTheme always stamps it. */
4405
+ const bits = token.fontStyle ?? 0;
4406
+ if ((bits & 1) !== 0) style.fontStyle = "italic";
4407
+ if ((bits & 2) !== 0) style.fontWeight = "bold";
4408
+ const decorations = DECORATION_BITS.filter(([bit]) => (bits & bit) !== 0);
4409
+ if (decorations.length > 0) style.textDecoration = decorations.map(([, value]) => value).join(" ");
4410
+ return style;
4411
+ }
4412
+ /**
4413
+ * Narrow one tokenized line to the runs a `<span style>` renders, folding a
4414
+ * whitespace-only run into the token that follows it — shiki's default
4415
+ * `mergeWhitespaces` HTML behavior — with each run styled through
4416
+ * {@link spanStyle}, so the streaming spans and the settled `codeToHtml`
4417
+ * swap render one identical span tree. shiki exempts underlined/struck
4418
+ * whitespace from the fold; under the css-variables theme that case cannot
4419
+ * occur — its only underline rule styles inline-link scopes, whose spaced
4420
+ * text tokenizes as one run, and it injects no strikethrough rule — so the
4421
+ * unconditional fold here stays equivalent (the markdown arm-parity test
4422
+ * pins it). A line-trailing whitespace-only run has no follower and keeps
4423
+ * its own span, as in shiki.
4424
+ */
4425
+ function lineSpans(line) {
4426
+ const spans = [];
4427
+ let pendingWhitespace = "";
4428
+ for (const [index, token] of line.entries()) {
4429
+ if (/^\s+$/.test(token.content) && index + 1 < line.length) {
4430
+ pendingWhitespace += token.content;
4431
+ continue;
4432
+ }
4433
+ spans.push({
4434
+ text: pendingWhitespace + token.content,
4435
+ style: spanStyle(token)
4436
+ });
4437
+ pendingWhitespace = "";
4438
+ }
4439
+ return spans;
4440
+ }
4441
+ /**
4442
+ * Incremental highlighter for one growing streaming fence. TextMate
4443
+ * tokenization is line-based and forward-only — a line's tokens depend only on
4444
+ * its own text and the grammar state entering it — so appended text never
4445
+ * changes a completed line's tokens. The session caches the spans of every
4446
+ * completed line together with the grammar state after them;
4447
+ * {@link updateFrame} reports only newly completed lines plus the still-growing
4448
+ * last line, while {@link update} materializes the complete compatibility
4449
+ * result. Per-call tokenization cost therefore excludes the completed prefix,
4450
+ * and the result equals a from-scratch tokenization of the same code.
4451
+ * Non-append input and a change of resolved grammar reset the cache and
4452
+ * re-tokenize fully, so any input stays correct.
4453
+ */
4454
+ var StreamingHighlightSession = class {
4455
+ /** Grammar id the cache was built with; a different resolution resets it. */
4456
+ resolved;
4457
+ /** Newline-terminated source prefix covered by {@link spans}. */
4458
+ prefix = "";
4459
+ /** Cached spans, one entry per completed line of {@link prefix}. */
4460
+ spans = [];
4461
+ /** Grammar state after {@link prefix}; undefined = the grammar's initial state. */
4462
+ state;
4463
+ lastCode;
4464
+ lastLang;
4465
+ lastResult;
4466
+ generation = 0;
4467
+ lastFrame;
4468
+ reset(resolved) {
4469
+ this.resolved = resolved;
4470
+ this.prefix = "";
4471
+ this.spans = [];
4472
+ this.state = void 0;
4473
+ this.generation += 1;
4474
+ this.lastFrame = void 0;
4475
+ }
4476
+ /** Tokenize `text` with `resolved`, resuming from the cached grammar state when one exists. */
4477
+ tokenize(resolved, text) {
4478
+ return highlighter().codeToTokensBase(text, {
4479
+ lang: resolved,
4480
+ theme: "css-variables",
4481
+ ...this.state === void 0 ? {} : { grammarState: this.state }
4482
+ });
4483
+ }
4484
+ /**
4485
+ * Tokenize one update as a delta for a retained renderer.
4486
+ * @param code - the fence text accumulated so far.
4487
+ * @param lang - the language hint.
4488
+ * @returns Newly completed lines plus the current tail, or `undefined` for the plain arm.
4489
+ */
4490
+ updateFrame(code, lang) {
4491
+ if (code === this.lastCode && lang === this.lastLang && this.lastFrame !== void 0) return this.lastFrame;
4492
+ this.lastCode = code;
4493
+ this.lastLang = lang;
4494
+ this.lastResult = void 0;
4495
+ const resolved = lang === void 0 ? void 0 : LANG_ALIASES.get(lang.toLowerCase());
4496
+ if (resolved === void 0 || !ensureGrammar(resolved)) {
4497
+ this.reset(void 0);
4498
+ return;
4499
+ }
4500
+ if (resolved !== this.resolved || !code.startsWith(this.prefix)) this.reset(resolved);
4501
+ const firstNewLine = this.spans.length;
4502
+ const rest = code.slice(this.prefix.length);
4503
+ const lastNewline = rest.lastIndexOf("\n");
4504
+ if (lastNewline >= 0) {
4505
+ const grownEnd = rest[lastNewline - 1] === "\r" ? lastNewline - 1 : lastNewline;
4506
+ const tokens = this.tokenize(resolved, rest.slice(0, grownEnd));
4507
+ for (const line of tokens) this.spans.push(lineSpans(line));
4508
+ this.state = highlighter().getLastGrammarState(tokens);
4509
+ this.prefix = code.slice(0, this.prefix.length + lastNewline + 1);
4510
+ }
4511
+ this.lastFrame = {
4512
+ generation: this.generation,
4513
+ appended: this.spans.slice(firstNewLine),
4514
+ tail: this.tokenize(resolved, rest.slice(lastNewline + 1)).map(lineSpans)
4515
+ };
4516
+ return this.lastFrame;
4517
+ }
4518
+ /**
4519
+ * Tokenize the fence's current text into per-line highlighted runs;
4520
+ * `undefined` means the caller renders its plain fallback. Idempotent per
4521
+ * (`code`, `lang`) input — repeated calls return the identical result array —
4522
+ * and a retained line keeps its span-array identity across growing calls, so
4523
+ * a React caller can reuse cached line elements. A lazy grammar not yet
4524
+ * loaded returns `undefined` and loads in the background exactly as
4525
+ * {@link highlightToHtml} does; the next call after it registers highlights.
4526
+ * @param code - the fence text accumulated so far (display-trimmed, no synthetic trailing newline).
4527
+ * @param lang - the language hint (a markdown fence info string).
4528
+ * @returns one entry per line of `code` (each an array of runs), or `undefined` for unknown or not-yet-loaded languages.
4529
+ */
4530
+ update(code, lang) {
4531
+ if (code === this.lastCode && lang === this.lastLang && this.lastResult !== void 0) return this.lastResult;
4532
+ const frame = this.updateFrame(code, lang);
4533
+ if (frame === void 0) return void 0;
4534
+ this.lastResult = [...this.spans, ...frame.tail];
4535
+ return this.lastResult;
4536
+ }
4537
+ };
4011
4538
  /**
4012
4539
  * Tokenize `code` into per-line highlighted runs when `lang` maps to a
4013
4540
  * registered grammar; `undefined` means the caller renders its plain fallback.
@@ -4016,8 +4543,9 @@ function highlightToHtml(code, lang) {
4016
4543
  * so this returns shiki's own 2D line/token structure narrowed to what a run
4017
4544
  * renders. Each run's color is a `--shiki-*` custom property, keeping token
4018
4545
  * colors on the theme package's sheets exactly as the HTML path does; the
4019
- * css-variables theme carries no font-style bits, matching that path's
4020
- * color-only output. The trailing newline shiki appends as a final empty line
4546
+ * markup font-style bits the theme lets through (bold/italic/underline in
4547
+ * markdown scopes) are dropped — the line-numbered file view renders
4548
+ * color-only runs. The trailing newline shiki appends as a final empty line
4021
4549
  * is dropped so the run count matches the caller's own line array.
4022
4550
  * @param code - the source text.
4023
4551
  * @param lang - the language hint (a file-extension-derived language id).
@@ -4038,6 +4566,73 @@ function highlightLines(code, lang) {
4038
4566
  })));
4039
4567
  }
4040
4568
  //#endregion
4569
+ //#region lib/types/markdown/useViewportHighlighting.js
4570
+ const noop = () => {};
4571
+ /** One document-wide observer; activated elements leave it permanently. */
4572
+ var HighlightViewport = class {
4573
+ observer;
4574
+ activators = /* @__PURE__ */ new Map();
4575
+ observe(element, activate) {
4576
+ if (typeof IntersectionObserver === "undefined") {
4577
+ activate();
4578
+ return noop;
4579
+ }
4580
+ this.observer ??= new IntersectionObserver((entries) => {
4581
+ for (const entry of entries) {
4582
+ if (!entry.isIntersecting) continue;
4583
+ const current = this.activators.get(entry.target);
4584
+ /* v8 ignore next -- the observer reports only elements still registered with it. */
4585
+ if (current === void 0) continue;
4586
+ this.activators.delete(entry.target);
4587
+ this.observer?.unobserve(entry.target);
4588
+ current();
4589
+ }
4590
+ this.releaseEmptyObserver();
4591
+ });
4592
+ this.activators.set(element, activate);
4593
+ this.observer.observe(element);
4594
+ return () => {
4595
+ this.activators.delete(element);
4596
+ this.observer?.unobserve(element);
4597
+ this.releaseEmptyObserver();
4598
+ };
4599
+ }
4600
+ releaseEmptyObserver() {
4601
+ if (this.activators.size > 0) return;
4602
+ this.observer?.disconnect();
4603
+ this.observer = void 0;
4604
+ }
4605
+ };
4606
+ const highlightViewport = new HighlightViewport();
4607
+ /**
4608
+ * Activate one supported code surface when it first intersects the viewport.
4609
+ * Activation lasts for the component lifetime; browsers without
4610
+ * IntersectionObserver activate immediately.
4611
+ * @param target - Code surface whose plain rendering reserves its geometry.
4612
+ * @param lang - Optional language hint.
4613
+ * @returns Whether this component may build highlighted output.
4614
+ */
4615
+ function useViewportHighlighting(target, lang) {
4616
+ const supported = supportsHighlighting(lang);
4617
+ const [activated, setActivated] = useState(false);
4618
+ const activate = useCallback(() => {
4619
+ setActivated(true);
4620
+ }, []);
4621
+ useEffect(() => {
4622
+ if (activated || !supported) return;
4623
+ const element = target.current;
4624
+ /* v8 ignore next -- React attaches the host ref before running effects. */
4625
+ if (element === null) return;
4626
+ return highlightViewport.observe(element, activate);
4627
+ }, [
4628
+ activate,
4629
+ activated,
4630
+ supported,
4631
+ target
4632
+ ]);
4633
+ return activated && supported;
4634
+ }
4635
+ //#endregion
4041
4636
  //#region lib/types/ReadBlock.js
4042
4637
  /**
4043
4638
  * Content lines shown before the height cap collapses the middle. Matches
@@ -4045,13 +4640,6 @@ function highlightLines(code, lang) {
4045
4640
  * same place in the same flow.
4046
4641
  */
4047
4642
  const DEFAULT_READ_MAX_LINES = 16;
4048
- /**
4049
- * Render one line's highlighted runs. The css-variables theme colors every run,
4050
- * so each run is a styled span; a line with no highlighting at all takes the
4051
- * bare-text path in the caller instead (an unknown or absent language).
4052
- * @param spans - the line's styled runs.
4053
- * @returns the line's children.
4054
- */
4055
4643
  function renderSpans(spans) {
4056
4644
  return spans.map((span, index) => jsx("span", {
4057
4645
  style: span.style,
@@ -4064,9 +4652,12 @@ function renderSpans(spans) {
4064
4652
  * @param props - see {@link ReadBlockProps}.
4065
4653
  * @returns the read block element.
4066
4654
  */
4067
- function ReadBlock({ label, lines, totalLines, lang, maxLines = 16, className }) {
4655
+ function ReadBlock({ label, labels, lines, totalLines, lang, maxLines = 16, className }) {
4656
+ const rootRef = useRef(null);
4657
+ const highlighting = useViewportHighlighting(rootRef, lang);
4068
4658
  const raw = useMemo(() => lines.map((line) => line.text).join("\n"), [lines]);
4069
- const highlighted = useMemo(() => highlightLines(raw, lang), [
4659
+ const highlighted = useMemo(() => highlighting ? highlightLines(raw, lang) : void 0, [
4660
+ highlighting,
4070
4661
  raw,
4071
4662
  lang,
4072
4663
  useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount, grammarLoadCount)
@@ -4091,61 +4682,56 @@ function ReadBlock({ label, lines, totalLines, lang, maxLines = 16, className })
4091
4682
  const headLines = Math.ceil(maxLines / 2);
4092
4683
  const tailLines = maxLines - headLines;
4093
4684
  const windowed = lines.length < totalLines;
4094
- /**
4095
- * Render a slice of the line array as gutter-numbered rows.
4096
- * @param slice - the lines to draw, each with its aligned run array.
4097
- * @returns the row elements.
4098
- */
4099
4685
  const rows = (slice) => slice.map(([line, spans]) => jsxs("div", {
4100
- className: css$15.line,
4686
+ className: css$16.line,
4101
4687
  children: [jsx("span", {
4102
- className: css$15.gutter,
4688
+ className: css$16.gutter,
4103
4689
  "aria-hidden": true,
4104
4690
  children: line.number
4105
4691
  }), jsx("span", {
4106
- className: css$15.content,
4692
+ className: css$16.content,
4107
4693
  children: spans === void 0 ? line.text : renderSpans(spans)
4108
4694
  })]
4109
4695
  }, line.number));
4110
4696
  const paired = lines.map((line, index) => [line, highlighted?.[index]]);
4111
4697
  return jsxs("div", {
4112
- className: clsx(css$15.block, className),
4698
+ ref: rootRef,
4699
+ className: clsx(css$16.block, className),
4113
4700
  "data-read": "",
4114
4701
  children: [jsxs("div", {
4115
- className: css$15.banner,
4702
+ className: css$16.banner,
4116
4703
  children: [jsx("div", {
4117
- className: css$15.label,
4704
+ className: css$16.label,
4118
4705
  children: label ?? ""
4119
4706
  }), jsxs("div", {
4120
- className: css$15.action,
4707
+ className: css$16.action,
4121
4708
  children: [
4122
4709
  windowed && jsx("span", {
4123
- className: css$15.count,
4124
- children: `显示 ${lines.length} / ${totalLines} 行`
4710
+ className: css$16.count,
4711
+ children: labels.window(lines.length, totalLines)
4125
4712
  }),
4126
4713
  jsx("span", {
4127
- className: css$15.lang,
4714
+ className: css$16.lang,
4128
4715
  children: lang ?? ""
4129
4716
  }),
4130
4717
  lines.length > 0 && jsx("button", {
4131
4718
  type: "button",
4132
- className: css$15.copyButton,
4719
+ className: css$16.copyButton,
4133
4720
  onClick: onCopy,
4134
- children: copied ? "复制成功" : "复制"
4721
+ children: copied ? labels.copied : labels.copy
4135
4722
  })
4136
4723
  ]
4137
4724
  })]
4138
4725
  }), jsxs("div", {
4139
- className: css$15.body,
4726
+ className: css$16.body,
4140
4727
  children: [
4141
4728
  rows(capped ? paired.slice(0, headLines) : paired),
4142
- hidden > 0 && jsx("button", {
4143
- type: "button",
4144
- className: css$15.expand,
4145
- "aria-expanded": expanded,
4146
- "aria-label": expanded ? "收起内容" : `展开其余 ${hidden} 行`,
4147
- onClick: onToggle,
4148
- children: expanded ? "收起" : `… 其余 ${hidden} 行`
4729
+ hidden > 0 && jsx(FoldToggle, {
4730
+ className: css$16.expand,
4731
+ expanded,
4732
+ hidden,
4733
+ labels,
4734
+ onToggle
4149
4735
  }),
4150
4736
  capped && rows(paired.slice(paired.length - tailLines))
4151
4737
  ]
@@ -4154,11 +4740,7 @@ function ReadBlock({ label, lines, totalLines, lang, maxLines = 16, className })
4154
4740
  }
4155
4741
  //#endregion
4156
4742
  //#region lib/types/DiffBlock.js
4157
- /**
4158
- * Output lines shown before the height cap collapses the middle. Matches
4159
- * {@link DEFAULT_TERMINAL_MAX_LINES} so a diff card and a terminal card cut a
4160
- * long body at the same place.
4161
- */
4743
+ /** Output lines shown before the height cap collapses the middle. */
4162
4744
  const DEFAULT_DIFF_MAX_LINES = 16;
4163
4745
  /** Local exhaustiveness helper — this package does not depend on `dsh-llm`. */
4164
4746
  /* v8 ignore next 3 -- closed-union backstop; only reached if a row kind is forged */
@@ -4167,26 +4749,44 @@ function assertNever(value) {
4167
4749
  }
4168
4750
  /** The dim class per row kind (path/gap chrome vs the diff's own +/- colors). */
4169
4751
  const ROW_CLASS = {
4170
- path: css$16.path,
4171
- del: css$16.del,
4172
- add: css$16.add,
4173
- gap: css$16.gap
4752
+ path: css$17.path,
4753
+ del: css$17.del,
4754
+ add: css$17.add,
4755
+ gap: css$17.gap
4174
4756
  };
4175
4757
  /**
4758
+ * Total added/removed line counts across hunks — the same numbers the footer
4759
+ * prints, exported so a summary row can show them without rebuilding the body.
4760
+ * Every old-side line counts toward `removed` and every new-side line toward
4761
+ * `added`, under {@link contentLines}'s terminator rule.
4762
+ * @param diffs - the hunks to count.
4763
+ * @returns the +/- totals.
4764
+ */
4765
+ function diffTotals(diffs) {
4766
+ let added = 0;
4767
+ let removed = 0;
4768
+ for (const diff of diffs) {
4769
+ if (diff.oldText !== null) removed += contentLines(diff.oldText).length;
4770
+ added += contentLines(diff.newText).length;
4771
+ }
4772
+ return {
4773
+ added,
4774
+ removed
4775
+ };
4776
+ }
4777
+ /**
4176
4778
  * Flatten the hunks into the body's rows plus the footer counts. A path header
4177
4779
  * opens each new file; a same-file second hunk (a scattered edit) opens with a
4178
- * `⋯` gap instead of repeating the path. Every old-side line counts toward
4179
- * `removed` and every new-side line toward `added`. The file count is of
4180
- * DISTINCT paths, matching the TUI diff card's footer, so two hunks in one file
4181
- * read as `1 file` on both front ends.
4780
+ * `⋯` gap instead of repeating the path. The +/- totals are
4781
+ * {@link diffTotals}'s. The file count is of DISTINCT paths, matching the TUI
4782
+ * diff card's footer, so two hunks in one file read as `1 file` on both front
4783
+ * ends.
4182
4784
  * @param diffs - the hunks to render.
4183
4785
  * @returns the body rows, the +/- totals, and the distinct-file count.
4184
4786
  */
4185
4787
  function buildRows(diffs) {
4186
4788
  const rows = [];
4187
4789
  const paths = /* @__PURE__ */ new Set();
4188
- let added = 0;
4189
- let removed = 0;
4190
4790
  let prevPath;
4191
4791
  for (const diff of diffs) {
4192
4792
  paths.add(diff.path);
@@ -4199,25 +4799,18 @@ function buildRows(diffs) {
4199
4799
  text: "⋯"
4200
4800
  });
4201
4801
  prevPath = diff.path;
4202
- if (diff.oldText !== null) for (const line of contentLines(diff.oldText)) {
4203
- rows.push({
4204
- kind: "del",
4205
- text: line
4206
- });
4207
- removed++;
4208
- }
4209
- for (const line of contentLines(diff.newText)) {
4210
- rows.push({
4211
- kind: "add",
4212
- text: line
4213
- });
4214
- added++;
4215
- }
4802
+ if (diff.oldText !== null) for (const line of contentLines(diff.oldText)) rows.push({
4803
+ kind: "del",
4804
+ text: line
4805
+ });
4806
+ for (const line of contentLines(diff.newText)) rows.push({
4807
+ kind: "add",
4808
+ text: line
4809
+ });
4216
4810
  }
4217
4811
  return {
4218
4812
  rows,
4219
- added,
4220
- removed,
4813
+ ...diffTotals(diffs),
4221
4814
  files: paths.size
4222
4815
  };
4223
4816
  }
@@ -4258,7 +4851,7 @@ function copyText$1(rows) {
4258
4851
  * @param props - see {@link DiffBlockProps}.
4259
4852
  * @returns the diff block element.
4260
4853
  */
4261
- function DiffBlock({ diffs, maxLines = 16, className }) {
4854
+ function DiffBlock({ diffs, labels, maxLines = 16, className }) {
4262
4855
  const { rows, added, removed, files } = useMemo(() => buildRows(diffs), [diffs]);
4263
4856
  const [expanded, setExpanded] = useState(false);
4264
4857
  const [copied, setCopied] = useState(false);
@@ -4283,47 +4876,44 @@ function DiffBlock({ diffs, maxLines = 16, className }) {
4283
4876
  const head = capped ? rows.slice(0, headLines) : rows;
4284
4877
  const tail = capped ? rows.slice(rows.length - tailLines) : [];
4285
4878
  return jsxs("div", {
4286
- className: clsx(css$16.block, className),
4879
+ className: clsx(css$17.block, className),
4287
4880
  "data-diff": "",
4288
4881
  children: [
4289
4882
  jsx("button", {
4290
4883
  type: "button",
4291
- className: css$16.copyButton,
4884
+ className: css$17.copyButton,
4292
4885
  onClick: onCopy,
4293
- children: copied ? "复制成功" : "复制"
4886
+ children: copied ? labels.copied : labels.copy
4294
4887
  }),
4295
4888
  jsxs("div", {
4296
- className: css$16.body,
4889
+ className: css$17.body,
4297
4890
  children: [
4298
4891
  head.map((row, index) => jsx("div", {
4299
- className: clsx(css$16.line, ROW_CLASS[row.kind]),
4892
+ className: clsx(css$17.line, ROW_CLASS[row.kind]),
4300
4893
  children: row.text
4301
4894
  }, index)),
4302
- hidden > 0 && jsx("button", {
4303
- type: "button",
4304
- className: css$16.expand,
4305
- "aria-expanded": expanded,
4306
- "aria-label": expanded ? "收起差异" : `展开其余 ${hidden} 行差异`,
4307
- onClick: onToggle,
4308
- children: expanded ? "收起" : `… 其余 ${hidden} 行`
4895
+ hidden > 0 && jsx(FoldToggle, {
4896
+ className: css$17.expand,
4897
+ expanded,
4898
+ hidden,
4899
+ labels,
4900
+ onToggle
4309
4901
  }),
4310
4902
  tail.map((row, index) => jsx("div", {
4311
- className: clsx(css$16.line, ROW_CLASS[row.kind]),
4903
+ className: clsx(css$17.line, ROW_CLASS[row.kind]),
4312
4904
  children: row.text
4313
4905
  }, index))
4314
4906
  ]
4315
4907
  }),
4316
4908
  jsxs("div", {
4317
- className: css$16.footer,
4909
+ className: css$17.footer,
4318
4910
  children: [
4319
4911
  "└ +",
4320
4912
  added,
4321
4913
  " -",
4322
4914
  removed,
4323
4915
  " · ",
4324
- files,
4325
- " file",
4326
- files === 1 ? "" : "s"
4916
+ labels.files(files)
4327
4917
  ]
4328
4918
  })
4329
4919
  ]
@@ -4371,8 +4961,7 @@ function shownCount(props) {
4371
4961
  * @returns the summary text.
4372
4962
  */
4373
4963
  function summaryText(props, shown, truncated, total) {
4374
- const count = truncated ? `显示 ${shown} / 共 ${total}` : `${shown}`;
4375
- return props.kind === "paths" ? `${count} 个路径` : `${count} 处匹配 · ${props.files.length} 个文件`;
4964
+ return props.kind === "paths" ? props.labels.pathsSummary(shown, total, truncated) : props.labels.matchesSummary(shown, total, props.files.length, truncated);
4376
4965
  }
4377
4966
  /**
4378
4967
  * Flatten a card's shape into its render rows, dropping a collapsed file
@@ -4453,60 +5042,60 @@ function SearchBlock(props) {
4453
5042
  const tail = tailHeader === void 0 ? naturalTail : naturalTail.slice(1);
4454
5043
  const renderRow = (row) => {
4455
5044
  if (row.type === "path") return jsx("div", {
4456
- className: css$17.line,
5045
+ className: css$18.line,
4457
5046
  children: row.path
4458
5047
  });
4459
5048
  if (row.type === "match") return jsxs("div", {
4460
- className: css$17.line,
5049
+ className: css$18.line,
4461
5050
  children: [jsxs("span", {
4462
- className: css$17.lineNumber,
5051
+ className: css$18.lineNumber,
4463
5052
  children: [row.lineNumber, ": "]
4464
5053
  }), row.line]
4465
5054
  });
4466
5055
  return jsxs("button", {
4467
5056
  type: "button",
4468
- className: css$17.fileHeader,
5057
+ className: css$18.fileHeader,
4469
5058
  "aria-expanded": !row.collapsed,
4470
5059
  onClick: () => {
4471
5060
  toggleFile(row.index);
4472
5061
  },
4473
5062
  children: [jsx("span", {
4474
- className: css$17.filePath,
5063
+ className: css$18.filePath,
4475
5064
  children: row.path
4476
5065
  }), jsx("span", {
4477
- className: css$17.fileCount,
5066
+ className: css$18.fileCount,
4478
5067
  children: row.count
4479
5068
  })]
4480
5069
  });
4481
5070
  };
4482
5071
  return jsxs("div", {
4483
- className: clsx(css$17.block, className),
5072
+ className: clsx(css$18.block, className),
4484
5073
  "data-search": props.kind,
4485
5074
  children: [jsxs("div", {
4486
- className: css$17.header,
5075
+ className: css$18.header,
4487
5076
  children: [jsx("span", {
4488
- className: css$17.summary,
5077
+ className: css$18.summary,
4489
5078
  children: summaryText(props, shown, truncated, total)
4490
5079
  }), !empty && jsx("button", {
4491
5080
  type: "button",
4492
- className: css$17.copyButton,
5081
+ className: css$18.copyButton,
4493
5082
  onClick: onCopy,
4494
- children: copied ? "复制成功" : "复制"
5083
+ children: copied ? props.labels.copied : props.labels.copy
4495
5084
  })]
4496
5085
  }), empty ? jsx("div", {
4497
- className: css$17.empty,
4498
- children: "无结果"
5086
+ className: css$18.empty,
5087
+ children: props.labels.noResults
4499
5088
  }) : jsxs("div", {
4500
- className: css$17.body,
5089
+ className: css$18.body,
4501
5090
  children: [
4502
5091
  head.map((row) => jsx("div", { children: renderRow(row) }, rowKey(row))),
4503
5092
  hidden > 0 && jsx("button", {
4504
5093
  type: "button",
4505
- className: css$17.expand,
5094
+ className: css$18.expand,
4506
5095
  "aria-expanded": expanded,
4507
- "aria-label": expanded ? "收起结果" : `展开其余 ${hidden} 行结果`,
5096
+ "aria-label": expanded ? props.labels.collapseAria : props.labels.expandAria(hidden),
4508
5097
  onClick: onToggle,
4509
- children: expanded ? "收起" : `… 其余 ${hidden} 行`
5098
+ children: expanded ? props.labels.collapse : props.labels.expand(hidden)
4510
5099
  }),
4511
5100
  tailHeader !== void 0 && jsx("div", { children: renderRow(tailHeader) }, `tailHeader:${rowKey(tailHeader)}`),
4512
5101
  tail.map((row) => jsx("div", { children: renderRow(row) }, rowKey(row)))
@@ -4522,19 +5111,23 @@ function SearchBlock(props) {
4522
5111
  * Re-parsing the whole accumulated document on every streaming chunk is
4523
5112
  * quadratic in the final reply length. CommonMark block parsing is line-based
4524
5113
  * and appended text can only reshape the parse frontier — the last top-level
4525
- * block (a paragraph becoming a setext heading or a table, a list continuing
4526
- * after a blank line, an unclosed fence swallowing lines) — so earlier blocks
4527
- * are final. This parser therefore freezes all but the trailing
4528
- * {@link UNSTABLE_TAIL_BLOCKS} blocks and re-parses only the source tail
4529
- * behind them: each source region is parsed O(1) times over the stream
4530
- * instead of once per chunk.
5114
+ * block (a paragraph becoming a setext heading or a table, or a list
5115
+ * continuing after a blank line) — so earlier blocks are final. This parser
5116
+ * therefore freezes all but the trailing {@link UNSTABLE_TAIL_BLOCKS} blocks
5117
+ * and re-parses only the source tail behind them. A final unclosed top-level
5118
+ * fence cannot freeze as a block, so its completed content lines use a second
5119
+ * frontier: only the last completed line and current partial line return
5120
+ * through the caller's grammar. Each source region is therefore parsed a
5121
+ * bounded number of times over the stream instead of once per chunk.
4531
5122
  *
4532
- * The freeze boundary comes from the parser's own `position` offsets, never
4533
- * from custom source scanning. The cut sits at the *end offset* of the last
4534
- * frozen block (not the next block's start): a following block's start offset
4535
- * excludes up to three spaces of insignificant leading indentation, which is
4536
- * harmless to drop, but cutting at the previous end also keeps the
4537
- * inter-block blank lines in the tail so the sliced source stays verbatim.
5123
+ * The block freeze boundary comes from the parser's own `position` offsets.
5124
+ * The cut sits at the *end offset* of the last frozen block (not the next
5125
+ * block's start): a following block's start offset excludes up to three spaces
5126
+ * of insignificant leading indentation, which is harmless to drop, but
5127
+ * cutting at the previous end also keeps the inter-block blank lines in the
5128
+ * tail so the sliced source stays verbatim. Fence scanning only recognizes a
5129
+ * parser-confirmed code node and closing delimiter; ambiguous input returns to
5130
+ * the normal tail parse.
4538
5131
  *
4539
5132
  * Known deviation, shared with any prefix-freeze scheme: micromark resolves
4540
5133
  * reference-style links and footnotes document-wide at parse time, so a
@@ -4559,6 +5152,84 @@ function blockKey(node, base, index) {
4559
5152
  const offset = node.position?.start.offset;
4560
5153
  return offset === void 0 ? -(index + 1) : base + offset;
4561
5154
  }
5155
+ /** Return the first line terminator at or after `start`, including a CRLF pair. */
5156
+ function lineTerminatorEnd(text, start) {
5157
+ for (let index = start; index < text.length; index += 1) {
5158
+ const char = text[index];
5159
+ if (char === "\n") return index + 1;
5160
+ if (char === "\r") return text[index + 1] === "\n" ? index + 2 : index + 1;
5161
+ }
5162
+ }
5163
+ /**
5164
+ * Source prefix before the last completed line. Keeping that line beside the
5165
+ * current partial line lets the grammar retain its trailing-newline semantics.
5166
+ */
5167
+ function committableLinePrefixLength(text) {
5168
+ let previousEnd = 0;
5169
+ let end = 0;
5170
+ for (let index = 0; index < text.length; index += 1) {
5171
+ const char = text[index];
5172
+ if (char === "\n") {
5173
+ previousEnd = end;
5174
+ end = index + 1;
5175
+ continue;
5176
+ }
5177
+ if (char !== "\r" || index + 1 >= text.length) continue;
5178
+ if (text[index + 1] === "\n") index += 1;
5179
+ previousEnd = end;
5180
+ end = index + 1;
5181
+ }
5182
+ return previousEnd;
5183
+ }
5184
+ /** Exact source terminator ending a non-empty committable prefix. */
5185
+ function trailingLineTerminator(text) {
5186
+ return text.endsWith("\r\n") ? "\r\n" : text.endsWith("\r") ? "\r" : "\n";
5187
+ }
5188
+ /** Whether `text` contains a CommonMark closing fence on one of its logical lines. */
5189
+ function containsClosingFence(text, marker, markerLength) {
5190
+ let start = 0;
5191
+ while (start <= text.length) {
5192
+ let end = start;
5193
+ while (end < text.length && text[end] !== "\n" && text[end] !== "\r") end += 1;
5194
+ const line = text.slice(start, end);
5195
+ let indent = 0;
5196
+ while (indent < 3 && line[indent] === " ") indent += 1;
5197
+ let run = indent;
5198
+ while (line[run] === marker) run += 1;
5199
+ if (run - indent >= markerLength && /^[ \t]*$/.test(line.slice(run))) return true;
5200
+ if (end === text.length) return false;
5201
+ start = text[end] === "\r" && text[end + 1] === "\n" ? end + 2 : end + 1;
5202
+ }
5203
+ /* v8 ignore next -- each loop iteration returns at EOF or advances past a line terminator. */
5204
+ return false;
5205
+ }
5206
+ /** Advance an mdast point across one append while treating a split CRLF as one line ending. */
5207
+ function advancePoint(point, appended, precededByCarriageReturn) {
5208
+ let line = point.line;
5209
+ let column = point.column;
5210
+ let afterCarriageReturn = precededByCarriageReturn;
5211
+ for (const char of appended) {
5212
+ if (char === "\n") {
5213
+ if (!afterCarriageReturn) line += 1;
5214
+ column = 1;
5215
+ afterCarriageReturn = false;
5216
+ continue;
5217
+ }
5218
+ if (char === "\r") {
5219
+ line += 1;
5220
+ column = 1;
5221
+ afterCarriageReturn = true;
5222
+ continue;
5223
+ }
5224
+ column += 1;
5225
+ afterCarriageReturn = false;
5226
+ }
5227
+ return {
5228
+ line,
5229
+ column,
5230
+ offset: point.offset + appended.length
5231
+ };
5232
+ }
4562
5233
  /**
4563
5234
  * Append-only incremental parser over a caller-supplied grammar. One instance
4564
5235
  * accumulates one streaming document; non-append input resets it.
@@ -4570,10 +5241,112 @@ var IncrementalMarkdownParser = class {
4570
5241
  frozen = [];
4571
5242
  generation = 0;
4572
5243
  cached = null;
5244
+ openFence = null;
4573
5245
  /** @param parse - Grammar shared with whatever renders the blocks, so boundaries agree. */
4574
5246
  constructor(parse) {
4575
5247
  this.parse = parse;
4576
5248
  }
5249
+ /** Parse one unclosed-fence content slice through the caller's grammar. */
5250
+ fenceValue(state, text) {
5251
+ const root = this.parse(`${state.syntheticPrefix}${text}`);
5252
+ if (root.children.length !== 1) return void 0;
5253
+ const node = root.children[0];
5254
+ return node.type === "code" ? node.value : void 0;
5255
+ }
5256
+ /** Recognize the parsed tail's final unclosed fence and prepare its incremental content frontier. */
5257
+ openFenceState(text, base, tail, frozen) {
5258
+ const codeIndex = tail.length - 1;
5259
+ const block = tail[codeIndex];
5260
+ if (block?.node.type !== "code") return null;
5261
+ const node = block.node;
5262
+ const startOffset = node.position?.start.offset;
5263
+ const end = node.position?.end;
5264
+ if (startOffset === void 0 || end?.offset === void 0) return null;
5265
+ /* v8 ignore next -- the caller's parse slice ends at text.length, so its final node ends there. */
5266
+ if (base + end.offset !== text.length) return null;
5267
+ const source = text.slice(base);
5268
+ const previousLf = source.lastIndexOf("\n", startOffset - 1);
5269
+ const previousCr = source.lastIndexOf("\r", startOffset - 1);
5270
+ const lineStart = Math.max(previousLf, previousCr) + 1;
5271
+ const terminatorEnd = lineTerminatorEnd(source, startOffset);
5272
+ /* v8 ignore next -- a parser-confirmed fenced code node requires its opening line terminator. */
5273
+ if (terminatorEnd === void 0) return null;
5274
+ if (terminatorEnd === source.length && source.endsWith("\r")) return null;
5275
+ const openingLine = source.slice(lineStart, terminatorEnd).replace(/[\r\n]+$/, "");
5276
+ const opening = /^( {0,3})(`{3,}|~{3,})/.exec(openingLine);
5277
+ if (opening === null) return null;
5278
+ const indent = opening[1];
5279
+ const run = opening[2];
5280
+ /* v8 ignore next -- mdast positions a fenced code node at the matched delimiter after indentation. */
5281
+ if (lineStart + indent.length !== startOffset) return null;
5282
+ const marker = run[0];
5283
+ const contentStart = base + terminatorEnd;
5284
+ const content = text.slice(contentStart);
5285
+ if (containsClosingFence(content, marker, run.length)) return null;
5286
+ const syntheticPrefix = `${indent}${run}\n`;
5287
+ const stableLength = committableLinePrefixLength(content);
5288
+ const stableValue = stableLength === 0 ? "" : this.fenceValue({ syntheticPrefix }, content.slice(0, stableLength));
5289
+ if (stableValue === void 0) return null;
5290
+ const pendingStart = contentStart + stableLength;
5291
+ const stableSource = content.slice(0, stableLength);
5292
+ const valuePrefix = stableLength === 0 ? "" : `${stableValue}${trailingLineTerminator(stableSource)}`;
5293
+ const pendingValue = this.fenceValue({ syntheticPrefix }, text.slice(pendingStart));
5294
+ if (pendingValue === void 0 || `${valuePrefix}${pendingValue}` !== node.value) return null;
5295
+ return {
5296
+ marker,
5297
+ markerLength: run.length,
5298
+ syntheticPrefix,
5299
+ codeIndex,
5300
+ frozen,
5301
+ tail,
5302
+ pendingStart,
5303
+ valuePrefix,
5304
+ end: {
5305
+ line: end.line,
5306
+ column: end.column,
5307
+ offset: end.offset
5308
+ },
5309
+ endedWithCarriageReturn: text.endsWith("\r")
5310
+ };
5311
+ }
5312
+ /** Extend a recognized unclosed fence without parsing its completed content prefix again. */
5313
+ updateOpenFence(state, text, previousText) {
5314
+ const pending = text.slice(state.pendingStart);
5315
+ if (containsClosingFence(pending, state.marker, state.markerLength)) return void 0;
5316
+ const pendingValue = this.fenceValue(state, pending);
5317
+ if (pendingValue === void 0) return void 0;
5318
+ const stableLength = committableLinePrefixLength(pending);
5319
+ const stableValue = stableLength === 0 ? "" : this.fenceValue(state, pending.slice(0, stableLength));
5320
+ if (stableValue === void 0) return void 0;
5321
+ const previousNode = state.tail[state.codeIndex].node;
5322
+ const end = advancePoint(state.end, text.slice(previousText.length), state.endedWithCarriageReturn);
5323
+ const node = {
5324
+ ...previousNode,
5325
+ value: `${state.valuePrefix}${pendingValue}`,
5326
+ position: {
5327
+ start: previousNode.position.start,
5328
+ end
5329
+ }
5330
+ };
5331
+ const tail = state.tail.map((entry, index) => index === state.codeIndex ? {
5332
+ ...entry,
5333
+ node
5334
+ } : entry);
5335
+ const cached = {
5336
+ frozen: state.frozen,
5337
+ tail,
5338
+ generation: this.generation
5339
+ };
5340
+ this.openFence = {
5341
+ ...state,
5342
+ tail,
5343
+ pendingStart: state.pendingStart + stableLength,
5344
+ valuePrefix: stableLength === 0 ? state.valuePrefix : `${state.valuePrefix}${stableValue}${trailingLineTerminator(pending.slice(0, stableLength))}`,
5345
+ end,
5346
+ endedWithCarriageReturn: text.endsWith("\r")
5347
+ };
5348
+ return cached;
5349
+ }
4577
5350
  /**
4578
5351
  * Fold the current accumulated text and return the frozen/tail split.
4579
5352
  * Idempotent for identical input (the previous result is returned as-is),
@@ -4587,8 +5360,19 @@ var IncrementalMarkdownParser = class {
4587
5360
  this.prevText = "";
4588
5361
  this.tailStart = 0;
4589
5362
  this.frozen = [];
5363
+ this.openFence = null;
4590
5364
  this.generation += 1;
4591
5365
  }
5366
+ const previousText = this.prevText;
5367
+ if (previousText !== "" && this.openFence !== null) {
5368
+ const incremental = this.updateOpenFence(this.openFence, text, previousText);
5369
+ if (incremental !== void 0) {
5370
+ this.prevText = text;
5371
+ this.cached = incremental;
5372
+ return incremental;
5373
+ }
5374
+ this.openFence = null;
5375
+ }
4592
5376
  this.prevText = text;
4593
5377
  const base = this.tailStart;
4594
5378
  const blocks = this.parse(text.slice(base)).children;
@@ -4613,6 +5397,7 @@ var IncrementalMarkdownParser = class {
4613
5397
  tail,
4614
5398
  generation: this.generation
4615
5399
  };
5400
+ this.openFence = this.openFenceState(text, base, tail, this.cached.frozen);
4616
5401
  return this.cached;
4617
5402
  }
4618
5403
  };
@@ -4987,14 +5772,114 @@ function parseGfmWithMath(text) {
4987
5772
  }
4988
5773
  //#endregion
4989
5774
  //#region lib/types/markdown/CodeBlock.js
4990
- function CodeBlock({ code, lang, className, copyLabel = "复制", copiedLabel = "复制成功" }) {
5775
+ /**
5776
+ * The `pre` attributes shiki's HTML arm emits for the css-variables theme,
5777
+ * mirrored so the streaming arm's tree is interchangeable with the settled
5778
+ * swap (`tests/streaming-code-block.client.spec.tsx` pins the two arms'
5779
+ * parity).
5780
+ */
5781
+ const SHIKI_PRE_PROPS = {
5782
+ className: "shiki css-variables",
5783
+ style: {
5784
+ backgroundColor: "var(--shiki-background)",
5785
+ color: "var(--shiki-foreground)"
5786
+ },
5787
+ tabIndex: 0
5788
+ };
5789
+ /** Completed-line group size; React reconciles groups while the DOM remains line-for-line identical. */
5790
+ const STREAMING_LINE_GROUP_SIZE = 32;
5791
+ function renderLine(line, index) {
5792
+ return jsxs(Fragment$1, { children: [index > 0 && "\n", jsx("span", {
5793
+ className: "line",
5794
+ children: line.map((span, spanIndex) => jsx("span", {
5795
+ style: span.style,
5796
+ children: span.text
5797
+ }, spanIndex))
5798
+ })] }, index);
5799
+ }
5800
+ function CodeBlock({ code, lang, streaming, className, copyLabel, copiedLabel }) {
4991
5801
  const trimmed = code.endsWith("\n") ? code.slice(0, -1) : code;
4992
- const html = useMemo(() => highlightToHtml(trimmed, lang), [
5802
+ const rootRef = useRef(null);
5803
+ const highlighting = useViewportHighlighting(rootRef, lang);
5804
+ const loaded = useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount, grammarLoadCount);
5805
+ const sessionRef = useRef(null);
5806
+ const lineCacheRef = useRef(null);
5807
+ const settledRef = useRef(false);
5808
+ const streamedBody = useMemo(() => {
5809
+ if (!highlighting) {
5810
+ sessionRef.current = null;
5811
+ lineCacheRef.current = null;
5812
+ settledRef.current = false;
5813
+ return;
5814
+ }
5815
+ if (streaming !== true) {
5816
+ const previous = lineCacheRef.current;
5817
+ if (previous !== null && previous.code === trimmed && previous.lang === lang) {
5818
+ settledRef.current = true;
5819
+ return previous.body;
5820
+ }
5821
+ sessionRef.current = null;
5822
+ lineCacheRef.current = null;
5823
+ settledRef.current = true;
5824
+ return;
5825
+ }
5826
+ if (settledRef.current) {
5827
+ sessionRef.current = null;
5828
+ lineCacheRef.current = null;
5829
+ settledRef.current = false;
5830
+ }
5831
+ sessionRef.current ??= new StreamingHighlightSession();
5832
+ const frame = sessionRef.current.updateFrame(trimmed, lang);
5833
+ if (frame === void 0) {
5834
+ lineCacheRef.current = null;
5835
+ return;
5836
+ }
5837
+ const previous = lineCacheRef.current;
5838
+ if (previous?.frame === frame && previous.code === trimmed && previous.lang === lang) return previous.body;
5839
+ const sameGeneration = previous?.generation === frame.generation;
5840
+ const groups = sameGeneration ? [...previous.groups] : [];
5841
+ let pending = sameGeneration ? [...previous.pending] : [];
5842
+ let nextLine = sameGeneration ? previous.nextLine : 0;
5843
+ for (const line of frame.appended) {
5844
+ pending.push(renderLine(line, nextLine));
5845
+ nextLine += 1;
5846
+ if (pending.length !== STREAMING_LINE_GROUP_SIZE) continue;
5847
+ const start = nextLine - pending.length;
5848
+ groups.push(jsx(Fragment$1, { children: pending }, start));
5849
+ pending = [];
5850
+ }
5851
+ const tail = frame.tail.map((line, index) => renderLine(line, nextLine + index));
5852
+ const tailGroup = jsx(Fragment$1, { children: [...pending, ...tail] }, nextLine - pending.length);
5853
+ const body = jsx("pre", {
5854
+ ...SHIKI_PRE_PROPS,
5855
+ children: jsxs("code", { children: [groups, tailGroup] })
5856
+ });
5857
+ lineCacheRef.current = {
5858
+ code: trimmed,
5859
+ lang,
5860
+ generation: frame.generation,
5861
+ frame,
5862
+ groups,
5863
+ pending,
5864
+ nextLine,
5865
+ body
5866
+ };
5867
+ return body;
5868
+ }, [
5869
+ streaming,
5870
+ highlighting,
4993
5871
  trimmed,
4994
5872
  lang,
4995
- useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount, grammarLoadCount)
5873
+ loaded
5874
+ ]);
5875
+ const html = useMemo(() => highlighting && streaming !== true && streamedBody === void 0 ? highlightToHtml(trimmed, lang) : void 0, [
5876
+ streaming,
5877
+ highlighting,
5878
+ streamedBody,
5879
+ trimmed,
5880
+ lang,
5881
+ loaded
4996
5882
  ]);
4997
- const rootRef = useRef(null);
4998
5883
  const [copied, setCopied] = useState(false);
4999
5884
  const onCopy = useCallback(() => {
5000
5885
  if (copied) return;
@@ -5006,25 +5891,25 @@ function CodeBlock({ code, lang, className, copyLabel = "复制", copiedLabel =
5006
5891
  }, 1e3);
5007
5892
  });
5008
5893
  }, [copied, trimmed]);
5009
- const body = html === void 0 ? jsx("pre", {
5010
- className: css$18.plain,
5894
+ const body = streamedBody !== void 0 ? streamedBody : html === void 0 ? jsx("pre", {
5895
+ className: css$19.plain,
5011
5896
  children: jsx("code", { children: trimmed })
5012
5897
  }) : jsx("div", { dangerouslySetInnerHTML: { __html: html } });
5013
5898
  return jsxs("div", {
5014
5899
  ref: rootRef,
5015
- className: clsx(css$18.block, "md-code-block", className),
5900
+ className: clsx(css$19.block, "md-code-block", className),
5016
5901
  children: [jsx("div", {
5017
- className: css$18.bannerWrap,
5902
+ className: css$19.bannerWrap,
5018
5903
  children: jsxs("div", {
5019
- className: css$18.banner,
5904
+ className: css$19.banner,
5020
5905
  children: [jsx("div", {
5021
- className: css$18.infostring,
5906
+ className: css$19.infostring,
5022
5907
  children: lang ?? ""
5023
5908
  }), jsx("div", {
5024
- className: css$18.action,
5909
+ className: css$19.action,
5025
5910
  children: jsx("button", {
5026
5911
  type: "button",
5027
- className: css$18.copyButton,
5912
+ className: css$19.copyButton,
5028
5913
  onClick: onCopy,
5029
5914
  children: copied ? copiedLabel : copyLabel
5030
5915
  })
@@ -5244,7 +6129,7 @@ function renderNode(node, key, context) {
5244
6129
  const mention = context.inLink === true ? void 0 : context.fileMentions?.resolve(value);
5245
6130
  if (mention !== void 0) return jsx("code", { children: jsx("button", {
5246
6131
  type: "button",
5247
- className: css$19.fileMention,
6132
+ className: css$20.fileMention,
5248
6133
  title: mention.title,
5249
6134
  "aria-label": mention.label,
5250
6135
  onClick: mention.open,
@@ -5279,9 +6164,10 @@ function renderCode(node, key, context) {
5279
6164
  if (!context.streaming && lang === "math") return jsx(Fragment$1, { children: renderTexToReact(`${node.value}\n`, true) }, key);
5280
6165
  return jsx(CodeBlock, {
5281
6166
  code: `${node.value}\n`,
5282
- lang: context.streaming ? void 0 : lang,
5283
- copyLabel: context.codeLabels?.copyLabel,
5284
- copiedLabel: context.codeLabels?.copiedLabel
6167
+ lang,
6168
+ streaming: context.streaming,
6169
+ copyLabel: context.labels.code.copyLabel,
6170
+ copiedLabel: context.labels.code.copiedLabel
5285
6171
  }, key);
5286
6172
  }
5287
6173
  /** A list is loose when it or any of its items is spread; every item then keeps its paragraphs. */
@@ -5338,7 +6224,7 @@ function renderTable(node, key, context) {
5338
6224
  const [headRow, ...bodyRows] = node.children;
5339
6225
  const wide = (align === null ? headRow?.children.length ?? 0 : align.length) >= 4 && context.inBlockquote !== true;
5340
6226
  return jsx("div", {
5341
- className: clsx(css$19.tableScroll, wide ? "md-table-wide" : css$19.tableFill),
6227
+ className: clsx(css$20.tableScroll, wide ? "md-table-wide" : css$20.tableFill),
5342
6228
  tabIndex: wide ? 0 : void 0,
5343
6229
  children: jsxs("table", { children: [headRow !== void 0 && jsx("thead", { children: renderTableRow(headRow, "th", align, 0, context) }), bodyRows.length > 0 && jsx("tbody", { children: bodyRows.map((row, index) => renderTableRow(row, "td", align, index + 1, context)) })] })
5344
6230
  }, key);
@@ -5389,11 +6275,11 @@ function inlineCodeHttpUrl(value) {
5389
6275
  function renderImage(url, alt, key) {
5390
6276
  const imageSrc = remoteImageUrl(sanitizeUrl(normalizeUri(url)));
5391
6277
  if (imageSrc === void 0) return jsx("span", {
5392
- className: css$19.imageAlt,
6278
+ className: css$20.imageAlt,
5393
6279
  children: alt
5394
6280
  }, key);
5395
6281
  return jsx("img", {
5396
- className: css$19.image,
6282
+ className: css$20.image,
5397
6283
  src: imageSrc,
5398
6284
  alt,
5399
6285
  loading: "lazy",
@@ -5466,7 +6352,7 @@ function renderFootnoteSection(context) {
5466
6352
  children: [jsx("h2", {
5467
6353
  id: "footnote-label",
5468
6354
  className: "sr-only",
5469
- children: "Footnotes"
6355
+ children: context.labels.footnotes
5470
6356
  }), jsx("ol", { children: items })]
5471
6357
  }, "footnotes");
5472
6358
  }
@@ -5485,13 +6371,13 @@ function renderFootnoteSection(context) {
5485
6371
  * full parse self-heals it.
5486
6372
  */
5487
6373
  /** One settled full render: parse with math, resolve references, append the footnote section. */
5488
- function renderSettled(text, codeLabels, fileMentions) {
6374
+ function renderSettled(text, labels, fileMentions) {
5489
6375
  const root = parseGfmWithMath(text);
5490
6376
  const targets = createReferenceTargets();
5491
6377
  collectReferenceTargets(root.children, targets);
5492
6378
  const context = {
5493
6379
  streaming: false,
5494
- codeLabels,
6380
+ labels,
5495
6381
  fileMentions,
5496
6382
  targets,
5497
6383
  footnoteOrder: [],
@@ -5499,7 +6385,8 @@ function renderSettled(text, codeLabels, fileMentions) {
5499
6385
  };
5500
6386
  const blocks = wrapBlockChildren(renderBlocks(root.children.map((node, index) => ({
5501
6387
  node,
5502
- key: index
6388
+ /* v8 ignore next -- parseFull uses parseGfm, which stamps every top-level node. */
6389
+ key: node.position?.start.offset ?? -(index + 1)
5503
6390
  })), context), false);
5504
6391
  const section = renderFootnoteSection(context);
5505
6392
  return section === null ? blocks : [
@@ -5515,7 +6402,7 @@ function renderSettled(text, codeLabels, fileMentions) {
5515
6402
  * final, so the tail continues from a copy of it each frame).
5516
6403
  */
5517
6404
  var StreamingRenderer = class {
5518
- codeLabels;
6405
+ labels;
5519
6406
  parser = new IncrementalMarkdownParser(parseGfm);
5520
6407
  generation = -1;
5521
6408
  frozenCount = 0;
@@ -5525,9 +6412,9 @@ var StreamingRenderer = class {
5525
6412
  frozenFootnoteCounts = /* @__PURE__ */ new Map();
5526
6413
  lastText = null;
5527
6414
  lastRendered = [];
5528
- /** @param codeLabels - Fence copy labels baked into cached elements; the owner replaces the renderer when they change. */
5529
- constructor(codeLabels) {
5530
- this.codeLabels = codeLabels;
6415
+ /** @param labels - Localized Markdown chrome baked into cached elements; the owner replaces the renderer when it changes. */
6416
+ constructor(labels) {
6417
+ this.labels = labels;
5531
6418
  }
5532
6419
  /**
5533
6420
  * Render the current accumulated text. Idempotent per text value, so React
@@ -5556,7 +6443,7 @@ var StreamingRenderer = class {
5556
6443
  if (newlyFrozen.length > 0) {
5557
6444
  const frozenContext = {
5558
6445
  streaming: true,
5559
- codeLabels: this.codeLabels,
6446
+ labels: this.labels,
5560
6447
  fileMentions: void 0,
5561
6448
  targets: frameTargets,
5562
6449
  footnoteOrder: this.frozenFootnoteOrder,
@@ -5572,7 +6459,7 @@ var StreamingRenderer = class {
5572
6459
  }
5573
6460
  const tailContext = {
5574
6461
  streaming: true,
5575
- codeLabels: this.codeLabels,
6462
+ labels: this.labels,
5576
6463
  fileMentions: void 0,
5577
6464
  targets: frameTargets,
5578
6465
  footnoteOrder: [...this.frozenFootnoteOrder],
@@ -5593,9 +6480,10 @@ var StreamingRenderer = class {
5593
6480
  /**
5594
6481
  * Render untrusted assistant-authored Markdown as semantic React elements.
5595
6482
  * @param props - Markdown source text preserved by the session projection;
5596
- * `streaming` renders fences and TeX plain (highlighting and KaTeX land on
5597
- * the finalize swap) and parses incrementally across chunks; `codeLabels`
5598
- * forwards localized copy-button labels to fence CodeBlocks — pass a
6483
+ * `streaming` parses incrementally across chunks and highlights fences as
6484
+ * they grow (each fence re-tokenizes only appended text; TeX stays literal
6485
+ * until the finalize swap so incomplete formulae never flash errors);
6486
+ * `labels` forwards localized fence and footnote chrome — pass a
5599
6487
  * reference-stable object (memoized per locale revision), because a new
5600
6488
  * identity discards the streaming render cache mid-message. `fileMentions`
5601
6489
  * links inline-code tokens its resolver recognizes as real files; this is
@@ -5606,27 +6494,27 @@ var StreamingRenderer = class {
5606
6494
  * relative links, and unsafe protocols are disabled, while absolute HTTP(S)
5607
6495
  * images render directly.
5608
6496
  */
5609
- const MarkdownText = memo(function MarkdownText({ text, streaming = false, codeLabels, fileMentions }) {
6497
+ const MarkdownText = memo(function MarkdownText({ text, streaming = false, labels, fileMentions }) {
5610
6498
  const streamRef = useRef(null);
5611
- const streamLabelsRef = useRef(codeLabels);
6499
+ const streamLabelsRef = useRef(labels);
5612
6500
  const children = useMemo(() => {
5613
6501
  if (!streaming) {
5614
6502
  streamRef.current = null;
5615
- return renderSettled(text, codeLabels, fileMentions);
6503
+ return renderSettled(text, labels, fileMentions);
5616
6504
  }
5617
- if (streamRef.current === null || streamLabelsRef.current !== codeLabels) {
5618
- streamRef.current = new StreamingRenderer(codeLabels);
5619
- streamLabelsRef.current = codeLabels;
6505
+ if (streamRef.current === null || streamLabelsRef.current !== labels) {
6506
+ streamRef.current = new StreamingRenderer(labels);
6507
+ streamLabelsRef.current = labels;
5620
6508
  }
5621
6509
  return streamRef.current.render(text);
5622
6510
  }, [
5623
6511
  text,
5624
6512
  streaming,
5625
- codeLabels,
6513
+ labels,
5626
6514
  fileMentions
5627
6515
  ]);
5628
6516
  return jsx("div", {
5629
- className: css$19.markdown,
6517
+ className: css$20.markdown,
5630
6518
  children
5631
6519
  });
5632
6520
  });
@@ -5701,20 +6589,20 @@ function SafeLink({ url, label, className }) {
5701
6589
  */
5702
6590
  function SourceItem({ source, ordinal }) {
5703
6591
  return jsxs("li", {
5704
- className: css$20.source,
6592
+ className: css$21.source,
5705
6593
  value: ordinal,
5706
6594
  children: [
5707
6595
  jsx(SafeLink, {
5708
6596
  url: source.url,
5709
6597
  label: linkLabel(source.url, source.title),
5710
- className: css$20.sourceLink
6598
+ className: css$21.sourceLink
5711
6599
  }),
5712
6600
  source.snippet !== void 0 && source.snippet !== "" && jsx("div", {
5713
- className: css$20.snippet,
6601
+ className: css$21.snippet,
5714
6602
  children: source.snippet
5715
6603
  }),
5716
6604
  source.publishedAt !== void 0 && source.publishedAt !== "" && jsx("div", {
5717
- className: css$20.published,
6605
+ className: css$21.published,
5718
6606
  children: source.publishedAt
5719
6607
  })
5720
6608
  ]
@@ -5726,29 +6614,32 @@ function SourceItem({ source, ordinal }) {
5726
6614
  * @param props - see {@link WebSearchBlockProps}.
5727
6615
  * @returns the search card element.
5728
6616
  */
5729
- function WebSearchBlock({ answer, sources, truncated, className }) {
6617
+ function WebSearchBlock({ answer, sources, truncated, labels, className }) {
5730
6618
  const empty = (answer === void 0 || answer === "") && sources.length === 0;
5731
6619
  return jsxs("div", {
5732
- className: clsx(css$20.block, className),
6620
+ className: clsx(css$21.block, className),
5733
6621
  "data-web": "search",
5734
6622
  children: [
5735
6623
  answer !== void 0 && answer !== "" && jsx("div", {
5736
- className: css$20.answer,
5737
- children: jsx(MarkdownText, { text: answer })
6624
+ className: css$21.answer,
6625
+ children: jsx(MarkdownText, {
6626
+ text: answer,
6627
+ labels: labels.markdown
6628
+ })
5738
6629
  }),
5739
6630
  empty ? jsx("div", {
5740
- className: css$20.empty,
5741
- children: "未找到结果"
6631
+ className: css$21.empty,
6632
+ children: labels.noResults
5742
6633
  }) : jsx("ol", {
5743
- className: css$20.sources,
6634
+ className: css$21.sources,
5744
6635
  children: sources.map((source, index) => jsx(SourceItem, {
5745
6636
  source,
5746
6637
  ordinal: index + 1
5747
6638
  }, index))
5748
6639
  }),
5749
6640
  truncated && jsx("div", {
5750
- className: css$20.truncated,
5751
- children: "来源列表已截断"
6641
+ className: css$21.truncated,
6642
+ children: labels.sourcesTruncated
5752
6643
  })
5753
6644
  ]
5754
6645
  });
@@ -5758,22 +6649,26 @@ function WebSearchBlock({ answer, sources, truncated, className }) {
5758
6649
  * @param props - see {@link WebFetchBlockProps}.
5759
6650
  * @returns the fetch card element.
5760
6651
  */
5761
- function WebFetchBlock({ url, statusCode, truncated, className }) {
6652
+ function WebFetchBlock({ url, statusCode, truncated, labels, className }) {
5762
6653
  return jsxs("div", {
5763
- className: clsx(css$20.block, css$20.fetch, className),
6654
+ className: clsx(css$21.block, css$21.fetch, className),
5764
6655
  "data-web": "fetch",
5765
6656
  children: [jsx(SafeLink, {
5766
6657
  url,
5767
6658
  label: url,
5768
- className: css$20.fetchUrl
6659
+ className: css$21.fetchUrl
5769
6660
  }), jsxs("div", {
5770
- className: css$20.fetchMeta,
6661
+ className: css$21.fetchMeta,
5771
6662
  children: [jsxs("span", {
5772
- className: css$20.status,
5773
- children: ["HTTP ", statusCode]
6663
+ className: css$21.status,
6664
+ children: [
6665
+ labels.http,
6666
+ " ",
6667
+ statusCode
6668
+ ]
5774
6669
  }), truncated && jsx("span", {
5775
- className: css$20.truncated,
5776
- children: "内容已截断"
6670
+ className: css$21.truncated,
6671
+ children: labels.contentTruncated
5777
6672
  })]
5778
6673
  })]
5779
6674
  });
@@ -5789,11 +6684,7 @@ function WebBlock(props) {
5789
6684
  //#endregion
5790
6685
  //#region lib/types/markdown/JsonBlock.js
5791
6686
  const MAX_CHARS = 2e4;
5792
- /** Default truncation footer; the owner passes a localized formatter. */
5793
- function defaultTruncatedLabel(total) {
5794
- return `… 已截断,共 ${total} 字符`;
5795
- }
5796
- function JsonBlock({ label, payload, defaultOpen = false, truncatedLabel = defaultTruncatedLabel }) {
6687
+ function JsonBlock({ label, payload, defaultOpen = false, truncatedLabel }) {
5797
6688
  const [open, setOpen] = useState(defaultOpen);
5798
6689
  const body = useMemo(() => {
5799
6690
  if (!open) return "";
@@ -5810,10 +6701,10 @@ function JsonBlock({ label, payload, defaultOpen = false, truncatedLabel = defau
5810
6701
  truncatedLabel
5811
6702
  ]);
5812
6703
  return jsxs("div", {
5813
- className: css$21.root,
6704
+ className: css$22.root,
5814
6705
  children: [jsxs("button", {
5815
6706
  type: "button",
5816
- className: css$21.toggle,
6707
+ className: css$22.toggle,
5817
6708
  onClick: () => {
5818
6709
  setOpen((v) => !v);
5819
6710
  },
@@ -5823,7 +6714,7 @@ function JsonBlock({ label, payload, defaultOpen = false, truncatedLabel = defau
5823
6714
  label
5824
6715
  ]
5825
6716
  }), open && jsx("pre", {
5826
- className: css$21.body,
6717
+ className: css$22.body,
5827
6718
  children: body
5828
6719
  })]
5829
6720
  });
@@ -5832,7 +6723,7 @@ function JsonBlock({ label, payload, defaultOpen = false, truncatedLabel = defau
5832
6723
  //#region lib/types/markdown/MessageText.js
5833
6724
  function MessageText({ text }) {
5834
6725
  return jsx("div", {
5835
- className: css$22.text,
6726
+ className: css$23.text,
5836
6727
  children: text
5837
6728
  });
5838
6729
  }
@@ -5908,6 +6799,6 @@ function extractMarkdownPlainText(markdown, options = {}) {
5908
6799
  }
5909
6800
  }
5910
6801
  //#endregion
5911
- export { BrandWordmark, Button, CodeBlock, ConnectionBanner, DEFAULT_DIFF_MAX_LINES, DEFAULT_READ_MAX_LINES, DEFAULT_SEARCH_MAX_LINES, DEFAULT_TERMINAL_MAX_LINES, DiffBlock, DisclosureRow, FishLogo, HoverCard, IconAgentPresetOutline16, IconApiOutline14, IconArchiveOutline20, IconBranchOutline16, IconBrowseOutline16, IconCheckOutline14, IconCheckOutline16, IconChecklistOutline14, IconChevronDownOutline14, IconChevronLeftOutline14, IconChevronRightOutline14, IconChevronUpOutline14, IconCloseFill14, IconCloseOutline16, IconCodeOutline16, IconCopyOutline16, IconCordisPluginOutline14, IconDarkOutline16, IconDataOutline16, IconDislikeFill16, IconDislikeOutline16, IconDownloadOutline16, IconEditOutline16, IconEllipsisOutline16, IconEnhanceOutline16, IconFolderClose16, IconFolderOpen16, IconFolderOpenOutline16, IconFollowsystemOutline16, IconFullscreenOutline16, IconGlobeOutline14, IconGoalOutline16, IconInspectOutline12, IconLightOutline16, IconLikeFill16, IconLikeOutline16, IconLinkOutline14, IconLinkOutline16, IconListPenOutline16, IconLoadingOutline16, IconNewChatOutline16, IconPanelLeftOutline16, IconPaperclipOutline16, IconPauseOutline16, IconPersonalizationOutline16, IconPlayOutline16, IconPlusOutline16, IconProjectAddOutline16, IconQuestionOutline14, IconQueueOutline14, IconRefreshOutline14, IconRefreshOutline16, IconRightUpOutline14, IconRightUpOutline16, IconSearchOutline16, IconSendOutline14, IconSendOutline16, IconSettingsOutline14, IconSettingsOutline16, IconShareOutline16, IconSkillOutline16, IconSparkle16, IconStopFill16, IconThinkOutline14, IconThinkOutline16, IconTrashOutline16, IconTreeCorner8x10, IconTriangleRightFill14, IconUserOutline16, IconWarningOutline16, Input, JsonBlock, JsonTree, MarkdownText, Menu, MessageText, Modal, OnboardingSurface, Pill, ReadBlock, RiskConfirmation, SearchBlock, StateDot, TerminalBlock, Toast, Tooltip, WebBlock, extractMarkdownPlainText, useAnchoredMaxHeight, useAnchoredPosition, useDismissOnOutsidePointer, writeClipboard };
6802
+ export { BrandWordmark, Button, CodeBlock, ConnectionIndicator, DEFAULT_DIFF_MAX_LINES, DEFAULT_READ_MAX_LINES, DEFAULT_SEARCH_MAX_LINES, DEFAULT_TERMINAL_MAX_LINES, DiffBlock, DisclosureRow, FISH_LOGO_PATH, FISH_LOGO_VIEWBOX, FishLogo, HoverCard, IconAgentPresetOutline16, IconAlarmClockOutline16, IconApiOutline14, IconArchiveOutline20, IconBranchOutline16, IconBrowseOutline16, IconCheckOutline14, IconCheckOutline16, IconChecklistOutline14, IconChevronDownOutline14, IconChevronLeftOutline14, IconChevronRightOutline14, IconChevronUpOutline14, IconClockOutline16, IconCloseFill14, IconCloseOutline16, IconCodeOutline16, IconContextInjectionOutline16, IconCopyOutline16, IconCordisPluginOutline14, IconDarkOutline16, IconDataOutline16, IconDatabaseOutline16, IconDislikeFill16, IconDislikeOutline16, IconDownloadOutline16, IconEditOutline16, IconEllipsisOutline16, IconEnhanceOutline16, IconFolderClose16, IconFolderOpen16, IconFolderOpenOutline16, IconFollowsystemOutline16, IconFullscreenOutline16, IconGlobeOutline14, IconGoalOutline16, IconInspectOutline12, IconLightOutline16, IconLikeFill16, IconLikeOutline16, IconLinkOutline14, IconLinkOutline16, IconListPenOutline16, IconLoadingOutline16, IconNewChatOutline16, IconPanelLeftOutline16, IconPaperclipOutline16, IconPauseOutline16, IconPersonalizationOutline16, IconPlayOutline16, IconPlusOutline16, IconProjectAddOutline16, IconQuestionOutline14, IconQueueOutline14, IconRefreshOutline14, IconRefreshOutline16, IconRightUpOutline14, IconRightUpOutline16, IconSearchOutline16, IconSendOutline14, IconSendOutline16, IconSettingsOutline14, IconSettingsOutline16, IconShareOutline16, IconSkillOutline16, IconSparkle16, IconStopFill16, IconThinkOutline14, IconThinkOutline16, IconTrashOutline16, IconTreeCorner8x10, IconTriangleRightFill14, IconUserOutline16, IconWarningOutline16, Input, JsonBlock, JsonTree, MarkdownText, Menu, MessageText, Modal, OnboardingSurface, Pill, ReadBlock, ReferenceIcon, RiskConfirmation, SearchBlock, StateDot, TerminalBlock, Toast, Tooltip, WebBlock, diffTotals, extractMarkdownPlainText, projectUserText, relativeTime, useAnchoredMaxHeight, useAnchoredPosition, useDismissOnOutsidePointer, writeClipboard };
5912
6803
 
5913
6804
  //# sourceMappingURL=index.js.map