@linxiraos/pi-tui 1.2.0 → 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 (276) hide show
  1. package/CHANGELOG.md +188 -120
  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 +22 -26
  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 +35 -43
  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/agent-hub.ts +2 -2
  157. package/src/overlays/annotation-overlay.ts +422 -168
  158. package/src/overlays/approval-dialog.ts +389 -0
  159. package/src/overlays/ask-dialog.ts +244 -105
  160. package/src/overlays/btw-history-panel.ts +208 -147
  161. package/src/overlays/extensions/extension-list.ts +4 -6
  162. package/src/overlays/hook-editor.ts +90 -32
  163. package/src/overlays/hook-input.ts +105 -19
  164. package/src/overlays/job-tree.ts +177 -0
  165. package/src/overlays/jobs-panel.ts +222 -142
  166. package/src/overlays/login-dialog.ts +201 -91
  167. package/src/overlays/mcp-add-wizard.ts +562 -279
  168. package/src/overlays/model-browser.ts +32 -7
  169. package/src/overlays/model-hub.ts +4 -10
  170. package/src/overlays/model-picker.ts +58 -21
  171. package/src/overlays/pause-screen.ts +81 -23
  172. package/src/overlays/plan-review-overlay.ts +16 -0
  173. package/src/overlays/plugin-selector.ts +17 -1
  174. package/src/overlays/plugin-settings.ts +8 -2
  175. package/src/overlays/queue-mode-selector.ts +10 -0
  176. package/src/overlays/report-panel.ts +6 -2
  177. package/src/overlays/reset-usage-selector.ts +130 -21
  178. package/src/overlays/session-account-selector.ts +36 -0
  179. package/src/overlays/session-info-overlay.ts +199 -76
  180. package/src/overlays/session-selector.ts +15 -10
  181. package/src/overlays/settings-selector.ts +3 -0
  182. package/src/overlays/show-images-selector.ts +1 -0
  183. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  184. package/src/overlays/theme-selector.ts +6 -1
  185. package/src/overlays/thinking-selector.ts +8 -4
  186. package/src/overlays/tree-selector.ts +117 -2
  187. package/src/overlays/usage-dashboard.ts +498 -144
  188. package/src/prompt/composer-cache.ts +4 -6
  189. package/src/prompt/composer.ts +196 -96
  190. package/src/prompt/custom-editor.ts +168 -65
  191. package/src/prompt/image-references.d.ts +22 -0
  192. package/src/prompt/image-references.ts +79 -2
  193. package/src/prompt/internal-url-autocomplete.ts +1 -0
  194. package/src/prompt/macos-spelling.ts +28 -5
  195. package/src/prompt/prose-gate.ts +3 -0
  196. package/src/prompt/queued-messages.ts +9 -2
  197. package/src/prompt/tips.txt +1 -2
  198. package/src/prompt/typing-word.ts +59 -0
  199. package/src/prompt/welcome.ts +256 -423
  200. package/src/render/code-cell.ts +18 -5
  201. package/src/render/hanging-wrap.d.ts +7 -0
  202. package/src/render/hanging-wrap.ts +58 -0
  203. package/src/render/hyperlink.d.ts +2 -0
  204. package/src/render/hyperlink.ts +5 -0
  205. package/src/render/render-utils.d.ts +9 -1
  206. package/src/render/render-utils.ts +16 -2
  207. package/src/render/utils.d.ts +1 -1
  208. package/src/render/utils.ts +1 -1
  209. package/src/render/width-aware-text.ts +6 -5
  210. package/src/setup/scenes/splash.ts +137 -60
  211. package/src/status-line/component.ts +23 -10
  212. package/src/status-line/context-usage.ts +176 -73
  213. package/src/status-line/footer.ts +18 -35
  214. package/src/status-line/host.ts +6 -1
  215. package/src/status-line/segments.ts +16 -5
  216. package/src/status-line/startup.ts +6 -6
  217. package/src/status-line/types.ts +1 -0
  218. package/src/stdin-buffer.d.ts +7 -0
  219. package/src/stdin-buffer.ts +11 -1
  220. package/src/terminal-capabilities.d.ts +16 -1
  221. package/src/terminal-capabilities.ts +66 -28
  222. package/src/terminal-multiplexer.d.ts +7 -0
  223. package/src/terminal-multiplexer.ts +14 -0
  224. package/src/terminal.d.ts +8 -4
  225. package/src/terminal.ts +160 -46
  226. package/src/theme/symbols.d.ts +1 -1
  227. package/src/theme/symbols.ts +1 -0
  228. package/src/theme/theme-class.ts +1 -0
  229. package/src/theme/theme.d.ts +11 -0
  230. package/src/theme/theme.ts +46 -14
  231. package/src/thinking.d.ts +5 -0
  232. package/src/thinking.ts +9 -1
  233. package/src/tmux.d.ts +39 -4
  234. package/src/tmux.ts +110 -16
  235. package/src/tools/ask.ts +20 -9
  236. package/src/tools/ast-edit.d.ts +2 -2
  237. package/src/tools/ast-edit.ts +30 -7
  238. package/src/tools/ast-grep.d.ts +2 -2
  239. package/src/tools/ast-grep.ts +39 -35
  240. package/src/tools/bash.d.ts +6 -1
  241. package/src/tools/bash.ts +15 -9
  242. package/src/tools/debug.ts +2 -1
  243. package/src/tools/default-renderer.ts +3 -2
  244. package/src/tools/edit.d.ts +2 -0
  245. package/src/tools/edit.ts +100 -21
  246. package/src/tools/eval.d.ts +53 -4
  247. package/src/tools/eval.ts +173 -137
  248. package/src/tools/find.d.ts +14 -4
  249. package/src/tools/find.ts +71 -18
  250. package/src/tools/glob.d.ts +3 -3
  251. package/src/tools/glob.ts +51 -17
  252. package/src/tools/grep.d.ts +2 -2
  253. package/src/tools/grep.ts +99 -22
  254. package/src/tools/grouped-file-output.d.ts +9 -4
  255. package/src/tools/grouped-file-output.ts +21 -6
  256. package/src/tools/lsp.ts +30 -18
  257. package/src/tools/mcp.ts +2 -2
  258. package/src/tools/native-view.d.ts +1 -1
  259. package/src/tools/native-view.ts +2 -2
  260. package/src/tools/read.d.ts +32 -10
  261. package/src/tools/read.ts +89 -43
  262. package/src/tools/renderer.d.ts +2 -0
  263. package/src/tools/renderer.ts +2 -0
  264. package/src/tools/task.d.ts +31 -0
  265. package/src/tools/task.ts +40 -7
  266. package/src/tools/todo.ts +3 -2
  267. package/src/tools/wait.ts +16 -30
  268. package/src/tools/web-search.ts +2 -1
  269. package/src/tools/write.d.ts +2 -0
  270. package/src/tools/write.ts +36 -12
  271. package/src/tools/yield.ts +2 -2
  272. package/src/tui.d.ts +20 -3
  273. package/src/tui.ts +140 -36
  274. package/src/utils.ts +5 -3
  275. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  276. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -8,6 +8,7 @@ import { code, md, span } from "../native/describe";
