@linxiraos/pi-tui 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +245 -224
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/status-notice.d.ts +2 -0
  21. package/dist/types/chrome/transcript-container.d.ts +2 -0
  22. package/dist/types/components/image.d.ts +2 -0
  23. package/dist/types/components/markdown.d.ts +49 -3
  24. package/dist/types/components/text.d.ts +2 -0
  25. package/dist/types/debug-server.d.ts +2 -0
  26. package/dist/types/keybinding-matchers.d.ts +6 -0
  27. package/dist/types/multiplexers/tmux.d.ts +1 -0
  28. package/dist/types/multiplexers/types.d.ts +6 -0
  29. package/dist/types/native/backend.d.ts +5 -4
  30. package/dist/types/native/blobs.d.ts +24 -12
  31. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  32. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  33. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  34. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  35. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  36. package/dist/types/overlays/hook-editor.d.ts +7 -2
  37. package/dist/types/overlays/hook-input.d.ts +12 -4
  38. package/dist/types/overlays/job-tree.d.ts +50 -0
  39. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  40. package/dist/types/overlays/login-dialog.d.ts +8 -3
  41. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  42. package/dist/types/overlays/model-browser.d.ts +6 -1
  43. package/dist/types/overlays/model-picker.d.ts +2 -0
  44. package/dist/types/overlays/pause-screen.d.ts +5 -0
  45. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  46. package/dist/types/overlays/report-panel.d.ts +5 -1
  47. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  48. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  49. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  50. package/dist/types/overlays/session-selector.d.ts +3 -1
  51. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  52. package/dist/types/prompt/composer.d.ts +23 -20
  53. package/dist/types/prompt/custom-editor.d.ts +20 -10
  54. package/dist/types/prompt/image-references.d.ts +22 -0
  55. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  56. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  57. package/dist/types/prompt/prose-gate.d.ts +3 -0
  58. package/dist/types/prompt/queued-messages.d.ts +1 -1
  59. package/dist/types/prompt/typing-word.d.ts +25 -0
  60. package/dist/types/prompt/welcome.d.ts +26 -6
  61. package/dist/types/render/hanging-wrap.d.ts +7 -0
  62. package/dist/types/render/hyperlink.d.ts +2 -0
  63. package/dist/types/render/render-utils.d.ts +9 -1
  64. package/dist/types/render/utils.d.ts +1 -1
  65. package/dist/types/setup/scenes/splash.d.ts +6 -5
  66. package/dist/types/status-line/context-usage.d.ts +4 -1
  67. package/dist/types/status-line/host.d.ts +6 -4
  68. package/dist/types/status-line/startup.d.ts +6 -6
  69. package/dist/types/status-line/types.d.ts +3 -0
  70. package/dist/types/stdin-buffer.d.ts +7 -0
  71. package/dist/types/terminal-capabilities.d.ts +16 -1
  72. package/dist/types/terminal-multiplexer.d.ts +7 -0
  73. package/dist/types/terminal.d.ts +8 -4
  74. package/dist/types/theme/symbols.d.ts +1 -1
  75. package/dist/types/theme/theme.d.ts +11 -0
  76. package/dist/types/thinking.d.ts +5 -0
  77. package/dist/types/tmux.d.ts +39 -4
  78. package/dist/types/tools/ast-edit.d.ts +2 -2
  79. package/dist/types/tools/ast-grep.d.ts +2 -2
  80. package/dist/types/tools/bash.d.ts +6 -1
  81. package/dist/types/tools/edit.d.ts +2 -0
  82. package/dist/types/tools/eval.d.ts +53 -4
  83. package/dist/types/tools/find.d.ts +14 -4
  84. package/dist/types/tools/glob.d.ts +3 -3
  85. package/dist/types/tools/grep.d.ts +2 -2
  86. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  87. package/dist/types/tools/native-view.d.ts +1 -1
  88. package/dist/types/tools/read.d.ts +32 -10
  89. package/dist/types/tools/renderer.d.ts +2 -0
  90. package/dist/types/tools/task.d.ts +31 -0
  91. package/dist/types/tools/web-search-types.d.ts +1 -0
  92. package/dist/types/tools/write.d.ts +2 -0
  93. package/dist/types/tui.d.ts +20 -3
  94. package/package.json +10 -10
  95. package/src/app-keybindings.d.ts +18 -8
  96. package/src/app-keybindings.ts +20 -8
  97. package/src/apps/autoresearch-dashboard.ts +444 -27
  98. package/src/apps/debug/raw-sse.ts +2 -2
  99. package/src/apps/git/diff-pane.ts +39 -41
  100. package/src/apps/git/git-tui.ts +80 -14
  101. package/src/apps/git/sidebar.ts +10 -1
  102. package/src/apps/git/state.ts +12 -0
  103. package/src/autocomplete.d.ts +2 -0
  104. package/src/autocomplete.ts +21 -19
  105. package/src/bracketed-paste.ts +9 -2
  106. package/src/charts/chart-svg.d.ts +7 -13
  107. package/src/charts/chart-svg.ts +19 -22
  108. package/src/charts/table-data.ts +17 -6
  109. package/src/chat/advisor-message.d.ts +4 -1
  110. package/src/chat/advisor-message.ts +77 -24
  111. package/src/chat/assistant-message.d.ts +9 -4
  112. package/src/chat/assistant-message.ts +127 -31
  113. package/src/chat/bash-execution.ts +7 -2
  114. package/src/chat/figure-markdown.d.ts +7 -2
  115. package/src/chat/figure-markdown.ts +10 -3
  116. package/src/chat/image-loading.ts +9 -6
  117. package/src/chat/messages.d.ts +3 -1
  118. package/src/chat/messages.ts +4 -1
  119. package/src/chat/read-tool-group.d.ts +2 -2
  120. package/src/chat/read-tool-group.ts +97 -42
  121. package/src/chat/svg-figure.ts +3 -1
  122. package/src/chat/svg-source.ts +6 -3
  123. package/src/chat/table-chart.d.ts +11 -15
  124. package/src/chat/table-chart.ts +24 -57
  125. package/src/chat/tool-execution.d.ts +32 -20
  126. package/src/chat/tool-execution.ts +103 -36
  127. package/src/chat/user-message.d.ts +3 -2
  128. package/src/chat/user-message.ts +54 -34
  129. package/src/chrome/countdown-timer.ts +3 -3
  130. package/src/chrome/diff.d.ts +2 -3
  131. package/src/chrome/diff.ts +4 -9
  132. package/src/chrome/status-notice.ts +5 -0
  133. package/src/chrome/transcript-container.d.ts +2 -0
  134. package/src/chrome/transcript-container.ts +212 -28
  135. package/src/components/editor.ts +12 -12
  136. package/src/components/image.d.ts +2 -0
  137. package/src/components/image.ts +54 -20
  138. package/src/components/markdown.d.ts +49 -3
  139. package/src/components/markdown.ts +482 -12
  140. package/src/components/settings-list.ts +2 -4
  141. package/src/components/text.d.ts +2 -0
  142. package/src/components/text.ts +11 -1
  143. package/src/debug-server.d.ts +2 -0
  144. package/src/debug-server.ts +51 -69
  145. package/src/hotkeys-markdown.ts +2 -0
  146. package/src/keybinding-matchers.d.ts +6 -0
  147. package/src/keybinding-matchers.ts +19 -0
  148. package/src/multiplexers/tmux.d.ts +1 -0
  149. package/src/multiplexers/tmux.ts +3 -0
  150. package/src/multiplexers/types.d.ts +6 -0
  151. package/src/multiplexers/types.ts +6 -0
  152. package/src/native/backend.d.ts +5 -4
  153. package/src/native/backend.ts +13 -5
  154. package/src/native/blobs.d.ts +24 -12
  155. package/src/native/blobs.ts +56 -7
  156. package/src/native/reconcile.ts +43 -21
  157. package/src/overlays/advisor-config.ts +259 -8
  158. package/src/overlays/agent-hub-projection.d.ts +10 -5
  159. package/src/overlays/agent-hub-projection.ts +63 -12
  160. package/src/overlays/annotation-overlay.ts +422 -168
  161. package/src/overlays/approval-dialog.ts +389 -0
  162. package/src/overlays/ask-dialog.ts +244 -105
  163. package/src/overlays/btw-history-panel.ts +208 -147
  164. package/src/overlays/extensions/extension-list.ts +4 -6
  165. package/src/overlays/hook-editor.ts +90 -32
  166. package/src/overlays/hook-input.ts +105 -19
  167. package/src/overlays/job-tree.ts +177 -0
  168. package/src/overlays/jobs-panel.ts +222 -142
  169. package/src/overlays/login-dialog.ts +201 -91
  170. package/src/overlays/mcp-add-wizard.ts +562 -279
  171. package/src/overlays/model-browser.ts +32 -7
  172. package/src/overlays/model-hub.ts +4 -10
  173. package/src/overlays/model-picker.ts +58 -21
  174. package/src/overlays/pause-screen.ts +81 -23
  175. package/src/overlays/plan-review-overlay.ts +16 -0
  176. package/src/overlays/plugin-selector.ts +17 -1
  177. package/src/overlays/queue-mode-selector.ts +10 -0
  178. package/src/overlays/report-panel.ts +6 -2
  179. package/src/overlays/reset-usage-selector.ts +130 -21
  180. package/src/overlays/session-account-selector.ts +36 -0
  181. package/src/overlays/session-info-overlay.ts +199 -76
  182. package/src/overlays/session-selector.ts +15 -10
  183. package/src/overlays/settings-selector.ts +3 -0
  184. package/src/overlays/show-images-selector.ts +1 -0
  185. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  186. package/src/overlays/theme-selector.ts +6 -1
  187. package/src/overlays/thinking-selector.ts +8 -4
  188. package/src/overlays/tree-selector.ts +117 -2
  189. package/src/overlays/usage-dashboard.ts +498 -144
  190. package/src/prompt/composer-cache.ts +4 -1
  191. package/src/prompt/composer.ts +200 -74
  192. package/src/prompt/custom-editor.ts +168 -65
  193. package/src/prompt/image-references.d.ts +22 -0
  194. package/src/prompt/image-references.ts +79 -2
  195. package/src/prompt/internal-url-autocomplete.ts +1 -0
  196. package/src/prompt/macos-spelling.ts +28 -5
  197. package/src/prompt/prose-gate.ts +3 -0
  198. package/src/prompt/queued-messages.ts +9 -2
  199. package/src/prompt/tips.txt +1 -2
  200. package/src/prompt/typing-word.ts +59 -0
  201. package/src/prompt/welcome.ts +97 -45
  202. package/src/render/code-cell.ts +18 -5
  203. package/src/render/hanging-wrap.d.ts +7 -0
  204. package/src/render/hanging-wrap.ts +58 -0
  205. package/src/render/hyperlink.d.ts +2 -0
  206. package/src/render/hyperlink.ts +5 -0
  207. package/src/render/render-utils.d.ts +9 -1
  208. package/src/render/render-utils.ts +16 -2
  209. package/src/render/utils.d.ts +1 -1
  210. package/src/render/utils.ts +1 -1
  211. package/src/render/width-aware-text.ts +6 -5
  212. package/src/setup/scenes/splash.ts +22 -8
  213. package/src/status-line/component.ts +79 -11
  214. package/src/status-line/context-usage.ts +176 -73
  215. package/src/status-line/footer.ts +18 -35
  216. package/src/status-line/host.ts +6 -1
  217. package/src/status-line/segments.ts +16 -5
  218. package/src/status-line/startup.ts +6 -6
  219. package/src/status-line/types.ts +1 -0
  220. package/src/stdin-buffer.d.ts +7 -0
  221. package/src/stdin-buffer.ts +300 -62
  222. package/src/terminal-capabilities.d.ts +16 -1
  223. package/src/terminal-capabilities.ts +66 -28
  224. package/src/terminal-multiplexer.d.ts +7 -0
  225. package/src/terminal-multiplexer.ts +14 -0
  226. package/src/terminal.d.ts +8 -4
  227. package/src/terminal.ts +190 -63
  228. package/src/theme/symbols.d.ts +1 -1
  229. package/src/theme/symbols.ts +1 -0
  230. package/src/theme/theme-class.ts +1 -0
  231. package/src/theme/theme.d.ts +11 -0
  232. package/src/theme/theme.ts +46 -14
  233. package/src/thinking.d.ts +5 -0
  234. package/src/thinking.ts +9 -1
  235. package/src/tmux.d.ts +39 -4
  236. package/src/tmux.ts +110 -16
  237. package/src/tools/ask.ts +20 -9
  238. package/src/tools/ast-edit.d.ts +2 -2
  239. package/src/tools/ast-edit.ts +30 -7
  240. package/src/tools/ast-grep.d.ts +2 -2
  241. package/src/tools/ast-grep.ts +39 -35
  242. package/src/tools/bash.d.ts +6 -1
  243. package/src/tools/bash.ts +15 -9
  244. package/src/tools/debug.ts +2 -1
  245. package/src/tools/default-renderer.ts +3 -2
  246. package/src/tools/edit.d.ts +2 -0
  247. package/src/tools/edit.ts +100 -21
  248. package/src/tools/eval.d.ts +53 -4
  249. package/src/tools/eval.ts +173 -137
  250. package/src/tools/find.d.ts +14 -4
  251. package/src/tools/find.ts +71 -18
  252. package/src/tools/glob.d.ts +3 -3
  253. package/src/tools/glob.ts +51 -17
  254. package/src/tools/grep.d.ts +2 -2
  255. package/src/tools/grep.ts +99 -22
  256. package/src/tools/grouped-file-output.d.ts +9 -4
  257. package/src/tools/grouped-file-output.ts +21 -6
  258. package/src/tools/lsp.ts +30 -18
  259. package/src/tools/mcp.ts +2 -2
  260. package/src/tools/native-view.d.ts +1 -1
  261. package/src/tools/native-view.ts +2 -2
  262. package/src/tools/read.d.ts +32 -10
  263. package/src/tools/read.ts +89 -43
  264. package/src/tools/renderer.d.ts +2 -0
  265. package/src/tools/renderer.ts +2 -0
  266. package/src/tools/task.d.ts +31 -0
  267. package/src/tools/task.ts +40 -7
  268. package/src/tools/todo.ts +3 -2
  269. package/src/tools/wait.ts +16 -30
  270. package/src/tools/web-search-types.d.ts +1 -0
  271. package/src/tools/web-search-types.ts +1 -0
  272. package/src/tools/web-search.ts +2 -1
  273. package/src/tools/write.d.ts +2 -0
  274. package/src/tools/write.ts +36 -12
  275. package/src/tools/yield.ts +2 -2
  276. package/src/tui.d.ts +20 -3
  277. package/src/tui.ts +160 -43
  278. package/src/utils.ts +5 -3
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  280. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -7,7 +7,8 @@
7
7
  * scrollbar, right sidebar (file management + commit form while dirty, HEAD
8
8
  * commit details with author avatar when clean). Submitting an empty commit
