@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/terminal.ts CHANGED
@@ -25,12 +25,13 @@ import {
25
25
  NotifyProtocol,
26
26
  setCellDimensions,
27
27
  setOsc99Supported,
28
+ setTerminalBackgroundColor,
28
29
  setTerminalGlyphProtocol,
29
30
  TERMINAL,
30
31
  } from "./terminal-capabilities";
31
32
  import { tmuxMultiplexer } from "./multiplexers/tmux";
32
33
  import type { TerminalMultiplexerBackgroundColorCache } from "./multiplexers/types";
33
- import { terminalMultiplexerSessions } from "./terminal-multiplexer";
34
+ import { classifyTerminalMultiplexerModule, terminalMultiplexerSessions } from "./terminal-multiplexer";
34
35
  import { setTmuxPaneApcTitles, tmuxSessionSpeaksTsp } from "./tmux";
35
36
  import { setHangulCompatibilityJamoWidth } from "./utils";
36
37
  import { translateWindowsAltGrSequence } from "./windows-altgr";
@@ -55,6 +56,38 @@ const TSP_OSC_PREFIX = `\x1b]877;${TSP_PREFIX.slice(2)}`;
55
56
  /** How long a TSP hello waits for its reply inside tmux, which answers DA1 itself. */
56
57
  const TSP_TMUX_HELLO_TIMEOUT_MS = 1000;
57
58
 
59
+ /** Tern names itself in `TERM_PROGRAM`; a multiplexer started in a Tern pane may leave it behind. */
60
+ function isTernPane(env: NodeJS.ProcessEnv): boolean {
61
+ return env.TERM_PROGRAM?.toLowerCase() === "tern";
62
+ }
63
+
64
+ /**
65
+ * Why a TSP `hello` cannot reach the terminal from this environment, or
66
+ * `null` when nothing in between is known to drop APC.
67
+ *
68
+ * In a Tern pane only a multiplexer's session marker counts: Tern sets
69
+ * `TERM=xterm-kitty`, so a `screen*`/`tmux*` TERM next to `TERM_PROGRAM=tern`
70
+ * without a marker was rewritten (shell rc, dev shell, `.env`), not set by a
71
+ * multiplexer. Elsewhere the TERM fallback keeps classifying sessions whose
72
+ * markers were stripped.
73
+ *
74
+ * devenv (2.0+) runs `devenv shell` in its own PTY and redraws the pane from a
75
+ * terminal emulator to keep its reload status line: it forwards DA1 to the
76
+ * real terminal but drops APC, so the DA1 reply always wins. It exports
77
+ * `DEVENV_RELOAD_FILE` only into that reloading shell; `devenv shell
78
+ * --no-reload` runs the shell on the real terminal.
79
+ */
80
+ function tspPathBlocker(env: NodeJS.ProcessEnv): string | null {
81
+ const multiplexer = isTernPane(env)
82
+ ? terminalMultiplexerSessions(env).find(session => session.ownsScreenGrid)
83
+ : classifyTerminalMultiplexerModule(env);
84
+ if (multiplexer?.ownsScreenGrid) return `inside a ${multiplexer.id} session, which swallows APC`;
85
+ if (env.DEVENV_RELOAD_FILE) {
86
+ return "inside devenv's reloading shell, which redraws the pane and drops APC; `devenv shell --no-reload` avoids it";
87
+ }
88
+ return null;
89
+ }
90
+
58
91
  /**
59
92
  * The TSP message a reassembled input string holds, in APC form, or
60
93
  * `undefined` while its terminator has not arrived. An OSC 877 message may end
@@ -133,9 +166,22 @@ export function chunkForConPTY(data: string, maxChunkBytes: number = MAX_CONPTY_
133
166
  let pos = 0;
134
167
  while (pos < len) {
135
168
  let bytes = 0;
136
- // Index just past the most recent `\n` we've consumed inside [pos, i):
137
- // the natural cut point that leaves escape sequences intact.
169
+ // Index just past the most recent line terminator we have consumed
170
+ // inside [pos, i): the natural cut point that leaves escape sequences
171
+ // and rows intact.
172
+ //
173
+ // Both terminators must be tracked. A full paint separates rows with
174
+ // `\r\n` and a newline alone is enough. A *diff* paint does not: TUI's
175
+ // row diff (`TUI#renderProviderFrame`, the `diffable` branch) addresses
176
+ // every changed row with `CSI row;1H` and closes it with a bare `\r`,
177
+ // emitting no `\n` anywhere in the frame. A newline-only tracker
178
+ // therefore finds no boundary in a diff paint and falls through to the
179
+ // hard cut at `i`, slicing the buffer through the middle of a row's
180
+ // text. Every boundary between two `process.stdout.write` calls is a
181
+ // point a foreign writer can inject into, so a hard cut lets unrelated
182
+ // output land inside a row instead of between rows.
138
183
  let lastNewlineEnd = -1;
184
+ let lastCarriageReturnEnd = -1;
139
185
  let i = pos;
140
186
  while (i < len) {
141
187
  const cu = data.charCodeAt(i);
@@ -161,16 +207,25 @@ export function chunkForConPTY(data: string, maxChunkBytes: number = MAX_CONPTY_
161
207
  cuBytes = 3;
162
208
  }
163
209
  if (bytes + cuBytes > maxChunkBytes && i > pos) {
164
- // Would overflow the cap. Cut at the last newline if we found one,
165
- // otherwise hard-cut at the current code-point boundary.
166
- const cut = lastNewlineEnd > pos ? lastNewlineEnd : i;
167
- chunks.push(data.slice(pos, cut));
168
- pos = cut;
210
+ // Cut at the last line terminator. A `\r` that opens a `\r\n` pair is
211
+ // never tracked (see below), so the pair always stays in one
212
+ // `WriteFile` call. Falls back to the code-point boundary `i` only
213
+ // when the window holds no terminator at all (a single row longer
214
+ // than the cap), which still makes forward progress.
215
+ const cut = Math.max(lastNewlineEnd, lastCarriageReturnEnd);
216
+ let end = cut > pos ? cut : i;
217
+ // A hard cut landing between `\r` and `\n` moves back before the `\r`.
218
+ if (end === i && data.charCodeAt(i) === 0x0a && data.charCodeAt(i - 1) === 0x0d && i - 1 > pos) end = i - 1;
219
+ chunks.push(data.slice(pos, end));
220
+ pos = end;
169
221
  break;
170
222
  }
171
223
  bytes += cuBytes;
172
224
  i += cuLen;
225
+ // A `\r` directly followed by `\n` is half of a `\r\n` pair, even when the
226
+ // `\n` lies past the cap, so it is never a cut point on its own.
173
227
  if (cu === 0x0a) lastNewlineEnd = i;
228
+ else if (cu === 0x0d && data.charCodeAt(i) !== 0x0a) lastCarriageReturnEnd = i;
174
229
  }
175
230
  if (i >= len) {
176
231
  chunks.push(data.slice(pos));
@@ -490,7 +545,7 @@ export type GlyphProtocolReportHandler = (supported: boolean) => void;
490
545
  /**
491
546
  * Outcome of the Tern Surface Protocol `hello` probe: the terminal's reply,
492
547
  * or null when it answered the DA1 sentinel first (no TSP) or the probe was
493
- * skipped (`PI_TUI_NATIVE=0`, multiplexers).
548
+ * skipped (`PI_TUI_NATIVE=0`, multiplexers, devenv's reloading shell).
494
549
  */