8
8
  import type { NativeNode } from "../native/node";
9
9
  import { getMarkdownTheme, highlightCode, type Theme } from "../theme/theme";
10
10
  import { formatDuration, formatExpandHint, formatMoreItems, formatStatusIcon, replaceTabs } from "./render-utils";
11
+ import { wrapPrefixedLine } from "./hanging-wrap";
11
12
  import {
12
13
  describeOutputBlock,
13
14
  type NativeOutputBlockSection,
@@ -167,6 +168,7 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
167
168
  }
168
169
  }
169
170
 
171
+ const numberedCodeLines: string[] = [];
170
172
  if (lineNumberWidth > 0 && visibleLineNumbers) {
171
173
  for (let i = 0; i < codeLines.length; i++) {
172
174
  const lineNum = visibleLineNumbers[i];
@@ -174,10 +176,14 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
174
176
  lineNum !== null && lineNum !== undefined
175
177
  ? String(lineNum).padStart(lineNumberWidth, " ")
176
178
  : " ".repeat(lineNumberWidth);
177
- codeLines[i] = theme.fg("dim", `${gutter} `) + codeLines[i];
179
+ const prefix = theme.fg("dim", `${gutter} `);
180
+ numberedCodeLines.push(
181
+ ...wrapPrefixedLine(prefix, " ".repeat(lineNumberWidth + 1), codeLines[i]!, outputBlockContentWidth(width)),
182
+ );
178
183
  }
179
184
  }
180
185
 
