@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
package/src/tui.ts CHANGED
@@ -31,10 +31,12 @@ import type { DescribeContext, NativeNode, NativeScreen, NativeSurfaceProvider,
31
31
  import { STDOUT_BACKLOG_CLEAR_BYTES, setAltScreenActive, type Terminal } from "./terminal";
32
32
  import { classifyTerminalMultiplexerModule, terminalMultiplexerSessions } from "./terminal-multiplexer";
33
33
  import {
34
+ directPlacementLift,
34
35
  encodeKittyDeleteAllImages,
35
36
  encodeKittyDeleteImage,
36
37
  encodeKittyDeletePlacement,
37
38
  encodeKittyPlacementLine,
39
+ getTerminalBackgroundColor,
38
40
  ImageProtocol,
39
41
  isImageProtocolForced,
40
42
  isInsideTerminalMultiplexer,
@@ -45,7 +47,9 @@ import {
45
47
  synchronizedOutputUserOverride,
46
48
  TERMINAL,
47
49
  } from "./terminal-capabilities";
50
+ import { onTerminalBackgroundFallbackChange } from "./theme/theme";
48
51
  import { compositeLineAt } from "./render/composite";
52
+ import { tmuxMultiplexer } from "./multiplexers/tmux";
49
53
  import {
50
54
  Ellipsis,
51
55
  getWidthConfigEpoch,
@@ -56,6 +60,7 @@ import {
56
60
  truncateToWidth,
57
61
  visibleWidth,
58
62
  } from "./utils";
63
+ import { tmuxClientRendersSixel } from "./tmux";
59
64
 
60
65
  /** Full-attribute reset terminating each rendered content row. */
61
66
  export const SEGMENT_RESET = "\x1b[0m";
@@ -254,10 +259,6 @@ export interface Component {
254
259
  */
255
260
  render(width: number): readonly string[];
256
261
 
257
- /** Inline decision panels must keep displaced settled transcript rows in
258
- * native scrollback even when mounted inside a transient editor container. */
259
- readonly retireDisplacedTranscript?: boolean;
260
-
261
262
  /**
262
263
  * Describe the component semantically for a Tern Surface Protocol
263
264
  * terminal (see `native/node.ts`). Called instead of `render()` when the
@@ -823,6 +824,10 @@ export class TUI extends Container {
823
824
  // rows the paint prepended for a short viewport. Negative while prepended
824
825
  // blanks outweigh replaced rows; zero on ordinary frames.
825
826
  #providerViewportPadTop = 0;
827
+ // Whether native history holds rows the provider committed since the last
828
+ // destructive reset. Only paints write history and only a destructive reset
829
+ // erases it; an `append` resize replay keeps the rows it replays beneath.
830
+ #providerHistoryWritten = false;
826
831
  // Viewport-relative row of the hardware cursor after the last normal paint
827
832
  // (0 = parked at the viewport top). A resize reflows the normal buffer
828
833
  // before the app hears about it; terminals keep the cursor attached to its
@@ -928,6 +933,8 @@ export class TUI extends Container {
928
933
  #debugNextWindowTop = 0;
929
934
  #inputListeners = new Set<InputListener>();
930
935
  #startListeners = new Set<StartListener>();
936
+ /** True from the end of start() until stop(): the terminal modes start listeners enable are live. */
937
+ #started = false;
931
938
  #paintListeners = new Set<PaintListener>();
932
939
 
933
940
  /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
@@ -1020,6 +1027,7 @@ export class TUI extends Container {
1020
1027
  #forceViewportRepaintOnNextRender = false;
1021
1028
  #hasEverRendered = false;
1022
1029
  #stopped = false;
1030
+ #appearanceReportUnsubscribe: (() => void) | undefined;
1023
1031
  #cancelPostmortemRestore?: () => void;
1024
1032
  /** True between a `deferInput` start() and enableInput(). */
1025
1033
  #inputDeferred = false;
@@ -1108,6 +1116,7 @@ export class TUI extends Container {
1108
1116
  overlayStack: {
1109
1117
  component: Component;
1110
1118
  options?: OverlayOptions;
1119
+ /** Focus restored after closing; tracks live focus changes beneath an input-less key holder. */
1111
1120
  preFocus: Component | null;
1112
1121
  hidden: boolean;
1113
1122
  /**
@@ -1315,6 +1324,9 @@ export class TUI extends Container {
1315
1324
  setFocus(component: Component | null): void {
1316
1325
  const holder = this.#getKeyHolderOverlay();
1317
1326
  if (holder && !isOverlayFocusTarget(holder.component, component)) {
1327
+ if (!holder.component.handleInput && this.#focusedComponent === holder.component) {
1328
+ holder.preFocus = component;
1329
+ }
1318
1330
  const currentFocus = this.#focusedComponent;
1319
1331
  component = isOverlayFocusTarget(holder.component, currentFocus) ? currentFocus : holder.component;
1320
1332
  }
@@ -1461,6 +1473,27 @@ export class TUI extends Container {
1461
1473
  return { top: this.#providerViewportTop - this.#providerViewportPadTop, length: this.#providerWindow.length };
1462
1474
  }
1463
1475
 
1476
+ /**
1477
+ * Rows of native history the last paint left on screen above a mutable
1478
+ * viewport that reached the bottom row, or `undefined` when that paint left
1479
+ * rows free below it (or none happened, or a destructive reset will repaint
1480
+ * from row zero). Rows above a bottom-reaching viewport may already sit in
1481
+ * native scrollback, which a later shrink cannot pull back down.
1482
+ */
1483
+ getFilledHistoryRows(): number | undefined {
1484
+ if (!this.#hasEverRendered || this.#clearScrollbackOnNextRender) return undefined;
1485
+ const height = this.terminal.rows;
1486
+ if (this.#providerWindow.length === 0 || this.#providerViewportTop + this.#providerWindow.length < height) {
1487
+ return undefined;
1488
+ }
1489
+ return Math.min(this.#providerViewportTop, height);
1490
+ }
1491
+
1492
+ /** Whether native history holds rows the frame provider committed since the last destructive reset. */
1493
+ hasProviderHistory(): boolean {
1494
+ return this.#providerHistoryWritten && !this.#clearScrollbackOnNextRender;
1495
+ }
1496
+
1464
1497
  /**
1465
1498
  * Probe for opt-in normal-buffer click capture. The provider is read every
1466
1499
  * frame; while it returns true (and no fullscreen overlay owns the
@@ -1544,6 +1577,34 @@ export class TUI extends Container {
1544
1577
 
1545
1578
  start(options?: TUIStartOptions): void {
1546
1579
  this.#stopped = false;
1580
+ this.#appearanceReportUnsubscribe?.();
1581
+ let backgroundRgb: number | undefined;
1582
+ let subscribed = true;
1583
+ const repaintForBackgroundChange = () => {
1584
+ if (!subscribed || this.#stopped) return;
1585
+ const nextBackgroundRgb = getTerminalBackgroundColor();
1586
+ // Track RGB before the SIXEL guard: the startup OSC 11 reply usually
1587
+ // arrives before DA1 enables SIXEL, so revisiting the same color once
1588
+ // SIXEL is live must not replay identical rows.
1589
+ if (nextBackgroundRgb === backgroundRgb) return;
1590
+ backgroundRgb = nextBackgroundRgb;
1591
+ if (TERMINAL.imageProtocol !== ImageProtocol.Sixel) return;
1592
+ // Track RGB even during optimistic native ownership: fallback paints
1593
+ // with the latest color, so a later return to the startup RGB must repaint.
1594
+ // Never write rows while the native surface still owns rendering.
1595
+ if (this.#nativeLive) return;
1596
+ this.invalidate();
1597
+ // Match theme changes: replay owned history on direct terminals, but
1598
+ // leave multiplexer scrollback intact and repaint only the viewport.
1599
+ this.requestRender(true, { clearScrollback: !isInsideTerminalMultiplexer() });
1600
+ };
1601
+ const unsubscribe = this.terminal.onAppearanceReport?.(repaintForBackgroundChange);
1602
+ const unsubscribeFallback = onTerminalBackgroundFallbackChange(repaintForBackgroundChange);
1603
+ this.#appearanceReportUnsubscribe = () => {
1604
+ subscribed = false;
1605
+ unsubscribe?.();
1606
+ unsubscribeFallback();
1607
+ };
1547
1608
  this.#debugPaint = undefined;
1548
1609
  this.#debugServer?.stop();
1549
1610
  this.#debugServer = undefined;
@@ -1602,6 +1663,9 @@ export class TUI extends Container {
1602
1663
  this.terminal.onSixelSupport?.(supported => {
1603
1664
  if (!supported || this.#stopped || this.#nativeLive || this.terminal.tspExpected) return;
1604
1665
  if (isImageProtocolForced()) return;
1666
+ // tmux answers DA1 for its own emulator; only its record of the
1667
+ // attached client says whether SIXEL reaches the outer terminal.
1668
+ if (tmuxMultiplexer.isInside() && !tmuxClientRendersSixel()) return;
1605
1669
  // Keep consuming an already-requested XTSMGRAPHICS reply until it
1606
1670
  // arrives or times out, even though DA1 has established support.
1607
1671
  this.#enableSixelProtocol();
@@ -1668,16 +1732,14 @@ export class TUI extends Container {
1668
1732
  () => this.stop(),
1669
1733
  { deferInput: this.#inputDeferred, isLoopStalled: () => this.#watchdog.isStalled() },
1670
1734
  );
1735
+ // ProcessTerminal.start() resets the previous owner's RGB. Seed only
1736
+ // after that reset so an identical OSC 11 reply still repaints fallback rows.
1737
+ backgroundRgb = TERMINAL.imageProtocol === ImageProtocol.Sixel ? getTerminalBackgroundColor() : undefined;
1671
1738
  if (this.#stopped) return;
1672
1739
  this.#cancelPostmortemRestore?.();
1673
1740
  this.#cancelPostmortemRestore = postmortem.register("tui-restore", () => this.stop());
1674
- for (const listener of this.#startListeners) {
1675
- try {
1676
- listener();
1677
- } catch {
1678
- // Startup listeners are feature hooks; one broken hook must not prevent rendering.
1679
- }
1680
- }
1741
+ for (const listener of this.#startListeners) this.#runStartListener(listener);
1742
+ this.#started = !this.#stopped;
1681
1743
  this.terminal.hideCursor();
1682
1744
  this.#recordHardwareCursorHidden();
1683
1745
  if (!this.#inputDeferred) {
@@ -1819,7 +1881,7 @@ export class TUI extends Container {
1819
1881
  this.#resizeSettleTimer?.cancel();
1820
1882
  this.#resizeSettleTimer = undefined;
1821
1883
  if (this.#altActive || this.#resizeAltActive) {
1822
- this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
1884
+ this.terminal.write(`${this.#takePendingAltEnter()}${this.#altScreenExit()}`);
1823
1885
  setAltScreenActive(false);
1824
1886
  this.#altActive = false;
1825
1887
  this.#resizeAltActive = false;
@@ -2133,7 +2195,7 @@ export class TUI extends Container {
2133
2195
  this.#suppressResizeUntil = this.#renderScheduler.now() + 100;
2134
2196
  this.#altPreviousLines = [];
2135
2197
  this.#altPreparedRows = [];
2136
- const exitSequence = `${this.#keyboardEnhancementExit()}\x1b[?1049l`;
2198
+ const exitSequence = this.#altScreenExit();
2137
2199
  // Only the provider plan frame emits a pending exit, and an exit already
2138
2200
  // pending (a fused overlay close) owns that slot.
2139
2201
  if (this.#frameProvider !== undefined && this.#pendingAltExit === "") {
@@ -2445,13 +2507,29 @@ export class TUI extends Container {
2445
2507
  if (this.#heldInput !== undefined && this.#heldFocus === previous) this.#heldFocus = next;
2446
2508
  }
2447
2509
 
2510
+ /**
2511
+ * Register a hook that re-enables terminal modes on every start(). A hook
2512
+ * added while the TUI is already running (a prepaint composer starts it
2513
+ * before the interactive mode wires its controllers) missed that start, so
2514
+ * it runs immediately instead of waiting for a stop/start cycle that may
2515
+ * never come.
2516
+ */
2448
2517
  addStartListener(listener: StartListener): () => void {
2449
2518
  this.#startListeners.add(listener);
2519
+ if (this.#started) this.#runStartListener(listener);
2450
2520
  return () => {
2451
2521
  this.#startListeners.delete(listener);
2452
2522
  };
2453
2523
  }
2454
2524
 
2525
+ #runStartListener(listener: StartListener): void {
2526
+ try {
2527
+ listener();
2528
+ } catch {
2529
+ // Startup listeners are feature hooks; one broken hook must not prevent rendering.
2530
+ }
2531
+ }
2532
+
2455
2533
  addInputListener(listener: InputListener): () => void {
2456
2534
  this.#inputListeners.add(listener);
2457
2535
  return () => {
@@ -2635,6 +2713,8 @@ export class TUI extends Container {
2635
2713
  }
2636
2714
 
2637
2715
  stop(): void {
2716
+ this.#appearanceReportUnsubscribe?.();
2717
+ this.#appearanceReportUnsubscribe = undefined;
2638
2718
  this.#cancelPostmortemRestore?.();
2639
2719
  this.#cancelPostmortemRestore = undefined;
2640
2720
  this.#debugServer?.stop();
@@ -2662,7 +2742,7 @@ export class TUI extends Container {
2662
2742
  this.#cancelResizeProbe();
2663
2743
  if (this.#resizeAltActive) {
2664
2744
  this.#resizeAltActive = false;
2665
- this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
2745
+ this.terminal.write(`${this.#takePendingAltEnter()}${this.#altScreenExit()}`);
2666
2746
  setAltScreenActive(false);
2667
2747
  }
2668
2748
  if (this.#altActive || this.#pendingAltExit) {
@@ -2674,7 +2754,7 @@ export class TUI extends Container {
2674
2754
  const mouseExit = this.#mouseTracking !== "off" ? MOUSE_TRACKING_OFF : "";
2675
2755
  const exitSequence = this.#pendingAltExit
2676
2756
  ? `${this.#takePendingAltEnter()}${this.#pendingAltExit}${mouseExit}`
2677
- : `${mouseExit}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
2757
+ : `${mouseExit}${this.#altScreenExit()}`;
2678
2758
  this.terminal.write(exitSequence);
2679
2759
  setAltScreenActive(false);
2680
2760
  this.#altActive = false;
@@ -2704,6 +2784,7 @@ export class TUI extends Container {
2704
2784
  // (and live-session ghosts are already bounded by the inline-image budget).
2705
2785
  this.#clearSixelProbeState();
2706
2786
  this.#stopped = true;
2787
+ this.#started = false;
2707
2788
  this.#watchdog.stop();
2708
2789
  if (this.#renderTimer) {
2709
2790
  this.#renderTimer.cancel();
@@ -2771,6 +2852,13 @@ export class TUI extends Container {
2771
2852
  this.#requestOrdinaryRender();
2772
2853
  }
2773
2854
 
2855
+ /** Run `callback` after `delayMs` on the render scheduler's clock, unless the TUI has stopped. */
2856
+ scheduleAfter(delayMs: number, callback: () => void): RenderTimer {
2857
+ return this.#renderScheduler.scheduleRender(() => {
2858
+ if (!this.#stopped) callback();
2859
+ }, delayMs);
2860
+ }
2861
+
2774
2862
  /**
2775
2863
  * Paint a forced frame synchronously when startup must hand off an already
2776
2864
  * visible component tree before further async initialization. Same as
@@ -2986,9 +3074,9 @@ export class TUI extends Container {
2986
3074
  this.#inputRenderGraceUntilMs = this.#renderScheduler.now() + TUI.#INPUT_RENDER_GRACE_MS;
2987
3075
  }
2988
3076
 
2989
- // A focused input owner can reserve its gesture before TUI-wide listeners
3077
+ // The effective input owner can reserve its gesture before TUI-wide listeners
2990
3078
  // and debug shortcuts see the key, then handle the raw event exactly once.
2991
- const focusedForCapture = this.#focusedComponent;
3079
+ const focusedForCapture = this.#inputTarget();
2992
3080
  if (
2993
3081
  focusedForCapture?.handleInput &&
2994
3082
  (!isKeyRelease(data) || focusedForCapture.wantsKeyRelease) &&
@@ -3027,22 +3115,28 @@ export class TUI extends Container {
3027
3115
  return;
3028
3116
  }
3029
3117
 
3030
- // Pass input to focused component (including Ctrl+C).
3031
- // The focused component can decide how to handle Ctrl+C.
3032
- // Opted-in components only dirty their focused subtree. Unregistered
3033
- // components retain the legacy full compose because their callbacks may
3034
- // mutate siblings; focus changes also require the new surface to paint.
3035
- const focused = this.#focusedComponent;
3036
- if (focused?.handleInput) {
3118
+ const target = this.#inputTarget();
3119
+ if (target?.handleInput) {
3037
3120
  // Filter out key release events unless component opts in
3038
- if (isKeyRelease(data) && !focused.wantsKeyRelease) {
3121
+ if (isKeyRelease(data) && !target.wantsKeyRelease) {
3039
3122
  return;
3040
3123
  }
3041
- focused.handleInput(data);
3124
+ target.handleInput(data);
3042
3125
  this.requestRender();
3043
3126
  }
3044
3127
  }
3045
3128
 
3129
+ /**
3130
+ * Component that receives keyboard input. An input-less key-holding overlay
3131
+ * leaves input with the focus it displaced; other focus owners keep their input.
3132
+ */
3133
+ #inputTarget(): Component | null {
3134
+ const focused = this.#focusedComponent;
3135
+ if (focused?.handleInput) return focused;
3136
+ const holder = this.#getKeyHolderOverlay();
3137
+ return holder?.component === focused ? holder.preFocus : null;
3138
+ }
3139
+
3046
3140
  /**
3047
3141
  * Queue a keystroke typed before the app installed its key handlers; input
3048
3142
  * listeners see it once, on replay. The cell-size reply is a terminal report,
@@ -3278,7 +3372,12 @@ export class TUI extends Container {
3278
3372
  return output;
3279
3373
  }
3280
3374
 
3281
- /** Clip a direct placement and record its live owner; history placements have no mutable owner. */
3375
+ /**
3376
+ * Clip a Kitty direct placement and record its live owner; history
3377
+ * placements have no mutable owner. Other direct placements (SIXEL,
3378
+ * iTerm2) have no delete command and composite over whatever pixels they
3379
+ * land on, so the rows they paint above `screenRow` are erased first.
3380
+ */
3282
3381
  #imageLineSequence(
3283
3382
  line: string,
3284
3383
  screenRow: number,
@@ -3287,7 +3386,10 @@ export class TUI extends Container {
3287
3386
  ): string {
3288
3387
  if (screenRow < 0) return line;
3289
3388
  const parsed = parseKittyDirectPlacementLine(line);
3290
- if (!parsed) return line;
3389
+ if (!parsed) {
3390
+ const lift = Math.min(directPlacementLift(line), screenRow);
3391
+ return lift > 0 ? `\x1b[${lift}A${`${ERASE_LINE}\x1b[B`.repeat(lift)}${line}` : line;
3392
+ }
3291
3393
  const previous = placements?.get(placementRow);
3292
3394
  const placementId = previous?.placementId ?? this.#nextImagePlacementId++;
3293
3395
  const erase = previous ? encodeKittyDeletePlacement(previous.imageId, previous.placementId) : "";
@@ -3569,6 +3671,7 @@ export class TUI extends Container {
3569
3671
  this.#providerViewportTop = 0;
3570
3672
  this.#providerWindow = [];
3571
3673
  this.#providerPreparedRows = [];
3674
+ this.#providerHistoryWritten = false;
3572
3675
  }
3573
3676
  // The viewport stays anchored directly below whatever history remains on
3574
3677
  // screen. Appending K history rows moves the anchor down by K; the write
@@ -3799,6 +3902,7 @@ export class TUI extends Container {
3799
3902
  this.#resizeBurstWidthChanged = false;
3800
3903
  this.#resizeBurstResized = false;
3801
3904
  this.#previousFrameLength = mutablePreparedLines.length;
3905
+ if (preparedHistory.lines.length > 0 || replayViewportRows > 0) this.#providerHistoryWritten = true;
3802
3906
  this.#clearScrollbackOnNextRender = false;
3803
3907
  this.#forceViewportRepaintOnNextRender = false;
3804
3908
  this.#hasEverRendered = true;
@@ -3899,8 +4003,7 @@ export class TUI extends Container {
3899
4003
  // restore it in the fused exit or later frames see matching states
3900
4004
  // and inline click/hover stays dead until the setting toggles.
3901
4005
  const mouseEnter = wantMouse !== "off" && this.#mouseTracking === "off" ? MOUSE_TRACKING_ON : "";
3902
- const enhancementExit = this.#keyboardEnhancementExit();
3903
- const exitSequence = `${mouseExit}${mouseEnter}${enhancementExit}\x1b[?1049l`;
4006
+ const exitSequence = `${mouseExit}${mouseEnter}${this.#altScreenExit()}`;
3904
4007
  // Session replacement finishes while its fullscreen selector still
3905
4008
  // covers the old normal buffer. Fuse the restore into the destructive
3906
4009
  // repaint so no stale frame can become visible between writes.
@@ -4443,14 +4546,15 @@ export class TUI extends Container {
4443
4546
  }
4444
4547
 
4445
4548
  /**
4446
- * Resolve the active keyboard-enhancement exit sequence. Falls back to popping
4447
- * kitty whenever a custom Terminal exposes its push sequence but predates the
4448
- * `keyboardEnhancementExitSequence` property.
4549
+ * Leave the alternate screen: pop its keyboard-enhancement frame first, then
4550
+ * re-assert the main-screen mode for terminals that lose it on `?1049l`.
4551
+ * Falls back to popping kitty whenever a custom Terminal exposes its push
4552
+ * sequence but predates the `keyboardEnhancementExitSequence` property.
4449
4553
  */
4450
- #keyboardEnhancementExit(): string {
4554
+ #altScreenExit(): string {
4451
4555
  const exit = this.terminal.keyboardEnhancementExitSequence;
4452
- if (exit !== undefined) return exit ?? "";
4453
- return this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
4556
+ const keyboardExit = exit !== undefined ? (exit ?? "") : this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
4557
+ return `${keyboardExit}\x1b[?1049l${this.terminal.keyboardEnhancementRestoreSequence ?? ""}`;
4454
4558
  }
4455
4559
 
4456
4560
  /**
package/src/utils.ts CHANGED
@@ -180,9 +180,11 @@ export function truncateToWidth(
180
180
  pad?: boolean | null,
181
181
  ): string {
182
182
  maxWidth = Math.max(0, maxWidth | 0);
183
- // Fast path: every UTF-16 unit is at most 3 cells wide, so a string whose
184
- // `length * 3` already fits within `safeWidth` cannot need truncation.
185
- if (!pad && text.length * 3 <= maxWidth) {
183
+ // Fast path: every UTF-16 unit is at most 3 cells wide, so a plain string
184
+ // whose `length * 3` already fits within `maxWidth` cannot need truncation.
185
+ // OSC 66 spans are excluded because their display width is metadata-driven,
186
+ // not proportional to their UTF-16 length.
187
+ if (!pad && !text.includes("\x1b]66") && text.length * 3 <= maxWidth) {
186
188
  return text;
187
189
  }
188
190
  return nativeTruncateToWidth(
@@ -1,5 +0,0 @@
1
- /** Format tool schemas as TypeScript declarations for code-mode discovery. */
2
- export declare function generateCodeModeDeclarations(tools: ReadonlyArray<{
3
- name: string;
4
- parameters: unknown;
5
- }>): string;
@@ -1,61 +0,0 @@
1
- /**
2
- * Generate the TypeScript method signatures advertising eval-bridged tools
3
- * under Codex Code Mode (spliced into the `eval-code-mode.md` prompt),
4
- * mirroring codex-rs augment_tool_spec_for_code_mode.
5
- */
6
- import { arkToWireSchema, isArkSchema } from "@linxiraos/pi-ai/utils/schema/wire";
7
-
8
- interface JsonSchema {
9
- type?: string;
10
- properties?: Record<string, JsonSchema>;
11
- required?: string[];
12
- items?: JsonSchema;
13
- enum?: unknown[];
14
- }
15
-
16
- const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
17
-
18
- function tsType(schema: JsonSchema | undefined, depth: number): string {
19
- if (!schema || depth > 2) return "unknown";
20
- if (Array.isArray(schema.enum) && schema.enum.every(value => typeof value === "string")) {
21
- return schema.enum.map(value => JSON.stringify(value)).join(" | ");
22
- }
23
- switch (schema.type) {
24
- case "string":
25
- return "string";
26
- case "number":
27
- case "integer":
28
- return "number";
29
- case "boolean":
30
- return "boolean";
31
- case "array": {
32
- const item = tsType(schema.items, depth + 1);
33
- // `"a" | "b"[]` parses as `"a" | ("b"[])`, so a union item needs parens.
34
- return /[|&]/.test(item) ? `(${item})[]` : `${item}[]`;
35
- }
36
- case "object": {
37
- if (!schema.properties) return "Record<string, unknown>";
38
- const required = new Set(schema.required ?? []);
39
- const entries = Object.entries(schema.properties).map(([key, value]) => {
40
- const printedKey = TS_IDENTIFIER.test(key) ? key : JSON.stringify(key);
41
- return `${printedKey}${required.has(key) ? "" : "?"}: ${tsType(value, depth + 1)}`;
42
- });
43
- return `{ ${entries.join("; ")} }`;
44
- }
45
- default:
46
- return "unknown";
47
- }
48
- }
49
-
50
- /** Format tool schemas as TypeScript declarations for code-mode discovery. */
51
- export function generateCodeModeDeclarations(tools: ReadonlyArray<{ name: string; parameters: unknown }>): string {
52
- const lines = tools.map(tool => {
53
- const printedName = TS_IDENTIFIER.test(tool.name) ? tool.name : JSON.stringify(tool.name);
54
- const wire = isArkSchema(tool.parameters)
55
- ? (arkToWireSchema(tool.parameters) as JsonSchema)
56
- : (tool.parameters as JsonSchema | undefined);
57
- const args = wire?.type === "object" && wire.properties ? tsType(wire, 0) : "unknown";
58
- return ` ${printedName}(args: ${args}): Promise<unknown>;`;
59
- });
60
- return lines.join("\n");
61
- }