@oh-my-pi/pi-tui 18.6.1 → 18.7.0

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 (102) hide show
  1. package/CHANGELOG.md +50 -58
  2. package/README.md +6 -2
  3. package/dist/types/app-keybindings.d.ts +7 -2
  4. package/dist/types/autocomplete.d.ts +5 -2
  5. package/dist/types/charts/chart-plan.d.ts +92 -0
  6. package/dist/types/charts/chart-svg.d.ts +30 -0
  7. package/dist/types/charts/table-data.d.ts +107 -0
  8. package/dist/types/chat/assistant-message.d.ts +10 -1
  9. package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
  10. package/dist/types/chat/figure-markdown.d.ts +42 -0
  11. package/dist/types/chat/svg-figure.d.ts +33 -0
  12. package/dist/types/chat/svg-source.d.ts +49 -0
  13. package/dist/types/chat/table-chart.d.ts +88 -0
  14. package/dist/types/chat/user-message.d.ts +2 -2
  15. package/dist/types/chrome/status-notice.d.ts +15 -3
  16. package/dist/types/components/editor.d.ts +5 -1
  17. package/dist/types/components/image.d.ts +23 -0
  18. package/dist/types/components/input.d.ts +1 -0
  19. package/dist/types/components/loader.d.ts +5 -6
  20. package/dist/types/components/markdown.d.ts +9 -0
  21. package/dist/types/fuzzy.d.ts +14 -0
  22. package/dist/types/key-hint-format.d.ts +8 -0
  23. package/dist/types/keybindings.d.ts +1 -1
  24. package/dist/types/native/backend.d.ts +2 -2
  25. package/dist/types/native/node.d.ts +6 -0
  26. package/dist/types/native/overlay.d.ts +3 -3
  27. package/dist/types/overlays/agent-hub.d.ts +1 -0
  28. package/dist/types/overlays/agent-transcript-viewer.d.ts +1 -0
  29. package/dist/types/overlays/agents-hub.d.ts +9 -0
  30. package/dist/types/overlays/annotation-overlay.d.ts +5 -2
  31. package/dist/types/overlays/btw-history-panel.d.ts +5 -1
  32. package/dist/types/overlays/copy-selector.d.ts +1 -0
  33. package/dist/types/overlays/model-browser.d.ts +25 -3
  34. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  35. package/dist/types/overlays/rewind-selector.d.ts +1 -0
  36. package/dist/types/overlays/usage-dashboard.d.ts +4 -4
  37. package/dist/types/prompt/custom-editor.d.ts +10 -4
  38. package/dist/types/prompt/interactive-context-helpers.d.ts +3 -0
  39. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  40. package/dist/types/render/composite.d.ts +2 -0
  41. package/dist/types/space-hold.d.ts +40 -25
  42. package/dist/types/terminal-capabilities.d.ts +13 -0
  43. package/dist/types/tools/glob.d.ts +4 -0
  44. package/dist/types/tui.d.ts +5 -0
  45. package/dist/types/utils.d.ts +6 -0
  46. package/package.json +9 -9
  47. package/src/app-keybindings.ts +6 -1
  48. package/src/apps/git/diff-pane.ts +42 -16
  49. package/src/apps/git/git-tui.ts +9 -9
  50. package/src/apps/git/sidebar.ts +6 -1
  51. package/src/autocomplete.ts +8 -10
  52. package/src/charts/chart-plan.ts +367 -0
  53. package/src/charts/chart-svg.ts +693 -0
  54. package/src/charts/table-data.ts +436 -0
  55. package/src/chat/assistant-message.ts +165 -25
  56. package/src/chat/chat-transcript-builder.ts +8 -0
  57. package/src/chat/figure-markdown.ts +174 -0
  58. package/src/chat/svg-figure.ts +364 -0
  59. package/src/chat/svg-source.ts +188 -0
  60. package/src/chat/table-chart.ts +301 -0
  61. package/src/chat/tool-execution.ts +6 -1
  62. package/src/chat/user-message.ts +16 -5
  63. package/src/chrome/status-notice.ts +25 -7
  64. package/src/chrome/transcript-container.ts +13 -4
  65. package/src/components/editor.ts +66 -30
  66. package/src/components/image.ts +95 -2
  67. package/src/components/input.ts +14 -6
  68. package/src/components/loader.ts +10 -17
  69. package/src/components/markdown.ts +6 -3
  70. package/src/fuzzy.ts +59 -19
  71. package/src/hotkeys-markdown.ts +9 -2
  72. package/src/key-hint-format.ts +25 -10
  73. package/src/keybindings.ts +1 -1
  74. package/src/native/backend.ts +10 -3
  75. package/src/native/node.ts +6 -0
  76. package/src/native/overlay.ts +4 -4
  77. package/src/overlays/agent-hub.ts +4 -0
  78. package/src/overlays/agent-transcript-viewer.ts +4 -0
  79. package/src/overlays/agents-hub.ts +14 -1
  80. package/src/overlays/annotation-overlay.ts +179 -80
  81. package/src/overlays/ask-dialog.ts +1 -1
  82. package/src/overlays/btw-history-panel.ts +9 -2
  83. package/src/overlays/copy-selector.ts +2 -0
  84. package/src/overlays/extensions/extension-dashboard.ts +4 -4
  85. package/src/overlays/model-browser.ts +169 -76
  86. package/src/overlays/pause-screen.ts +2 -2
  87. package/src/overlays/plan-review-overlay.ts +41 -4
  88. package/src/overlays/rewind-selector.ts +2 -0
  89. package/src/overlays/usage-dashboard.ts +17 -8
  90. package/src/overlays/usage-row.ts +22 -8
  91. package/src/prompt/composer.ts +2 -14
  92. package/src/prompt/custom-editor.ts +188 -67
  93. package/src/prompt/interactive-context-helpers.ts +5 -0
  94. package/src/prompt/model-mention-autocomplete.ts +14 -14
  95. package/src/prompt/queued-messages.ts +6 -4
  96. package/src/render/composite.ts +62 -0
  97. package/src/space-hold.ts +156 -52
  98. package/src/status-line/segments.ts +3 -1
  99. package/src/terminal-capabilities.ts +42 -23
  100. package/src/tools/glob.ts +23 -6
  101. package/src/tui.ts +154 -119
  102. package/src/utils.ts +19 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,55 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.7.0] - 2026-10-06
