@linxiraos/pi-tui 1.2.1 → 1.3.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 (274) hide show
  1. package/CHANGELOG.md +188 -125
  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/transcript-container.d.ts +2 -0
  21. package/dist/types/components/image.d.ts +2 -0
  22. package/dist/types/components/markdown.d.ts +49 -3
  23. package/dist/types/components/text.d.ts +2 -0
  24. package/dist/types/debug-server.d.ts +2 -0
  25. package/dist/types/keybinding-matchers.d.ts +6 -0
  26. package/dist/types/multiplexers/tmux.d.ts +1 -0
  27. package/dist/types/multiplexers/types.d.ts +6 -0
  28. package/dist/types/native/backend.d.ts +5 -4
  29. package/dist/types/native/blobs.d.ts +24 -12
  30. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  31. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  32. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  33. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  34. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  35. package/dist/types/overlays/hook-editor.d.ts +7 -2
  36. package/dist/types/overlays/hook-input.d.ts +12 -4
  37. package/dist/types/overlays/job-tree.d.ts +50 -0
  38. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  39. package/dist/types/overlays/login-dialog.d.ts +8 -3
  40. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  41. package/dist/types/overlays/model-browser.d.ts +6 -1
  42. package/dist/types/overlays/model-picker.d.ts +2 -0
  43. package/dist/types/overlays/pause-screen.d.ts +5 -0
  44. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  45. package/dist/types/overlays/report-panel.d.ts +5 -1
  46. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  47. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  48. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  49. package/dist/types/overlays/session-selector.d.ts +3 -1
  50. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  51. package/dist/types/prompt/composer.d.ts +21 -20
  52. package/dist/types/prompt/custom-editor.d.ts +20 -10
  53. package/dist/types/prompt/image-references.d.ts +22 -0
  54. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  55. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  56. package/dist/types/prompt/prose-gate.d.ts +3 -0
  57. package/dist/types/prompt/queued-messages.d.ts +1 -1
  58. package/dist/types/prompt/typing-word.d.ts +25 -0
  59. package/dist/types/prompt/welcome.d.ts +26 -6
  60. package/dist/types/render/hanging-wrap.d.ts +7 -0
  61. package/dist/types/render/hyperlink.d.ts +2 -0
  62. package/dist/types/render/render-utils.d.ts +9 -1
  63. package/dist/types/render/utils.d.ts +1 -1
  64. package/dist/types/setup/scenes/splash.d.ts +6 -5
  65. package/dist/types/status-line/context-usage.d.ts +4 -1
  66. package/dist/types/status-line/host.d.ts +6 -4
  67. package/dist/types/status-line/startup.d.ts +6 -6
  68. package/dist/types/status-line/types.d.ts +3 -0
  69. package/dist/types/stdin-buffer.d.ts +7 -0
  70. package/dist/types/terminal-capabilities.d.ts +16 -1
  71. package/dist/types/terminal-multiplexer.d.ts +7 -0
  72. package/dist/types/terminal.d.ts +8 -4
  73. package/dist/types/theme/symbols.d.ts +1 -1
  74. package/dist/types/theme/theme.d.ts +11 -0
  75. package/dist/types/thinking.d.ts +5 -0
  76. package/dist/types/tmux.d.ts +39 -4
  77. package/dist/types/tools/ast-edit.d.ts +2 -2
  78. package/dist/types/tools/ast-grep.d.ts +2 -2
  79. package/dist/types/tools/bash.d.ts +6 -1
  80. package/dist/types/tools/edit.d.ts +2 -0
  81. package/dist/types/tools/eval.d.ts +53 -4
  82. package/dist/types/tools/find.d.ts +14 -4
  83. package/dist/types/tools/glob.d.ts +3 -3
  84. package/dist/types/tools/grep.d.ts +2 -2
  85. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  86. package/dist/types/tools/native-view.d.ts +1 -1
  87. package/dist/types/tools/read.d.ts +32 -10
  88. package/dist/types/tools/renderer.d.ts +2 -0
  89. package/dist/types/tools/task.d.ts +31 -0
  90. package/dist/types/tools/write.d.ts +2 -0
  91. package/dist/types/tui.d.ts +20 -3
  92. package/package.json +10 -10
  93. package/src/app-keybindings.d.ts +18 -8
  94. package/src/app-keybindings.ts +20 -8
  95. package/src/apps/autoresearch-dashboard.ts +444 -27
  96. package/src/apps/debug/raw-sse.ts +2 -2
  97. package/src/apps/git/diff-pane.ts +39 -41
  98. package/src/apps/git/git-tui.ts +80 -14
  99. package/src/apps/git/sidebar.ts +10 -1
  100. package/src/apps/git/state.ts +12 -0
  101. package/src/autocomplete.d.ts +2 -0
  102. package/src/autocomplete.ts +21 -19
  103. package/src/bracketed-paste.ts +9 -2
  104. package/src/charts/chart-svg.d.ts +7 -13
  105. package/src/charts/chart-svg.ts +19 -22
  106. package/src/chat/advisor-message.d.ts +4 -1
  107. package/src/chat/advisor-message.ts +77 -24
  108. package/src/chat/assistant-message.d.ts +9 -4
  109. package/src/chat/assistant-message.ts +127 -31
  110. package/src/chat/bash-execution.ts +7 -2
  111. package/src/chat/figure-markdown.d.ts +7 -2
  112. package/src/chat/figure-markdown.ts +10 -3
  113. package/src/chat/image-loading.ts +9 -6
  114. package/src/chat/messages.d.ts +3 -1
  115. package/src/chat/messages.ts +4 -1
  116. package/src/chat/read-tool-group.d.ts +2 -2
  117. package/src/chat/read-tool-group.ts +97 -42
  118. package/src/chat/svg-figure.ts +3 -1
  119. package/src/chat/svg-source.ts +6 -3
  120. package/src/chat/table-chart.d.ts +11 -15
  121. package/src/chat/table-chart.ts +24 -57
  122. package/src/chat/tool-execution.d.ts +32 -20
  123. package/src/chat/tool-execution.ts +103 -36
  124. package/src/chat/user-message.d.ts +3 -2
  125. package/src/chat/user-message.ts +54 -34
  126. package/src/chrome/countdown-timer.ts +3 -3
  127. package/src/chrome/diff.d.ts +2 -3
  128. package/src/chrome/diff.ts +4 -9
  129. package/src/chrome/transcript-container.d.ts +2 -0
  130. package/src/chrome/transcript-container.ts +212 -28
  131. package/src/components/editor.ts +12 -12
  132. package/src/components/image.d.ts +2 -0
  133. package/src/components/image.ts +54 -20
  134. package/src/components/markdown.d.ts +49 -3
  135. package/src/components/markdown.ts +461 -11
  136. package/src/components/settings-list.ts +2 -4
  137. package/src/components/text.d.ts +2 -0
  138. package/src/components/text.ts +11 -1
  139. package/src/debug-server.d.ts +2 -0
  140. package/src/debug-server.ts +51 -69
  141. package/src/hotkeys-markdown.ts +2 -0
  142. package/src/keybinding-matchers.d.ts +6 -0
  143. package/src/keybinding-matchers.ts +19 -0
  144. package/src/multiplexers/tmux.d.ts +1 -0
  145. package/src/multiplexers/tmux.ts +3 -0
  146. package/src/multiplexers/types.d.ts +6 -0
  147. package/src/multiplexers/types.ts +6 -0
  148. package/src/native/backend.d.ts +5 -4
  149. package/src/native/backend.ts +13 -5
  150. package/src/native/blobs.d.ts +24 -12
  151. package/src/native/blobs.ts +56 -7
  152. package/src/native/reconcile.ts +43 -21
  153. package/src/overlays/advisor-config.ts +259 -8
  154. package/src/overlays/agent-hub-projection.d.ts +10 -5
  155. package/src/overlays/agent-hub-projection.ts +63 -12
  156. package/src/overlays/annotation-overlay.ts +422 -168
  157. package/src/overlays/approval-dialog.ts +389 -0
  158. package/src/overlays/ask-dialog.ts +244 -105
  159. package/src/overlays/btw-history-panel.ts +208 -147
  160. package/src/overlays/extensions/extension-list.ts +4 -6
  161. package/src/overlays/hook-editor.ts +90 -32
  162. package/src/overlays/hook-input.ts +105 -19
  163. package/src/overlays/job-tree.ts +177 -0
  164. package/src/overlays/jobs-panel.ts +222 -142
  165. package/src/overlays/login-dialog.ts +201 -91
  166. package/src/overlays/mcp-add-wizard.ts +562 -279
  167. package/src/overlays/model-browser.ts +32 -7
  168. package/src/overlays/model-hub.ts +4 -10
  169. package/src/overlays/model-picker.ts +58 -21
  170. package/src/overlays/pause-screen.ts +81 -23
  171. package/src/overlays/plan-review-overlay.ts +16 -0
  172. package/src/overlays/plugin-selector.ts +17 -1
  173. package/src/overlays/queue-mode-selector.ts +10 -0
  174. package/src/overlays/report-panel.ts +6 -2
  175. package/src/overlays/reset-usage-selector.ts +130 -21
  176. package/src/overlays/session-account-selector.ts +36 -0
  177. package/src/overlays/session-info-overlay.ts +199 -76
  178. package/src/overlays/session-selector.ts +15 -10
  179. package/src/overlays/settings-selector.ts +3 -0
  180. package/src/overlays/show-images-selector.ts +1 -0
  181. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  182. package/src/overlays/theme-selector.ts +6 -1
  183. package/src/overlays/thinking-selector.ts +8 -4
  184. package/src/overlays/tree-selector.ts +117 -2
  185. package/src/overlays/usage-dashboard.ts +498 -144
  186. package/src/prompt/composer-cache.ts +1 -1
  187. package/src/prompt/composer.ts +194 -74
  188. package/src/prompt/custom-editor.ts +168 -65
  189. package/src/prompt/image-references.d.ts +22 -0
  190. package/src/prompt/image-references.ts +79 -2
  191. package/src/prompt/internal-url-autocomplete.ts +1 -0
  192. package/src/prompt/macos-spelling.ts +28 -5
  193. package/src/prompt/prose-gate.ts +3 -0
  194. package/src/prompt/queued-messages.ts +9 -2
  195. package/src/prompt/tips.txt +1 -2
  196. package/src/prompt/typing-word.ts +59 -0
  197. package/src/prompt/welcome.ts +97 -45
  198. package/src/render/code-cell.ts +18 -5
  199. package/src/render/hanging-wrap.d.ts +7 -0
  200. package/src/render/hanging-wrap.ts +58 -0
  201. package/src/render/hyperlink.d.ts +2 -0
  202. package/src/render/hyperlink.ts +5 -0
  203. package/src/render/render-utils.d.ts +9 -1
  204. package/src/render/render-utils.ts +16 -2
  205. package/src/render/utils.d.ts +1 -1
  206. package/src/render/utils.ts +1 -1
  207. package/src/render/width-aware-text.ts +6 -5
  208. package/src/setup/scenes/splash.ts +22 -8
  209. package/src/status-line/component.ts +23 -10
  210. package/src/status-line/context-usage.ts +176 -73
  211. package/src/status-line/footer.ts +18 -35
  212. package/src/status-line/host.ts +6 -1
  213. package/src/status-line/segments.ts +16 -5
  214. package/src/status-line/startup.ts +6 -6
  215. package/src/status-line/types.ts +1 -0
  216. package/src/stdin-buffer.d.ts +7 -0
  217. package/src/stdin-buffer.ts +11 -1
  218. package/src/terminal-capabilities.d.ts +16 -1
  219. package/src/terminal-capabilities.ts +66 -28
  220. package/src/terminal-multiplexer.d.ts +7 -0
  221. package/src/terminal-multiplexer.ts +14 -0
  222. package/src/terminal.d.ts +8 -4
  223. package/src/terminal.ts +160 -46
  224. package/src/theme/symbols.d.ts +1 -1
  225. package/src/theme/symbols.ts +1 -0
  226. package/src/theme/theme-class.ts +1 -0
  227. package/src/theme/theme.d.ts +11 -0
  228. package/src/theme/theme.ts +46 -14
  229. package/src/thinking.d.ts +5 -0
  230. package/src/thinking.ts +9 -1
  231. package/src/tmux.d.ts +39 -4
  232. package/src/tmux.ts +110 -16
  233. package/src/tools/ask.ts +20 -9
  234. package/src/tools/ast-edit.d.ts +2 -2
  235. package/src/tools/ast-edit.ts +30 -7
  236. package/src/tools/ast-grep.d.ts +2 -2
  237. package/src/tools/ast-grep.ts +39 -35
  238. package/src/tools/bash.d.ts +6 -1
  239. package/src/tools/bash.ts +15 -9
  240. package/src/tools/debug.ts +2 -1
  241. package/src/tools/default-renderer.ts +3 -2
  242. package/src/tools/edit.d.ts +2 -0
  243. package/src/tools/edit.ts +100 -21
  244. package/src/tools/eval.d.ts +53 -4
  245. package/src/tools/eval.ts +173 -137
  246. package/src/tools/find.d.ts +14 -4
  247. package/src/tools/find.ts +71 -18
  248. package/src/tools/glob.d.ts +3 -3
  249. package/src/tools/glob.ts +51 -17
  250. package/src/tools/grep.d.ts +2 -2
  251. package/src/tools/grep.ts +99 -22
  252. package/src/tools/grouped-file-output.d.ts +9 -4
  253. package/src/tools/grouped-file-output.ts +21 -6
  254. package/src/tools/lsp.ts +30 -18
  255. package/src/tools/mcp.ts +2 -2
  256. package/src/tools/native-view.d.ts +1 -1
  257. package/src/tools/native-view.ts +2 -2
  258. package/src/tools/read.d.ts +32 -10
  259. package/src/tools/read.ts +89 -43
  260. package/src/tools/renderer.d.ts +2 -0
  261. package/src/tools/renderer.ts +2 -0
  262. package/src/tools/task.d.ts +31 -0
  263. package/src/tools/task.ts +40 -7
  264. package/src/tools/todo.ts +3 -2
  265. package/src/tools/wait.ts +16 -30
  266. package/src/tools/web-search.ts +2 -1
  267. package/src/tools/write.d.ts +2 -0
  268. package/src/tools/write.ts +36 -12
  269. package/src/tools/yield.ts +2 -2
  270. package/src/tui.d.ts +20 -3
  271. package/src/tui.ts +140 -36
  272. package/src/utils.ts +5 -3
  273. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  274. 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++) {
@@ -380,6 +380,7 @@ interface ContextUsageMemo {
380
380
  length: number;
381
381
  lastFingerprint: string | undefined;
382
382
  modelContextWindow: number;
383
+ toolSchemasInContext: boolean;
383
384
  contextUsageRevision: number;
384
385
  toolSchemaMetadataRevision: number;
385
386
  settingsRevision: number;
@@ -774,9 +775,12 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
774
775
  // than poison the fresh cache or advance its throttle.
775
776
  #jjCacheGeneration = 0;
776
777
 
777
- // PR lookup caching (invalidated on branch/repo context changes)
778
+ // PR lookup cache. `#cachedPrContext` is the branch/repo `#cachedPr` was
779
+ // resolved for; it is never shown for any other branch. Expiry forces a
780
+ // refetch while keeping that branch's PR visible until the lookup lands.
778
781
  #cachedPr: { number: number; url: string } | null | undefined = undefined;
779
782
  #cachedPrContext: PrCacheContext | undefined = undefined;
783
+ #cachedPrExpired = false;
780
784
  #prLookupInFlight = false;
781
785
  #defaultBranch?: string;
782
786
  #defaultBranchCwd: string | undefined = undefined;
@@ -1457,8 +1461,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1457
1461
  // was designed to avoid. Explicit Git/repository invalidation (watcher
1458
1462
  // HEAD-move, cwd/repo switch) goes through {@link invalidateGitCaches}.
1459
1463
  // 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;
1464
+ // settled PR on ordinary activity while leaving HEAD work intact.
1465
+ this.#cachedPrExpired = true;
1462
1466
  }
1463
1467
  #invalidateSessionCaches(): void {
1464
1468
  this.#clearUsageStartTimer();
@@ -1489,7 +1493,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1489
1493
  this.#branchResolveActive = undefined;
1490
1494
  this.#branchLastFetch = undefined;
1491
1495
  this.#branchCacheGeneration++;
1492
- this.#cachedPrContext = undefined;
1496
+ this.#cachedPrExpired = true;
1493
1497
  // A HEAD move (commit, checkout, reset) changes the dirty counts; keep the
1494
1498
  // stale counts on screen but refetch on the next render. The generation
1495
1499
  // bump stops an in-flight pre-move result from re-stamping freshness.
@@ -1810,11 +1814,16 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1810
1814
  const branch = this.#getBranchLabel(activeRepoCache, this.#resolveRepository(activeRepoCache));
1811
1815
  const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
1812
1816
 
1813
- if (canReuseCachedPr(this.#cachedPr, this.#cachedPrContext, currentContext)) {
1817
+ if (!this.#cachedPrExpired && canReuseCachedPr(this.#cachedPr, this.#cachedPrContext, currentContext)) {
1814
1818
  return this.#cachedPr ?? null;
1815
1819
  }
1816
1820
 
1817
- const stalePr = this.#cachedPr;
1821
+ // Keep an expired PR visible during the refetch only on the branch it
1822
+ // was resolved for; after a checkout the old PR must not linger.
1823
+ const stalePr =
1824
+ currentContext && isSamePrCacheContext(this.#cachedPrContext, currentContext)
1825
+ ? (this.#cachedPr ?? null)
1826
+ : null;
1818
1827
 
1819
1828
  if (!branch) {
1820
1829
  this.#cachedPr = null;
@@ -1822,10 +1831,9 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1822
1831
  return null;
1823
1832
  }
1824
1833
 
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
- }
1834
+ // Detached HEAD and the default branch never carry a PR; no lookup.
1835
+ if (branch === "detached" || this.#isDefaultBranch(branch, gitCwd)) return null;
1836
+ if (this.#prLookupInFlight) return stalePr;
1829
1837
 
1830
1838
  this.#prLookupInFlight = true;
1831
1839
  const lookupContext = currentContext;
@@ -1847,6 +1855,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1847
1855
  if (lookupContext && isSamePrCacheContext(latestContext, lookupContext)) {
1848
1856
  this.#cachedPr = value;
1849
1857
  this.#cachedPrContext = lookupContext;
1858
+ this.#cachedPrExpired = false;
1850
1859
  }
1851
1860
  };
1852
1861
  try {
@@ -1980,6 +1989,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1980
1989
  * cadence while a late successful fetch can still refresh the cached segment.
1981
1990
  */
1982
1991
  refreshUsageInBackground(): void {
1992
+ if (this.#disposed) return;
1983
1993
  const now = Date.now();
1984
1994
  const session = this.session;
1985
1995
  const usageContextKey = this.#getUsageContextKey(session);
@@ -2407,6 +2417,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
2407
2417
  getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
2408
2418
  const messages = this.session.messages ?? EMPTY_MESSAGES;
2409
2419
  const modelContextWindow = this.session.model?.contextWindow ?? 0;
2420
+ const toolSchemasInContext = this.session.model?.toolSchemasInContext !== false;
2410
2421
  const length = messages.length;
2411
2422
  const lastFingerprint = length > 0 ? messageFingerprint(messages[length - 1]!) : undefined;
2412
2423
  // Bumps when the in-flight pending snapshot is set/cleared. Without it a
@@ -2428,6 +2439,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
2428
2439
  cache.length === length &&
2429
2440
  cache.lastFingerprint === lastFingerprint &&
2430
2441
  cache.modelContextWindow === modelContextWindow &&
2442
+ cache.toolSchemasInContext === toolSchemasInContext &&
2431
2443
  cache.contextUsageRevision === contextUsageRevision &&
2432
2444
  cache.systemPromptRef === systemPrompt &&
2433
2445
  cache.toolsRef === tools &&
@@ -2448,6 +2460,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
2448
2460
  length,
2449
2461
  lastFingerprint,
2450
2462
  modelContextWindow,
2463
+ toolSchemasInContext,
2451
2464
  contextUsageRevision,
2452
2465
  toolSchemaMetadataRevision,
2453
2466
  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
  /**
@@ -176,22 +176,11 @@ export class FooterComponent implements Component {
176
176
  */
177
177
  describe(): NativeNode {
178
178
  const state = this.session.state;
179
- let input = 0;
180
- let output = 0;
181
- let cacheRead = 0;
182
- let cacheWrite = 0;
183
- let cost = 0;
184
- let premiumRequests = 0;
185
- for (const entry of this.session.sessionManager.getEntries()) {
186
- if (entry.type === "message" && entry.message?.role === "assistant") {
187
- input += entry.message.usage.input;
188
- output += entry.message.usage.output;
189
- cacheRead += entry.message.usage.cacheRead;
190
- cacheWrite += entry.message.usage.cacheWrite;
191
- cost += entry.message.usage.cost.total;
192
- premiumRequests += entry.message.usage.premiumRequests ?? 0;
193
- }
194
- }
179
+ // Cumulative usage: the session manager's running rollup spans every entry
180
+ // in the session (so it survives compaction) and already folds in the usage
181
+ // completed `task` tool results carry, which is where subagent spend lands.
182
+ const { input, output, cacheRead, cacheWrite, cost, premiumRequests } =
183
+ this.session.sessionManager.getUsageStatistics();
195
184
  const segs: NativeNode[] = [];
196
185
  const seg = (key: string, props: TspProps<"seg">): void => {
197
186
  segs.push(node("seg", { role: `zeta.footer.${key}`, ...props }, undefined, key));
@@ -257,25 +246,19 @@ export class FooterComponent implements Component {
257
246
  render(width: number): readonly string[] {
258
247
  const state = this.session.state;
259
248
 
260
- // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
261
- let totalInput = 0;
262
- let totalOutput = 0;
263
- let totalCacheRead = 0;
264
- let totalCacheWrite = 0;
265
- let totalCost = 0;
266
- let totalPremiumRequests = 0;
267
-
268
- for (const entry of this.session.sessionManager.getEntries()) {
269
- if (entry.type === "message" && entry.message?.role === "assistant") {
270
- totalInput += entry.message.usage.input;
271
- totalOutput += entry.message.usage.output;
272
- totalCacheRead += entry.message.usage.cacheRead;
273
- totalCacheWrite += entry.message.usage.cacheWrite;
274
- totalCost += entry.message.usage.cost.total;
275
- totalPremiumRequests += entry.message.usage.premiumRequests ?? 0;
276
- }
277
- }
278
-
249
+ // Cumulative usage comes from the session manager's running rollup, which
250
+ // spans every entry in the session and folds in the usage completed `task`
251
+ // tool results carry (subagent spend). Context usage below is a separate
252
+ // question and stays window-scoped on purpose. Destructured straight off the
253
+ // rollup: a repaint must not allocate a second copy of these six fields.
254
+ const {
255
+ input: totalInput,
256
+ output: totalOutput,
257
+ cacheRead: totalCacheRead,
258
+ cacheWrite: totalCacheWrite,
259
+ cost: totalCost,
260
+ premiumRequests: totalPremiumRequests,
261
+ } = this.session.sessionManager.getUsageStatistics();
279
262
  // Calculate context usage from session (handles compaction correctly).
280
263
  // After compaction, tokens are unknown until the next LLM response.
281
264
  const contextUsage = this.session.getContextUsage();
@@ -71,7 +71,12 @@ export interface FooterSession {
71
71
  autoResolvedThinkingLevel(): string | undefined;
72
72
  getContextUsage: StatusLineSession["getContextUsage"];
73
73
  modelRegistry: Pick<StatusLineSession["modelRegistry"], "isUsingOAuth">;
74
- sessionManager: { getEntries(): readonly { type: string; message?: AgentMessage }[] };
74
+ /**
75
+ * Cumulative session rollup. Unlike the active-window stats it spans every
76
+ * entry in the session, so compaction does not reset the counters, and it
77
+ * folds in usage carried by completed `task` tool results.
78
+ */
79
+ sessionManager: { getUsageStatistics: StatusLineSession["sessionManager"]["getUsageStatistics"] };
75
80
  }
76
81
 
77
82
  /**
@@ -6,6 +6,7 @@ import { SPINNER_ADVANCE_MS, TERMINAL } from "../index";
6
6
  import {
7
7
  formatDuration,
8
8
  formatNumber,
9
+ getActiveProfile,
9
10
  getProjectDir,
10
11
  normalizePathForComparison,
11
12
  relativePathWithinNormalizedRoot,
@@ -216,24 +217,34 @@ const piSegment: StatusLineSegment = {
216
217
  // Brand fg fades between dim gray (idle) and the accent (working) across
217
218
  // turn edges; the component samples the tween into `brandFgAnsi`.
218
219
  const fgAnsi = ctx.brandFgAnsi ?? theme.getFgAnsi("dim");
220
+ const profile = ctx.options.pi?.showProfile === false ? undefined : getActiveProfile();
219
221
  // While a turn runs the brand icon becomes a braille spinner plus a
220
222
  // whole-unit turn timer (port of rust zeta's status-band active brand).
221
223
  // No trailing pad: the group renderer owns inter-segment spacing, so a
222
224
  // trailing space here would double the gap at the first separator (#11103).
223
225
  const content =
224
226
  ctx.turnElapsedMs != null
225
- ? `${brandSpinnerFrame(ctx.now?.getTime())} ${brandTimer(ctx.turnElapsedMs)}`
226
- : theme.icon.omp
227
- ? theme.icon.omp
228
- : "";
227
+ ? `${brandMark(brandSpinnerFrame(ctx.now?.getTime()), profile)} ${brandTimer(ctx.turnElapsedMs)}`
228
+ : brandMark(theme.icon.omp, profile);
229
229
  return { content: `${fgAnsi}${content}\x1b[39m`, visible: true };
230
230
  },
231
231
  describe(ctx) {
232
232
  if (ctx.focusedAgentId) return segView([span(ctx.focusedAgentId, "warning")], "ghost", "warning");
233
233
  // The dock's working row owns activity natively: the brand stays still.
234
- return segView([], "zeta", "muted");
234
+ const profile = ctx.options.pi?.showProfile === false ? undefined : getActiveProfile();
235
+ return segView(profile ? [span(profile, "muted")] : [], "omp", "muted");
235
236
  },
236
237
  };
238
+ /**
239
+ * Brand glyph tagged with the active named profile (`π-work`) so sessions from
240
+ * different profiles are distinguishable at a glance; the bare glyph for the
241
+ * default profile or with `segmentOptions.pi.showProfile: false`. The tag
242
+ * stays put while the glyph slot animates.
243
+ */
244
+ function brandMark(glyph: string, profile: string | undefined): string {
245
+ if (!profile) return glyph;
246
+ return glyph ? `${glyph}-${profile}` : profile;
247
+ }
237
248
  /** Current braille-spinner glyph on the shared clock, at the Loader's 80ms cadence. */
238
249
  function brandSpinnerFrame(nowMs = Date.now()): string {
239
250
  const frames = theme.getSpinnerFrames("activity");