@linxiraos/pi-tui 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +245 -224
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/status-notice.d.ts +2 -0
  21. package/dist/types/chrome/transcript-container.d.ts +2 -0
  22. package/dist/types/components/image.d.ts +2 -0
  23. package/dist/types/components/markdown.d.ts +49 -3
  24. package/dist/types/components/text.d.ts +2 -0
  25. package/dist/types/debug-server.d.ts +2 -0
  26. package/dist/types/keybinding-matchers.d.ts +6 -0
  27. package/dist/types/multiplexers/tmux.d.ts +1 -0
  28. package/dist/types/multiplexers/types.d.ts +6 -0
  29. package/dist/types/native/backend.d.ts +5 -4
  30. package/dist/types/native/blobs.d.ts +24 -12
  31. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  32. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  33. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  34. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  35. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  36. package/dist/types/overlays/hook-editor.d.ts +7 -2
  37. package/dist/types/overlays/hook-input.d.ts +12 -4
  38. package/dist/types/overlays/job-tree.d.ts +50 -0
  39. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  40. package/dist/types/overlays/login-dialog.d.ts +8 -3
  41. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  42. package/dist/types/overlays/model-browser.d.ts +6 -1
  43. package/dist/types/overlays/model-picker.d.ts +2 -0
  44. package/dist/types/overlays/pause-screen.d.ts +5 -0
  45. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  46. package/dist/types/overlays/report-panel.d.ts +5 -1
  47. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  48. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  49. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  50. package/dist/types/overlays/session-selector.d.ts +3 -1
  51. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  52. package/dist/types/prompt/composer.d.ts +23 -20
  53. package/dist/types/prompt/custom-editor.d.ts +20 -10
  54. package/dist/types/prompt/image-references.d.ts +22 -0
  55. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  56. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  57. package/dist/types/prompt/prose-gate.d.ts +3 -0
  58. package/dist/types/prompt/queued-messages.d.ts +1 -1
  59. package/dist/types/prompt/typing-word.d.ts +25 -0
  60. package/dist/types/prompt/welcome.d.ts +26 -6
  61. package/dist/types/render/hanging-wrap.d.ts +7 -0
  62. package/dist/types/render/hyperlink.d.ts +2 -0
  63. package/dist/types/render/render-utils.d.ts +9 -1
  64. package/dist/types/render/utils.d.ts +1 -1
  65. package/dist/types/setup/scenes/splash.d.ts +6 -5
  66. package/dist/types/status-line/context-usage.d.ts +4 -1
  67. package/dist/types/status-line/host.d.ts +6 -4
  68. package/dist/types/status-line/startup.d.ts +6 -6
  69. package/dist/types/status-line/types.d.ts +3 -0
  70. package/dist/types/stdin-buffer.d.ts +7 -0
  71. package/dist/types/terminal-capabilities.d.ts +16 -1
  72. package/dist/types/terminal-multiplexer.d.ts +7 -0
  73. package/dist/types/terminal.d.ts +8 -4
  74. package/dist/types/theme/symbols.d.ts +1 -1
  75. package/dist/types/theme/theme.d.ts +11 -0
  76. package/dist/types/thinking.d.ts +5 -0
  77. package/dist/types/tmux.d.ts +39 -4
  78. package/dist/types/tools/ast-edit.d.ts +2 -2
  79. package/dist/types/tools/ast-grep.d.ts +2 -2
  80. package/dist/types/tools/bash.d.ts +6 -1
  81. package/dist/types/tools/edit.d.ts +2 -0
  82. package/dist/types/tools/eval.d.ts +53 -4
  83. package/dist/types/tools/find.d.ts +14 -4
  84. package/dist/types/tools/glob.d.ts +3 -3
  85. package/dist/types/tools/grep.d.ts +2 -2
  86. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  87. package/dist/types/tools/native-view.d.ts +1 -1
  88. package/dist/types/tools/read.d.ts +32 -10
  89. package/dist/types/tools/renderer.d.ts +2 -0
  90. package/dist/types/tools/task.d.ts +31 -0
  91. package/dist/types/tools/web-search-types.d.ts +1 -0
  92. package/dist/types/tools/write.d.ts +2 -0
  93. package/dist/types/tui.d.ts +20 -3
  94. package/package.json +10 -10
  95. package/src/app-keybindings.d.ts +18 -8
  96. package/src/app-keybindings.ts +20 -8
  97. package/src/apps/autoresearch-dashboard.ts +444 -27
  98. package/src/apps/debug/raw-sse.ts +2 -2
  99. package/src/apps/git/diff-pane.ts +39 -41
  100. package/src/apps/git/git-tui.ts +80 -14
  101. package/src/apps/git/sidebar.ts +10 -1
  102. package/src/apps/git/state.ts +12 -0
  103. package/src/autocomplete.d.ts +2 -0
  104. package/src/autocomplete.ts +21 -19
  105. package/src/bracketed-paste.ts +9 -2
  106. package/src/charts/chart-svg.d.ts +7 -13
  107. package/src/charts/chart-svg.ts +19 -22
  108. package/src/charts/table-data.ts +17 -6
  109. package/src/chat/advisor-message.d.ts +4 -1
  110. package/src/chat/advisor-message.ts +77 -24
  111. package/src/chat/assistant-message.d.ts +9 -4
  112. package/src/chat/assistant-message.ts +127 -31
  113. package/src/chat/bash-execution.ts +7 -2
  114. package/src/chat/figure-markdown.d.ts +7 -2
  115. package/src/chat/figure-markdown.ts +10 -3
  116. package/src/chat/image-loading.ts +9 -6
  117. package/src/chat/messages.d.ts +3 -1
  118. package/src/chat/messages.ts +4 -1
  119. package/src/chat/read-tool-group.d.ts +2 -2
  120. package/src/chat/read-tool-group.ts +97 -42
  121. package/src/chat/svg-figure.ts +3 -1
  122. package/src/chat/svg-source.ts +6 -3
  123. package/src/chat/table-chart.d.ts +11 -15
  124. package/src/chat/table-chart.ts +24 -57
  125. package/src/chat/tool-execution.d.ts +32 -20
  126. package/src/chat/tool-execution.ts +103 -36
  127. package/src/chat/user-message.d.ts +3 -2
  128. package/src/chat/user-message.ts +54 -34
  129. package/src/chrome/countdown-timer.ts +3 -3
  130. package/src/chrome/diff.d.ts +2 -3
  131. package/src/chrome/diff.ts +4 -9
  132. package/src/chrome/status-notice.ts +5 -0
  133. package/src/chrome/transcript-container.d.ts +2 -0
  134. package/src/chrome/transcript-container.ts +212 -28
  135. package/src/components/editor.ts +12 -12
  136. package/src/components/image.d.ts +2 -0
  137. package/src/components/image.ts +54 -20
  138. package/src/components/markdown.d.ts +49 -3
  139. package/src/components/markdown.ts +482 -12
  140. package/src/components/settings-list.ts +2 -4
  141. package/src/components/text.d.ts +2 -0
  142. package/src/components/text.ts +11 -1
  143. package/src/debug-server.d.ts +2 -0
  144. package/src/debug-server.ts +51 -69
  145. package/src/hotkeys-markdown.ts +2 -0
  146. package/src/keybinding-matchers.d.ts +6 -0
  147. package/src/keybinding-matchers.ts +19 -0
  148. package/src/multiplexers/tmux.d.ts +1 -0
  149. package/src/multiplexers/tmux.ts +3 -0
  150. package/src/multiplexers/types.d.ts +6 -0
  151. package/src/multiplexers/types.ts +6 -0
  152. package/src/native/backend.d.ts +5 -4
  153. package/src/native/backend.ts +13 -5
  154. package/src/native/blobs.d.ts +24 -12
  155. package/src/native/blobs.ts +56 -7
  156. package/src/native/reconcile.ts +43 -21
  157. package/src/overlays/advisor-config.ts +259 -8
  158. package/src/overlays/agent-hub-projection.d.ts +10 -5
  159. package/src/overlays/agent-hub-projection.ts +63 -12
  160. package/src/overlays/annotation-overlay.ts +422 -168
  161. package/src/overlays/approval-dialog.ts +389 -0
  162. package/src/overlays/ask-dialog.ts +244 -105
  163. package/src/overlays/btw-history-panel.ts +208 -147
  164. package/src/overlays/extensions/extension-list.ts +4 -6
  165. package/src/overlays/hook-editor.ts +90 -32
  166. package/src/overlays/hook-input.ts +105 -19
  167. package/src/overlays/job-tree.ts +177 -0
  168. package/src/overlays/jobs-panel.ts +222 -142
  169. package/src/overlays/login-dialog.ts +201 -91
  170. package/src/overlays/mcp-add-wizard.ts +562 -279
  171. package/src/overlays/model-browser.ts +32 -7
  172. package/src/overlays/model-hub.ts +4 -10
  173. package/src/overlays/model-picker.ts +58 -21
  174. package/src/overlays/pause-screen.ts +81 -23
  175. package/src/overlays/plan-review-overlay.ts +16 -0
  176. package/src/overlays/plugin-selector.ts +17 -1
  177. package/src/overlays/queue-mode-selector.ts +10 -0
  178. package/src/overlays/report-panel.ts +6 -2
  179. package/src/overlays/reset-usage-selector.ts +130 -21
  180. package/src/overlays/session-account-selector.ts +36 -0
  181. package/src/overlays/session-info-overlay.ts +199 -76
  182. package/src/overlays/session-selector.ts +15 -10
  183. package/src/overlays/settings-selector.ts +3 -0
  184. package/src/overlays/show-images-selector.ts +1 -0
  185. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  186. package/src/overlays/theme-selector.ts +6 -1
  187. package/src/overlays/thinking-selector.ts +8 -4
  188. package/src/overlays/tree-selector.ts +117 -2
  189. package/src/overlays/usage-dashboard.ts +498 -144
  190. package/src/prompt/composer-cache.ts +4 -1
  191. package/src/prompt/composer.ts +200 -74
  192. package/src/prompt/custom-editor.ts +168 -65
  193. package/src/prompt/image-references.d.ts +22 -0
  194. package/src/prompt/image-references.ts +79 -2
  195. package/src/prompt/internal-url-autocomplete.ts +1 -0
  196. package/src/prompt/macos-spelling.ts +28 -5
  197. package/src/prompt/prose-gate.ts +3 -0
  198. package/src/prompt/queued-messages.ts +9 -2
  199. package/src/prompt/tips.txt +1 -2
  200. package/src/prompt/typing-word.ts +59 -0
  201. package/src/prompt/welcome.ts +97 -45
  202. package/src/render/code-cell.ts +18 -5
  203. package/src/render/hanging-wrap.d.ts +7 -0
  204. package/src/render/hanging-wrap.ts +58 -0
  205. package/src/render/hyperlink.d.ts +2 -0
  206. package/src/render/hyperlink.ts +5 -0
  207. package/src/render/render-utils.d.ts +9 -1
  208. package/src/render/render-utils.ts +16 -2
  209. package/src/render/utils.d.ts +1 -1
  210. package/src/render/utils.ts +1 -1
  211. package/src/render/width-aware-text.ts +6 -5
  212. package/src/setup/scenes/splash.ts +22 -8
  213. package/src/status-line/component.ts +79 -11
  214. package/src/status-line/context-usage.ts +176 -73
  215. package/src/status-line/footer.ts +18 -35
  216. package/src/status-line/host.ts +6 -1
  217. package/src/status-line/segments.ts +16 -5
  218. package/src/status-line/startup.ts +6 -6
  219. package/src/status-line/types.ts +1 -0
  220. package/src/stdin-buffer.d.ts +7 -0
  221. package/src/stdin-buffer.ts +300 -62
  222. package/src/terminal-capabilities.d.ts +16 -1
  223. package/src/terminal-capabilities.ts +66 -28
  224. package/src/terminal-multiplexer.d.ts +7 -0
  225. package/src/terminal-multiplexer.ts +14 -0
  226. package/src/terminal.d.ts +8 -4
  227. package/src/terminal.ts +190 -63
  228. package/src/theme/symbols.d.ts +1 -1
  229. package/src/theme/symbols.ts +1 -0
  230. package/src/theme/theme-class.ts +1 -0
  231. package/src/theme/theme.d.ts +11 -0
  232. package/src/theme/theme.ts +46 -14
  233. package/src/thinking.d.ts +5 -0
  234. package/src/thinking.ts +9 -1
  235. package/src/tmux.d.ts +39 -4
  236. package/src/tmux.ts +110 -16
  237. package/src/tools/ask.ts +20 -9
  238. package/src/tools/ast-edit.d.ts +2 -2
  239. package/src/tools/ast-edit.ts +30 -7
  240. package/src/tools/ast-grep.d.ts +2 -2
  241. package/src/tools/ast-grep.ts +39 -35
  242. package/src/tools/bash.d.ts +6 -1
  243. package/src/tools/bash.ts +15 -9
  244. package/src/tools/debug.ts +2 -1
  245. package/src/tools/default-renderer.ts +3 -2
  246. package/src/tools/edit.d.ts +2 -0
  247. package/src/tools/edit.ts +100 -21
  248. package/src/tools/eval.d.ts +53 -4
  249. package/src/tools/eval.ts +173 -137
  250. package/src/tools/find.d.ts +14 -4
  251. package/src/tools/find.ts +71 -18
  252. package/src/tools/glob.d.ts +3 -3
  253. package/src/tools/glob.ts +51 -17
  254. package/src/tools/grep.d.ts +2 -2
  255. package/src/tools/grep.ts +99 -22
  256. package/src/tools/grouped-file-output.d.ts +9 -4
  257. package/src/tools/grouped-file-output.ts +21 -6
  258. package/src/tools/lsp.ts +30 -18
  259. package/src/tools/mcp.ts +2 -2
  260. package/src/tools/native-view.d.ts +1 -1
  261. package/src/tools/native-view.ts +2 -2
  262. package/src/tools/read.d.ts +32 -10
  263. package/src/tools/read.ts +89 -43
  264. package/src/tools/renderer.d.ts +2 -0
  265. package/src/tools/renderer.ts +2 -0
  266. package/src/tools/task.d.ts +31 -0
  267. package/src/tools/task.ts +40 -7
  268. package/src/tools/todo.ts +3 -2
  269. package/src/tools/wait.ts +16 -30
  270. package/src/tools/web-search-types.d.ts +1 -0
  271. package/src/tools/web-search-types.ts +1 -0
  272. package/src/tools/web-search.ts +2 -1
  273. package/src/tools/write.d.ts +2 -0
  274. package/src/tools/write.ts +36 -12
  275. package/src/tools/yield.ts +2 -2
  276. package/src/tui.d.ts +20 -3
  277. package/src/tui.ts +160 -43
  278. package/src/utils.ts +5 -3
  279. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  280. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
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";
@@ -105,6 +110,11 @@ const PAINT_END_NO_SYNC = ENABLE_AUTOWRAP;
105
110
  // native text selection.