186
+ const displayCodeLines = lineNumberWidth > 0 ? numberedCodeLines : codeLines;
181
187
  if (hiddenCodeLines > 0) {
182
188
  const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
183
189
  const gutterPad = lineNumberWidth > 0 ? " ".repeat(lineNumberWidth + 1) : "";
@@ -185,16 +191,16 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
185
191
  // Earlier rows scrolled above the live tail window — mark them on top so
186
192
  // the newest streamed line stays pinned to the bottom of the box.
187
193
  const earlier = `… ${hiddenCodeLines} earlier line${hiddenCodeLines === 1 ? "" : "s"}${hint ? ` ${hint}` : ""}`;
188
- codeLines.unshift(theme.fg("dim", gutterPad + earlier));
194
+ displayCodeLines.unshift(theme.fg("dim", gutterPad + earlier));
189
195
  } else {
190
196
  const moreLine = `${formatMoreItems(hiddenCodeLines, "line")}${hint ? ` ${hint}` : ""}`;
191
- codeLines.push(theme.fg("dim", gutterPad + moreLine));
197
+ displayCodeLines.push(theme.fg("dim", gutterPad + moreLine));
192
198
  }
193
199
  }
194
200
 
195
201
  const outputLines = renderCellOutput(output, expanded, outputMaxLines, theme);
196
202
 
197
- const sections: Array<{ label?: string; lines: readonly string[] }> = [{ lines: codeLines }];
203
+ const sections: Array<{ label?: string; lines: readonly string[] }> = [{ lines: displayCodeLines }];
198
204
  if (outputLines.length > 0) {
199
205
  sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
200
206
  }