6
+
7
+ ### Added
8
+
9
+ - Assistant SVG code blocks now render as inline, theme-colored images on terminals with graphics support, updating as responses stream and adapting to terminal width; SVG that cannot be rendered remains available as code.
10
+ - Numeric tables in assistant responses can now include automatically selected, themed charts based on the table’s structure and units.
11
+
12
+ ### Changed
13
+
14
+ - The status line now recognizes projects located in the user’s `repos` directory.
15
+ - Model mentions, `/switch` completions, and model-picker search now update immediately while typing, including with large model catalogs.
16
+
17
+ ### Fixed
18
+
19
+ - Fixed terminal resizing issues that could cause flicker or briefly display an empty frame.
20
+ - Improved `/annotate` handling for long source lines and filenames, preserving indentation and typed note text.
21
+ - Fixed fullscreen inputs that could hide the cursor when hardware-cursor support was enabled.
22
+ - Fixed model picker and mention-list ordering for same-provider `-latest` models so results remain alphabetically stable.
23
+ - Model browser performance metrics now show the correct measurements for each service tier and identify the tier.
24
+ - Fixed plan review keyboard navigation so horizontal options use Left/Right and model-slider adjustments use Shift+Left/Right.
25
+ - Improved the Ask dialog footer so question-switching keyboard shortcuts are clearly labeled.
26
+ - Fixed creating a new agent when its generated system prompt contains a Markdown code fence.
27
+ - Timed-out `glob` scans are now labeled as timed out rather than truncated.
28
+ - Ctrl+Delete now deletes the word after the cursor, matching Ctrl+Backspace behavior.
29
+
30
+ ## [18.6.3] - 2026-10-06
31
+
32
+ ### Breaking Changes
33
+
34
+ - `WorkingRowSpec` no longer takes `rate`: the native working row reads spinner, elapsed time, divider, then the intent, and the tok/s readout moved to the composer bar
35
+
36
+ ### Added
37
+
38
+ - `ComposerNativeState.rate` shows a tok/s readout in the native composer bar after the effort chip, and `ComposerNativeState.thinkingInModel` draws the thinking level as the model chip's icon instead of a separate chip
39
+ - `formatTooltipKey` formats a key for a native tooltip: unicode keycap glyphs whatever the symbol preset, `esc` for Escape
40
+ - `ImageOptions.requestRender` repaints an image without an image budget once its SIXEL encode lands ([#14529](https://github.com/can1357/oh-my-pi/pull/14529) by [@H4vC](https://github.com/H4vC))
41
+ - `AssistantMessageComponent` can keep finished thinking sections expanded via `setExpandThinkingBlocks()`; sections folded by hand stay folded ([#14519](https://github.com/can1357/oh-my-pi/pull/14519) by [@H4vC](https://github.com/H4vC))
42
+
43
+ ### Changed
44
+
45
+ - `/hotkeys` shows the effective `app.stt.pushToTalk` binding, including `Disabled` when unbound ([#6592](https://github.com/can1357/oh-my-pi/pull/6592) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)).
46
+ - SIXEL images are encoded off the main thread, so showing an image no longer stalls the terminal ([#14529](https://github.com/can1357/oh-my-pi/pull/14529) by [@H4vC](https://github.com/H4vC))
47
+
48
+ ### Fixed
49
+
50
+ - Fixed native tooltips (composer, working row, queue, pause screen, git and extension dashboards) naming keys with Nerd Font icons Tern's UI font lacks, or as raw key ids (`ctrl+g`); they use keycap glyphs (`⇧⇥`, `⌃G`)
51
+ - Fixed Tern's per-turn usage row showing a 24-hour time on a 12-hour terminal clock; the row and its tooltip now follow the terminal's clock, keeping the tooltip's ISO date ([#14565](https://github.com/can1357/oh-my-pi/pull/14565) by [@wolfiesch](https://github.com/wolfiesch))
52
+ - Fixed hold-Space push-to-talk stopping the instant recording began (showing "No speech detected.") when opening the microphone briefly froze the UI ([#14463](https://github.com/can1357/oh-my-pi/pull/14463) by [@pgkt04](https://github.com/pgkt04))
53
+
5
54
  ## [18.6.1] - 2026-10-04
6
55
 
7
56
  ### Fixed
@@ -2974,61 +3023,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
2974
3023
 
2975
3024
  - `Editor.getExpandedText()` method that returns text with paste markers expanded to their actual content ([#444](https://github.com/badlogic/pi-mono/pull/444) by [@aliou](https://github.com/aliou))
2976
3025
 
2977
- ## [0.33.0] - 2026-01-04
2978
-
2979
- ### Breaking Changes
2980
-
2981
- - **Key detection functions removed**: All `isXxx()` key detection functions (`isEnter()`, `isEscape()`, `isCtrlC()`, etc.) have been removed. Use `matchesKey(data, keyId)` instead (e.g., `matchesKey(data, "enter")`, `matchesKey(data, "ctrl+c")`). This affects hooks and custom tools that use `ctx.ui.custom()` with keyboard input handling. ([#405](https://github.com/badlogic/pi-mono/pull/405))
2982
-
2983
- ### Added
2984
-
2985
- - `Editor.insertTextAtCursor(text)` method for programmatic text insertion ([#419](https://github.com/badlogic/pi-mono/issues/419))
2986
- - `EditorKeybindingsManager` for configurable editor keybindings. Components now use `matchesKey()` and keybindings manager instead of individual `isXxx()` functions. ([#405](https://github.com/badlogic/pi-mono/pull/405) by [@hjanuschka](https://github.com/hjanuschka))
2987
-
2988
- ### Changed
2989
-
2990
- - Key detection refactored: consolidated `is*()` functions into generic `matchesKey(data, keyId)` function that accepts key identifiers like `"ctrl+c"`, `"shift+enter"`, `"alt+left"`, etc.
2991
-
2992
- ## [0.32.2] - 2026-01-03
2993
-
2994
- ### Fixed
2995
-
2996
- - Slash command autocomplete now triggers for commands starting with `.`, `-`, or `_` (e.g., `/.land`, `/-foo`) ([#422](https://github.com/badlogic/pi-mono/issues/422))
2997
-
2998
- ## [0.32.0] - 2026-01-03
2999
-
3000
- ### Changed
3001
-
3002
- - Editor component now uses word wrapping instead of character-level wrapping for better readability ([#382](https://github.com/badlogic/pi-mono/pull/382) by [@nickseelert](https://github.com/nickseelert))
3003
-
3004
- ### Fixed
3005
-
3006
- - Shift+Space, Shift+Backspace, and Shift+Delete now work correctly in Kitty-protocol terminals (Kitty, WezTerm, etc.) instead of being silently ignored ([#411](https://github.com/badlogic/pi-mono/pull/411) by [@nathyong](https://github.com/nathyong))
3007
-
3008
- ## [0.31.1] - 2026-01-02
3009
-
3010
- ### Fixed
3011
-
3012
- - `visibleWidth()` now strips OSC 8 hyperlink sequences, fixing text wrapping for clickable links ([#396](https://github.com/badlogic/pi-mono/pull/396) by [@Cursivez](https://github.com/Cursivez))
3013
-
3014
- ## [0.31.0] - 2026-01-02
3015
-
3016
- ### Added
3017
-
3018
- - `isShiftCtrlO()` key detection function for Shift+Ctrl+O (Kitty protocol)
3019
- - `isShiftCtrlD()` key detection function for Shift+Ctrl+D (Kitty protocol)
3020
- - `TUI.onDebug` callback for global debug key handling (Shift+Ctrl+D)
3021
- - `wrapTextWithAnsi()` utility now exported (wraps text to width, preserving ANSI codes)
3022
-
3023
- ### Changed
3024
-
3025
- - README.md completely rewritten with accurate component documentation, theme interfaces, and examples
3026
- - `visibleWidth()` reimplemented with grapheme-based width calculation, 10x faster on Bun and ~15% faster on Node ([#369](https://github.com/badlogic/pi-mono/pull/369) by [@nathyong](https://github.com/nathyong))
3027
-
3028
- ### Fixed
3029
-
3030
- - Markdown component now renders HTML tags as plain text instead of silently dropping them ([#359](https://github.com/badlogic/pi-mono/issues/359))
3031
- - Crash in `visibleWidth()` and grapheme iteration when encountering undefined code points ([#372](https://github.com/badlogic/pi-mono/pull/372) by [@HACKE-RC](https://github.com/HACKE-RC))
3032
- - ZWJ emoji sequences (rainbow flag, family, etc.) now render with correct width instead of being split into multiple characters ([#369](https://github.com/badlogic/pi-mono/pull/369) by [@nathyong](https://github.com/nathyong))
3033
-
3034
- Older entries are archived in [packages/tui/CHANGELOG.md@bac7e83b5b0e](https://github.com/can1357/oh-my-pi/blob/bac7e83b5b0eb86c909c17830a6666efc359578b/packages/tui/CHANGELOG.md).
3026
+ Older entries are archived in [packages/tui/CHANGELOG.md@58141d4e5fa8](https://github.com/can1357/oh-my-pi/blob/58141d4e5fa892166024e2168866c45e0baacde3/packages/tui/CHANGELOG.md).
package/README.md CHANGED
@@ -455,7 +455,7 @@ const spacer = new Spacer(2); // 2 empty lines (default: 1)
455
455
 
456
456
  ### Image
457
457
 
458
- Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm, and Warp on macOS/Linux) or iTerm2 inline images. Falls back to a text placeholder on unsupported terminals.
458
+ Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm, and Warp on macOS/Linux), iTerm2 inline images or SIXEL. Falls back to a text placeholder on unsupported terminals.
459
459
 
460
460
  ```typescript
461
461
  interface ImageTheme {
@@ -466,17 +466,21 @@ interface ImageOptions {
466
466
  maxWidthCells?: number;
467
467
  maxHeightCells?: number;
468
468
  filename?: string;
469
+ budget?: ImageBudget; // usually tui.imageBudget
470
+ requestRender?: () => void; // repaint once a SIXEL encode lands; defaults to the budget's
469
471
  }
470
472
 
471
473
  const image = new Image(
472
474
  base64Data, // base64-encoded image data
473
475
  "image/png", // MIME type
474
476
  theme, // ImageTheme
475
- options, // optional ImageOptions
477
+ { ...options, budget: tui.imageBudget }, // optional ImageOptions
476
478
  );
477
479
  tui.addChild(image);
478
480
  ```
479
481
 
482
+ SIXEL images encode off the JavaScript thread: the first render reserves the image's rows and the image appears on the repaint `budget` or `requestRender` triggers once the encode lands. Without either, it waits for an unrelated repaint.
483
+
480
484
  Supported formats: PNG, JPEG, GIF, WebP. Dimensions are parsed from the image headers automatically.
481
485
 
482
486
  ## Autocomplete
@@ -40,6 +40,7 @@ interface AppKeybindings {
40
40
  "app.tree.unfoldOrDown": true;
41
41
  "app.plan.toggle": true;
42
42
  "app.history.search": true;
43
+ "app.stt.pushToTalk": true;
43
44
  "app.stt.toggle": true;
44
45
  "app.live.toggle": true;
45
46
  }
@@ -118,7 +119,7 @@ export declare const KEYBINDINGS: {
118
119
  readonly description: "Delete word backward";
119
120
  };
120
121
  readonly "tui.editor.deleteWordForward": {
121
- readonly defaultKeys: ["alt+delete", "alt+d", "super+alt+delete", "super+alt+d"];
122
+ readonly defaultKeys: ["alt+delete", "alt+d", "ctrl+delete", "super+alt+delete", "super+alt+d"];
122
123
  readonly description: "Delete word forward";
123
124
  };
124
125
  readonly "tui.editor.deleteToLineStart": {
@@ -329,9 +330,13 @@ export declare const KEYBINDINGS: {
329
330
  readonly defaultKeys: "ctrl+r";
330
331
  readonly description: "Search history";
331
332
  };
333
+ readonly "app.stt.pushToTalk": {
334
+ readonly defaultKeys: "space";
335
+ readonly description: "Hold to record speech-to-text";
336
+ };
332
337
  readonly "app.stt.toggle": {
333
338
  readonly defaultKeys: [];
334
- readonly description: "Toggle speech-to-text (default gesture: hold Space)";
339
+ readonly description: "Toggle speech-to-text recording";
335
340
  };
336
341
  readonly "app.live.toggle": {
337
342
  readonly defaultKeys: "ctrl+l";
@@ -77,8 +77,11 @@ export interface AutocompleteProvider {
77
77
  };
78
78
  /** Get inline hint text to show as dim ghost text after the cursor */
79
79
  getInlineHint?(lines: string[], cursorLine: number, cursorCol: number): string | null;
80
- /** Synchronously try to complete a slash command at the start of a line (no async I/O). */
81
- /** Returns matched items and the full prefix, or null if not applicable. */
80
+ /**
81
+ * Synchronously list slash command-name completions for a leading slash token (no async I/O).
82
+ * Mirrors the command-name branch of {@link getSuggestions}, including the bare `/` listing and
83
+ * the collapsed `/skill:` namespace row. Returns null outside a command-name token or with no match.
84
+ */
82
85
  trySyncSlashCompletion?(textBeforeCursor: string): {
83
86
  items: AutocompleteItem[];
84
87
  prefix: string;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Which chart a table becomes and the data it plots. {@link planChart} is the
3
+ * local best guess over a {@link TableAnalysis}: the chart kind, the label
4
+ * column and the measure columns to draw. A host may substitute its own
5
+ * {@link ChartPlan} (a model's pick for a multi-series table); either way
6
+ * {@link buildChart} turns it into the {@link ChartSpec} that
7
+ * {@link renderChartSvg} draws, and {@link worthCharting} decides whether
8
+ * the picture says more than the table already does.
9
+ *
10
+ * The heuristics were fitted on ~7k tables from real assistant answers: the
11
+ * label is the first text column (index columns like `#`/`Rank` never are),
12
+ * table order is kept (most tables are deliberately unsorted), total rows stay
13
+ * out of the scale, and units never share an axis.
14
+ */
15
+ import { type Dimension, type TableAnalysis } from "./table-data.js";
16
+ /**
17
+ * Chart kinds, by the table shapes they fit:
18
+ * - `bar`: one measure across categories.
19
+ * - `paired`: two same-unit measures per category (before/after, A vs B), or `a → b` cells.
20
+ * - `grouped`: three or four same-unit measures per category.
21
+ * - `line`: measures over an ordered axis (dates, runs, sizes).
22
+ * - `heatmap`: a matrix of same-unit values.
23
+ * - `multiples`: measures in different units, each scaled on its own; transposed when each row is its own metric.
24
+ * - `share`: parts of a whole (percentages summing to 100).
25
+ * - `diverging`: signed changes around zero.
26
+ * - `scatter`: two continuous measures per item.
27
+ */
28
+ export type ChartKind = "bar" | "paired" | "grouped" | "line" | "heatmap" | "multiples" | "share" | "diverging" | "scatter";
29
+ /** What to draw from a table: the kind, the column naming each category, and the columns plotted. */
30
+ export interface ChartPlan {
31
+ readonly kind: ChartKind;
32
+ /** Column naming each category (or holding the x values of a line); rows are numbered without one. */
33
+ readonly label: number | undefined;
34
+ /** Plotted columns, in order. */
35
+ readonly series: readonly number[];
36
+ /** Each data row is its own series (a metric with its own unit) and the plotted columns are the categories. */
37
+ readonly transpose: boolean;
38
+ }
39
+ /** One plotted value. */
40
+ export interface ChartPoint {
41
+ /** Value in base units (seconds, bytes, …). */
42
+ readonly value: number;
43
+ /** The number as the table wrote it (`~$250`, `12.5 ms`), shown as the value label. */
44
+ readonly text: string;
45
+ /** The table bolded the cell. */
46
+ readonly emphasis: boolean;
47
+ /** Upper end of a range cell (`24–72 h`). */
48
+ readonly upper?: number;
49
+ }
50
+ export interface ChartSeries {
51
+ readonly name: string;
52
+ readonly dim: Dimension;
53
+ /** Currency symbol or rate unit the axis prints; empty for other dimensions. */
54
+ readonly unit: string;
55
+ /** One per category; `null` where the cell holds no number of this series' dimension. */
56
+ readonly points: readonly (ChartPoint | null)[];
57
+ /** Status words standing in for a value (`TIMEOUT`, `OOM`), one per category. */
58
+ readonly notes: readonly (string | undefined)[];
59
+ }
60
+ /** The data a chart draws, independent of how it is drawn. */
61
+ export interface ChartSpec {
62
+ readonly kind: ChartKind;
63
+ readonly categories: readonly string[];
64
+ readonly series: readonly ChartSeries[];
65
+ /** Numeric x positions of a line or the x measure of a scatter, one per category. */
66
+ readonly x?: {
67
+ readonly name: string;
68
+ readonly dim: Dimension;
69
+ readonly unit: string;
70
+ readonly values: readonly number[];
71
+ };
72
+ /** Total/summary rows, printed under the chart instead of skewing its scale. */
73
+ readonly caption?: string;
74
+ /** Name of the category axis (the label column's header). */
75
+ readonly axis: string;
76
+ }
77
+ /** The local best guess at a chart for `table`, or `undefined` when no chart fits. */
78
+ export declare function planChart(table: TableAnalysis): ChartPlan | undefined;
79
+ /**
80
+ * The data `plan` draws from `table`, or `undefined` when it leaves fewer than
81
+ * two categories or no series with two values. The kind is fitted to the
82
+ * series it ends up with ({@link fitKind}), so any plan — a model's pick
83
+ * included — draws as something the renderer supports.
84
+ */
85
+ export declare function buildChart(table: TableAnalysis, plan: ChartPlan): ChartSpec | undefined;
86
+ /**
87
+ * Whether a chart of `spec` reads faster than its table: at least four
88
+ * categories, and either nine plotted values or a 3× spread within a series.
89
+ * On a model-judged sample of assistant tables this gate kept 85% of the
90
+ * tables a chart clearly helped while rejecting most two- and three-row ones.
91
+ */
92
+ export declare function worthCharting(spec: ChartSpec): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Draws a {@link ChartSpec} as a standalone SVG document colored only with
3
+ * figure tokens — `var(--fg)`, `var(--muted)`, `var(--border)`,
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.
8
+ *
9
+ * Geometry follows the terminal: 16 user units per row and a monospace face,
10
+ * so 13-unit labels land near the terminal's own glyph size. Marks are flat
11
+ * and square (no rounding, gradients or shadows); bolded table cells draw at
12
+ * full strength while their neighbours recede.
13
+ */
14
+ import type { ChartSpec } from "./chart-plan.js";
15
+ import type { Dimension } from "./table-data.js";
16
+ /** A drawn chart: the SVG document and its size in user units. */
17
+ export interface ChartSvg {
18
+ readonly svg: string;
19
+ readonly width: number;
20
+ readonly height: number;
21
+ }
22
+ /** The SVG document for `spec`. */
23
+ export declare function renderChartSvg(spec: ChartSpec): ChartSvg;
24
+ /** One-line description of the chart, for screen readers and terminals that show the alt text. */
25
+ export declare function chartAlt(spec: ChartSpec): string;
26
+ /**
27
+ * A value in base units as an axis label: durations and sizes in the unit
28
+ * their magnitude reads best in, counts compacted (`12k`, `3.4M`).
29
+ */
30
+ export declare function formatValue(value: number, dim: Dimension, unit: string): string;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Numeric reading of GFM table cells and columns, the input to
3
+ * {@link planChart}: what number a cell holds (in base units, with its
4
+ * dimension and annotations) and which role each column plays — a measure to
5
+ * plot, a category label, an ordered x axis, or a row index to ignore.
6
+ *
7
+ * Cells are model-written prose, not CSV: `~1,400`, `**4** ✅`, `17 (21.5%)`,
8
+ * `24–72 h`, `12 → 15`, `5/8`, `1m 56s`, `TIMEOUT`. The reader keeps the first
9
+ * number and its unit and classifies the rest, so a column of mostly clean
10
+ * numbers survives a few annotated or sentinel cells.
11
+ */
12
+ /** What a number measures; values of one dimension share an axis after unit normalization. */
13
+ export type Dimension = "count" | "percent" | "duration" | "bytes" | "currency" | "ratio" | "fraction" | "rate";
14
+ /**
15
+ * How cleanly a cell reads as its number: bare (`12 ms`), with a short note
16
+ * (`17 (21.5%)`, `16 pass`), a range (`24–72 h`), a transition (`12 → 15`), a
17
+ * fraction (`5/8`), or a number leading prose (`~1,400w, nearly ready`).
18
+ */
19
+ export type CellFit = "pure" | "note" | "range" | "arrow" | "fraction" | "prose";
20
+ /** A cell that reads as a number. */
21
+ export interface NumberCell {
22
+ readonly kind: "number";
23
+ /** Value in base units: seconds, bytes, a plain count, percent points. */
24
+ readonly value: number;
25
+ readonly dim: Dimension;
26
+ /** Unit or currency symbol as written (`ms`, `KiB`, `$`, `tok/s`); empty for bare numbers. */
27
+ readonly unit: string;
28
+ readonly fit: CellFit;
29
+ /** Written as an estimate or bound (`~`, `≈`, `<`, `≥`). */
30
+ readonly approx: boolean;
31
+ /** Written with an explicit sign (`+12%`, `-3`). */
32
+ readonly signed: boolean;
33
+ /** The cell was bolded: models bold the value they want noticed. */
34
+ readonly emphasis: boolean;
35
+ /** The cell text without Markdown decoration. */
36
+ readonly text: string;
37
+ /** The number as written, with approximation, sign, currency and unit (`~$250`, `12.5 ms`, `24–72 h`). */
38
+ readonly figure: string;
39
+ /** `a → b`: `b`, in the same base units. */
40
+ readonly to?: number;
41
+ /** `a → b`: `b` as written. */
42
+ readonly toFigure?: string;
43
+ /** `a–b`: `b`, in the same base units. */
44
+ readonly upper?: number;
45
+ /** `a/b`: `b`. */
46
+ readonly denominator?: number;
47
+ }
48
+ /** A cell that is not a quantity. */
49
+ export interface OtherCell {
50
+ readonly kind: "missing" | "date" | "time" | "version" | "id" | "text";
51
+ readonly text: string;
52
+ readonly emphasis: boolean;
53
+ }
54
+ export type Cell = NumberCell | OtherCell;
55
+ /**
56
+ * What a column contributes to a chart: `measure` values to plot, `label`
57
+ * category names, `temporal` dates or times, `sequence` an ordered step
58
+ * (`Run`, `Day`, `Workers`), `index` a row number or identifier never plotted.
59
+ */
60
+ export type ColumnRole = "measure" | "label" | "temporal" | "sequence" | "index";
61
+ export interface TableColumn {
62
+ readonly index: number;
63
+ /** Header text without Markdown decoration. */
64
+ readonly header: string;
65
+ readonly role: ColumnRole;
66
+ readonly cells: readonly Cell[];
67
+ /** Dominant dimension of the numeric cells. */
68
+ readonly dim: Dimension | undefined;
69
+ /** Representative unit of the dominant dimension (`$`, `tok/s`); empty when bare. */
70
+ readonly unit: string;
71
+ /** Numeric cells disagree on dimension (a metric-per-row table). */
72
+ readonly mixed: boolean;
73
+ /** Every numeric data cell holds the same value: an echoed setting, not a measure. */
74
+ readonly constant: boolean;
75
+ /** Share of numeric cells written with an explicit sign. */
76
+ readonly signedShare: number;
77
+ /** Share of numeric cells written as `a → b`. */
78
+ readonly arrowShare: number;
79
+ }
80
+ /** A table read for charting. */
81
+ export interface TableAnalysis {
82
+ readonly columns: readonly TableColumn[];
83
+ /** Category labels: the first text or temporal column, else a sequence or index column. */
84
+ readonly label: TableColumn | undefined;
85
+ /** Plottable measure columns, constant ones excluded. */
86
+ readonly measures: readonly TableColumn[];
87
+ /** Data rows, in table order, without total/summary rows. */
88
+ readonly rows: readonly number[];
89
+ /** Total/summary rows (`Total`, `Average`, …), kept out of the scale. */
90
+ readonly totals: readonly number[];
91
+ }
92
+ /**
93
+ * Strip inline Markdown from a cell: links keep their text; bold, italics,
94
+ * strikethrough, code spans and `<br>` keep their content.
95
+ */
96
+ export declare function plainCell(markdown: string): {
97
+ text: string;
98
+ emphasis: boolean;
99
+ };
100
+ /** Parse the first number of a (Markdown) cell, or classify why it is not one. */
101
+ export declare function parseCell(markdown: string): Cell;
102
+ /** Read a table's columns and rows for charting. Cells are the raw Markdown of each cell. */
103
+ export declare function analyzeTable(header: readonly string[], rows: readonly (readonly string[])[]): TableAnalysis;
104
+ /** Strictly increasing or strictly decreasing. */
105
+ export declare function isMonotonic(values: readonly number[]): boolean;
106
+ /** Whether `cell` reads as a number. */
107
+ export declare function isNumber(cell: Cell | undefined): cell is NumberCell;
@@ -38,7 +38,7 @@ export declare class AssistantMessageComponent extends Container {
38
38
  * for lack of a target is re-rendered with its reaction stripped.
39
39
  */
40
40
  pickReactionTarget(transcript: readonly Component[]): void;
41
- constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: () => void, thinkingRenderers?: readonly AssistantThinkingRenderer[], imageBudget?: ImageBudget, proseOnlyThinking?: boolean, linkTargets?: ReadonlyMap<string, string>);
41
+ constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: () => void, thinkingRenderers?: readonly AssistantThinkingRenderer[], imageBudget?: ImageBudget, proseOnlyThinking?: boolean, linkTargets?: ReadonlyMap<string, string>, expandThinkingBlocks?: boolean);
42
42
  /**
43
43
  * Show or clear the trailing cache-invalidation divider. Set at `message_end`
44
44
  * (live) or during rebuild, once the turn's usage is known and compared
@@ -65,6 +65,11 @@ export declare class AssistantMessageComponent extends Container {
65
65
  */
66
66
  setMidStreamPublication(allowed: boolean): void;
67
67
  setProseOnlyThinking(proseOnly: boolean): void;
68
+ /**
69
+ * Keep finished thinking sections expanded instead of folding them to "Thought for 12s".
70
+ * Sections the user folded or unfolded by hand keep that choice.
71
+ */
72
+ setExpandThinkingBlocks(expand: boolean): void;
68
73
  dispose(): void;
69
74
  /**
70
75
  * Toggle suppression of the inline `Error: …` line while the same error is
@@ -81,6 +86,8 @@ export declare class AssistantMessageComponent extends Container {
81
86
  */
82
87
  setExpanded(expanded: boolean): void;
83
88
  isTranscriptBlockFinalized(): boolean;
89
+ /** Whether a figure still waits for the raster of its final source; the transcript holds retirement meanwhile. */
90
+ isTranscriptBlockPending(): boolean;
84
91
  render(width: number): readonly string[];
85
92
  /**
86
93
  * A `col` (role `omp.assistant`) of `md` nodes keyed by content index, so
@@ -118,6 +125,8 @@ export declare class AssistantMessageComponent extends Container {
118
125
  messagePersistenceKey(): string | undefined;
119
126
  /** Toggle rendering for assistant-native and tool-result images. */
120
127
  setImagesVisible(visible: boolean): void;
128
+ /** Toggle charts under numeric tables (the main session's transcript only, never a subagent's). */
129
+ setTableChartsVisible(visible: boolean): void;
121
130
  /** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
122
131
  setToolResultImagesVisible(visible: boolean): void;
123
132
  setToolResultImages(toolCallId: string, images: ImageContent[]): void;
@@ -25,8 +25,11 @@ export interface ChatTranscriptBuilderDeps {
25
25
  cwd: string;
26
26
  hideThinkingBlock?: () => boolean;
27
27
  proseOnlyThinking?: () => boolean;
28
+ expandThinkingBlocks?: () => boolean;
28
29
  /** Session-scoped resolved destinations for model-authored Markdown links. */
29
30
  linkTargets?: ReadonlyMap<string, string>;
31
+ /** Draw charts under numeric tables; false for transcripts that are not the main session's. Default: true. */
32
+ tableCharts?: boolean;
30
33
  requestRender: () => void;
31
34
  }
32
35
  export declare class ChatTranscriptBuilder {
@@ -0,0 +1,42 @@
1
+ import type { ImageBudget } from "../components/image.js";
2
+ import type { Markdown } from "../components/markdown.js";
3
+ import { Container } from "../tui.js";
4
+ export interface FigureMarkdownOptions {
5
+ /** Markdown for one prose run (and a failed figure's code fallback); FigureMarkdown drives its transience. */
6
+ markdown: (text: string) => Markdown;
7
+ /** Draw charts under numeric tables; off for transcripts that are not the main session's. */
8
+ charts: boolean;
9
+ /** Shared inline-image budget for the figures. */
10
+ budget?: ImageBudget;
11
+ /** A figure's rows changed outside a text update (raster landed, fell back to code). */
12
+ onChange: () => void;
13
+ }
14
+ /**
15
+ * Assistant Markdown whose top-level ```svg fences are lifted out of the prose
16
+ * and drawn as images ({@link SvgFigure}), and whose numeric tables get a chart
17
+ * drawn under them ({@link TableChartFigure}), with ordinary {@link Markdown}
18
+ * for the prose between them. Stands in for Markdown on an assistant text
19
+ * block that holds an svg fence or a chartable table: {@link setText} streams
20
+ * in place, reusing every part whose kind is unchanged, and
21
+ * {@link transientRenderCache} marks the block's tail as still streaming — the
22
+ * last prose run renders transient, the last figure throttles its rasters
23
+ * until its fence closes, and a table at the tail waits for its last row.
24
+ */
25
+ export declare class FigureMarkdown extends Container {
26
+ #private;
27
+ constructor(text: string, options: FigureMarkdownOptions);
28
+ /** Replace the source; returns whether it changed. */
29
+ setText(text: string): boolean;
30
+ get transientRenderCache(): boolean;
31
+ set transientRenderCache(value: boolean);
32
+ /**
33
+ * The trimmed prose ahead of the first figure, or "" when the block opens
34
+ * with one. These are the only rows plain Markdown reproduces byte for
35
+ * byte, so they are all a transcript may publish ahead of the figure.
36
+ */
37
+ get leadingProse(): string;
38
+ /** Whether a figure's final source still waits for its raster, or a chart for its pick or raster. */
39
+ get pending(): boolean;
40
+ /** Rebuild the prose runs through the Markdown factory after its theme changed; figures keep their rasters. */
41
+ restyle(): void;
42
+ }
@@ -0,0 +1,33 @@
1
+ import { type ImageBudget } from "../components/image.js";
2
+ import type { Markdown } from "../components/markdown.js";
3
+ import type { Component } from "../tui.js";
4
+ /** The active theme's color per figure token, for resolving a figure with `prepareSvg`. */
5
+ export declare function svgFigurePalette(): Record<string, string>;
6
+ /** Turn lifting ```svg fences into images on or off (the host's `tui.renderSvg`); the host rebuilds the transcript after. */
7
+ export declare function setSvgFigureRendering(enabled: boolean): void;
8
+ /** Whether assistant ```svg fences render as images where the terminal can show them. */
9
+ export declare function svgFigureRendering(): boolean;
10
+ export interface SvgFigureOptions {
11
+ /**
12
+ * Markdown for the fenced-code fallback shown when the final source does not
13
+ * render. Without it the figure decorates content shown elsewhere (a chart
14
+ * under its table): it stays blank until drawn, and when it cannot be.
15
+ */
16
+ markdown?: (text: string) => Markdown;
17
+ /** Shared inline-image budget; superseded rasters are released from it. */
18
+ budget?: ImageBudget;
19
+ /** The figure's rows changed outside a text update: a raster landed or rendering fell back to code. */
20
+ onChange: () => void;
21
+ }
22
+ export declare class SvgFigure implements Component {
23
+ #private;
24
+ constructor(options: SvgFigureOptions);
25
+ /** Feed the fence body; `final` once the fence closed or the message stopped streaming. */
26
+ update(source: string, final: boolean): void;
27
+ /** Whether the final source still waits for its raster (or for failing to render). */
28
+ get pending(): boolean;
29
+ render(width: number): readonly string[];
30
+ invalidate(): void;
31
+ dispose(): void;
32
+ debugState(): Record<string, unknown>;
33
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Source-level handling of ```svg fences in assistant Markdown: splitting
3
+ * prose around them ({@link splitSvgFences}), turning a fence that is still
4
+ * streaming into a document an SVG parser accepts ({@link closePartialSvg}),
5
+ * and resolving the theme tokens figures color themselves with
6
+ * ({@link prepareSvg}). `FigureMarkdown` lifts the fences out as `SvgFigure`
7
+ * images.
8
+ */
9
+ /** One run of an assistant text block: prose, or the body of a ```svg fence. */
10
+ export type FigureSegment = {
11
+ readonly kind: "markdown";
12
+ readonly text: string;
13
+ }
14
+ /** `closed` once the fence's closing line arrived. */
15
+ | {
16
+ readonly kind: "svg";
17
+ readonly source: string;
18
+ readonly closed: boolean;
19
+ };
20
+ /** Whether `markdown` holds a ```svg fence {@link splitSvgFences} would lift. */
21
+ export declare function hasSvgFence(markdown: string): boolean;
22
+ /**
23
+ * Split `markdown` into prose and ```svg fence bodies, in order. Only
24
+ * top-level fences lift; an svg fence nested in another fence, a list's
25
+ * indented block or a blockquote stays prose. Blank prose runs are dropped.
26
+ * An svg fence still open at the end is the last segment, `closed: false`.
27
+ */
28
+ export declare function splitSvgFences(markdown: string): FigureSegment[];
29
+ /**
30
+ * Turn a possibly truncated SVG source into a well-formed document: cut the
31
+ * trailing construct still being written (a tag without its `>`, an
32
+ * unterminated comment, CDATA section, processing instruction or declaration,
33
+ * a dangling `&entity`) and close every element left open, innermost first.
34
+ * A complete document passes through unchanged. Returns null until the root
35
+ * `<svg …>` start tag is complete — before that there is nothing to draw.
36
+ */
37
+ export declare function closePartialSvg(source: string): string | null;
38
+ /**
39
+ * Make a figure's source what an SVG rasterizer draws as written, in the
40
+ * reader's theme. Rasterizers resolve neither CSS custom properties nor an
41
+ * inherited text color, and reject a root without the SVG namespace, so:
42
+ * - `var(--name)` / `var(--name, fallback)` become `palette[name]` (an
43
+ * unknown name takes its fallback, else `palette.fg`);
44
+ * - a root `<svg>` lacking them gets `color` = `palette.fg` (so
45
+ * `currentColor` follows the theme), a sans-serif `font-family` (instead
46
+ * of the rasterizer's Times), `xmlns`, and `xmlns:xlink` when the source
47
+ * uses `xlink:` attributes.
48
+ */
49
+ export declare function prepareSvg(svg: string, palette: Readonly<Record<string, string>>): string;