@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/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
@@ -878,6 +941,7 @@ export class ProcessTerminal implements Terminal {
878
941
  #sixelSupportCallbacks: Array<(supported: boolean) => void> = [];
879
942
  #tspPending = false;
880
943
  #tspResult: TspHello | null | undefined;
944
+ #tspFallbackLogged = false;
881
945
  #tspCallbacks: TspHelloHandler[] = [];
882
946
  /** Hello deadline inside tmux, where no DA1 sentinel can end the probe. */
883
947
  #tspTimeout: NodeJS.Timeout | undefined;
@@ -889,6 +953,12 @@ export class ProcessTerminal implements Terminal {
889
953
  /** Resolved DECRQM support per private mode (mode → supported). */
890
954
  #privateModeSupport = new Map<number, boolean>();
891
955
  #privateModeCallbacks: PrivateModeReportHandler[] = [];
956
+ /**
957
+ * Whether output and input reassert DEC 2004: set once DECRPM confirms it,
958
+ * or at input attach inside a multiplexer that gates paste markers on the
959
+ * pane's own mode without answering DECRQM (tmux <= 3.5a, #15596).
960
+ */
961
+ #rearmBracketedPaste = false;
892
962
  /** Whether DEC 2048 in-band resize notifications are currently enabled. */
893
963
  #inBandResizeActive = false;
894
964
  /** Reassembly buffer for a DEC 2048 in-band resize report split across stdin reads. */
@@ -913,14 +983,22 @@ export class ProcessTerminal implements Terminal {
913
983
  }
914
984
 
915
985
  get kittyEnableSequence(): string | null {
916
- return this.#kittyProtocolActive ? this.#kittyEnableSeq : null;
986
+ return this.#kittyProtocolActive ? `\x1b[>${this.#kittyFlags}u` : null;
917
987
  }
918
988
 
919
989
  get keyboardEnhancementEnterSequence(): string | null {
920
- if (this.#kittyProtocolActive) return this.#kittyEnableSeq;
990
+ if (this.#kittyProtocolActive) return `\x1b[>${this.#kittyFlags}u`;
921
991
  return this.#modifyOtherKeysActive ? "\x1b[>4;2m" : null;
922
992
  }
923
993
 
994
+ get keyboardEnhancementRestoreSequence(): string | null {
995
+ // `CSI = flags ; 1 u` replaces the main-screen stack top, which is omp's own
996
+ // frame, so it is a no-op wherever per-screen stacks work. Windows Terminal
997
+ // 1.25–1.27 restore the pre-push flags on `?1049l` (microsoft/terminal#20763),
998
+ // dropping to legacy keys: Shift+Enter arrives as `\r`, Ctrl+Enter as `\n`.
999
+ return this.#kittyProtocolActive ? `\x1b[=${this.#kittyFlags};1u` : null;
1000
+ }
1001
+
924
1002
  get keyboardEnhancementExitSequence(): string | null {
925
1003
  // kitty is a stack push (per-screen), so the matching pop balances alt-screen
926
1004
  // entry. xterm modifyOtherKeys is a single global flag with no per-screen
@@ -1014,13 +1092,13 @@ export class ProcessTerminal implements Terminal {
1014
1092
  }
1015
1093
 
1016
1094
  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.
1095
+ // A multiplexer or relay started from Tern can leave `TERM_PROGRAM=tern`
1096
+ // behind while swallowing APC, even when `PI_TUI_NATIVE=1` forces the probe.
1019
1097
  return (
1020
- $env.TERM_PROGRAM?.toLowerCase() === "tern" &&
1021
- !isInsideTerminalMultiplexer($env) &&
1098
+ isTernPane($env) &&
1099
+ tspPathBlocker($env) === null &&
1022
1100
  !isTerminalHeadless() &&
1023
- this.#shouldQueryTspSupport()
1101
+ this.#tspProbeSkipReason() === null
1024
1102
  );
1025
1103
  }
1026
1104
 
@@ -1048,6 +1126,8 @@ export class ProcessTerminal implements Terminal {
1048
1126
  // Own stdout: out-of-band writers route through this terminal, and the
1049
1127
  // emergency restore finds it on crash.
1050
1128
  setActiveTerminal(this);
1129
+ // RGB belongs to this ownership session, including deferred-input prepaint.
1130
+ setTerminalBackgroundColor(undefined);
1051
1131
  terminalEverStarted = true;
1052
1132
  // Own the blocking write(2) on a pump thread (unix TTYs only). A stale
1053
1133
  // prebuilt natives module without the export falls back to direct writes.
@@ -1128,6 +1208,9 @@ export class ProcessTerminal implements Terminal {
1128
1208
 
1129
1209
  // Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~
1130
1210
  this.#safeWrite("\x1b[?2004h");
1211
+ this.#rearmBracketedPaste =
1212
+ terminalMultiplexerSessions($env).some(multiplexer => multiplexer.gatesBracketedPaste) ||
1213
+ classifyTerminalMultiplexerModule($env)?.gatesBracketedPaste === true;
1131
1214
 
1132
1215
  // Force normal cursor-key (DECCKM) and numeric-keypad mode (terminfo
1133
1216
  // `rmkx` = "\x1b[?1l\x1b>"). zeta decodes both CSI ("\x1b[A") and SS3
@@ -1501,7 +1584,10 @@ export class ProcessTerminal implements Terminal {
1501
1584
  }
1502
1585
  case "tsp": {
1503
1586
  // DA1 before any `tsp;r` reply: the terminal doesn't speak TSP.
1504
- this.#resolveTspSupport(null);
1587
+ this.#resolveTspSupport(
1588
+ null,
1589
+ "DA1 was answered before a hello reply: the terminal, or a layer in between, dropped the APC query",
1590
+ );
1505
1591
  break;
1506
1592
  }
1507
1593
  }
@@ -1536,19 +1622,17 @@ export class ProcessTerminal implements Terminal {
1536
1622
  // ConPTY (native Windows and WSL) drops Shift+letter keypresses
1537
1623
  // entirely when flag 4 (report alternate keys) is set. Use flag 1
1538
1624
  // (disambiguate only), preserving flag 2 if already active.
1539
- this.#kittyEnableSeq = (reportedFlags & 2) !== 0 ? "\x1b[>3u" : "\x1b[>1u";
1540
- this.#safeWrite(this.#kittyEnableSeq);
1625
+ this.#kittyFlags = (reportedFlags & 2) !== 0 ? 3 : 1;
1541
1626
  } else if ((reportedFlags & 2) !== 0) {
1542
1627
  // Preserve event-type reporting already enabled by a parent app.
1543
1628
  // Push level-2 to keep its shortcuts reporting consistently.
1544
- this.#kittyEnableSeq = "\x1b[>7u";
1545
- this.#safeWrite(this.#kittyEnableSeq);
1629
+ this.#kittyFlags = 7;
1546
1630
  } else {
1547
1631
  // Disambiguate escape codes and report base-layout keys for physical
1548
1632
  // shortcut matching, without event reporting that caused regression #3259.
1549
- this.#kittyEnableSeq = "\x1b[>5u";
1550
- this.#safeWrite(this.#kittyEnableSeq);
1633
+ this.#kittyFlags = 5;
1551
1634
  }
1635
+ this.#safeWrite(`\x1b[>${this.#kittyFlags}u`);
1552
1636
  return;
1553
1637
  }
1554
1638
 
@@ -1673,8 +1757,11 @@ export class ProcessTerminal implements Terminal {
1673
1757
 
1674
1758
  // Handler that pipes stdin data through the buffer
1675
1759
  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");
1760
+ // Recover modes reset by the host before the user's next paste. Not
1761
+ // mid-paste: the host is plainly in 2004 then, and per-key reads would
1762
+ // turn one paste into thousands of writes (#15757).
1763
+ if (data && this.#active && this.#rearmBracketedPaste && !this.#stdinBuffer!.pasting)
1764
+ this.#safeWrite("\x1b[?2004h");
1678
1765
  if (this.#win32PasteNormalizer) this.#win32PasteNormalizer.process(data);
1679
1766
  else this.#stdinBuffer!.process(data);
1680
1767
  };
@@ -1837,16 +1924,27 @@ export class ProcessTerminal implements Terminal {
1837
1924
  }
1838
1925
  }
1839
1926
 
1840
- #shouldQueryTspSupport(): boolean {
1927
+ /** Why the `hello` probe does not run, or `null` when it does. */
1928
+ #tspProbeSkipReason(): string | null {
1841
1929
  const override = $env.PI_TUI_NATIVE;
1842
- if (override === "0") return false;
1930
+ if (override === "0") return "PI_TUI_NATIVE=0";
1843
1931
  // tmux makes APC bodies the pane title unless the hosting terminal asked
1844
1932
  // 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();
1933
+ if (tmuxMultiplexer.isInside($env)) {
1934
+ return tmuxSessionSpeaksTsp($env) ? null : "inside a tmux session without the @tern-tsp option";
1935
+ }
1936
+ if (override === "1") return null;
1937
+ // The reply could never arrive.
1938
+ const blocker = tspPathBlocker($env);
1939
+ if (blocker !== null) return blocker;
1940
+ return isBunTestRuntime() && $env.PI_TUI_TSP_PROBE !== "1" ? "bun test runtime" : null;
1941
+ }
1942
+
1943
+ /** A Tern pane that ends up on the classic renderer says why in the log, once. */
1944
+ #logTspFallback(reason: string): void {
1945
+ if (this.#tspFallbackLogged || !isTernPane($env)) return;
1946
+ this.#tspFallbackLogged = true;
1947
+ logger.debug("TSP: Tern pane keeps the classic renderer", { reason });
1850
1948
  }
1851
1949
 
1852
1950
  #queryTspSupport(): void {
@@ -1854,7 +1952,9 @@ export class ProcessTerminal implements Terminal {
1854
1952
  this.#tspResult = undefined;
1855
1953
  this.#tspReplyBuffer = "";
1856
1954
  if (this.#dead) return;
1857
- if (!this.#shouldQueryTspSupport()) {
1955
+ const skipReason = this.#tspProbeSkipReason();
1956
+ if (skipReason !== null) {
1957
+ this.#logTspFallback(skipReason);
1858
1958
  this.#tspResult = null;
1859
1959
  for (const cb of this.#tspCallbacks) cb(null);
1860
1960
  return;
@@ -1866,7 +1966,7 @@ export class ProcessTerminal implements Terminal {
1866
1966
  setTmuxPaneApcTitles(false, $env);
1867
1967
  this.#tspTimeout = setTimeout(() => {
1868
1968
  this.#tspTimeout = undefined;
1869
- this.#resolveTspSupport(null);
1969
+ this.#resolveTspSupport(null, `no hello reply through tmux within ${TSP_TMUX_HELLO_TIMEOUT_MS} ms`);
1870
1970
  }, TSP_TMUX_HELLO_TIMEOUT_MS);
1871
1971
  this.#safeWrite(encodeTspHelloQuery());
1872
1972
  return;
@@ -1894,12 +1994,14 @@ export class ProcessTerminal implements Terminal {
1894
1994
  this.#inputHandler?.(sequence);
1895
1995
  }
1896
1996
 
1897
- #resolveTspSupport(hello: TspHello | null): void {
1997
+ /** `reason` says why a `null` hello means no TSP. */
1998
+ #resolveTspSupport(hello: TspHello | null, reason?: string): void {
1898
1999
  if (!this.#tspPending) return;
1899
2000
  this.#tspPending = false;
1900
2001
  const result = hello !== null && hello.v === TSP_VERSION ? hello : null;
1901
2002
  if (hello !== null && result === null) logger.warn("TSP: unsupported protocol version", { v: hello.v });
1902
2003
  if (result === null) {
2004
+ this.#logTspFallback(reason ?? `unsupported protocol version ${String(hello?.v)}`);
1903
2005
  this.#endTspTmuxPane();
1904
2006
  } else {
1905
2007
  clearTimeout(this.#tspTimeout);
@@ -1942,7 +2044,11 @@ export class ProcessTerminal implements Terminal {
1942
2044
  const max = 16 ** hex.length - 1;
1943
2045
  return max > 0 ? value / max : 0;
1944
2046
  };
1945
- const luminance = 0.299 * normalize(rHex) + 0.587 * normalize(gHex) + 0.114 * normalize(bHex);
2047
+ const r = normalize(rHex);
2048
+ const g = normalize(gHex);
2049
+ const b = normalize(bHex);
2050
+ setTerminalBackgroundColor((Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255));
2051
+ const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
1946
2052
  const mode: TerminalAppearance = luminance < 0.5 ? "dark" : "light";
1947
2053
  const changed = mode !== this.#appearance;
1948
2054
  this.#appearance = mode;
@@ -1967,10 +2073,11 @@ export class ProcessTerminal implements Terminal {
1967
2073
  #enableModifyOtherKeysFallback(): void {
1968
2074
  if (this.#kittyProtocolActive || this.#modifyOtherKeysActive || this.#win32InputDecoder) return;
1969
2075
  const ssh = isSshSession();
1970
- if (this.#conpty && this.#nativeWindowsConsole && !ssh) {
2076
+ if (this.#conpty && !ssh) {
1971
2077
  // 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.
2078
+ // into a bare CR (Ctrl+Enter into LF). win32-input-mode is answered by the
2079
+ // console host serving this process, so it works under every local ConPTY
2080
+ // terminal, and under WSL through the `wsl.exe` relay's console host too.
1974
2081
  // Under sshd the console host is fed the remote terminal's VT stream,
1975
2082
  // and the mode splits arrow keys into Escape plus literal text (#14034).
1976
2083
  this.#safeWrite("\x1b[?9001h");
@@ -1981,7 +2088,10 @@ export class ProcessTerminal implements Terminal {
1981
2088
  data => this.#stdinBuffer?.process(data),
1982
2089
  () => this.#releaseWin32PendingSequence(),
1983
2090
  );
1984
- return;
2091
+ // A WSL pty is not always relayed by a console host (tmux, VS Code
2092
+ // Remote-WSL, WSLg terminals). Those ignore 9001 and keep sending plain
2093
+ // VT, which the decoder passes through, so also ask for modifyOtherKeys.
2094
+ if (this.#nativeWindowsConsole) return;
1985
2095
  }
1986
2096
  // A remote terminal with no identifying env may not understand the request.
1987
2097
  if (ssh && (TERMINAL.id === "base" || TERMINAL.id === "trueColor")) return;
@@ -2075,6 +2185,7 @@ export class ProcessTerminal implements Terminal {
2075
2185
  // only true here after an explicit DECRPM reply (the DA1-sentinel
2076
2186
  // fallback resolves unsupported).
2077
2187
  if (mode === 2004 && supported) {
2188
+ this.#rearmBracketedPaste = true;
2078
2189
  this.#stdinBuffer?.setRawPasteStallProbe(this.#isLoopStalled);
2079
2190
  }
2080
2191
  }
@@ -2209,11 +2320,13 @@ export class ProcessTerminal implements Terminal {
2209
2320
  // Suppress observer/timer callbacks before any teardown can yield or throw.
2210
2321
  this.#active = false;
2211
2322
  this.#inputDeferred = false;
2212
- if (this.#headless) return;
2213
2323
  // Release stdout ownership (out-of-band writers and emergency cleanup)
2324
+ // before headless/teardown exits, without disturbing another owner's RGB.
2214
2325
  if (getActiveTerminal() === this) {
2215
2326
  setActiveTerminal(null);
2327
+ setTerminalBackgroundColor(undefined);
2216
2328
  }
2329
+ if (this.#headless) return;
2217
2330
 
2218
2331
  // Release terminal ownership of fd 2 first so external programs,
2219
2332
  // suspend, and shutdown see the real stderr even if a later teardown
@@ -2302,6 +2415,7 @@ export class ProcessTerminal implements Terminal {
2302
2415
  this.#da1SentinelOwners.length = 0;
2303
2416
  this.#privateModeCallbacks = [];
2304
2417
  this.#privateModeSupport.clear();
2418
+ this.#rearmBracketedPaste = false;
2305
2419
  this.#xtermScrollToBottomRestoreModes.clear();
2306
2420
  this.#reportedColumns = undefined;
2307
2421
  this.#reportedRows = undefined;
@@ -2413,7 +2527,7 @@ export class ProcessTerminal implements Terminal {
2413
2527
  write(data: string): void {
2414
2528
  // Hosts can reset bracketed paste (e.g. iTerm2's Terminal State toggle).
2415
2529
  // Reassert it with existing output, never on a timer while idle.
2416
- if (data && this.#active && this.#privateModeSupport.get(2004)) data = `\x1b[?2004h${data}`;
2530
+ if (data && this.#active && this.#rearmBracketedPaste) data = `\x1b[?2004h${data}`;
2417
2531
  this.#safeWrite(data);
2418
2532
  if (this.#writeLogPath) {
2419
2533
  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;
@@ -5,7 +5,7 @@ import type { Terminal, TerminalAppearance } from "../terminal";
5
5
  import { colorLuma } from "@linxiraos/pi-utils/color";
6
6
  import { getCustomThemesDir } from "@linxiraos/pi-utils/dirs";
7
7
  import * as logger from "@linxiraos/pi-utils/logger";
8
- import { terminalMultiplexerSessions } from "../terminal-multiplexer";
8
+ import { isTerminalBackgroundQueryTrusted } from "../terminal-multiplexer";
9
9
  import { setActiveSymbolTheme } from "./active-symbols";
10
10
  import { ansi256ToHex, resolveThemeColors, resolveVarRefs } from "./color";
11
11
  import {
@@ -50,18 +50,28 @@ var terminalReportedAppearance: "dark" | "light" | undefined;
50
50
  /** Appearance reported by the macOS fallback observer, or undefined if not yet available. */
51
51
  var macOSReportedAppearance: "dark" | "light" | undefined;
52
52
 
53
- function shouldUseMacOSAppearanceFallback(): boolean {
54
- // Some multiplexers break OSC 11 passthrough on some platforms (Zellij on
55
- // macOS), so terminal-derived appearance cannot be trusted there. Fall back
56
- // to host macOS appearance without letting it override valid terminal
57
- // signals elsewhere.
58
- const platform = process.platform;
59
- return terminalMultiplexerSessions().some(multiplexer => multiplexer.backgroundQueryBrokenOn?.includes(platform));
53
+ const backgroundFallbackListeners = new Set<() => void>();
54
+
55
+ /**
56
+ * Subscribe to native fallback appearance changes independently of theme changes.
57
+ * Consumers must re-check the effective background: COLORFGBG can override it.
58
+ */
59
+ export function onTerminalBackgroundFallbackChange(callback: () => void): () => void {
60
+ backgroundFallbackListeners.add(callback);
61
+ return () => {
62
+ backgroundFallbackListeners.delete(callback);
63
+ };
60
64
  }
61
65
 
62
- function detectTerminalBackground(): "dark" | "light" {
66
+ /**
67
+ * Detect appearance from trusted OSC 11 reports, then COLORFGBG, then native
68
+ * macOS appearance only on known-broken multiplexer background-query paths.
69
+ * Defaults to dark when none of those signals is available.
70
+ */
71
+ export function detectTerminalBackground(): "dark" | "light" {
72
+ const backgroundQueryTrusted = isTerminalBackgroundQueryTrusted();
63
73
  // Tier 1: terminal-reported appearance from OSC 11 luminance.
64
- if (!shouldUseMacOSAppearanceFallback() && terminalReportedAppearance) {
74
+ if (backgroundQueryTrusted && terminalReportedAppearance) {
65
75
  return terminalReportedAppearance;
66
76
  }
67
77
 
@@ -76,7 +86,7 @@ function detectTerminalBackground(): "dark" | "light" {
76
86
  }
77
87
 
78
88
  // Tier 3: host macOS appearance for known-broken terminal paths only.
79
- if (shouldUseMacOSAppearanceFallback()) {
89
+ if (!backgroundQueryTrusted) {
80
90
  const macAppearance = macOSReportedAppearance ?? detectMacOSAppearance();
81
91
  if (macAppearance) return macAppearance;
82
92
  }
@@ -214,14 +224,14 @@ export async function initTheme(
214
224
  assignTheme(await loadTheme(name, getCurrentThemeOptions()));
215
225
  if (enableWatcher) {
216
226
  await startThemeWatcher();
217
- startSigwinchListener();
218
227
  }
219
228
  } catch (err) {
220
229
  logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
221
230
  currentThemeName = "dark";
222
231
  assignTheme(await loadTheme("dark", getCurrentThemeOptions()));
223
- // Don't start watcher for fallback theme
232
+ // Skip file watching for the fallback theme, not appearance tracking.
224
233
  }
234
+ if (enableWatcher) startSigwinchListener();
225
235
  }
226
236
 
227
237
  export async function setTheme(
@@ -239,6 +249,14 @@ export async function setTheme(
239
249
  assignTheme(loadedTheme);
240
250
  if (enableWatcher) {
241
251
  await startThemeWatcher();
252
+ // startThemeWatcher() calls stopThemeWatcher(), which tears down
253
+ // the SIGWINCH handler and the macOS appearance observer that
254
+ // feeds the SIXEL background fallback. Restart it like initTheme()
255
+ // does so a runtime theme change does not silence fallback
256
+ // background repaints. Without this, `setTheme(name, true)` after
257
+ // startup would leave the SIXEL repaint path dead until the next
258
+ // process restart.
259
+ startSigwinchListener();
242
260
  }
243
261
  notifyThemeChange();
244
262
  return { success: true };
@@ -667,12 +685,26 @@ var macObserver: { stop(): void } | undefined;
667
685
 
668
686
  function startMacAppearanceObserver(): void {
669
687
  stopMacAppearanceObserver();
670
- if (!shouldUseMacOSAppearanceFallback()) return;
688
+ if (isTerminalBackgroundQueryTrusted()) return;
671
689
  try {
672
690
  macOSReportedAppearance = detectMacOSAppearance() ?? undefined;
673
691
  macObserver = MacAppearanceObserver.start((err, appearance) => {
674
692
  if (!err && (appearance === "dark" || appearance === "light")) {
693
+ const changed = appearance !== macOSReportedAppearance;
675
694
  macOSReportedAppearance = appearance;
695
+ if (changed) {
696
+ // oxlint-disable-next-line unicorn/no-useless-spread -- listeners may unsubscribe while reporting
697
+ for (const listener of [...backgroundFallbackListeners]) {
698
+ try {
699
+ listener();
700
+ } catch (err) {
701
+ // Debug-level: a listener failure must not break sibling
702
+ // listeners or the appearance observer. Visible only in
703
+ // local debug logs, never in normal runs.
704
+ logger.debug("macOS appearance fallback listener failed", { err });
705
+ }
706
+ }
707
+ }
676
708
  reevaluateAutoThemeForAppearance("macOS fallback");
677
709
  }
678
710
  });
package/src/thinking.d.ts CHANGED
@@ -37,6 +37,11 @@ export declare function shouldDisableReasoning(level: ThinkingLevel | undefined)
37
37
  * Resolves a selector against the current model while preserving explicit "off".
38
38
  */
39
39
  export declare function resolveThinkingLevelForModel(model: Model | undefined, level: ThinkingLevel | undefined): ResolvedThinkingLevel | undefined;
40
+ /**
41
+ * Level applied to `model` when the user has not picked one for it (carried,
42
+ * global default, auto). Unverified thinking stays unset until the user picks.
43
+ */
44
+ export declare function automaticThinkingLevel<T>(model: Model | undefined, fallback: T): T | undefined;
40
45
  /**
41
46
  * Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
42
47
  * inside the coding-agent layer: it is never an {@link Effort} or
package/src/thinking.ts CHANGED
@@ -132,6 +132,14 @@ export function resolveThinkingLevelForModel(
132
132
  return clampThinkingLevelForModel(model, level);
133
133
  }
134
134
 
135
+ /**
136
+ * Level applied to `model` when the user has not picked one for it (carried,
137
+ * global default, auto). Unverified thinking stays unset until the user picks.
138
+ */
139
+ export function automaticThinkingLevel<T>(model: Model | undefined, fallback: T): T | undefined {
140
+ return model?.thinking?.unverified ? undefined : fallback;
141
+ }
142
+
135
143
  /**
136
144
  * Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
137
145
  * inside the coding-agent layer: it is never an {@link Effort} or
@@ -374,7 +382,7 @@ export function modelSupportsEffortCeiling(model: Model, ceiling: Effort): boole
374
382
  * models.
375
383
  */
376
384
  export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
377
- if (!model?.reasoning) return undefined;
385
+ if (!model?.reasoning || model.thinking?.unverified) return undefined;
378
386
  const preferred = model.thinking?.defaultLevel ?? Effort.High;
379
387
  return clampAutoThinkingEffort(model, preferred === Effort.Max ? Effort.XHigh : preferred, Effort.XHigh);
380
388
  }