@@ -252,7 +258,14 @@ export function describeCodeCell(options: Omit<CodeCellOptions, "width">, theme:
252
258
  const sections: NativeOutputBlockSection[] = [
253
259
  {
254
260
  key: "code",
255
- body: [code(options.code ?? "", { lang: options.language, start, numbers: start !== undefined || undefined })],
261
+ body: [
262
+ code(options.code ?? "", {
263
+ lang: options.language,
264
+ start,
265
+ numbers: start !== undefined || undefined,
266
+ wrap: true,
267
+ }),
268
+ ],
256
269
  },
257
270
  ];
258
271
  const output = describeCellOutput(options.output, expanded, outputMaxLines);
@@ -0,0 +1,7 @@
1
+ import type { Theme } from "../theme/theme";
2
+ /** Wrap content under a distinct first-row prefix, preserving its column on continuation rows. */
3
+ export declare function wrapPrefixedLine(prefix: string, continuationPrefix: string, content: string, width: number): string[];
4
+ /** Preserve a tree connector's ancestor guides while hanging a long node beneath its label. */
5
+ export declare function wrapTreeLine(line: string, width: number, theme: Theme): string[] | undefined;
6
+ /** Hang a tree row beneath its connector, or a connector-less detail row beneath its guide rails. */
7
+ export declare function wrapGuidedLine(line: string, width: number, theme: Theme): string[];
@@ -0,0 +1,58 @@
1
+ import { visibleWidth, wrapTextWithAnsi } from "../utils";
2
+ import type { Theme } from "../theme/theme";
3
+
4
+ /** Wrap content under a distinct first-row prefix, preserving its column on continuation rows. */
5
+ export function wrapPrefixedLine(prefix: string, continuationPrefix: string, content: string, width: number): string[] {
6
+ const prefixWidth = visibleWidth(prefix);
7
+ const continuationWidth = visibleWidth(continuationPrefix);
8
+ if (width <= Math.max(prefixWidth, continuationWidth)) return wrapTextWithAnsi(prefix + content, width);
9
+ const rows = wrapTextWithAnsi(content, width - Math.max(prefixWidth, continuationWidth));
10
+ return rows.map((row, index) => `${index === 0 ? prefix : continuationPrefix}${row}`);
11
+ }
12
+
13
+ /** Preserve a tree connector's ancestor guides while hanging a long node beneath its label. */
14
+ export function wrapTreeLine(line: string, width: number, theme: Theme): string[] | undefined {
15
+ for (const connector of [theme.tree.branch, theme.tree.last]) {
16
+ const marker = theme.fg("dim", connector);
17
+ const index = line.indexOf(marker);
18
+ if (index < 0) continue;
19
+ const before = line.slice(0, index);
20
+ if (!/^ *$/.test(Bun.stripANSI(before).replaceAll(theme.tree.vertical, ""))) continue;
21
+ const end = index + marker.length + 1;
22
+ if (line[end - 1] !== " ") continue;
23
+ const connectorWidth = visibleWidth(connector);
24
+ const guide =
25
+ connector === theme.tree.last
26
+ ? " ".repeat(connectorWidth)
27
+ : `${theme.fg("dim", theme.tree.vertical)}${" ".repeat(Math.max(0, connectorWidth - visibleWidth(theme.tree.vertical)))}`;
28
+ return wrapPrefixedLine(line.slice(0, end), `${before}${guide} `, line.slice(end), width);
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ const SGR = /\x1b\[[0-9;]*m/y;
34
+
35
+ /** End of the leading run of spaces, `vertical` guides and SGR escapes that a tree detail row hangs beneath. */
36
+ function guideRunEnd(line: string, vertical: string): number {
37
+ let index = 0;
38
+ while (index < line.length) {
39
+ if (line[index] === " ") {
40
+ index++;
41
+ } else if (vertical && line.startsWith(vertical, index)) {
42
+ index += vertical.length;
43
+ } else {
44
+ SGR.lastIndex = index;
45
+ if (!SGR.test(line)) break;
46
+ index = SGR.lastIndex;
47
+ }
48
+ }
49
+ return index;
50
+ }
51
+
52
+ /** Hang a tree row beneath its connector, or a connector-less detail row beneath its guide rails. */
53
+ export function wrapGuidedLine(line: string, width: number, theme: Theme): string[] {
54
+ const tree = wrapTreeLine(line, width, theme);
55
+ if (tree) return tree;
56
+ const prefix = line.slice(0, guideRunEnd(line, theme.tree.vertical));
57
+ return wrapPrefixedLine(prefix, prefix, line.slice(prefix.length), width);
58
+ }
@@ -70,6 +70,8 @@ export declare function urlHyperlinkAlways(url: string, displayText: string): st
70
70
  * Non-HTTP(S) or unparsable URLs yield a plain span.
71
71
  */
72
72
  export declare function urlLinkSpan(target: string, displayText: string, s?: string): TspSpan;
73
+ /** Whether native link spans and nodes may carry an `href`: everything but an explicit `off` policy. */
74
+ export declare function isNativeLinkEnabled(): boolean;
73
75
  /**
74
76
  * A native link span to a filesystem path as a plain `file://` URI (the
75
77
  * terminal opens it through the system; the location stays in the visible
@@ -196,6 +196,11 @@ export function urlLinkSpan(target: string, displayText: string, s = "link"): Ts
196
196
  }
197
197
  }
198
198
 
199
+ /** Whether native link spans and nodes may carry an `href`: everything but an explicit `off` policy. */
200
+ export function isNativeLinkEnabled(): boolean {
201
+ return hyperlinkMode !== "off";
202
+ }
203
+
199
204
  /**
200
205
  * A native link span to a filesystem path as a plain `file://` URI (the
201
206
  * terminal opens it through the system; the location stays in the visible
@@ -105,14 +105,22 @@ export declare function cappedHeadLines(lines: readonly string[], max: number):
105
105
  };
106
106
  /** Get the first nonblank preview lines, trimmed and width-capped. */
107
107
  export declare function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis?: Ellipsis): string[];
108
+ /** Collapse a possibly multi-line string into one line; whitespace runs (tabs included) become one space. */
109
+ export declare function flattenLine(text: string): string;
108
110
  /**
109
111
  * Collapse a possibly multi-line string into a single line, then truncate it to
110
112
  * `maxWidth` display cells. {@link truncateToWidth} alone caps width but
111
113
  * newlines are zero-width, so multi-line content (markdown briefs, tool args,
112
114
  * provider errors) would otherwise spill a single status row across several
113
- * visual lines. Whitespace runs collapse to one space, so tabs are handled too.
115
+ * visual lines.
114
116
  */
115
117
  export declare function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string;
118
+ /**
119
+ * Clip a result row to `maxWidth` only while collapsed. Expanded rows stay whole
120
+ * so the frame wraps them: the expand key ({@link formatExpandHint}) must reveal
121
+ * text, not re-clip it.
122
+ */
123
+ export declare function clipCollapsed(text: string, maxWidth: number, expanded: boolean | undefined): string;
116
124
  /**
117
125
  * Extract domain from URL, stripping www. prefix.
118
126
  */
@@ -233,15 +233,29 @@ export function getPreviewLines(text: string, maxLines: number, maxLineLen: numb
233
233
  return lines.slice(0, maxLines).map(l => truncateToWidth(l.trim(), maxLineLen, ellipsis));
234
234
  }
235
235
 
236
+ /** Collapse a possibly multi-line string into one line; whitespace runs (tabs included) become one space. */
237
+ export function flattenLine(text: string): string {
238
+ return text.replace(/\s+/g, " ").trim();
239
+ }
240
+
236
241
  /**
237
242
  * Collapse a possibly multi-line string into a single line, then truncate it to
238
243
  * `maxWidth` display cells. {@link truncateToWidth} alone caps width but
239
244
  * newlines are zero-width, so multi-line content (markdown briefs, tool args,
240
245
  * provider errors) would otherwise spill a single status row across several
241
- * visual lines. Whitespace runs collapse to one space, so tabs are handled too.
246
+ * visual lines.
242
247
  */
243
248
  export function previewLine(text: string, maxWidth: number, ellipsis?: Ellipsis): string {
244
- return truncateToWidth(text.replace(/\s+/g, " ").trim(), maxWidth, ellipsis);
249
+ return truncateToWidth(flattenLine(text), maxWidth, ellipsis);
250
+ }
251
+
252
+ /**
253
+ * Clip a result row to `maxWidth` only while collapsed. Expanded rows stay whole
254
+ * so the frame wraps them: the expand key ({@link formatExpandHint}) must reveal
255
+ * text, not re-clip it.
256
+ */
257
+ export function clipCollapsed(text: string, maxWidth: number, expanded: boolean | undefined): string {
258
+ return expanded ? text : truncateToWidth(text, maxWidth);
245
259
  }
246
260
 
247
261
  // =============================================================================
@@ -32,7 +32,7 @@ export interface RenderCache {
32
32
  lines: string[];
33
33
  }
34
34
  /** Build indentation and continuing branches for ancestor levels. */
35
- export declare function buildTreePrefix(ancestors: boolean[], theme: Theme): string;
35
+ export declare function buildTreePrefix(ancestors: readonly boolean[], theme: Theme): string;
36
36
  /** Return the branch glyph for a final or continuing tree item. */
37
37
  export declare function getTreeBranch(isLast: boolean, theme: Theme): string;
38
38
  /** Return the continuation prefix for subsequent lines of a tree item. */
@@ -76,7 +76,7 @@ export interface RenderCache {
76
76
  }
77
77
 
78
78
  /** Build indentation and continuing branches for ancestor levels. */
79
- export function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
79
+ export function buildTreePrefix(ancestors: readonly boolean[], theme: Theme): string {
80
80
  return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
81
81
  }
82
82
 
@@ -25,7 +25,6 @@ export class WidthAwareText implements Component {
25
25
  #cachedContentWidth = -1;
26
26
  #cachedText: string | undefined;
27
27
  #ignoreTight = false;
28
- #nativeCols = -1;
29
28
  #native: NativeNode | undefined;
30
29
 
31
30
  constructor(format: (contentWidth: number) => string, paddingX = 1, paddingY = 1) {
@@ -71,10 +70,12 @@ export class WidthAwareText implements Component {
71
70
  * (the only width a describer knows) and again on `reformat()`.
72
71
  */
73
72
  describe(cx: DescribeContext): NativeNode {
74
- if (this.#cachedText === undefined || cx.cols !== this.#nativeCols) {
75
- this.#nativeCols = cx.cols;
76
- this.#cachedContentWidth = -1;
77
- this.#cachedText = this.#format(Math.max(1, cx.cols));
73
+ // Native describe emits no padding, so the formatter gets the full
74
+ // column budget — the same width a native Text.describe() would use.
75
+ const contentWidth = Math.max(1, cx.cols);
76
+ if (this.#cachedText === undefined || contentWidth !== this.#cachedContentWidth) {
77
+ this.#cachedContentWidth = contentWidth;
78
+ this.#cachedText = this.#format(contentWidth);
78
79
  this.#inner.setText(this.#cachedText);
79
80
  }
80
81
  this.#native ??= col([this.#inner]);
@@ -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;
@@ -27,18 +34,11 @@ const RESET = "\x1b[0m";
27
34
  const MIN_SCENE_WIDTH = 56;
28
35
  const MIN_SCENE_HEIGHT = 22;
29
36
 
37
+ /** Skip affordance; built at render time so it follows the live symbol preset. */
30
38
  function skipHint(): string {
31
- return `press ${formatKeyHint("enter")} to skip`;
39
+ return tuiText("setupSkipHint", "press {key} to skip").replace("{key}", formatKeyHint("enter"));
32
40
  }
33
41
 
34
- /** Density ramp for the rippling water, lightest → heaviest. */
35
- const WATER_RAMP = [
36
- { min: 0.62, char: "█" },
37
- { min: 0.5, char: "▓" },
38
- { min: 0.36, char: "▒" },
39
- { min: 0.24, char: "░" },
40
- ];
41
-
42
42
  function starAt(x: number, y: number, frame: number): string {
43
43
  const hash = (x * 73856093) ^ (y * 19349663) ^ (frame * 83492791);
44
44
  const bucket = Math.abs(hash) % 97;
@@ -85,39 +85,93 @@ function waterJitter(x: number, y: number): number {
85
85
  }
86
86
 
87
87
  /**
88
- * Rippling water amplitude in [0,1] at (x, y): three travelling sine waves
89
- * interfere, then a radial edge falloff and a downward fade concentrate the
90
- * ripples beneath the mark and dissolve them toward the edges/bottom. `t`
91
- * advances each tick, so the surface drifts.
88
+ * Time-invariant terms of the rippling water for one screen size: three
89
+ * travelling sine waves interfere, then a radial edge falloff and a downward
90
+ * fade concentrate the ripples beneath the mark and dissolve them toward the
91
+ * edges/bottom. Only the sine phases move per tick, so everything else is
92
+ * computed once per size.
92
93
  */
93
- function waterAmplitude(
94
- x: number,
95
- y: number,
96
- cx: number,
97
- waterTop: number,
98
- waterHeight: number,
99
- width: number,
100
- t: number,
101
- ): number {
102
- const dx = (x - cx) / 2;
103
- const dy = y - waterTop;
104
- const dist = Math.sqrt(dx * dx + dy * dy);
105
- const wave =
106
- 0.5 * Math.sin(dist * 0.55 - t) +
107
- 0.3 * Math.sin(x * 0.22 + y * 0.45 - t * 0.7) +
108
- 0.2 * Math.sin(Math.abs(dx) * 0.8 + dy * 0.5 - t * 1.4);
109
- const level = 0.5 + 0.5 * wave;
110
- const edge = Math.max(0, 1 - Math.abs(x - cx) / (width * 0.5));
111
- const fade = Math.max(0, 1 - (dy / Math.max(1, waterHeight)) * 0.55);
112
- return level * edge ** 0.7 * fade;
94
+ interface WaterField {
95
+ readonly width: number;
96
+ readonly height: number;
97
+ readonly waterTop: number;
98
+ /** Per cell (row-major from `waterTop`): `dist * 0.55`. */
99
+ readonly radial: Float64Array;
100
+ /** Per cell: `x * 0.22 + y * 0.45`. */
101
+ readonly diagonal: Float64Array;
102
+ /** Per cell: `|dx| * 0.8 + dy * 0.5`. */
103
+ readonly cross: Float64Array;
104
+ /** Per cell: `(waterJitter(x, y) - 0.5) * 0.06`. */
105
+ readonly jitter: Float64Array;
106
+ /** Per column: `edge ** 0.7`. */
107
+ readonly edge: Float64Array;
108
+ /** Per water row: downward fade. */
109
+ readonly fade: Float64Array;
110
+ }
111
+
112
+ let waterField: WaterField | undefined;
113
+
114
+ function getWaterField(width: number, height: number, cx: number, waterTop: number): WaterField {
115
+ const cached = waterField;
116
+ if (cached && cached.width === width && cached.height === height && cached.waterTop === waterTop) return cached;
117
+ const waterHeight = Math.max(1, height - waterTop);
118
+ const rows = Math.max(0, height - waterTop);
119
+ const radial = new Float64Array(rows * width);
120
+ const diagonal = new Float64Array(rows * width);
121
+ const cross = new Float64Array(rows * width);
122
+ const jitter = new Float64Array(rows * width);
123
+ const edge = new Float64Array(width);
124
+ const fade = new Float64Array(rows);
125
+ for (let x = 0; x < width; x++) edge[x] = Math.max(0, 1 - Math.abs(x - cx) / (width * 0.5)) ** 0.7;
126
+ for (let row = 0; row < rows; row++) {
127
+ const y = waterTop + row;
128
+ const dy = y - waterTop;
129
+ fade[row] = Math.max(0, 1 - (dy / waterHeight) * 0.55);
130
+ for (let x = 0; x < width; x++) {
131
+ const i = row * width + x;
132
+ const dx = (x - cx) / 2;
133
+ radial[i] = Math.sqrt(dx * dx + dy * dy) * 0.55;
134
+ diagonal[i] = x * 0.22 + y * 0.45;
135
+ cross[i] = Math.abs(dx) * 0.8 + dy * 0.5;
136
+ jitter[i] = (waterJitter(x, y) - 0.5) * 0.06;
137
+ }
138
+ }
139
+ waterField = { width, height, waterTop, radial, diagonal, cross, jitter, edge, fade };
140
+ return waterField;
141
+ }
142
+
143
+ /** Water glyph for an amplitude, lightest → heaviest density ramp; undefined below the floor. */
144
+ function waterChar(amp: number): string | undefined {
145
+ if (amp > 0.62) return "█";
146
+ if (amp > 0.5) return "▓";
147
+ if (amp > 0.36) return "▒";
148
+ if (amp > 0.24) return "░";
149
+ return undefined;
113
150
  }
114
151
 
152
+ /** Reused cell grid; resized when the screen changes. */
153
+ let splashCells: string[][] = [];
154
+
155
+ function resetCells(width: number, height: number): string[][] {
156
+ if (splashCells.length !== height || splashCells[0]?.length !== width) {
157
+ // oxlint-disable-next-line unicorn/no-new-array -- length preallocation
158
+ splashCells = Array.from({ length: height }, () => new Array<string>(width).fill(" "));
159
+ } else {
160
+ for (const row of splashCells) row.fill(" ");
161
+ }
162
+ return splashCells;
163
+ }
164
+
165
+ /** Per-frame gradient escapes, one per screen diagonal (the gradient only varies along it). */
166
+ let diagonalEscapes: (string | undefined)[] = [];
167
+
115
168
  /**
116
- * Animated setup splash, in the spirit of the omp landing page: the brand ζ
117
- * mark rendered with the live diagonal gradient + shine sweep, rising out of a
118
- * rippling, gradient-lit water surface, under a faint twinkling starfield. The
119
- * mark and water share one continuous gradient so the sweep reads across the
120
- * 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.
121
175
  */
122
176
  export function renderSetupSplash(width: number, height: number, elapsedMs: number): string[] {
123
177
  const w = Math.max(1, width);
@@ -132,23 +186,46 @@ export function renderSetupSplash(width: number, height: number, elapsedMs: numb
132
186
  const cx = Math.floor(w / 2);
133
187
  const surfaceTime = frame * 0.13;
134
188
 
135
- // [suppressed] length preallocation
136
- const cells: string[][] = Array.from({ length: h }, () => new Array<string>(w).fill(" "));
189
+ const cells = resetCells(w, h);
137
190
  const put = (x: number, y: number, glyph: string): void => {
138
191
  if (y >= 0 && y < h && x >= 0 && x < w) cells[y][x] = glyph;
139
192
  };
193
+ // Every cell on a diagonal shares one gradient position, so resolve each
194
+ // escape once per frame instead of once per cell.
195
+ const diagonals = w + h - 1;
196
+ if (diagonalEscapes.length !== diagonals) diagonalEscapes = Array.from({ length: diagonals });
197
+ else diagonalEscapes.fill(undefined);
198
+ const escapes = diagonalEscapes;
199
+ const gradient = (x: number, y: number): string => {
200
+ const d = x + (h - 1 - y);
201
+ let escape = escapes[d];
202
+ if (escape === undefined) {
203
+ escape = gradientEscape(screenGradientT(x, y, w, h, phase), shine);
204
+ escapes[d] = escape;
205
+ }
206
+ return escape;
207
+ };
140
208
 
141
209
  const hx = Math.floor((w - LOGO_WIDTH) / 2);
142
210
  const hy = Math.max(2, Math.floor(h * 0.16));
143
211
  const waterTop = hy + LOGO_HEIGHT;
144
- const waterHeight = Math.max(1, h - waterTop);
145
212
 
146
213
  // 1. rippling water surface (shares the screen-wide gradient with the mark)
214
+ const field = getWaterField(w, h, cx, waterTop);
215
+ const tDiagonal = surfaceTime * 0.7;
216
+ const tCross = surfaceTime * 1.4;
147
217
  for (let y = waterTop; y < h; y++) {
218
+ const row = y - waterTop;
219
+ const fade = field.fade[row];
148
220
  for (let x = 0; x < w; x++) {
149
- const amp = waterAmplitude(x, y, cx, waterTop, waterHeight, w, surfaceTime) + (waterJitter(x, y) - 0.5) * 0.06;
150
- const cell = WATER_RAMP.find(step => amp > step.min);
151
- if (cell) put(x, y, gradientEscape(screenGradientT(x, y, w, h, phase), shine) + cell.char + RESET);
221
+ const i = row * w + x;
222
+ const wave =
223
+ 0.5 * Math.sin(field.radial[i] - surfaceTime) +
224
+ 0.3 * Math.sin(field.diagonal[i] - tDiagonal) +
225
+ 0.2 * Math.sin(field.cross[i] - tCross);
226
+ const level = 0.5 + 0.5 * wave;
227
+ const char = waterChar(level * field.edge[x] * fade + field.jitter[i]);
228
+ if (char) cells[y][x] = gradient(x, y) + char + RESET;
152
229
  }
153
230
  }
154
231
  // 2. twinkling starfield in the sky above the water
@@ -162,18 +239,15 @@ export function renderSetupSplash(width: number, height: number, elapsedMs: numb
162
239
  LARGE_LOGO.forEach((line, row) => {
163
240
  let col = 0;
164
241
  for (const ch of line) {
165
- if (ch !== " ") {
166
- put(
167
- hx + col,
168
- hy + row,
169
- gradientEscape(screenGradientT(hx + col, hy + row, w, h, phase), shine) + ch + RESET,
170
- );
171
- }
242
+ const x = hx + col;
243
+ const y = hy + row;
244
+ if (ch !== " " && y >= 0 && y < h && x >= 0 && x < w) cells[y][x] = gradient(x, y) + ch + RESET;
172
245
  col++;
173
246
  }
174
247
  });
175
- // 4. skip hint on a cleared strip at the bottom so it stays legible over the water
176
- const hint = tuiText("setupSkipHint", "press enter to skip");
248
+ // 4. skip hint on a cleared strip at the bottom so it stays legible over the water.
249
+ // `formatKeyHint` yields plain glyphs (no ANSI), so per-character dim styling is safe.
250
+ const hint = skipHint();
177
251
  const hintWidth = visibleWidth(hint);
178
252
  const hintStart = Math.floor((w - hintWidth) / 2);
179
253
  const hintRow = h - 1;
@@ -199,7 +273,8 @@ export function describeSetupSplash(): NativeNode {
199
273
  [
200
274
  node("spacer", { grow: 1 }),
201
275
  logoNode(LARGE_LOGO, true),
202
- text([span("Z e t a", "strong")], { wrap: "none" }),
276
+ text([span("Zeta", "strong")], { wrap: "none" }),
277
+ text([span("Linxira-Zeta", "dim")], { wrap: "none" }),
203
278
  node("spacer", { grow: 1 }),
204
279
  text([span(hint, "dim")], { wrap: "none" }),
205
280
  ],
@@ -211,7 +286,12 @@ export function describeSetupSplash(): NativeNode {
211
286
  /** Centered fallback for windows too small to hold the full scene. */
212
287
  function renderCompactSplash(width: number, height: number, phase: number, shine: ShineConfig): string[] {
213
288
  const art = height >= 14 ? LARGE_LOGO : ZETA_LOGO;
214
- 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
+ ];
215
295
  const start = Math.max(0, Math.floor((height - content.length) / 2));
216
296
  const lines: string[] = [];
217
297
  for (let y = 0; y < height; y++) {
@@ -219,9 +299,6 @@ function renderCompactSplash(width: number, height: number, phase: number, shine
219
299
  lines.push(width > 0 ? padToWidth(item !== undefined ? centerLine(item, width) : "", width) : "");
220
300
  }
221
301
  if (height > 2)
222
- lines[height - 2] =
223
- width > 0
224
- ? padToWidth(centerLine(theme.fg("dim", tuiText("setupSkipHint", "press enter to skip")), width), width)
225
- : "";
302
+ lines[height - 2] = width > 0 ? padToWidth(centerLine(theme.fg("dim", skipHint()), width), width) : "";
226
303
  return lines;
227
304
  }
@@ -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,