@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
@@ -6,7 +6,14 @@ import { formatKeyHint } from "../../app-keybindings";
6
6
  import { col, node, span, text } from "../../native/describe";
7
7
  import type { NativeNode } from "../../native/node";
8
8
  import { Memo } from "../../native/memo";
9
- import { gradientEscape, gradientLogo, ZETA_LOGO, type ShineConfig, logoNode } from "../../prompt/welcome";
9
+ import {
10
+ gradientEscape,
11
+ gradientLogo,
12
+ ZETA_LOGO,
13
+ ZETA_SUBLINE,
14
+ type ShineConfig,
15
+ logoNode,
16
+ } from "../../prompt/welcome";
10
17
 
11
18
  export const SETUP_SPLASH_MS = 2600;
12
19
  export const SETUP_TICK_MS = 33;
@@ -159,11 +166,12 @@ function resetCells(width: number, height: number): string[][] {
159
166
  let diagonalEscapes: (string | undefined)[] = [];
160
167
 
161
168
  /**
162
- * Animated setup splash, in the spirit of the Zeta visual identity: the brand ζ
163
- * mark rendered with the live diagonal gradient + shine sweep, rising out of a
164
- * rippling, gradient-lit water surface, under a faint twinkling starfield. The
165
- * mark and water share one continuous gradient so the sweep reads across the
166
- * whole scene; the water surface drifts each frame.
169
+ * Animated setup splash, in the spirit of the Zeta visual identity: the brand
170
+ * mark — the FIGlet-rendered Latin `Z`, the ASCII form of `Ζ` — with the live
171
+ * diagonal gradient + shine sweep, rising out of a rippling, gradient-lit water
172
+ * surface, under a faint twinkling starfield. The mark and water share one
173
+ * continuous gradient so the sweep reads across the whole scene; the water
174
+ * surface drifts each frame.
167
175
  */
168
176
  export function renderSetupSplash(width: number, height: number, elapsedMs: number): string[] {
169
177
  const w = Math.max(1, width);
@@ -265,7 +273,8 @@ export function describeSetupSplash(): NativeNode {
265
273
  [
266
274
  node("spacer", { grow: 1 }),
267
275
  logoNode(LARGE_LOGO, true),
268
- text([span("Z e t a", "strong")], { wrap: "none" }),
276
+ text([span("Zeta", "strong")], { wrap: "none" }),
277
+ text([span("Linxira-Zeta", "dim")], { wrap: "none" }),
269
278
  node("spacer", { grow: 1 }),
270
279
  text([span(hint, "dim")], { wrap: "none" }),
271
280
  ],
@@ -277,7 +286,12 @@ export function describeSetupSplash(): NativeNode {
277
286
  /** Centered fallback for windows too small to hold the full scene. */
278
287
  function renderCompactSplash(width: number, height: number, phase: number, shine: ShineConfig): string[] {
279
288
  const art = height >= 14 ? LARGE_LOGO : ZETA_LOGO;
280
- const content = [...gradientLogo(art, phase, shine), "", theme.bold("Z e t a")];
289
+ const content = [
290
+ ...gradientLogo(art, phase, shine),
291
+ "",
292
+ theme.bold("Zeta"),
293
+ ...ZETA_SUBLINE.map(row => theme.fg("dim", row)),
294
+ ];
281
295
  const start = Math.max(0, Math.floor((height - content.length) / 2));
282
296
  const lines: string[] = [];
283
297
  for (let y = 0; y < height; y++) {
@@ -43,7 +43,7 @@ import type { TspMeterMark, TspProps } from "@linxiraos/pi-wire";
43
43
  import type { NativeNode, NativeUiEvent } from "../native/node";
44
44
  import { col, node, span } from "../native/describe";
45
45
  import { getContextMeterThresholds } from "../chrome/context-thresholds";
46
- import { isNativeRendering } from "../native/state";
46
+ import { isNativeRendering, onNativeRenderingChange } from "../native/state";
47
47
  import { getSeparator } from "./separators";
48
48
  import type {
49
49
  CollabStatus,
@@ -162,6 +162,10 @@ const WATCHER_FAILURE_POLL_TTL_MS = 5000;
162
162
  const BRAND_FADE_MS = 450;
163
163
  /** Repaint cadence while the brand fade is in flight (rust zeta's `FADE_FRAME`). */
164
164
  const BRAND_FADE_FRAME_MS = 40;
165
+ /** Repaint cadence while a turn's active-processing window is open: keeps the
166
+ * status bar's clock-driven readouts (pi spinner, turn timer) animating when
167
+ * no other component is requesting renders. */
168
+ const ACTIVE_WINDOW_REPAINT_MS = 250;
165
169
 
166
170
  /**
167
171
  * Providers whose subscription quota is a single monthly bucket, so their
@@ -380,6 +384,7 @@ interface ContextUsageMemo {
380
384
  length: number;
381
385
  lastFingerprint: string | undefined;
382
386
  modelContextWindow: number;
387
+ toolSchemasInContext: boolean;
383
388
  contextUsageRevision: number;
384
389
  toolSchemaMetadataRevision: number;
385
390
  settingsRevision: number;
@@ -697,6 +702,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
697
702
  #gitWatcherUnavailable = false;
698
703
  #onBranchChange: (() => void) | null = null;
699
704
  #disposed = false;
705
+ /** Unsubscribe for the native-rendering feed; re-arms the turn-window ticker when a TSP surface is revoked. */
706
+ #unwatchNativeRendering: (() => void) | undefined;
700
707
  #autoCompactEnabled: boolean = true;
701
708
  /** Pulse timer for the running-speculation indicator; live only while speculation runs. */
702
709
  #speculationBlinkTimer: NodeJS.Timeout | undefined;
@@ -707,6 +714,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
707
714
  #brandWorking = false;
708
715
  /** Frame timer driving repaints while the brand fade is unsettled. */
709
716
  #brandFadeTimer: NodeJS.Timeout | undefined;
717
+ /** Repaint ticker while a turn's active-processing window is open (see #syncActiveWindowRepaint). */
718
+ #activeWindowRepaintTimer: NodeJS.Timeout | undefined;
710
719
  /** One wall-clock wakeup for the active model's next tariff change, including while idle. */
711
720
  #pricingTimer: NodeJS.Timeout | undefined;
712
721
  #pricingTimerCost: ModelCost | undefined;
@@ -774,9 +783,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
774
783
  // than poison the fresh cache or advance its throttle.
775
784
  #jjCacheGeneration = 0;
776
785
 
777
- // PR lookup caching (invalidated on branch/repo context changes)
786
+ // PR lookup cache. `#cachedPrContext` is the branch/repo `#cachedPr` was
787
+ // resolved for; it is never shown for any other branch. Expiry forces a
788
+ // refetch while keeping that branch's PR visible until the lookup lands.
778
789
  #cachedPr: { number: number; url: string } | null | undefined = undefined;
779
790
  #cachedPrContext: PrCacheContext | undefined = undefined;
791
+ #cachedPrExpired = false;
780
792
  #prLookupInFlight = false;
781
793
  #defaultBranch?: string;
782
794
  #defaultBranchCwd: string | undefined = undefined;
@@ -829,6 +841,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
829
841
  private readonly host: StatusLineHost<TSession>,
830
842
  ) {
831
843
  this.#settings = host.getSettings();
844
+ this.#unwatchNativeRendering = onNativeRenderingChange(() => this.#syncActiveWindowRepaint());
832
845
  }
833
846
 
834
847
  #gitEnabled(): boolean {
@@ -935,6 +948,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
935
948
  this.#invalidateSessionCaches();
936
949
  this.#closeStaleActiveWindow();
937
950
  }
951
+ // The ticker self-stops once its tick sees no open window for the
952
+ // newly-attached session; re-pointing focus at a session whose turn
953
+ // is still running must re-arm it, and no other path reliably fires
954
+ // here (closeStaleActiveWindow returns early while streaming, and
955
+ // the synthesized agent_start no-ops on an already-open window).
956
+ this.#syncActiveWindowRepaint();
938
957
  this.invalidate();
939
958
  }
940
959
 
@@ -1033,6 +1052,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1033
1052
  meter.activeMs = 0;
1034
1053
  meter.activeStartedAt = null;
1035
1054
  this.#invalidateStatusLineRenderCache();
1055
+ this.#syncActiveWindowRepaint();
1036
1056
  }
1037
1057
 
1038
1058
  /**
@@ -1047,6 +1067,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1047
1067
  if (meter.activeStartedAt !== null) return;
1048
1068
  meter.activeStartedAt = Date.now();
1049
1069
  this.#invalidateStatusLineRenderCache();
1070
+ this.#syncActiveWindowRepaint();
1050
1071
  }
1051
1072
 
1052
1073
  /**
@@ -1061,6 +1082,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1061
1082
  meter.activeMs += Math.max(0, Date.now() - meter.activeStartedAt);
1062
1083
  meter.activeStartedAt = null;
1063
1084
  this.#invalidateStatusLineRenderCache();
1085
+ this.#syncActiveWindowRepaint();
1064
1086
  }
1065
1087
 
1066
1088
  /**
@@ -1290,8 +1312,11 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1290
1312
  this.#onBranchChange = null;
1291
1313
  this.#stopSpeculationBlink();
1292
1314
  this.#stopBrandFadeTimer();
1315
+ this.#stopActiveWindowRepaint();
1293
1316
  this.#stopPricingTimer();
1294
1317
  this.#clearUsageStartTimer();
1318
+ this.#unwatchNativeRendering?.();
1319
+ this.#unwatchNativeRendering = undefined;
1295
1320
  this.#onCodexResetFireworks = undefined;
1296
1321
  this.#onResetExpiryNotice = undefined;
1297
1322
  this.#codexResetSnapshots.clear();
@@ -1395,6 +1420,40 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1395
1420
  this.#brandFadeTimer = undefined;
1396
1421
  }
1397
1422
 
1423
+ /**
1424
+ * Keep the status bar repainting while a turn's active-processing window
1425
+ * is open. The `pi` segment's spinner and turn timer are painted from the
1426
+ * wall clock at render time, so they only animate when frames flow; during
1427
+ * a quiet turn the working-row loader's 80 ms tick is normally the sole
1428
+ * repaint source, and paths that stop or bypass that loader mid-turn (an
1429
+ * error banner, a stale retry/compaction loader reference) froze the
1430
+ * spinner until the next keystroke. The status line owns the active
1431
+ * window, so it owns a repaint source for it; a native surface clocks its
1432
+ * own spinner and needs none.
1433
+ */
1434
+ #syncActiveWindowRepaint(): void {
1435
+ if (this.#disposed || isNativeRendering() || this.getTurnElapsedMs() === null) {
1436
+ this.#stopActiveWindowRepaint();
1437
+ return;
1438
+ }
1439
+ if (this.#activeWindowRepaintTimer) return;
1440
+ const timer = setInterval(() => {
1441
+ if (this.#disposed || isNativeRendering() || this.getTurnElapsedMs() === null) {
1442
+ this.#stopActiveWindowRepaint();
1443
+ return;
1444
+ }
1445
+ this.#onBranchChange?.();
1446
+ }, ACTIVE_WINDOW_REPAINT_MS);
1447
+ timer.unref?.();
1448
+ this.#activeWindowRepaintTimer = timer;
1449
+ }
1450
+
1451
+ #stopActiveWindowRepaint(): void {
1452
+ if (!this.#activeWindowRepaintTimer) return;
1453
+ clearInterval(this.#activeWindowRepaintTimer);
1454
+ this.#activeWindowRepaintTimer = undefined;
1455
+ }
1456
+
1398
1457
  #stopPricingTimer(): void {
1399
1458
  clearTimeout(this.#pricingTimer);
1400
1459
  this.#pricingTimer = undefined;
@@ -1457,8 +1516,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1457
1516
  // was designed to avoid. Explicit Git/repository invalidation (watcher
1458
1517
  // HEAD-move, cwd/repo switch) goes through {@link invalidateGitCaches}.
1459
1518
  // A tool may open, close, or merge a PR without moving HEAD. Expire the
1460
- // settled PR context on ordinary activity while leaving HEAD work intact.
1461
- this.#cachedPrContext = undefined;
1519
+ // settled PR on ordinary activity while leaving HEAD work intact.
1520
+ this.#cachedPrExpired = true;
1462
1521
  }
1463
1522
  #invalidateSessionCaches(): void {
1464
1523
  this.#clearUsageStartTimer();
@@ -1489,7 +1548,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1489
1548
  this.#branchResolveActive = undefined;
1490
1549
  this.#branchLastFetch = undefined;
1491
1550
  this.#branchCacheGeneration++;
1492
- this.#cachedPrContext = undefined;
1551
+ this.#cachedPrExpired = true;
1493
1552
  // A HEAD move (commit, checkout, reset) changes the dirty counts; keep the
1494
1553
  // stale counts on screen but refetch on the next render. The generation
1495
1554
  // bump stops an in-flight pre-move result from re-stamping freshness.
@@ -1810,11 +1869,16 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1810
1869
  const branch = this.#getBranchLabel(activeRepoCache, this.#resolveRepository(activeRepoCache));
1811
1870
  const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
1812
1871
 
1813
- if (canReuseCachedPr(this.#cachedPr, this.#cachedPrContext, currentContext)) {
1872
+ if (!this.#cachedPrExpired && canReuseCachedPr(this.#cachedPr, this.#cachedPrContext, currentContext)) {
1814
1873
  return this.#cachedPr ?? null;
1815
1874
  }
1816
1875
 
1817
- const stalePr = this.#cachedPr;
1876
+ // Keep an expired PR visible during the refetch only on the branch it
1877
+ // was resolved for; after a checkout the old PR must not linger.
1878
+ const stalePr =
1879
+ currentContext && isSamePrCacheContext(this.#cachedPrContext, currentContext)
1880
+ ? (this.#cachedPr ?? null)
1881
+ : null;
1818
1882
 
1819
1883
  if (!branch) {
1820
1884
  this.#cachedPr = null;
@@ -1822,10 +1886,9 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1822
1886
  return null;
1823
1887
  }
1824
1888
 
1825
- // Don't look up if detached, default branch, or already in flight.
1826
- if (branch === "detached" || this.#isDefaultBranch(branch, gitCwd) || this.#prLookupInFlight) {
1827
- return stalePr ?? null;
1828
- }
1889
+ // Detached HEAD and the default branch never carry a PR; no lookup.
1890
+ if (branch === "detached" || this.#isDefaultBranch(branch, gitCwd)) return null;
1891
+ if (this.#prLookupInFlight) return stalePr;
1829
1892
 
1830
1893
  this.#prLookupInFlight = true;
1831
1894
  const lookupContext = currentContext;
@@ -1847,6 +1910,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1847
1910
  if (lookupContext && isSamePrCacheContext(latestContext, lookupContext)) {
1848
1911
  this.#cachedPr = value;
1849
1912
  this.#cachedPrContext = lookupContext;
1913
+ this.#cachedPrExpired = false;
1850
1914
  }
1851
1915
  };
1852
1916
  try {
@@ -1980,6 +2044,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1980
2044
  * cadence while a late successful fetch can still refresh the cached segment.
1981
2045
  */
1982
2046
  refreshUsageInBackground(): void {
2047
+ if (this.#disposed) return;
1983
2048
  const now = Date.now();
1984
2049
  const session = this.session;
1985
2050
  const usageContextKey = this.#getUsageContextKey(session);
@@ -2407,6 +2472,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
2407
2472
  getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
2408
2473
  const messages = this.session.messages ?? EMPTY_MESSAGES;
2409
2474
  const modelContextWindow = this.session.model?.contextWindow ?? 0;
2475
+ const toolSchemasInContext = this.session.model?.toolSchemasInContext !== false;
2410
2476
  const length = messages.length;
2411
2477
  const lastFingerprint = length > 0 ? messageFingerprint(messages[length - 1]!) : undefined;
2412
2478
  // Bumps when the in-flight pending snapshot is set/cleared. Without it a
@@ -2428,6 +2494,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
2428
2494
  cache.length === length &&
2429
2495
  cache.lastFingerprint === lastFingerprint &&
2430
2496
  cache.modelContextWindow === modelContextWindow &&
2497
+ cache.toolSchemasInContext === toolSchemasInContext &&
2431
2498
  cache.contextUsageRevision === contextUsageRevision &&
2432
2499
  cache.systemPromptRef === systemPrompt &&
2433
2500
  cache.toolsRef === tools &&
@@ -2448,6 +2515,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
2448
2515
  length,
2449
2516
  lastFingerprint,
2450
2517
  modelContextWindow,
2518
+ toolSchemasInContext,
2451
2519
  contextUsageRevision,
2452
2520
  toolSchemaMetadataRevision,
2453
2521
  settingsRevision,
@@ -143,6 +143,7 @@ export function computeCompactionBoundaries(
143
143
 
144
144
  /** Stable inputs used to cache non-message token estimates. */
145
145
  export interface NonMessageTokenSource {
146
+ readonly model?: Model;
146
147
  readonly systemPrompt?: readonly string[];
147
148
  readonly agent?: {
148
149
  readonly state?: {
@@ -272,30 +273,32 @@ export function estimateToolSchemaTokens(tools: ToolSchemaSource, tokenizer: Tok
272
273
  return tokens;
273
274
  }
274
275
 
276
+ /** Count schemas carried on every request, excluding server-side deferred catalogs. */
277
+ export function estimateContextToolSchemaTokens(
278
+ tools: ToolSchemaSource,
279
+ tokenizer: Tokenizer,
280
+ model: Model | undefined,
281
+ sourceRevision = 0,
282
+ ): number {
283
+ return model?.toolSchemasInContext === false ? 0 : estimateToolSchemaTokens(tools, tokenizer, sourceRevision);
284
+ }
285
+
275
286
  /**
276
287
  * Compute just the NON-MESSAGE token total: system prompt (with its skills
277
288
  * section subtracted, since skills are tokenized separately) + system context
278
289
  * (the rest of the system-prompt array) + tools + skills.
279
290
  *
280
291
  * Exposed so callers like `StatusLineComponent` can cache the non-message
281
- * total separately from the message total. Non-message inputs (skills,
282
- * tools, system prompt) change rarely; the message list grows on every
283
- * streaming turn. Splitting the two lets the caller refresh each on its own
284
- * cadence — non-message recomputed only when the inputs identity changes,
285
- * messages walked incrementally as new entries append.
292
+ * total separately from the growing message total. A model switch can change
293
+ * which tool schemas occupy context without changing the roster.
286
294
  */
287
- // Non-message inputs (system prompt, tools, skills) change rarely — at most
288
- // once per turn via setSystemPrompt/setTools — but the per-turn compaction and
289
- // threshold paths call these helpers several times: getContextBreakdown calls
290
- // both, and #estimateStoredContextTokens adds a third. Memoize on the identity
291
- // of the three input arrays so the expensive parts (system-prompt tokenization
292
- // and the per-tool JSON.stringify(toolWireSchema) inside estimateToolSchemaTokens)
293
- // run at most once per input change rather than per call. The identity keys are
294
- // the same stable references the StatusLineComponent cache already trusts
295
- // (setSystemPrompt/setTools replace the array reference rather than mutating it).
295
+ // The per-turn threshold and compaction paths call these helpers repeatedly.
296
+ // Cache by prompt, roster, skills, tokenizer, revision, and model schema policy;
297
+ // setSystemPrompt/setTools replace their arrays rather than mutating them.
296
298
  interface NonMessageTokenCache {
297
299
  systemPromptRef: readonly string[];
298
300
  toolsRef: ToolSchemaSource;
301
+ toolSchemasInContext: boolean;
299
302
  toolsRevision: number;
300
303
  sourceRevision: number;
301
304
  skillful: boolean;
@@ -328,6 +331,7 @@ function nonMessageTokenCacheEntry(
328
331
  const cachedSession: CachedNonMessageTokenSource = session;
329
332
  const systemPromptRef = session.systemPrompt ?? EMPTY_STRING_PARTS;
330
333
  const toolsRef = session.agent?.state?.tools ?? EMPTY_TOOLS;
334
+ const toolSchemasInContext = session.model?.toolSchemasInContext !== false;
331
335
  const toolsRevision = getToolSchemaMetadataRevision(toolsRef);
332
336
  const skillsRef = session.renderedSkills ?? session.skills ?? EMPTY_SKILLS;
333
337
  let entry = cachedSession[NON_MESSAGE_TOKEN_CACHE];
@@ -335,6 +339,7 @@ function nonMessageTokenCacheEntry(
335
339
  entry &&
336
340
  entry.systemPromptRef === systemPromptRef &&
337
341
  entry.toolsRef === toolsRef &&
342
+ entry.toolSchemasInContext === toolSchemasInContext &&
338
343
  entry.toolsRevision === toolsRevision &&
339
344
  entry.sourceRevision === sourceRevision &&
340
345
  entry.skillsRef === skillsRef &&
@@ -345,6 +350,7 @@ function nonMessageTokenCacheEntry(
345
350
  entry = {
346
351
  systemPromptRef,
347
352
  toolsRef,
353
+ toolSchemasInContext,
348
354
  toolsRevision,
349
355
  sourceRevision,
350
356
  skillful: true,
@@ -368,7 +374,7 @@ export function computeNonMessageTokens(
368
374
  const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
369
375
  const tokens =
370
376
  tokenizer.countTokens(Array.from(systemPromptParts, part => part ?? "")) +
371
- estimateToolSchemaTokens(tools, tokenizer, sourceRevision);
377
+ estimateContextToolSchemaTokens(tools, tokenizer, session.model, sourceRevision);
372
378
  entry.tokens = tokens;
373
379
  return tokens;
374
380
  }
@@ -400,7 +406,7 @@ export function computeNonMessageBreakdown(
400
406
  renderedSkills(session.renderedSkills ?? session.skills ?? EMPTY_SKILLS, tools),
401
407
  tokenizer,
402
408
  );
403
- const toolsTokens = estimateToolSchemaTokens(tools, tokenizer, sourceRevision);
409
+ const toolsTokens = estimateContextToolSchemaTokens(tools, tokenizer, session.model, sourceRevision);
404
410
  const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
405
411
  const systemContextTokens = tokenizer.countTokens(Array.from(systemPromptParts.slice(1), part => part ?? ""));
406
412
  const systemPromptTokens = Math.max(0, tokenizer.countTokens(systemPromptParts[0] ?? "") - skillsTokens);
@@ -742,86 +748,183 @@ function contextMeterParts(
742
748
  }
743
749
 
744
750
  /**
745
- * The `/context` body for terminals that draw `meter`: a blocks meter
746
- * beside a legend `kv` (swatch · label → tokens · %), then a full-width bar
747
- * of the same parts marked where auto-compaction fires, then the snapcompact
748
- * estimate when one is on. {@link contextUsageHead} names the model and window.
751
+ * One line of the native `/context` breakdown: a swatch in the part's bar
752
+ * color, its name, its tokens and its share of the window; the exact count
753
+ * is its tooltip.
754
+ */
755
+ function contextShareRow(
756
+ key: string,
757
+ role: string,
758
+ swatch: TspSpan,
759
+ label: string,
760
+ tokens: number,
761
+ window: number,
762
+ ): NativeNode {
763
+ return node(
764
+ "row",
765
+ { role, align: "center", title: `${label} · ${tokens.toLocaleString("en-US")} tokens` },
766
+ [
767
+ text([swatch], { role: "zeta.context.swatch", wrap: "none" }),
768
+ text(label, { role: "zeta.context.label", grow: 1, wrap: "none", truncate: "end" }),
769
+ text(formatNumber(tokens), { role: "zeta.context.tokens", wrap: "none" }),
770
+ text(percentString(tokens, window), { role: "zeta.context.share", wrap: "none" }),
771
+ ],
772
+ key,
773
+ );
774
+ }
775
+
776
+ /**
777
+ * The snapcompact estimate as a titled group of lines (what is imaged, how,
778
+ * and what it saves), or a note saying why it is inactive; `undefined` when
779
+ * no snapcompact setting is on.
780
+ */
781
+ function describeSnapcompact(breakdown: ContextBreakdown): NativeNode | undefined {
782
+ const snap = breakdown.snapcompact;
783
+ if (!snap) return undefined;
784
+ const head = text([span("Snapcompact", "strong"), span(" · estimated savings on the wire", "dim")], {
785
+ role: "zeta.context.snap.head",
786
+ });
787
+ if (!snap.visionCapable) {
788
+ const note = text("Inactive: the model has no image input.", { role: "zeta.context.snap.note" });
789
+ return node("col", { role: "zeta.context.snap" }, [head, note], "snapcompact");
790
+ }
791
+ const lines: { label: string; detail: string; value?: TspSpan }[] = [];
792
+ const sp = snap.systemPrompt;
793
+ if (sp) {
794
+ const label = `System prompt · ${sp.scope === "agents-md" ? "AGENTS.md" : "all"}`;
795
+ if (sp.applied) {
796
+ lines.push({
797
+ label,
798
+ detail: `${formatNumber(sp.textTokens)} text → ${sp.frames} frame${sp.frames === 1 ? "" : "s"} ≈ ${formatNumber(sp.imageTokens)}`,
799
+ value: span(`−${formatNumber(sp.savedTokens)}`, "success"),
800
+ });
801
+ } else {
802
+ const reason =
803
+ sp.reason === "budget"
804
+ ? "image budget exhausted"
805
+ : sp.reason === "empty"
806
+ ? "nothing to image"
807
+ : "frames would not save tokens";
808
+ lines.push({ label, detail: `Stays text: ${reason}` });
809
+ }
810
+ }
811
+ const tr = snap.toolResults;
812
+ if (tr) {
813
+ lines.push(
814
+ tr.swapped > 0
815
+ ? {
816
+ label: "Tool results",
817
+ detail: `${tr.swapped} of ${tr.total} imaged · ${formatNumber(tr.textTokens)} text → ${tr.frames} frames ≈ ${formatNumber(tr.imageTokens)}`,
818
+ value: span(`−${formatNumber(tr.savedTokens)}`, "success"),
819
+ }
820
+ : { label: "Tool results", detail: `None imaged · ${tr.total} in history` },
821
+ );
822
+ }
823
+ if (snap.savedTokens > 0) {
824
+ lines.push({
825
+ label: "Next request",
826
+ detail: "tokens on the wire",
827
+ value: span(`~${formatNumber(Math.max(0, breakdown.usedTokens - snap.savedTokens))}`, "strong"),
828
+ });
829
+ }
830
+ const rows = lines.map((line, index) =>
831
+ node(
832
+ "row",
833
+ { role: "zeta.context.snap.row", align: "baseline" },
834
+ [
835
+ text(line.label, { role: "zeta.context.label", wrap: "none" }),
836
+ text(line.detail, { role: "zeta.context.detail", grow: 1, wrap: "none", truncate: "end" }),
837
+ ...(line.value ? [text([line.value], { role: "zeta.context.tokens", wrap: "none" })] : []),
838
+ ],
839
+ `snap-${index}`,
840
+ ),
841
+ );
842
+ return node("col", { role: "zeta.context.snap" }, [head, ...rows], "snapcompact");
843
+ }
844
+
845
+ /**
846
+ * The `/context` body for terminals that draw `meter`: the tokens used as
847
+ * the figure that leads, beside the room left before auto-compaction; one
848
+ * stacked bar of the parts (categories, free space as the empty track, the
849
+ * hatched buffer) ticked where compaction fires; the breakdown in the bar's
850
+ * order with free space and the buffer under it; then the snapcompact
851
+ * estimate when one is on. {@link contextUsageHead} names the model.
749
852
  */
750
853
  function describeContextFrame(breakdown: ContextBreakdown): NativeNode {
751
854
  const { contextWindow: window, usedTokens, freeTokens, autoCompactBufferTokens, thresholdTokens } = breakdown;
752
855
  const parts = contextMeterParts(breakdown);
753
856
  const used = Math.min(1, usedTokens / window);
754
- const swatch = (token: string, glyph = "■"): TspSpan => span(glyph, token);
755
- const figure = (tokens: number): TspText => [
756
- span(formatNumber(tokens)),
757
- span(` ${percentString(tokens, window)}`, "dim"),
758
- ];
759
- const items: { k: TspText; v: TspText }[] = [
760
- {
761
- k: [span("Used", "strong")],
762
- v: [
763
- span(`${formatNumber(usedTokens)} / ${formatNumber(window).toLowerCase()}`),
764
- span(` ${percentString(usedTokens, window)}`, "dim"),
765
- ],
766
- },
767
- ];
768
- for (const category of breakdown.categories) {
769
- items.push({ k: [swatch(category.color), span(` ${category.label}`)], v: figure(category.tokens) });
770
- }
771
- items.push({ k: [swatch("dim", "□"), span(" Free space")], v: figure(freeTokens) });
772
- if (autoCompactBufferTokens > 0) {
773
- items.push({ k: [swatch("warning", "▨"), span(" Autocompact buffer")], v: figure(autoCompactBufferTokens) });
774
- }
857
+ const room =
858
+ thresholdTokens === undefined
859
+ ? `${formatNumber(freeTokens)} free`
860
+ : usedTokens >= thresholdTokens
861
+ ? `Past auto-compaction at ${percentString(thresholdTokens, window, 0)}`
862
+ : `${formatNumber(thresholdTokens - usedTokens)} left before auto-compaction at ${percentString(thresholdTokens, window, 0)}`;
863
+ const summary = node(
864
+ "row",
865
+ { role: "zeta.context.summary", align: "baseline", wrap: true },
866
+ [
867
+ text(
868
+ [
869
+ span(formatNumber(usedTokens), "strong"),
870
+ span(` of ${formatNumber(window)} tokens · ${percentString(usedTokens, window, 0)}`, "dim"),
871
+ ],
872
+ { role: "zeta.context.used", grow: 1, wrap: "none" },
873
+ ),
874
+ text(room, { role: "zeta.context.room", wrap: "none" }),
875
+ ],
876
+ "summary",
877
+ );
775
878
  const marks =
776
879
  thresholdTokens !== undefined
777
880
  ? [
778
881
  {
779
882
  at: thresholdTokens / window,
780
883
  tone: "warning" as const,
781
- title: `Auto-compaction at ${percentString(thresholdTokens, window, 0)} (${formatNumber(thresholdTokens)} tokens)`,
884
+ title: `Auto-compaction at ${percentString(thresholdTokens, window, 0)} · ${formatNumber(thresholdTokens)} tokens`,
782
885
  },
783
886
  ]
784
887
  : undefined;
785
- const children = [
786
- node(
787
- "row",
788
- { role: "zeta.context.body" },
789
- [
790
- node("meter", { value: used, style: "blocks", size: "lg", parts, aria: "Context usage by category" }),
791
- node("kv", { items, layout: "grid", role: "zeta.context.legend" }),
792
- ],
793
- "body",
794
- ),
795
- node(
796
- "meter",
797
- { value: used, style: "bar", size: "lg", parts, ...(marks ? { marks } : {}), grow: 1 },
798
- undefined,
799
- "bar",
888
+ const bar = node(
889
+ "meter",
890
+ { value: used, style: "bar", size: "lg", parts, ...(marks ? { marks } : {}), aria: "Context usage by category" },
891
+ undefined,
892
+ "bar",
893
+ );
894
+ const shares = breakdown.categories.map(category =>
895
+ contextShareRow(
896
+ category.id,
897
+ "zeta.context.cat",
898
+ span("■", category.color),
899
+ category.label,
900
+ category.tokens,
901
+ window,
800
902
  ),
801
- ];
802
- const snap = buildSnapcompactParts(breakdown);
803
- if (snap.length > 0) {
804
- children.push(
805
- node(
806
- "section",
807
- {},
808
- snap.map((line, index) => node("text", { spans: line, wrap: "word" }, undefined, `snap-${index}`)),
809
- "snapcompact",
903
+ );
904
+ shares.push(contextShareRow("free", "zeta.context.cat.free", span("□", "dim"), "Free space", freeTokens, window));
905
+ if (autoCompactBufferTokens > 0) {
906
+ shares.push(
907
+ contextShareRow(
908
+ "buffer",
909
+ "zeta.context.cat.buffer",
910
+ span("▨", "warning"),
911
+ "Autocompact buffer",
912
+ autoCompactBufferTokens,
913
+ window,
810
914
  ),
811
915
  );
812
916
  }
813
- return col(children, { gap: "md", role: "zeta.context" });
917
+ const children = [summary, bar, node("col", { role: "zeta.context.cats" }, shares, "cats")];
918
+ const snap = describeSnapcompact(breakdown);
919
+ if (snap) children.push(snap);
920
+ return col(children, { role: "zeta.context" });
814
921
  }
815
922
 
816
- /** The `/context` title: the model and its window once a model is selected. */
923
+ /** The `/context` title: the model once one is selected (the body leads with its window). */
817
924
  export function contextUsageHead(breakdown: ContextBreakdown): TspText {
818
925
  if (breakdown.contextWindow <= 0) return [span("Context usage", "strong")];
819
926
  const modelName = breakdown.model?.name ?? breakdown.model?.id ?? "no model";
820
- return [
821
- span("Context", "strong"),
822
- span(` · ${modelName}`, "muted"),
823
- span(` · ${formatNumber(breakdown.contextWindow).toLowerCase()}`, "dim"),
824
- ];
927
+ return [span("Context", "strong"), span(` · ${modelName}`, "muted")];
825
928
  }
826
929
 
827
930
  /**