9
9
  * form generates an llm-git-compatible message; submitting populated fields
10
- * commits them. A footer shows key hints.
10
+ * commits them. A footer shows key hints; the staging and discarding ones are
11
+ * omitted while the tree is clean, where they cannot fire.
11
12
  *
12
13
  * `tab` moves focus between the diff and the sidebar; both panes take
13
14
  * arrows/PgUp/PgDn, vim motions (`j`/`k`/`h`/`l`/`g`/`G`), and mouse
@@ -22,7 +23,8 @@
22
23
  * refreshes. In the sidebar tree `←`/`→` collapse/expand directories,
23
24
  * `enter` opens the selected file in the diff pane, `space` stages or
24
25
  * unstages the selected row, and `delete` discards it — on a directory,
25
- * every file underneath it.
26
+ * every file underneath it. All of the staging and discarding keys are inert
27
+ * on a clean tree, which shows the HEAD commit instead of a change list.
26
28
  */
27
29
 
28
30
  import type { TspSpan } from "@linxiraos/pi-wire";
@@ -32,7 +34,7 @@ import { Stack } from "../../components/layout/stack";
32
34
  import { matchesKey } from "../../keys";
33
35
  import { ProcessTerminal } from "../../terminal";
34
36
  import { routeSgrMouseInput } from "../../mouse";