495
550
  export type TspHelloHandler = (hello: TspHello | null) => void;
496
551
 
@@ -590,6 +645,11 @@ export interface Terminal {
590
645
  // so custom Terminals built against older pi-tui versions keep working.
591
646
  readonly keyboardEnhancementExitSequence?: string | null;
592
647
 
648
+ // Re-asserts the main-screen enhanced keyboard mode after alternate-screen
649
+ // exit, or null when none is needed. Optional so custom Terminals built
650
+ // against older pi-tui versions keep working.
651
+ readonly keyboardEnhancementRestoreSequence?: string | null;
652
+
593
653
  // Cursor positioning (relative to current position)
594
654
  moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines
595
655
 
@@ -764,9 +824,11 @@ export interface ProcessTerminalOptions {
764
824
  conpty?: boolean;
765
825
  /**
766
826
  * Whether stdin is a native Windows console handle (win32, not WSL), whose
767
- * console host answers `CSI ? 9001 h` with win32-input-mode key records.
768
- * Defaults to `process.platform === "win32"`; only consulted when `conpty`
769
- * is also true.
827
+ * console host is certain to answer `CSI ? 9001 h` with win32-input-mode key
828
+ * records. When false (WSL), the keyboard fallback requests modifyOtherKeys
829
+ * alongside win32-input-mode, since a WSL pty may not be relayed by a console
830
+ * host. Defaults to `process.platform === "win32"`; only consulted when
831
+ * `conpty` is also true.
770
832
  */
771
833
  nativeWindowsConsole?: boolean;
772
834
  }
@@ -783,7 +845,8 @@ export class ProcessTerminal implements Terminal {
783
845
  #isLoopStalled?: () => boolean;
784
846
  #stdoutResizeListener?: () => void;
785
847
  #kittyProtocolActive = false;
786
- #kittyEnableSeq: string | null = null;
848
+ // Kitty flags omp pushed on its own stack frame; meaningful only while kitty is active.
849
+ #kittyFlags = 0;
787
850
  #modifyOtherKeysActive = false;
788
851
  #modifyOtherKeysTimeout?: Timer;
789
852
  // Windows console fallback when kitty is unavailable: key records arrive as
@@ -793,6 +856,8 @@ export class ProcessTerminal implements Terminal {
793
856
  // Releases a relayed lone ESC the decoder held for a sequence tail that never came.
794
857
  #win32PendingFlushTimer?: Timer;
795
858
  #stdinBuffer?: StdinBuffer;
859
+ /** Parsed input path shared by direct VT and win32-input-mode relayed text. */
860
+ #stdinSequenceHandler?: (sequence: string) => void;
796
861
  #stdinDataHandler?: (data: string) => void;
797
862
  #disconnectHandler?: () => void;
798
863
  #stdinEndHandler = () => {
@@ -878,6 +943,7 @@ export class ProcessTerminal implements Terminal {
878
943
  #sixelSupportCallbacks: Array<(supported: boolean) => void> = [];
879
944
  #tspPending = false;
880
945
  #tspResult: TspHello | null | undefined;
946
+ #tspFallbackLogged = false;
881
947
  #tspCallbacks: TspHelloHandler[] = [];
882
948
  /** Hello deadline inside tmux, where no DA1 sentinel can end the probe. */
883
949
  #tspTimeout: NodeJS.Timeout | undefined;
@@ -889,6 +955,12 @@ export class ProcessTerminal implements Terminal {
889
955
  /** Resolved DECRQM support per private mode (mode → supported). */
890
956
  #privateModeSupport = new Map<number, boolean>();
891
957
  #privateModeCallbacks: PrivateModeReportHandler[] = [];
958
+ /**
959
+ * Whether output and input reassert DEC 2004: set once DECRPM confirms it,
960
+ * or at input attach inside a multiplexer that gates paste markers on the
961
+ * pane's own mode without answering DECRQM (tmux <= 3.5a, #15596).
962
+ */
963
+ #rearmBracketedPaste = false;
892
964
  /** Whether DEC 2048 in-band resize notifications are currently enabled. */
893
965
  #inBandResizeActive = false;
894
966
  /** Reassembly buffer for a DEC 2048 in-band resize report split across stdin reads. */
@@ -913,14 +985,22 @@ export class ProcessTerminal implements Terminal {
913
985
  }
914
986
 
915
987
  get kittyEnableSequence(): string | null {
916
- return this.#kittyProtocolActive ? this.#kittyEnableSeq : null;
988
+ return this.#kittyProtocolActive ? `\x1b[>${this.#kittyFlags}u` : null;
917
989
  }
918
990
 
919
991
  get keyboardEnhancementEnterSequence(): string | null {
920
- if (this.#kittyProtocolActive) return this.#kittyEnableSeq;
992
+ if (this.#kittyProtocolActive) return `\x1b[>${this.#kittyFlags}u`;
921
993
  return this.#modifyOtherKeysActive ? "\x1b[>4;2m" : null;
922
994
  }
923
995
 
996
+ get keyboardEnhancementRestoreSequence(): string | null {
997
+ // `CSI = flags ; 1 u` replaces the main-screen stack top, which is omp's own
998
+ // frame, so it is a no-op wherever per-screen stacks work. Windows Terminal
999
+ // 1.25–1.27 restore the pre-push flags on `?1049l` (microsoft/terminal#20763),
1000
+ // dropping to legacy keys: Shift+Enter arrives as `\r`, Ctrl+Enter as `\n`.
1001
+ return this.#kittyProtocolActive ? `\x1b[=${this.#kittyFlags};1u` : null;
1002
+ }
1003
+
924
1004
  get keyboardEnhancementExitSequence(): string | null {
925
1005
  // kitty is a stack push (per-screen), so the matching pop balances alt-screen
926
1006
  // entry. xterm modifyOtherKeys is a single global flag with no per-screen
@@ -1014,13 +1094,13 @@ export class ProcessTerminal implements Terminal {
1014
1094
  }
1015
1095
 
1016
1096
  get tspExpected(): boolean {
1017
- // A multiplexer started from Tern can leave `TERM_PROGRAM=tern` behind
1018
- // while swallowing APC, even when `PI_TUI_NATIVE=1` forces the probe.
1097
+ // A multiplexer or relay started from Tern can leave `TERM_PROGRAM=tern`
1098
+ // behind while swallowing APC, even when `PI_TUI_NATIVE=1` forces the probe.
1019
1099
  return (
1020
- $env.TERM_PROGRAM?.toLowerCase() === "tern" &&
1021
- !isInsideTerminalMultiplexer($env) &&
1100
+ isTernPane($env) &&
1101
+ tspPathBlocker($env) === null &&
1022
1102
  !isTerminalHeadless() &&
1023
- this.#shouldQueryTspSupport()
1103
+ this.#tspProbeSkipReason() === null
1024
1104
  );
1025
1105
  }
1026
1106
 
@@ -1048,6 +1128,8 @@ export class ProcessTerminal implements Terminal {
1048
1128
  // Own stdout: out-of-band writers route through this terminal, and the
1049
1129
  // emergency restore finds it on crash.
1050
1130
  setActiveTerminal(this);
1131
+ // RGB belongs to this ownership session, including deferred-input prepaint.
1132
+ setTerminalBackgroundColor(undefined);
1051
1133
  terminalEverStarted = true;
1052
1134
  // Own the blocking write(2) on a pump thread (unix TTYs only). A stale
1053
1135
  // prebuilt natives module without the export falls back to direct writes.
@@ -1128,6 +1210,9 @@ export class ProcessTerminal implements Terminal {
1128
1210
 
1129
1211
  // Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
1130
1212
  this.#safeWrite("\x1b[?2004h");
1213
+ this.#rearmBracketedPaste =
1214
+ terminalMultiplexerSessions($env).some(multiplexer => multiplexer.gatesBracketedPaste) ||
1215
+ classifyTerminalMultiplexerModule($env)?.gatesBracketedPaste === true;
1131
1216
 
1132
1217
  // Force normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
1133
1218
  // `rmkx` = "\x1b[?1l\x1b>"). zeta decodes both CSI ("\x1b[A") and SS3
@@ -1304,7 +1389,30 @@ export class ProcessTerminal implements Terminal {
1304
1389
  // skip the subparameter tail instead of dropping the whole report.
1305
1390
  const inBandResizePattern = /^\x1b\[48;(\d+)(?::[\d:]*)?;(\d+)(?::[\d:]*)?;(\d+)(?::[\d:]*)?;(\d+)(?::[\d:]*)?t$/;
1306
1391
 
1307
- this.#stdinBuffer.on("data", (sequence: string) => {
1392
+ this.#stdinSequenceHandler = (sequence: string) => {
1393
+ const decoder = this.#win32InputDecoder;
1394
+ const win32Keys = decoder?.decode(sequence);
1395
+ if (decoder && win32Keys !== undefined) {
1396
+ clearTimeout(this.#win32PendingFlushTimer);
1397
+ this.#win32PendingFlushTimer = undefined;
1398
+ // The record is ESC-led, so it interrupts any direct reply still being
1399
+ // reassembled, just as a raw escape does below. Decoded keys carry no ESC
1400
+ // of their own and would otherwise be appended to that fragment.
1401
+ this.#privateCsiResponseBuffer = "";
1402
+ this.#inBandResizeBuffer = "";
1403
+ this.#osc11ResponseBuffer = "";
1404
+ this.#osc99ResponseBuffer = "";
1405
+ this.#glyphProtocolReplyBuffer = "";
1406
+ this.#tspReplyBuffer = "";
1407
+ for (const key of win32Keys) this.#stdinSequenceHandler?.(key);
1408
+ if (decoder.hasPendingSequence) {
1409
+ this.#win32PendingFlushTimer = setTimeout(
1410
+ () => this.#releaseWin32PendingSequence(),
1411
+ WIN32_RELAYED_ESCAPE_TIMEOUT_MS,
1412
+ );
1413
+ }
1414
+ return;
1415
+ }
1308
1416
  // Fast path for plain-text bytes: every escape-probe regex below
1309
1417
  // anchors on `^\x1b…`, so a byte that is not ESC can never match. A
1310
1418
  // non-bracketed paste of N printable chars arrives as N per-scalar
@@ -1501,7 +1609,10 @@ export class ProcessTerminal implements Terminal {
1501
1609
  }
1502
1610
  case "tsp": {
1503
1611
  // DA1 before any `tsp;r` reply: the terminal doesn't speak TSP.
1504
- this.#resolveTspSupport(null);
1612
+ this.#resolveTspSupport(
1613
+ null,
1614
+ "DA1 was answered before a hello reply: the terminal, or a layer in between, dropped the APC query",
1615
+ );
1505
1616
  break;
1506
1617
  }
1507
1618
  }
@@ -1536,19 +1647,17 @@ export class ProcessTerminal implements Terminal {
1536
1647
  // ConPTY (native Windows and WSL) drops Shift+letter keypresses
1537
1648
  // entirely when flag 4 (report alternate keys) is set. Use flag 1
1538
1649
  // (disambiguate only), preserving flag 2 if already active.
1539
- this.#kittyEnableSeq = (reportedFlags & 2) !== 0 ? "\x1b[>3u" : "\x1b[>1u";
1540
- this.#safeWrite(this.#kittyEnableSeq);
1650
+ this.#kittyFlags = (reportedFlags & 2) !== 0 ? 3 : 1;
1541
1651
  } else if ((reportedFlags & 2) !== 0) {
1542
1652
  // Preserve event-type reporting already enabled by a parent app.
1543
1653
  // Push level-2 to keep its shortcuts reporting consistently.
1544
- this.#kittyEnableSeq = "\x1b[>7u";
1545
- this.#safeWrite(this.#kittyEnableSeq);
1654
+ this.#kittyFlags = 7;
1546
1655
  } else {
1547
1656
  // Disambiguate escape codes and report base-layout keys for physical
1548
1657
  // shortcut matching, without event reporting that caused regression #3259.
1549
- this.#kittyEnableSeq = "\x1b[>5u";
1550
- this.#safeWrite(this.#kittyEnableSeq);
1658
+ this.#kittyFlags = 5;
1551
1659
  }
1660
+ this.#safeWrite(`\x1b[>${this.#kittyFlags}u`);
1552
1661
  return;
1553
1662
  }
1554
1663
 
@@ -1635,20 +1744,6 @@ export class ProcessTerminal implements Terminal {
1635
1744
  return;
1636
1745
  }
1637
1746
  if (this.#inputHandler) {
1638
- const decoder = this.#win32InputDecoder;
1639
- const win32Keys = decoder?.decode(sequence);
1640
- if (decoder && win32Keys !== undefined) {
1641
- clearTimeout(this.#win32PendingFlushTimer);
1642
- this.#win32PendingFlushTimer = undefined;
1643
- for (const key of win32Keys) this.#inputHandler(key);
1644
- if (decoder.hasPendingSequence) {
1645
- this.#win32PendingFlushTimer = setTimeout(
1646
- () => this.#releaseWin32PendingSequence(),
1647
- WIN32_RELAYED_ESCAPE_TIMEOUT_MS,
1648
- );
1649
- }
1650
- return;
1651
- }
1652
1747
  // Windows console hosts drop AltGr text under kitty (AltGr+F → `CSI 102;3u`);
1653
1748
  // recover it from the active layout before any keybinding sees an Alt chord.
1654
1749
  const altGrText =
@@ -1657,7 +1752,8 @@ export class ProcessTerminal implements Terminal {
1657
1752
  : undefined;
1658
1753
  this.#inputHandler(altGrText ?? sequence);
1659
1754
  }
1660
- });
1755
+ };
1756
+ this.#stdinBuffer.on("data", this.#stdinSequenceHandler);
1661
1757
 
1662
1758
  // Re-wrap paste content with bracketed paste markers for existing editor
1663
1759
  // handling. An Enter that shared the paste's stdin read rides along so
@@ -1673,8 +1769,11 @@ export class ProcessTerminal implements Terminal {
1673
1769
 
1674
1770
  // Handler that pipes stdin data through the buffer
1675
1771
  this.#stdinDataHandler = (data: string) => {
1676
- // Recover modes reset by the host before the user's next paste.
1677
- if (data && this.#active && this.#privateModeSupport.get(2004)) this.#safeWrite("\x1b[?2004h");
1772
+ // Recover modes reset by the host before the user's next paste. Not
1773
+ // mid-paste: the host is plainly in 2004 then, and per-key reads would
1774
+ // turn one paste into thousands of writes (#15757).
1775
+ if (data && this.#active && this.#rearmBracketedPaste && !this.#stdinBuffer!.pasting)
1776
+ this.#safeWrite("\x1b[?2004h");
1678
1777
  if (this.#win32PasteNormalizer) this.#win32PasteNormalizer.process(data);
1679
1778
  else this.#stdinBuffer!.process(data);
1680
1779
  };
@@ -1837,16 +1936,27 @@ export class ProcessTerminal implements Terminal {
1837
1936
  }
1838
1937
  }
1839
1938
 
1840
- #shouldQueryTspSupport(): boolean {
1939
+ /** Why the `hello` probe does not run, or `null` when it does. */
1940
+ #tspProbeSkipReason(): string | null {
1841
1941
  const override = $env.PI_TUI_NATIVE;
1842
- if (override === "0") return false;
1942
+ if (override === "0") return "PI_TUI_NATIVE=0";
1843
1943
  // tmux makes APC bodies the pane title unless the hosting terminal asked
1844
1944
  // for TSP through `@tern-tsp`: no override may force the probe there.
1845
- if (tmuxMultiplexer.isInside($env)) return tmuxSessionSpeaksTsp($env);
1846
- if (override === "1") return true;
1847
- // Multiplexers swallow APC, so the reply could never arrive.
1848
- if (isInsideTerminalMultiplexer($env)) return false;
1849
- return !isBunTestRuntime();
1945
+ if (tmuxMultiplexer.isInside($env)) {
1946
+ return tmuxSessionSpeaksTsp($env) ? null : "inside a tmux session without the @tern-tsp option";
1947
+ }
1948
+ if (override === "1") return null;
1949
+ // The reply could never arrive.
1950
+ const blocker = tspPathBlocker($env);
1951
+ if (blocker !== null) return blocker;
1952
+ return isBunTestRuntime() && $env.PI_TUI_TSP_PROBE !== "1" ? "bun test runtime" : null;
1953
+ }
1954
+
1955
+ /** A Tern pane that ends up on the classic renderer says why in the log, once. */
1956
+ #logTspFallback(reason: string): void {
1957
+ if (this.#tspFallbackLogged || !isTernPane($env)) return;
1958
+ this.#tspFallbackLogged = true;
1959
+ logger.debug("TSP: Tern pane keeps the classic renderer", { reason });
1850
1960
  }
1851
1961
 
1852
1962
  #queryTspSupport(): void {
@@ -1854,7 +1964,9 @@ export class ProcessTerminal implements Terminal {
1854
1964
  this.#tspResult = undefined;
1855
1965
  this.#tspReplyBuffer = "";
1856
1966
  if (this.#dead) return;
1857
- if (!this.#shouldQueryTspSupport()) {
1967
+ const skipReason = this.#tspProbeSkipReason();
1968
+ if (skipReason !== null) {
1969
+ this.#logTspFallback(skipReason);
1858
1970
  this.#tspResult = null;
1859
1971
  for (const cb of this.#tspCallbacks) cb(null);
1860
1972
  return;
@@ -1866,7 +1978,7 @@ export class ProcessTerminal implements Terminal {
1866
1978
  setTmuxPaneApcTitles(false, $env);
1867
1979
  this.#tspTimeout = setTimeout(() => {
1868
1980
  this.#tspTimeout = undefined;
1869
- this.#resolveTspSupport(null);
1981
+ this.#resolveTspSupport(null, `no hello reply through tmux within ${TSP_TMUX_HELLO_TIMEOUT_MS} ms`);
1870
1982
  }, TSP_TMUX_HELLO_TIMEOUT_MS);
1871
1983
  this.#safeWrite(encodeTspHelloQuery());
1872
1984
  return;
@@ -1894,12 +2006,14 @@ export class ProcessTerminal implements Terminal {
1894
2006
  this.#inputHandler?.(sequence);
1895
2007
  }
1896
2008
 
1897
- #resolveTspSupport(hello: TspHello | null): void {
2009
+ /** `reason` says why a `null` hello means no TSP. */
2010
+ #resolveTspSupport(hello: TspHello | null, reason?: string): void {
1898
2011
  if (!this.#tspPending) return;
1899
2012
  this.#tspPending = false;
1900
2013
  const result = hello !== null && hello.v === TSP_VERSION ? hello : null;
1901
2014
  if (hello !== null && result === null) logger.warn("TSP: unsupported protocol version", { v: hello.v });
1902
2015
  if (result === null) {
2016
+ this.#logTspFallback(reason ?? `unsupported protocol version ${String(hello?.v)}`);
1903
2017
  this.#endTspTmuxPane();
1904
2018
  } else {
1905
2019
  clearTimeout(this.#tspTimeout);
@@ -1942,7 +2056,11 @@ export class ProcessTerminal implements Terminal {
1942
2056
  const max = 16 ** hex.length - 1;
1943
2057
  return max > 0 ? value / max : 0;
1944
2058
  };
1945
- const luminance = 0.299 * normalize(rHex) + 0.587 * normalize(gHex) + 0.114 * normalize(bHex);
2059
+ const r = normalize(rHex);
2060
+ const g = normalize(gHex);
2061
+ const b = normalize(bHex);
2062
+ setTerminalBackgroundColor((Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255));
2063
+ const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
1946
2064
  const mode: TerminalAppearance = luminance < 0.5 ? "dark" : "light";
1947
2065
  const changed = mode !== this.#appearance;
1948
2066
  this.#appearance = mode;
@@ -1967,10 +2085,11 @@ export class ProcessTerminal implements Terminal {
1967
2085
  #enableModifyOtherKeysFallback(): void {
1968
2086
  if (this.#kittyProtocolActive || this.#modifyOtherKeysActive || this.#win32InputDecoder) return;
1969
2087
  const ssh = isSshSession();
1970
- if (this.#conpty && this.#nativeWindowsConsole && !ssh) {
2088
+ if (this.#conpty && !ssh) {
1971
2089
  // The Windows console host ignores modifyOtherKeys and folds Shift+Enter
1972
- // into a bare CR. win32-input-mode is answered by the console host
1973
- // serving this process, so it works under every local ConPTY terminal.
2090
+ // into a bare CR (Ctrl+Enter into LF). win32-input-mode is answered by the
2091
+ // console host serving this process, so it works under every local ConPTY
2092
+ // terminal, and under WSL through the `wsl.exe` relay's console host too.
1974
2093
  // Under sshd the console host is fed the remote terminal's VT stream,
1975
2094
  // and the mode splits arrow keys into Escape plus literal text (#14034).
1976
2095
  this.#safeWrite("\x1b[?9001h");
@@ -1981,7 +2100,10 @@ export class ProcessTerminal implements Terminal {
1981
2100
  data => this.#stdinBuffer?.process(data),
1982
2101
  () => this.#releaseWin32PendingSequence(),
1983
2102
  );
1984
- return;
2103
+ // A WSL pty is not always relayed by a console host (tmux, VS Code
2104
+ // Remote-WSL, WSLg terminals). Those ignore 9001 and keep sending plain
2105
+ // VT, which the decoder passes through, so also ask for modifyOtherKeys.
2106
+ if (this.#nativeWindowsConsole) return;
1985
2107
  }
1986
2108
  // A remote terminal with no identifying env may not understand the request.
1987
2109
  if (ssh && (TERMINAL.id === "base" || TERMINAL.id === "trueColor")) return;
@@ -1994,7 +2116,7 @@ export class ProcessTerminal implements Terminal {
1994
2116
  clearTimeout(this.#win32PendingFlushTimer);
1995
2117
  this.#win32PendingFlushTimer = undefined;
1996
2118
  const pending = this.#win32InputDecoder?.flushPendingSequence() ?? [];
1997
- for (const key of pending) this.#inputHandler?.(key);
2119
+ for (const key of pending) this.#stdinSequenceHandler?.(key);
1998
2120
  }
1999
2121
 
2000
2122
  #disableWin32InputMode(): void {
@@ -2075,6 +2197,7 @@ export class ProcessTerminal implements Terminal {
2075
2197
  // only true here after an explicit DECRPM reply (the DA1-sentinel
2076
2198
  // fallback resolves unsupported).
2077
2199
  if (mode === 2004 && supported) {
2200
+ this.#rearmBracketedPaste = true;
2078
2201
  this.#stdinBuffer?.setRawPasteStallProbe(this.#isLoopStalled);
2079
2202
  }
2080
2203
  }
@@ -2209,11 +2332,13 @@ export class ProcessTerminal implements Terminal {
2209
2332
  // Suppress observer/timer callbacks before any teardown can yield or throw.
2210
2333
  this.#active = false;
2211
2334
  this.#inputDeferred = false;
2212
- if (this.#headless) return;
2213
2335
  // Release stdout ownership (out-of-band writers and emergency cleanup)
2336
+ // before headless/teardown exits, without disturbing another owner's RGB.
2214
2337
  if (getActiveTerminal() === this) {
2215
2338
  setActiveTerminal(null);
2339
+ setTerminalBackgroundColor(undefined);
2216
2340
  }
2341
+ if (this.#headless) return;
2217
2342
 
2218
2343
  // Release terminal ownership of fd 2 first so external programs,
2219
2344
  // suspend, and shutdown see the real stderr even if a later teardown
@@ -2302,6 +2427,7 @@ export class ProcessTerminal implements Terminal {
2302
2427
  this.#da1SentinelOwners.length = 0;
2303
2428
  this.#privateModeCallbacks = [];
2304
2429
  this.#privateModeSupport.clear();
2430
+ this.#rearmBracketedPaste = false;
2305
2431
  this.#xtermScrollToBottomRestoreModes.clear();
2306
2432
  this.#reportedColumns = undefined;
2307
2433
  this.#reportedRows = undefined;
@@ -2328,6 +2454,7 @@ export class ProcessTerminal implements Terminal {
2328
2454
  this.#stdinBuffer.destroy();
2329
2455
  this.#stdinBuffer = undefined;
2330
2456
  }
2457
+ this.#stdinSequenceHandler = undefined;
2331
2458
 
2332
2459
  // Remove event handlers
2333
2460
  if (this.#stdinDataHandler) {
@@ -2413,7 +2540,7 @@ export class ProcessTerminal implements Terminal {
2413
2540
  write(data: string): void {
2414
2541
  // Hosts can reset bracketed paste (e.g. iTerm2's Terminal State toggle).
2415
2542
  // Reassert it with existing output, never on a timer while idle.
2416
- if (data && this.#active && this.#privateModeSupport.get(2004)) data = `\x1b[?2004h${data}`;
2543
+ if (data && this.#active && this.#rearmBracketedPaste) data = `\x1b[?2004h${data}`;
2417
2544
  this.#safeWrite(data);
2418
2545
  if (this.#writeLogPath) {
2419
2546
  try {
@@ -9,7 +9,7 @@ export type SymbolMap = Record<SymbolKey, string>;
9
9
  * resolves through `Theme.cmd` to either a dedicated `cmd.*` symbol or an
10
10
  * existing `icon.*` symbol shared with the rest of the UI.
11
11
  */
12
- export type SlashCommandIconName = "action" | "prompt" | "extension" | "settings" | "gear" | "shield" | "wave" | "compass" | "inbox" | "swap" | "expand" | "computer" | "eye" | "todo" | "stats" | "news" | "keyboard" | "export" | "clipboard" | "share" | "broadcast" | "globe" | "copy" | "plus" | "restart" | "eraser" | "trash" | "compress" | "vibrate" | "handoff" | "history" | "question" | "rocket" | "stethoscope" | "redo" | "bug" | "memory" | "pencil" | "folderMove" | "folderPlus" | "folderMinus" | "hammer" | "power" | "cart" | "model" | "plan" | "prewalk" | "goal" | "pause" | "loop" | "session" | "jobs" | "gauge" | "context" | "agents" | "branch" | "tree" | "signIn" | "signOut" | "advisor" | "host" | "package" | "fast" | "voice" | "tools" | "rule" | "skill" | "mcp" | "pin";
12
+ export type SlashCommandIconName = "action" | "prompt" | "extension" | "settings" | "gear" | "shield" | "wave" | "compass" | "inbox" | "swap" | "expand" | "computer" | "eye" | "todo" | "stats" | "news" | "keyboard" | "export" | "clipboard" | "share" | "broadcast" | "globe" | "copy" | "plus" | "restart" | "eraser" | "trash" | "compress" | "vibrate" | "handoff" | "history" | "question" | "rocket" | "stethoscope" | "redo" | "bug" | "memory" | "pencil" | "folderMove" | "folderPlus" | "folderMinus" | "hammer" | "power" | "cart" | "model" | "plan" | "prewalk" | "goal" | "pause" | "loop" | "session" | "jobs" | "gauge" | "context" | "ghost" | "agents" | "branch" | "tree" | "signIn" | "signOut" | "advisor" | "host" | "package" | "fast" | "voice" | "tools" | "rule" | "skill" | "mcp" | "pin";
13
13
  export declare const SYMBOL_PRESETS: Record<SymbolPreset, SymbolMap>;
14
14
  export type SpinnerType = "status" | "activity";
15
15
  export declare const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>>;
@@ -371,6 +371,7 @@ export type SlashCommandIconName =
371
371
  | "jobs"
372
372
  | "gauge"
373
373
  | "context"
374
+ | "ghost"
374
375
  | "agents"
375
376
  | "branch"
376
377
  | "tree"
@@ -729,6 +729,7 @@ export class Theme {
729
729
  jobs: this.#symbols["icon.job"],
730
730
  gauge: this.#symbols["icon.throughput"],
731
731
  context: this.#symbols["icon.context"],
732
+ ghost: this.#symbols["icon.ghost"],
732
733
  agents: this.#symbols["icon.agents"],
733
734
  branch: this.#symbols["icon.branch"],
734
735
  tree: this.#symbols["icon.worktree"],
@@ -7,6 +7,17 @@ export { isValidThemeColor, type ThemeBg, type ThemeColor } from "./schema";
7
7
  export { getAvailableSymbolPresets, isValidSymbolPreset, type SpinnerType, type SymbolKey, type SymbolPreset, } from "./symbols";
8
8
  export { Theme } from "./theme-class";
9
9
  export { createHighlightStream, getEditorTheme, getMarkdownTheme, getMarkdownThemeWithLinkTargets, getSelectListTheme, getSettingsListTheme, getSymbolTheme, highlightCode, setMarkdownMermaidRendering, warmHighlighter, } from "./tui-adapters";
10
+ /**
11
+ * Subscribe to native fallback appearance changes independently of theme changes.
12
+ * Consumers must re-check the effective background: COLORFGBG can override it.
13
+ */
14
+ export declare function onTerminalBackgroundFallbackChange(callback: () => void): () => void;
15
+ /**
16
+ * Detect appearance from trusted OSC 11 reports, then COLORFGBG, then native
17
+ * macOS appearance only on known-broken multiplexer background-query paths.
18
+ * Defaults to dark when none of those signals is available.
19
+ */
20
+ export declare function detectTerminalBackground(): "dark" | "light";
10
21
  /** Active theme; imports remain live across initialization, previews, and theme switches. */
11
22
  export declare var theme: Theme;
12
23
  type ThemeBinding = (value: Theme) => void;