106
111
  const MOUSE_TRACKING_ON = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
107
112
  const MOUSE_TRACKING_OFF = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
113
+ // Cell-size replies: CSI 6 ; height ; width t in pixels, and iTerm2's
114
+ // OSC 1337 ; ReportCellSize = height ; width [; scale] ST in points.
115
+ const CELL_SIZE_REPLY = /^\x1b\[6;(\d+);(\d+)t$/;
116
+ const ITERM2_CELL_SIZE_REPLY =
117
+ /^\x1b\]1337;ReportCellSize=(\d+(?:\.\d+)?);(\d+(?:\.\d+)?)(?:;(\d+(?:\.\d+)?))?(?:\x07|\x1b\\)$/;
108
118
 
109
119
  type MouseTrackingState = "off" | "inline" | "full";
110
120
 
@@ -254,10 +264,6 @@ export interface Component {
254
264
  */
255
265
  render(width: number): readonly string[];
256
266
 
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
267
  /**
262
268
  * Describe the component semantically for a Tern Surface Protocol
263
269
  * terminal (see `native/node.ts`). Called instead of `render()` when the
@@ -823,6 +829,10 @@ export class TUI extends Container {
823
829
  // rows the paint prepended for a short viewport. Negative while prepended
824
830
  // blanks outweigh replaced rows; zero on ordinary frames.
825
831
  #providerViewportPadTop = 0;
832
+ // Whether native history holds rows the provider committed since the last
833
+ // destructive reset. Only paints write history and only a destructive reset
834
+ // erases it; an `append` resize replay keeps the rows it replays beneath.
835
+ #providerHistoryWritten = false;
826
836
  // Viewport-relative row of the hardware cursor after the last normal paint
827
837
  // (0 = parked at the viewport top). A resize reflows the normal buffer
828
838
  // before the app hears about it; terminals keep the cursor attached to its
@@ -928,6 +938,8 @@ export class TUI extends Container {
928
938
  #debugNextWindowTop = 0;
929
939
  #inputListeners = new Set<InputListener>();
930
940
  #startListeners = new Set<StartListener>();
941
+ /** True from the end of start() until stop(): the terminal modes start listeners enable are live. */
942
+ #started = false;
931
943
  #paintListeners = new Set<PaintListener>();
932
944
 
933
945
  /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
@@ -1020,6 +1032,7 @@ export class TUI extends Container {
1020
1032
  #forceViewportRepaintOnNextRender = false;
1021
1033
  #hasEverRendered = false;
1022
1034
  #stopped = false;
1035
+ #appearanceReportUnsubscribe: (() => void) | undefined;
1023
1036
  #cancelPostmortemRestore?: () => void;
1024
1037
  /** True between a `deferInput` start() and enableInput(). */
1025
1038
  #inputDeferred = false;
@@ -1108,6 +1121,7 @@ export class TUI extends Container {
1108
1121
  overlayStack: {
1109
1122
  component: Component;
1110
1123
  options?: OverlayOptions;
1124
+ /** Focus restored after closing; tracks live focus changes beneath an input-less key holder. */
1111
1125
  preFocus: Component | null;
1112
1126
  hidden: boolean;
1113
1127
  /**
@@ -1315,6 +1329,9 @@ export class TUI extends Container {
1315
1329
  setFocus(component: Component | null): void {
1316
1330
  const holder = this.#getKeyHolderOverlay();
1317
1331
  if (holder && !isOverlayFocusTarget(holder.component, component)) {
1332
+ if (!holder.component.handleInput && this.#focusedComponent === holder.component) {
1333
+ holder.preFocus = component;
1334
+ }
1318
1335
  const currentFocus = this.#focusedComponent;
1319
1336
  component = isOverlayFocusTarget(holder.component, currentFocus) ? currentFocus : holder.component;
1320
1337
  }
@@ -1461,6 +1478,27 @@ export class TUI extends Container {
1461
1478
  return { top: this.#providerViewportTop - this.#providerViewportPadTop, length: this.#providerWindow.length };
1462
1479
  }
1463
1480
 
1481
+ /**
1482
+ * Rows of native history the last paint left on screen above a mutable
1483
+ * viewport that reached the bottom row, or `undefined` when that paint left
1484
+ * rows free below it (or none happened, or a destructive reset will repaint
1485
+ * from row zero). Rows above a bottom-reaching viewport may already sit in
1486
+ * native scrollback, which a later shrink cannot pull back down.
1487
+ */
1488
+ getFilledHistoryRows(): number | undefined {
1489
+ if (!this.#hasEverRendered || this.#clearScrollbackOnNextRender) return undefined;
1490
+ const height = this.terminal.rows;
1491
+ if (this.#providerWindow.length === 0 || this.#providerViewportTop + this.#providerWindow.length < height) {
1492
+ return undefined;
1493
+ }
1494
+ return Math.min(this.#providerViewportTop, height);
1495
+ }
1496
+
1497
+ /** Whether native history holds rows the frame provider committed since the last destructive reset. */
1498
+ hasProviderHistory(): boolean {
1499
+ return this.#providerHistoryWritten && !this.#clearScrollbackOnNextRender;
1500
+ }
1501
+
1464
1502
  /**
1465
1503
  * Probe for opt-in normal-buffer click capture. The provider is read every
1466
1504
  * frame; while it returns true (and no fullscreen overlay owns the
@@ -1544,6 +1582,34 @@ export class TUI extends Container {
1544
1582
 
1545
1583
  start(options?: TUIStartOptions): void {
1546
1584
  this.#stopped = false;
1585
+ this.#appearanceReportUnsubscribe?.();
1586
+ let backgroundRgb: number | undefined;
1587
+ let subscribed = true;
1588
+ const repaintForBackgroundChange = () => {
1589
+ if (!subscribed || this.#stopped) return;
1590
+ const nextBackgroundRgb = getTerminalBackgroundColor();
1591
+ // Track RGB before the SIXEL guard: the startup OSC 11 reply usually
1592
+ // arrives before DA1 enables SIXEL, so revisiting the same color once
1593
+ // SIXEL is live must not replay identical rows.
1594
+ if (nextBackgroundRgb === backgroundRgb) return;
1595
+ backgroundRgb = nextBackgroundRgb;
1596
+ if (TERMINAL.imageProtocol !== ImageProtocol.Sixel) return;
1597
+ // Track RGB even during optimistic native ownership: fallback paints
1598
+ // with the latest color, so a later return to the startup RGB must repaint.
1599
+ // Never write rows while the native surface still owns rendering.
1600
+ if (this.#nativeLive) return;
1601
+ this.invalidate();
1602
+ // Match theme changes: replay owned history on direct terminals, but
1603
+ // leave multiplexer scrollback intact and repaint only the viewport.
1604
+ this.requestRender(true, { clearScrollback: !isInsideTerminalMultiplexer() });
1605
+ };
1606
+ const unsubscribe = this.terminal.onAppearanceReport?.(repaintForBackgroundChange);
1607
+ const unsubscribeFallback = onTerminalBackgroundFallbackChange(repaintForBackgroundChange);
1608
+ this.#appearanceReportUnsubscribe = () => {
1609
+ subscribed = false;
1610
+ unsubscribe?.();
1611
+ unsubscribeFallback();
1612
+ };
1547
1613
  this.#debugPaint = undefined;
1548
1614
  this.#debugServer?.stop();
1549
1615
  this.#debugServer = undefined;
@@ -1602,6 +1668,9 @@ export class TUI extends Container {
1602
1668
  this.terminal.onSixelSupport?.(supported => {
1603
1669
  if (!supported || this.#stopped || this.#nativeLive || this.terminal.tspExpected) return;
1604
1670
  if (isImageProtocolForced()) return;
1671
+ // tmux answers DA1 for its own emulator; only its record of the
1672
+ // attached client says whether SIXEL reaches the outer terminal.
1673
+ if (tmuxMultiplexer.isInside() && !tmuxClientRendersSixel()) return;
1605
1674
  // Keep consuming an already-requested XTSMGRAPHICS reply until it
1606
1675
  // arrives or times out, even though DA1 has established support.
1607
1676
  this.#enableSixelProtocol();
@@ -1668,16 +1737,14 @@ export class TUI extends Container {
1668
1737
  () => this.stop(),
1669
1738
  { deferInput: this.#inputDeferred, isLoopStalled: () => this.#watchdog.isStalled() },
1670
1739
  );
1740
+ // ProcessTerminal.start() resets the previous owner's RGB. Seed only
1741
+ // after that reset so an identical OSC 11 reply still repaints fallback rows.
1742
+ backgroundRgb = TERMINAL.imageProtocol === ImageProtocol.Sixel ? getTerminalBackgroundColor() : undefined;
1671
1743
  if (this.#stopped) return;
1672
1744
  this.#cancelPostmortemRestore?.();
1673
1745
  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
- }
1746
+ for (const listener of this.#startListeners) this.#runStartListener(listener);
1747
+ this.#started = !this.#stopped;
1681
1748
  this.terminal.hideCursor();
1682
1749
  this.#recordHardwareCursorHidden();
1683
1750
  if (!this.#inputDeferred) {
@@ -1819,7 +1886,7 @@ export class TUI extends Container {
1819
1886
  this.#resizeSettleTimer?.cancel();
1820
1887
  this.#resizeSettleTimer = undefined;
1821
1888
  if (this.#altActive || this.#resizeAltActive) {
1822
- this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
1889
+ this.terminal.write(`${this.#takePendingAltEnter()}${this.#altScreenExit()}`);
1823
1890
  setAltScreenActive(false);
1824
1891
  this.#altActive = false;
1825
1892
  this.#resizeAltActive = false;
@@ -2133,7 +2200,7 @@ export class TUI extends Container {
2133
2200
  this.#suppressResizeUntil = this.#renderScheduler.now() + 100;
2134
2201
  this.#altPreviousLines = [];
2135
2202
  this.#altPreparedRows = [];
2136
- const exitSequence = `${this.#keyboardEnhancementExit()}\x1b[?1049l`;
2203
+ const exitSequence = this.#altScreenExit();
2137
2204
  // Only the provider plan frame emits a pending exit, and an exit already
2138
2205
  // pending (a fused overlay close) owns that slot.
2139
2206
  if (this.#frameProvider !== undefined && this.#pendingAltExit === "") {
@@ -2445,13 +2512,29 @@ export class TUI extends Container {
2445
2512
  if (this.#heldInput !== undefined && this.#heldFocus === previous) this.#heldFocus = next;
2446
2513
  }
2447
2514
 
2515
+ /**
2516
+ * Register a hook that re-enables terminal modes on every start(). A hook
2517
+ * added while the TUI is already running (a prepaint composer starts it
2518
+ * before the interactive mode wires its controllers) missed that start, so
2519
+ * it runs immediately instead of waiting for a stop/start cycle that may
2520
+ * never come.
2521
+ */
2448
2522
  addStartListener(listener: StartListener): () => void {
2449
2523
  this.#startListeners.add(listener);
2524
+ if (this.#started) this.#runStartListener(listener);
2450
2525
  return () => {
2451
2526
  this.#startListeners.delete(listener);
2452
2527
  };
2453
2528
  }
2454
2529
 
2530
+ #runStartListener(listener: StartListener): void {
2531
+ try {
2532
+ listener();
2533
+ } catch {
2534
+ // Startup listeners are feature hooks; one broken hook must not prevent rendering.
2535
+ }
2536
+ }
2537
+
2455
2538
  addInputListener(listener: InputListener): () => void {
2456
2539
  this.#inputListeners.add(listener);
2457
2540
  return () => {
@@ -2583,6 +2666,9 @@ export class TUI extends Container {
2583
2666
  // Query terminal for cell size in pixels: CSI 16 t
2584
2667
  // Response format: CSI 6 ; height ; width t
2585
2668
  this.terminal.write("\x1b[16t");
2669
+ // iTerm2 never answers CSI 16 t. Without a size, rows reserved for an image
2670
+ // and crops of a partly scrolled one assume 9x18 cells and land offset.
2671
+ if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) this.terminal.write("\x1b]1337;ReportCellSize\x1b\\");
2586
2672
  }
2587
2673
 
2588
2674
  /**
@@ -2635,6 +2721,8 @@ export class TUI extends Container {
2635
2721
  }
2636
2722
 
2637
2723
  stop(): void {
2724
+ this.#appearanceReportUnsubscribe?.();
2725
+ this.#appearanceReportUnsubscribe = undefined;
2638
2726
  this.#cancelPostmortemRestore?.();
2639
2727
  this.#cancelPostmortemRestore = undefined;
2640
2728
  this.#debugServer?.stop();
@@ -2662,7 +2750,7 @@ export class TUI extends Container {
2662
2750
  this.#cancelResizeProbe();
2663
2751
  if (this.#resizeAltActive) {
2664
2752
  this.#resizeAltActive = false;
2665
- this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
2753
+ this.terminal.write(`${this.#takePendingAltEnter()}${this.#altScreenExit()}`);
2666
2754
  setAltScreenActive(false);
2667
2755
  }
2668
2756
  if (this.#altActive || this.#pendingAltExit) {
@@ -2674,7 +2762,7 @@ export class TUI extends Container {
2674
2762
  const mouseExit = this.#mouseTracking !== "off" ? MOUSE_TRACKING_OFF : "";
2675
2763
  const exitSequence = this.#pendingAltExit
2676
2764
  ? `${this.#takePendingAltEnter()}${this.#pendingAltExit}${mouseExit}`
2677
- : `${mouseExit}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
2765
+ : `${mouseExit}${this.#altScreenExit()}`;
2678
2766
  this.terminal.write(exitSequence);
2679
2767
  setAltScreenActive(false);
2680
2768
  this.#altActive = false;
@@ -2704,6 +2792,7 @@ export class TUI extends Container {
2704
2792
  // (and live-session ghosts are already bounded by the inline-image budget).
2705
2793
  this.#clearSixelProbeState();
2706
2794
  this.#stopped = true;
2795
+ this.#started = false;
2707
2796
  this.#watchdog.stop();
2708
2797
  if (this.#renderTimer) {
2709
2798
  this.#renderTimer.cancel();
@@ -2771,6 +2860,13 @@ export class TUI extends Container {
2771
2860
  this.#requestOrdinaryRender();
2772
2861
  }
2773
2862
 
2863
+ /** Run `callback` after `delayMs` on the render scheduler's clock, unless the TUI has stopped. */
2864
+ scheduleAfter(delayMs: number, callback: () => void): RenderTimer {
2865
+ return this.#renderScheduler.scheduleRender(() => {
2866
+ if (!this.#stopped) callback();
2867
+ }, delayMs);
2868
+ }
2869
+
2774
2870
  /**
2775
2871
  * Paint a forced frame synchronously when startup must hand off an already
2776
2872
  * visible component tree before further async initialization. Same as
@@ -2986,9 +3082,9 @@ export class TUI extends Container {
2986
3082
  this.#inputRenderGraceUntilMs = this.#renderScheduler.now() + TUI.#INPUT_RENDER_GRACE_MS;
2987
3083
  }
2988
3084
 
2989
- // A focused input owner can reserve its gesture before TUI-wide listeners
3085
+ // The effective input owner can reserve its gesture before TUI-wide listeners
2990
3086
  // and debug shortcuts see the key, then handle the raw event exactly once.
2991
- const focusedForCapture = this.#focusedComponent;
3087
+ const focusedForCapture = this.#inputTarget();
2992
3088
  if (
2993
3089
  focusedForCapture?.handleInput &&
2994
3090
  (!isKeyRelease(data) || focusedForCapture.wantsKeyRelease) &&
@@ -3027,22 +3123,28 @@ export class TUI extends Container {
3027
3123
  return;
3028
3124
  }
3029
3125
 
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) {
3126
+ const target = this.#inputTarget();
3127
+ if (target?.handleInput) {
3037
3128
  // Filter out key release events unless component opts in
3038
- if (isKeyRelease(data) && !focused.wantsKeyRelease) {
3129
+ if (isKeyRelease(data) && !target.wantsKeyRelease) {
3039
3130
  return;
3040
3131
  }
3041
- focused.handleInput(data);
3132
+ target.handleInput(data);
3042
3133
  this.requestRender();
3043
3134
  }
3044
3135
  }
3045
3136
 
3137
+ /**
3138
+ * Component that receives keyboard input. An input-less key-holding overlay
3139
+ * leaves input with the focus it displaced; other focus owners keep their input.
3140
+ */
3141
+ #inputTarget(): Component | null {
3142
+ const focused = this.#focusedComponent;
3143
+ if (focused?.handleInput) return focused;
3144
+ const holder = this.#getKeyHolderOverlay();
3145
+ return holder?.component === focused ? holder.preFocus : null;
3146
+ }
3147
+
3046
3148
  /**
3047
3149
  * Queue a keystroke typed before the app installed its key handlers; input
3048
3150
  * listeners see it once, on replay. The cell-size reply is a terminal report,
@@ -3056,14 +3158,19 @@ export class TUI extends Container {
3056
3158
  }
3057
3159
 
3058
3160
  #consumeCellSizeResponse(data: string): boolean {
3059
- // Response format: ESC [ 6 ; height ; width t
3060
- const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
3061
- if (!match) {
3062
- return false;
3161
+ let heightPx: number;
3162
+ let widthPx: number;
3163
+ const pixels = CELL_SIZE_REPLY.exec(data);
3164
+ if (pixels) {
3165
+ heightPx = parseInt(pixels[1], 10);
3166
+ widthPx = parseInt(pixels[2], 10);
3167
+ } else {
3168
+ const points = ITERM2_CELL_SIZE_REPLY.exec(data);
3169
+ if (!points) return false;
3170
+ const scale = points[3] ? Number(points[3]) : 1;
3171
+ heightPx = Math.round(Number(points[1]) * scale);
3172
+ widthPx = Math.round(Number(points[2]) * scale);
3063
3173
  }
3064
-
3065
- const heightPx = parseInt(match[1], 10);
3066
- const widthPx = parseInt(match[2], 10);
3067
3174
  if (heightPx <= 0 || widthPx <= 0) {
3068
3175
  return true;
3069
3176
  }
@@ -3278,7 +3385,12 @@ export class TUI extends Container {
3278
3385
  return output;
3279
3386
  }
3280
3387
 
3281
- /** Clip a direct placement and record its live owner; history placements have no mutable owner. */
3388
+ /**
3389
+ * Clip a Kitty direct placement and record its live owner; history
3390
+ * placements have no mutable owner. Other direct placements (SIXEL,
3391
+ * iTerm2) have no delete command and composite over whatever pixels they
3392
+ * land on, so the rows they paint above `screenRow` are erased first.
3393
+ */
3282
3394
  #imageLineSequence(
3283
3395
  line: string,
3284
3396
  screenRow: number,
@@ -3287,7 +3399,10 @@ export class TUI extends Container {
3287
3399
  ): string {
3288
3400
  if (screenRow < 0) return line;
3289
3401
  const parsed = parseKittyDirectPlacementLine(line);
3290
- if (!parsed) return line;
3402
+ if (!parsed) {
3403
+ const lift = Math.min(directPlacementLift(line), screenRow);
3404
+ return lift > 0 ? `\x1b[${lift}A${`${ERASE_LINE}\x1b[B`.repeat(lift)}${line}` : line;
3405
+ }
3291
3406
  const previous = placements?.get(placementRow);
3292
3407
  const placementId = previous?.placementId ?? this.#nextImagePlacementId++;
3293
3408
  const erase = previous ? encodeKittyDeletePlacement(previous.imageId, previous.placementId) : "";
@@ -3569,6 +3684,7 @@ export class TUI extends Container {
3569
3684
  this.#providerViewportTop = 0;
3570
3685
  this.#providerWindow = [];
3571
3686
  this.#providerPreparedRows = [];
3687
+ this.#providerHistoryWritten = false;
3572
3688
  }
3573
3689
  // The viewport stays anchored directly below whatever history remains on
3574
3690
  // screen. Appending K history rows moves the anchor down by K; the write
@@ -3799,6 +3915,7 @@ export class TUI extends Container {
3799
3915
  this.#resizeBurstWidthChanged = false;
3800
3916
  this.#resizeBurstResized = false;
3801
3917
  this.#previousFrameLength = mutablePreparedLines.length;
3918
+ if (preparedHistory.lines.length > 0 || replayViewportRows > 0) this.#providerHistoryWritten = true;
3802
3919
  this.#clearScrollbackOnNextRender = false;
3803
3920
  this.#forceViewportRepaintOnNextRender = false;
3804
3921
  this.#hasEverRendered = true;
@@ -3899,8 +4016,7 @@ export class TUI extends Container {
3899
4016
  // restore it in the fused exit or later frames see matching states
3900
4017
  // and inline click/hover stays dead until the setting toggles.
3901
4018
  const mouseEnter = wantMouse !== "off" && this.#mouseTracking === "off" ? MOUSE_TRACKING_ON : "";
3902
- const enhancementExit = this.#keyboardEnhancementExit();
3903
- const exitSequence = `${mouseExit}${mouseEnter}${enhancementExit}\x1b[?1049l`;
4019
+ const exitSequence = `${mouseExit}${mouseEnter}${this.#altScreenExit()}`;
3904
4020
  // Session replacement finishes while its fullscreen selector still
3905
4021
  // covers the old normal buffer. Fuse the restore into the destructive
3906
4022
  // repaint so no stale frame can become visible between writes.
@@ -4443,14 +4559,15 @@ export class TUI extends Container {
4443
4559
  }
4444
4560
 
4445
4561
  /**
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.
4562
+ * Leave the alternate screen: pop its keyboard-enhancement frame first, then
4563
+ * re-assert the main-screen mode for terminals that lose it on `?1049l`.
4564
+ * Falls back to popping kitty whenever a custom Terminal exposes its push
4565
+ * sequence but predates the `keyboardEnhancementExitSequence` property.
4449
4566
  */
4450
- #keyboardEnhancementExit(): string {
4567
+ #altScreenExit(): string {
4451
4568
  const exit = this.terminal.keyboardEnhancementExitSequence;
4452
- if (exit !== undefined) return exit ?? "";
4453
- return this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
4569
+ const keyboardExit = exit !== undefined ? (exit ?? "") : this.terminal.kittyEnableSequence ? "\x1b[<u" : "";
4570
+ return `${keyboardExit}\x1b[?1049l${this.terminal.keyboardEnhancementRestoreSequence ?? ""}`;
4454
4571
  }
4455
4572
 
4456
4573
  /**
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
- }