35
- import { col, compact, keyed, node, row, span, text } from "../../native/describe";
37
+ import { col, compact, keyed, node, row, span, stableKey, text } from "../../native/describe";
36
38
  import { Memo } from "../../native/memo";
37
39
  import type { DescribeContext, NativeNode, NativeUiEvent } from "../../native/node";
38
40
  import { actionButton } from "../../native/overlay";
@@ -42,6 +44,7 @@ import { theme, warmHighlighter } from "../../theme/theme";
42
44
  import type { AvatarSource } from "./avatar";
43
45
  import { pill, softPill, tintChip } from "./colors";
44
46
  import {
47
+ assetTitle,
45
48
  buildDiffDocument,
46
49
  type DiffDocument,
47
50
  buildLineSelectionPatch,
@@ -53,7 +56,7 @@ import {
53
56
  } from "./diff-pane";
54
57
  import { GitHelpSheet } from "./help";
55
58
  import { Sidebar, type SidebarAction, type GitCommitMessage } from "./sidebar";
56
- import type { AssetFileContents, ChangedFile, FileContents, GitTuiModel } from "./state";
59
+ import type { AssetFileContents, ChangedFile, FileAssetSide, FileContents, GitTuiModel, MediaRef } from "./state";
57
60
 
58
61
  /** AI staging counts displayed after filtering the working tree. */
59
62
  export interface AiStageOutcome {
@@ -88,6 +91,28 @@ const STATUS_TTL_MS = 6_000;
88
91
 
89
92
  type Focus = "diff" | "sidebar";
90
93
 
94
+ /**
95
+ * Footer key hints. A clean tree renders the HEAD commit view, where stage
96
+ * and discard cannot fire: `#patchTargetFor` returns null and
97
+ * `#discardCurrentFile` returns early for `area: "commit"` rows, and the
98
+ * sidebar only builds stage/discard actions for the unstaged/staged
99
+ * sections. `commit` needs the commit form (absent on a clean tree) and
100
+ * `select` feeds the line-level stage/discard actions (no-ops on commit
101
+ * rows), so the commit view drops those too and says what it is showing.
102
+ */
103
+ export function gitFooterHints(clean: boolean, focus: Focus): string {
104
+ const edit = clean
105
+ ? ""
106
+ : focus === "diff"
107
+ ? ` · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard`
108
+ : ` · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard`;
109
+ const commit = clean ? "" : ` · ${formatKeyHint("c")} commit`;
110
+ const select = clean || focus !== "diff" ? "" : ` · ${formatKeyHints(["shift+up", "shift+down"])} select`;
111
+ return focus === "diff"
112
+ ? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file${select}${edit} · ${formatKeyHint("v")} view${commit} · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`
113
+ : `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold${edit} · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk${commit} · ${formatKeyHint("t")} tree · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`;
114
+ }
115
+
91
116
  /** Tone of a status message: its colour in the header and its span token natively. */
92
117
  type StatusTone = "success" | "warning" | "error" | "accent" | "dim";
93
118
 
@@ -792,14 +817,7 @@ class GitTuiComponent implements Component {
792
817
 
793
818
  // The empty middle carries the key hints (or a fresh status message).
794
819
  const status = this.#visibleStatus() ? theme.fg(this.#statusTone, this.#status) : "";
795
- const middle =
796
- status ||
797
- theme.fg(
798
- "dim",
799
- this.#focus === "diff"
800
- ? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard · ${formatKeyHint("v")} view · ${formatKeyHint("c")} commit · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`
801
- : `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHint("c")} commit · ${formatKeyHint("t")} tree · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`,
802
- );
820
+ const middle = status || theme.fg("dim", gitFooterHints(this.#model.clean, this.#focus));
803
821
  const free = width - row.width - right.width - 1;
804
822
  const middleText = free > visibleWidth(middle) + 4 ? middle : truncateToWidth(middle, Math.max(0, free - 4));
805
823
  const leftPad = Math.max(1, Math.floor((free - visibleWidth(middleText)) / 2));
@@ -929,8 +947,9 @@ class GitTuiComponent implements Component {
929
947
  this.#contents,
930
948
  this.#currentFile,
931
949
  this.#pane.emptyMessage,
950
+ cx.supports("media"),
932
951
  ],
933
- () => this.#describeBody(),
952
+ () => this.#describeBody(cx),
934
953
  );
935
954
  const side = this.#sidebar.describe(cx);
936
955
  return this.#nativeRoot.get([bar, body, side, narrow, status, this.#status, this.#statusTone], () => {
@@ -1086,7 +1105,7 @@ class GitTuiComponent implements Component {
1086
1105
  }
1087
1106
 
1088
1107
  /** The diff for the view mode, or the loading, empty and media states. */
1089
- #describeBody(): NativeNode {
1108
+ #describeBody(cx: DescribeContext): NativeNode {
1090
1109
  const doc = this.#pane.doc;
1091
1110
  const file = this.#currentFile;
1092
1111
  if (doc && this.#pane.state === "ready") {
@@ -1104,6 +1123,18 @@ class GitTuiComponent implements Component {
1104
1123
  }
1105
1124
  const contents = this.#contents;
1106
1125
  if (this.#pane.state === "asset" && file && contents?.kind === "asset") {
1126
+ if (cx.supports("media")) {
1127
+ return keyed(
1128
+ row(
1129
+ [
1130
+ mediaColumn("before", contents.old, contents.oldRef),
1131
+ mediaColumn("after", contents.new, contents.newRef),
1132
+ ],
1133
+ { gap: "sm", align: "stretch", grow: 1, role: "zeta.app.git.compare" },
1134
+ ),
1135
+ "compare",
1136
+ );
1137
+ }
1107
1138
  return keyed(
1108
1139
  col(
1109
1140
  [
@@ -1190,6 +1221,41 @@ function unifiedDiff(doc: DiffDocument, whole: boolean): string {
1190
1221
  return out.join("\n");
1191
1222
  }
1192
1223
 
1224
+ /** `media` nodes of the before/after compare share this link, so they pan and zoom together. */
1225
+ const MEDIA_COMPARE_LINK = "git-compare";
1226
+
1227
+ /**
1228
+ * One column of the native media compare: its heading (`Before · PNG`) over a
1229
+ * `media` node Tern loads from `ref`, keyed by path so another file remounts,
1230
+ * or a muted line when the side has no file.
1231
+ */
1232
+ function mediaColumn(side: "before" | "after", asset: FileAssetSide, ref: MediaRef | undefined): NativeNode {
1233
+ const head = keyed(
1234
+ text([span(assetTitle(side === "before" ? "Before" : "After", asset), "strong")], {
1235
+ truncate: "end",
1236
+ role: "zeta.app.git.media-head",
1237
+ }),
1238
+ "head",
1239
+ );
1240
+ const body = ref
1241
+ ? node(
1242
+ "media",
1243
+ { ...ref, link: MEDIA_COMPARE_LINK, grow: 1, role: "zeta.app.git.media" },
1244
+ undefined,
1245
+ `media-${stableKey(ref.path)}`,
1246
+ )
1247
+ : keyed(
1248
+ text(
1249
+ asset.kind === "empty"
1250
+ ? [span(side === "before" ? "Did not exist" : "Deleted", "muted")]
1251
+ : assetText(asset).map(fact => span(fact.t, "muted")),
1252
+ { role: "zeta.app.git.media-none" },
1253
+ ),
1254
+ "none",
1255
+ );
1256
+ return keyed(col([head, body], { gap: "xs", grow: 1, basis: 0, role: "zeta.app.git.media-side" }), side);
1257
+ }
1258
+
1193
1259
  /** One side of a media/binary change as a short fact line. */
1194
1260
  function assetText(asset: AssetFileContents["old"]): TspSpan[] {
1195
1261
  const size = (bytes: number | undefined): string =>
@@ -1681,6 +1681,9 @@ export class Sidebar {
1681
1681
  text: ` ${theme.fg("dim", tuiText("gitParentLabel", "parent:"))} ${theme.fg("accent", head.parents.map(sha => sha.slice(0, 8)).join(" "))}`,
1682
1682
  });
1683
1683
  }
1684
+ // This view only appears when there is nothing to stage or discard, so say so
1685
+ // rather than letting the file list read as pending work.
1686
+ rows.push({ text: theme.fg("success", ` Working tree clean — showing committed ${head.shortSha}`) });
1684
1687
  rows.push({ text: theme.fg("borderMuted", "─".repeat(Math.max(0, width))) });
1685
1688
  if (!head.filesLoaded) {
1686
1689
  rows.push({ text: theme.fg("dim", tuiText("gitLoadingChangedFiles", " Loading changed files…")) });
@@ -1689,8 +1692,14 @@ export class Sidebar {
1689
1692
 
1690
1693
  const additions = head.files.reduce((sum, file) => sum + (file.additions ?? 0), 0);
1691
1694
  const deletions = head.files.reduce((sum, file) => sum + (file.deletions ?? 0), 0);
1695
+ // Upstream's redesigned row ("N files in <sha>"); the count and the sha
1696
+ // fragment both come from the catalogue so a zh session localizes them.
1697
+ const count =
1698
+ head.files.length === 1
1699
+ ? tuiTextFmt("gitModifiedCountOneFmt", "%s file", head.files.length)
1700
+ : tuiTextFmt("gitModifiedCountManyFmt", "%s files", head.files.length);
1692
1701
  rows.push({
1693
- text: ` ${theme.bold(tuiTextFmt("gitModifiedCountFmt", "%s modified", head.files.length))} ${theme.fg("success", `+${additions}`)} ${theme.fg("error", `−${deletions}`)} ${theme.fg("dim", `· ${head.shortSha}`)}`,
1702
+ text: ` ${theme.bold(count)} ${theme.fg("dim", tuiTextFmt("gitInShaFmt", `in ${head.shortSha}`, head.shortSha))} ${theme.fg("success", `+${additions}`)} ${theme.fg("error", `−${deletions}`)}`,
1694
1703
  });
1695
1704
  rows.push(this.#viewToggleRow(width));
1696
1705
  rows.push(...this.#entryRows(head.files, "commit"));
@@ -66,11 +66,23 @@ export interface TextFileContents {
66
66
  readonly streamResult: DiffStreamResult;
67
67
  }
68
68
 
69
+ /** Where one side of a change lives, so a host viewer (Tern's TSP `media` node) can load it by path. */
70
+ export interface MediaRef {
71
+ /** Absolute path of the file (repository root joined with its repository path). */
72
+ readonly path: string;
73
+ /** Git revision holding this side (`HEAD`, a sha, `:0` for the index); absent for the worktree file. */
74
+ readonly rev?: string;
75
+ }
76
+
69
77
  /** Non-text sides rendered as previews or explicit placeholders. */
70
78
  export interface AssetFileContents {
71
79
  readonly kind: "asset";
72
80
  readonly old: FileAssetSide;
73
81
  readonly new: FileAssetSide;
82
+ /** Source of {@link old}; absent when that side is empty. */
83
+ readonly oldRef?: MediaRef;
84
+ /** Source of {@link new}; absent when that side is empty. */
85
+ readonly newRef?: MediaRef;
74
86
  }
75
87
 
76
88
  /** Loaded file content selected for text diffing or asset preview. */
@@ -6,6 +6,8 @@
6
6
  */
7
7
  export declare function findLeadingSlashCommandStart(text: string): number | null;
8
8
  export declare function findTrailingSlashCommandStart(text: string): number | null;
9
+ /** Extract the live whitespace-delimited or double-quoted `@` file token ending at the cursor. */
10
+ export declare function extractAtFilePrefix(text: string): string | null;
9
11
  /**
10
12
  * Whether an autocomplete value represents a directory: trailing slash or
11
13
  * backslash, optionally followed by a closing quote for quoted paths.
@@ -14,6 +14,11 @@ const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
14
14
  */
15
15
  const AT_PARTIAL_DELAY_MS = 150;
16
16
 
17
+ /**
18
+ * Without a deadline the native walk follows symlinks at unbounded depth and never returns.
19
+ */
20
+ const AT_FUZZY_TIMEOUT_MS = 1_500;
21
+
17
22
  function buildAutocompleteFuzzyDiscoveryProfile(
18
23
  query: string,
19
24
  basePath: string,
@@ -25,6 +30,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
25
30
  hidden: boolean;
26
31
  gitignore: boolean;
27
32
  cache: boolean;
33
+ timeoutMs: number;
28
34
  signal?: AbortSignal;
29
35
  } {
30
36
  return {
@@ -34,6 +40,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
34
40
  hidden: true,
35
41
  gitignore: true,
36
42
  cache: true,
43
+ timeoutMs: AT_FUZZY_TIMEOUT_MS,
37
44
  ...(signal ? { signal } : {}),
38
45
  };
39
46
  }
@@ -106,6 +113,18 @@ function extractQuotedPrefix(text: string): string | null {
106
113
  return text.slice(quoteStart);
107
114
  }
108
115
 
116
+ /** Extract the live whitespace-delimited or double-quoted `@` file token ending at the cursor. */
117
+ export function extractAtFilePrefix(text: string): string | null {
118
+ const quotedPrefix = extractQuotedPrefix(text);
119
+ if (quotedPrefix?.startsWith('@"')) {
120
+ return quotedPrefix;
121
+ }
122
+
123
+ const lastDelimiterIndex = findLastDelimiter(text);
124
+ const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
125
+ return text[tokenStart] === "@" ? text.slice(tokenStart) : null;
126
+ }
127
+
109
128
  function parsePathPrefix(prefix: string): { rawPrefix: string; isAtPrefix: boolean; isQuotedPrefix: boolean } {
110
129
  if (prefix.startsWith('@"')) {
111
130
  return { rawPrefix: prefix.slice(2), isAtPrefix: true, isQuotedPrefix: true };
@@ -719,7 +738,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
719
738
  }
720
739
 
721
740
  // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
722
- const atPrefix = this.#extractAtPrefix(textBeforeCursor);
741
+ const atPrefix = extractAtFilePrefix(textBeforeCursor);
723
742
  if (atPrefix) {
724
743
  const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
725
744
  // Recursive fuzzy walks rooted outside the project (e.g. `@../`,
@@ -862,7 +881,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
862
881
 
863
882
  // Check if we're completing a file attachment (prefix starts with "@")
864
883
  if (prefix.startsWith("@")) {
865
- const liveAtPrefix = this.#extractAtPrefix(textBeforeCursor);
884
+ const liveAtPrefix = extractAtFilePrefix(textBeforeCursor);
866
885
  if (liveAtPrefix) {
867
886
  beforePrefix = currentLine.slice(0, cursorCol - liveAtPrefix.length);
868
887
  }
@@ -896,23 +915,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
896
915
  };
897
916
  }
898
917
 
899
- // Extract @ prefix for fuzzy file suggestions
900
- #extractAtPrefix(text: string): string | null {
901
- const quotedPrefix = extractQuotedPrefix(text);
902
- if (quotedPrefix?.startsWith('@"')) {
903
- return quotedPrefix;
904
- }
905
-
906
- const lastDelimiterIndex = findLastDelimiter(text);
907
- const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
908
-
909
- if (text[tokenStart] === "@") {
910
- return text.slice(tokenStart);
911
- }
912
-
913
- return null;
914
- }
915
-
916
918
  // Extract a path-like prefix from the text before cursor
917
919
  #extractPathPrefix(text: string, forceExtract: boolean = false): string | null {
918
920
  const quotedPrefix = extractQuotedPrefix(text);
@@ -68,6 +68,7 @@ const DEFAULT_BYTE_LIMIT = 64 * 1024 * 1024;
68
68
  */
69
69
  export class BracketedPasteHandler {
70
70
  #buffer = "";
71
+ #bufferBytes = 0;
71
72
  #active = false;
72
73
  readonly #byteLimit: number;
73
74
 
@@ -88,7 +89,10 @@ export class BracketedPasteHandler {
88
89
  const start = data.indexOf(PASTE_START);
89
90
  const end = data.indexOf(PASTE_END);
90
91
  if (start !== -1 && (end === -1 || start < end)) {
91
- if (!this.#active) this.#buffer = "";
92
+ if (!this.#active) {
93
+ this.#buffer = "";
94
+ this.#bufferBytes = 0;
95
+ }
92
96
  this.#active = true;
93
97
  // Only strip nested starts before this paste's first end. Later starts
94
98
  // belong to the next paste in `remaining`.
@@ -100,6 +104,7 @@ export class BracketedPasteHandler {
100
104
 
101
105
  if (!this.#active) return { handled: false };
102
106
 
107
+ this.#bufferBytes += Buffer.byteLength(data, "utf8");
103
108
  this.#buffer += data;
104
109
 
105
110
  const endIndex = this.#buffer.indexOf(PASTE_END);
@@ -108,6 +113,7 @@ export class BracketedPasteHandler {
108
113
  const remaining = this.#buffer.substring(endIndex + PASTE_END.length);
109
114
 
110
115
  this.#buffer = "";
116
+ this.#bufferBytes = 0;
111
117
  this.#active = false;
112
118
 
113
119
  return { handled: true, pasteContent, remaining };
@@ -118,9 +124,10 @@ export class BracketedPasteHandler {
118
124
  // neither lost nor held forever, and reset paste mode so subsequent
119
125
  // input recovers. See `StdinBuffer#abortPaste` for the sibling recovery
120
126
  // semantics inside `StdinBuffer`.
121
- if (this.#buffer.length > this.#byteLimit) {
127
+ if (this.#bufferBytes > this.#byteLimit) {
122
128
  const pasteContent = this.#buffer;
123
129
  this.#buffer = "";
130
+ this.#bufferBytes = 0;
124
131
  this.#active = false;
125
132
  return { handled: true, pasteContent, remaining: "" };
126
133
  }
@@ -2,9 +2,11 @@
2
2
  * Draws a {@link ChartSpec} as a standalone SVG document colored only with
3
3
  * figure tokens — `var(--fg)`, `var(--muted)`, `var(--border)`,
4
4
  * `var(--accent)`, `var(--surface)`, `var(--c1)`…`var(--c6)` — on a
5
- * transparent background — the token vocabulary of assistant ```svg figures —
6
- * so the host resolves them against the live theme (`prepareSvg`) for both
7
- * the rasterized terminal image and the native TSP blob.
5
+ * transparent background — the token vocabulary of assistant ```svg figures:
6
+ * the host resolves them against the live theme (`prepareSvg`) for the
7
+ * rasterized terminal image, and a TSP terminal draws the chart as an ```svg
8
+ * figure in the palette it shows. The root carries {@link chartAlt} as its
9
+ * `aria-label`.
8
10
  *
9
11
  * Styled after Apple's charts and Tern's UI: a UI sans (Geist where Tern
10
12
  * provides it), muted labels, hairline gridlines that recede behind the
@@ -39,16 +41,8 @@
39
41
  */
40
42
  import { type ChartSpec } from "./chart-plan";
41
43
  import { type Dimension } from "./table-data";
42
- /** A drawn chart: the SVG document and its size in user units. */
43
- export interface ChartSvg {
44
- readonly svg: string;
45
- readonly width: number;
46
- readonly height: number;
47
- }
48
- /** The SVG document for `spec`. */
49
- export declare function renderChartSvg(spec: ChartSpec): ChartSvg;
50
- /** One-line description of the chart, for screen readers and terminals that show the alt text. */
51
- export declare function chartAlt(spec: ChartSpec): string;
44
+ /** The SVG document for `spec`, labeled for screen readers with {@link chartAlt}. */
45
+ export declare function renderChartSvg(spec: ChartSpec): string;
52
46
  /**
53
47
  * A value in base units as an axis label: durations and sizes in the unit,
54
48
  * counts compacted under the suffix (`12k`, `3.4M`), that `magnitude` reads
@@ -2,9 +2,11 @@
2
2
  * Draws a {@link ChartSpec} as a standalone SVG document colored only with
3
3
  * figure tokens — `var(--fg)`, `var(--muted)`, `var(--border)`,
4
4
  * `var(--accent)`, `var(--surface)`, `var(--c1)`…`var(--c6)` — on a
5
- * transparent background — the token vocabulary of assistant ```svg figures —
6
- * so the host resolves them against the live theme (`prepareSvg`) for both
7
- * the rasterized terminal image and the native TSP blob.
5
+ * transparent background — the token vocabulary of assistant ```svg figures:
6
+ * the host resolves them against the live theme (`prepareSvg`) for the
7
+ * rasterized terminal image, and a TSP terminal draws the chart as an ```svg
8
+ * figure in the palette it shows. The root carries {@link chartAlt} as its
9
+ * `aria-label`.
8
10
  *
9
11
  * Styled after Apple's charts and Tern's UI: a UI sans (Geist where Tern
10
12
  * provides it), muted labels, hairline gridlines that recede behind the
@@ -236,15 +238,8 @@ interface Tier {
236
238
  readonly high: number;
237
239
  }
238
240
 
239
- /** A drawn chart: the SVG document and its size in user units. */
240
- export interface ChartSvg {
241
- readonly svg: string;
242
- readonly width: number;
243
- readonly height: number;
244
- }
245
-
246
- /** The SVG document for `spec`. */
247
- export function renderChartSvg(spec: ChartSpec): ChartSvg {
241
+ /** The SVG document for `spec`, labeled for screen readers with {@link chartAlt}. */
242
+ export function renderChartSvg(spec: ChartSpec): string {
248
243
  const layer = drawChart(spec);
249
244
  const header = drawHeader(spec, layer);
250
245
  const head = matrixHead(spec);
@@ -268,8 +263,8 @@ export function renderChartSvg(spec: ChartSpec): ChartSvg {
268
263
  height += 20;
269
264
  }
270
265
  height = Math.ceil(height + PAD);
271
- const svg = [
272
- `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" font-family="${FONT}" font-size="${LABEL_SIZE}">`,
266
+ return [
267
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" role="img" aria-label="${escapeXml(chartAlt(spec))}" font-family="${FONT}" font-size="${LABEL_SIZE}">`,
273
268
  layer.body.includes(`url(#${INK_MASK})`) ? INK_DEFS : "",
274
269
  `<g transform="translate(0 ${PAD})">${header.body}</g>`,
275
270
  head.body && `<g transform="translate(0 ${num(PAD + header.height)})">${head.body}</g>`,
@@ -278,11 +273,10 @@ export function renderChartSvg(spec: ChartSpec): ChartSvg {
278
273
  caption,
279
274
  "</svg>",
280
275
  ].join("");
281
- return { svg, width: WIDTH, height };
282
276
  }
283
277
 
284
- /** One-line description of the chart, for screen readers and terminals that show the alt text. */
285
- export function chartAlt(spec: ChartSpec): string {
278
+ /** One-line description of the chart, for screen readers. */
279
+ function chartAlt(spec: ChartSpec): string {
286
280
  const joint =
287
281
  spec.kind === "stacked" ? " + " : spec.kind === "slope" ? " → " : spec.kind === "butterfly" ? " vs " : ", ";
288
282
  const names = spec.series.map(entry => entry.name).join(joint);
@@ -4432,6 +4426,13 @@ function token(name: string): string {
4432
4426
  return `var(--${name})`;
4433
4427
  }
4434
4428
 
4429
+ /** `raw` safe as XML text or a double-quoted attribute value. */
4430
+ function escapeXml(raw: string): string {
4431
+ return raw.replace(/[&<>"]/g, char =>
4432
+ char === "&" ? "&amp;" : char === "<" ? "&lt;" : char === ">" ? "&gt;" : "&quot;",
4433
+ );
4434
+ }
4435
+
4435
4436
  function num(value: number): string {
4436
4437
  return Number(value.toFixed(1)).toString();
4437
4438
  }
@@ -4469,10 +4470,6 @@ function text(x: number, y: number, content: string | readonly TextRun[], style:
4469
4470
  style.weight ? `font-weight="${style.weight}"` : "",
4470
4471
  style.italic ? `font-style="italic"` : "",
4471
4472
  ].filter(Boolean);
4472
- const escape = (raw: string) =>
4473
- raw.replace(/[&<>"]/g, char =>
4474
- char === "&" ? "&amp;" : char === "<" ? "&lt;" : char === ">" ? "&gt;" : "&quot;",
4475
- );
4476
4473
  const body = runs
4477
4474
  .filter(run => run.content)
4478
4475
  .map(run => {
@@ -4480,7 +4477,7 @@ function text(x: number, y: number, content: string | readonly TextRun[], style:
4480
4477
  run.fill ? ` fill="${token(run.fill)}"` : "",
4481
4478
  run.weight ? ` font-weight="${run.weight}"` : "",
4482
4479
  ].join("");
4483
- return own ? `<tspan${own}>${escape(run.content)}</tspan>` : escape(run.content);
4480
+ return own ? `<tspan${own}>${escapeXml(run.content)}</tspan>` : escapeXml(run.content);
4484
4481
  })
4485
4482
  .join("");
4486
4483
  return `<text ${attrs.join(" ")}>${body}</text>`;
@@ -148,7 +148,17 @@ export interface TableAnalysis {
148
148
  readonly composition?: Composition;
149
149
  }
150
150
 
151
- const NUMBER = String.raw`(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+`;
151
+ /**
152
+ * A whole number as written: bare, or grouped in thousands by commas (`1,988`)
153
+ * or by spaces as many locales write it (`1 988`, also U+00A0 and U+202F).
154
+ * Space groups take exactly three digits, so `12 5` and `128 2560` stay apart.
155
+ */
156
+ const INTEGER = String.raw`\d{1,3}(?:,\d{3})+|\d{1,3}(?:[ \u00A0\u202F]\d{3})+(?!\d)|\d+`;
157
+ const NUMBER = String.raw`(?:${INTEGER})(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+`;
158
+ /** A cell holding one whole number and nothing else (`38`, `1,980`). */
159
+ const WHOLE = new RegExp(String.raw`^(?:${INTEGER})$`);
160
+ /** The first number of a figure, its whole part and its decimals apart. */
161
+ const WRITTEN = new RegExp(String.raw`(${INTEGER})(?:\.(\d+))?`);
152
162
  const CELL = new RegExp(
153
163
  String.raw`^(?<approx>~|≈|<=|>=|<|>|≤|≥)?\s*(?<sign>[+\-−±])?\s*(?<cur>[$€£¥])?\s*(?<num>${NUMBER})(?<plus>\+(?=\s*[A-Za-zµμ%]))?\s*(?<unit>%|[A-Za-zµμ×]+(?:\/[A-Za-z]+)?|\/s)?(?<rest>.*)$`,
154
164
  );
@@ -517,8 +527,9 @@ export function parseCell(markdown: string): Cell {
517
527
  return { ...cell, fit: "prose" };
518
528
  }
519
529
 
530
+ /** The value of a number {@link NUMBER} matched, its comma or space group separators removed. */
520
531
  function toNumber(written: string): number {
521
- return Number(written.replaceAll(",", ""));
532
+ return Number(written.replace(/[,\s]/g, ""));
522
533
  }
523
534
 
524
535
  /** A number of a list or sum: its value as written, its unit (known or a noun) and how it was marked. */
@@ -632,7 +643,7 @@ function listCell(text: string, base: CellBase): NumberCell | undefined {
632
643
  // A spaced pair of bare numbers that is no score (`25.0 / 27.8`, `12.6 / 9.4`: the second is no whole, or
633
644
  // the first exceeds it) lists two values of the column's quantity; `27 / 30` stays a score.
634
645
  const [first, second] = items;
635
- const whole = /^\d[\d,]*$/.test(text.slice(text.indexOf("/") + 1, end).trim());
646
+ const whole = WHOLE.test(text.slice(text.indexOf("/") + 1, end).trim());
636
647
  const score = whole && first!.value >= 0 && first!.value <= second!.value;
637
648
  const pair = items.length === 2 && !marked.some(Boolean) && separators[0] !== "/" && !score;
638
649
  if (!pair && (!marked.at(-1) || !(marked.every(Boolean) || carried))) return undefined;
@@ -876,9 +887,9 @@ const WHOLE_HEADER = /\b(?:total|all|sum|overall)\b/i;
876
887
  */
877
888
  export function writtenSlack(cell: NumberCell): number {
878
889
  if (cell.denominator !== undefined && cell.dim === "percent") return 0;
879
- const match = /(\d[\d,]*)(?:\.(\d+))?/.exec(cell.figure);
890
+ const match = WRITTEN.exec(cell.figure);
880
891
  if (!match) return 0;
881
- const written = Number(`${match[1]!.replaceAll(",", "")}${match[2] ? `.${match[2]}` : ""}`);
892
+ const written = toNumber(`${match[1]!}${match[2] ? `.${match[2]}` : ""}`);
882
893
  const factor = written ? Math.abs(cell.value) / written : 1;
883
894
  const decimals = match[2]?.length ?? 0;
884
895
  if (cell.dim === "count" && decimals === 0 && factor === 1 && !cell.approx) return 0;
@@ -1097,7 +1108,7 @@ const MIN_LISTS = 2;
1097
1108
  /** A spaced slash between list items. */
1098
1109
  const LIST_SLASH = /\s+\/\s+/;
1099
1110
  /** A list item that is one number, as written (`12`, `~1,400`, `3.5 ms`, `40%`). */
1100
- const LISTED_NUMBER = /^[~≈]?[$€£¥]?\d[\d,]*(?:\.\d+)?\s*(?:%|[A-Za-zµμ]+)?$/;
1111
+ const LISTED_NUMBER = new RegExp(String.raw`^[~≈]?[$€£¥]?(?:${INTEGER})(?:\.\d+)?\s*(?:%|[A-Za-zµμ]+)?$`);
1101
1112
 
1102
1113
  /**
1103
1114
  * Rows folding several rows into one (`response anthropic / chat / responses /
@@ -1,6 +1,6 @@
1
1
  import { type Component } from "../tui";
2
2
  import type { AdvisorMessageDetails } from "./messages";
3
- import { type Theme } from "../theme";
3
+ import type { Theme } from "../theme";
4
4
  import { type NativeUiEvent } from "../native/node";
5
5
  /** The advisor card: shared by the controlled expansion getter and native toggles. */
6
6
  export interface AdvisorMessageCard extends Component {
@@ -14,6 +14,9 @@ export interface AdvisorMessageCard extends Component {
14
14
  * output (whose `thinkingText` color equals `toolOutput` in most themes):
15
15
  * a bold `customMessageLabel` header tag (skill-card convention), a heavy
16
16
  * rail tinted per-note severity, and the note body on the default text color.
17
+ * Natively it is a callout tinted by its worst note: that severity's icon and
18
+ * the tally head it, the advisor's name ends the head, and with several notes
19
+ * each leads with its own severity icon.
17
20
  *
18
21
  * `getExpanded` owns expansion; a native toggle overrides it for this card
19
22
  * until the next transcript-wide change.