@linxiraos/pi-tui 1.1.27 → 1.2.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 (376) hide show
  1. package/CHANGELOG.md +50 -461
  2. package/README.md +7 -5
  3. package/dist/types/app-keybindings.d.ts +1 -1
  4. package/dist/types/apps/cleanse-board.d.ts +1 -1
  5. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  6. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  7. package/dist/types/apps/git/git-tui.d.ts +2 -2
  8. package/dist/types/apps/git/help.d.ts +3 -1
  9. package/dist/types/apps/if-bench-board.d.ts +2 -2
  10. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  11. package/dist/types/autocomplete.d.ts +5 -2
  12. package/dist/types/charts/chart-plan.d.ts +674 -0
  13. package/dist/types/charts/chart-svg.d.ts +59 -0
  14. package/dist/types/charts/table-data.d.ts +175 -0
  15. package/dist/types/chat/assistant-message.d.ts +5 -0
  16. package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
  17. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  18. package/dist/types/chat/execution-shared.d.ts +6 -5
  19. package/dist/types/chat/fence-figure.d.ts +42 -0
  20. package/dist/types/chat/figure-markdown.d.ts +42 -0
  21. package/dist/types/chat/messages.d.ts +1 -1
  22. package/dist/types/chat/svg-figure.d.ts +33 -0
  23. package/dist/types/chat/svg-source.d.ts +49 -0
  24. package/dist/types/chat/table-chart.d.ts +89 -0
  25. package/dist/types/chat/tool-execution.d.ts +2 -0
  26. package/dist/types/chat/user-message.d.ts +15 -2
  27. package/dist/types/chrome/shared.d.ts +9 -0
  28. package/dist/types/chrome/transcript-container.d.ts +6 -0
  29. package/dist/types/components/box.d.ts +1 -0
  30. package/dist/types/components/disclosure.d.ts +1 -0
  31. package/dist/types/components/editor.d.ts +15 -4
  32. package/dist/types/components/form.d.ts +1 -0
  33. package/dist/types/components/image.d.ts +12 -44
  34. package/dist/types/components/key-value-list.d.ts +1 -0
  35. package/dist/types/components/layout/row.d.ts +1 -0
  36. package/dist/types/components/layout/stack.d.ts +1 -0
  37. package/dist/types/components/markdown.d.ts +36 -3
  38. package/dist/types/components/scroll-view.d.ts +1 -0
  39. package/dist/types/components/section.d.ts +1 -0
  40. package/dist/types/components/table.d.ts +1 -0
  41. package/dist/types/components/text.d.ts +1 -0
  42. package/dist/types/components/tree-view.d.ts +7 -0
  43. package/dist/types/components/truncated-text.d.ts +1 -0
  44. package/dist/types/fuzzy.d.ts +29 -1
  45. package/dist/types/key-hint-format.d.ts +1 -1
  46. package/dist/types/keybinding-matchers.d.ts +3 -0
  47. package/dist/types/keybindings.d.ts +1 -1
  48. package/dist/types/kitty-graphics.d.ts +2 -2
  49. package/dist/types/loop-watchdog.d.ts +8 -7
  50. package/dist/types/multiplexers/cmux.d.ts +16 -0
  51. package/dist/types/multiplexers/herdr.d.ts +26 -0
  52. package/dist/types/multiplexers/notify.d.ts +13 -0
  53. package/dist/types/multiplexers/orca.d.ts +12 -0
  54. package/dist/types/multiplexers/screen.d.ts +9 -0
  55. package/dist/types/multiplexers/tmux.d.ts +29 -0
  56. package/dist/types/multiplexers/types.d.ts +100 -0
  57. package/dist/types/multiplexers/wmux.d.ts +12 -0
  58. package/dist/types/multiplexers/zellij.d.ts +12 -0
  59. package/dist/types/native/backend.d.ts +3 -2
  60. package/dist/types/native/blobs.d.ts +7 -3
  61. package/dist/types/native/encode.d.ts +3 -4
  62. package/dist/types/native/overlay.d.ts +5 -0
  63. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  64. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  65. package/dist/types/overlays/agent-hub.d.ts +2 -0
  66. package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
  67. package/dist/types/overlays/agents-hub.d.ts +9 -0
  68. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  69. package/dist/types/overlays/annotation-types.d.ts +2 -0
  70. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  71. package/dist/types/overlays/history-search.d.ts +10 -0
  72. package/dist/types/overlays/hook-selector.d.ts +9 -4
  73. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  74. package/dist/types/overlays/model-browser.d.ts +54 -7
  75. package/dist/types/overlays/model-hub.d.ts +23 -0
  76. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  77. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  78. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  79. package/dist/types/overlays/session-selector.d.ts +6 -2
  80. package/dist/types/overlays/settings-defs.d.ts +1 -0
  81. package/dist/types/overlays/usage-dashboard.d.ts +8 -4
  82. package/dist/types/overlays/usage-display.d.ts +3 -1
  83. package/dist/types/prompt/composer.d.ts +2 -7
  84. package/dist/types/prompt/custom-editor.d.ts +11 -8
  85. package/dist/types/prompt/image-source.d.ts +2 -1
  86. package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
  87. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  88. package/dist/types/prompt/welcome.d.ts +19 -42
  89. package/dist/types/render/composite.d.ts +2 -0
  90. package/dist/types/render/output-pane.d.ts +1 -0
  91. package/dist/types/render/render-utils.d.ts +1 -0
  92. package/dist/types/render/tool-card.d.ts +6 -0
  93. package/dist/types/render/width-aware-text.d.ts +2 -0
  94. package/dist/types/setup/scenes/splash.d.ts +1 -1
  95. package/dist/types/setup/scenes/types.d.ts +1 -0
  96. package/dist/types/status-line/component.d.ts +3 -0
  97. package/dist/types/status-line/host.d.ts +4 -1
  98. package/dist/types/status-line/segments.d.ts +2 -0
  99. package/dist/types/status-line/types.d.ts +14 -3
  100. package/dist/types/terminal-capabilities.d.ts +27 -35
  101. package/dist/types/terminal-multiplexer.d.ts +127 -14
  102. package/dist/types/terminal.d.ts +22 -6
  103. package/dist/types/theme/active-symbols.d.ts +1 -1
  104. package/dist/types/theme/schema.d.ts +11 -2
  105. package/dist/types/theme/symbols.d.ts +1 -1
  106. package/dist/types/theme/theme-class.d.ts +7 -0
  107. package/dist/types/theme/theme.d.ts +1 -1
  108. package/dist/types/theme/tui-adapters.d.ts +3 -0
  109. package/dist/types/tmux.d.ts +11 -4
  110. package/dist/types/tools/daemon.d.ts +1 -1
  111. package/dist/types/tools/eval.d.ts +22 -3
  112. package/dist/types/tools/glob.d.ts +4 -0
  113. package/dist/types/tools/native-view.d.ts +47 -0
  114. package/dist/types/tools/output-meta.d.ts +11 -1
  115. package/dist/types/tools/renderer.d.ts +37 -2
  116. package/dist/types/tools/streaming-output.d.ts +20 -1
  117. package/dist/types/tools/task.d.ts +0 -4
  118. package/dist/types/tools/write.d.ts +9 -2
  119. package/dist/types/tools/yield.d.ts +22 -0
  120. package/dist/types/tui.d.ts +31 -0
  121. package/dist/types/utils.d.ts +6 -0
  122. package/dist/types/vim.d.ts +19 -5
  123. package/package.json +13 -9
  124. package/src/app-keybindings.d.ts +1 -1
  125. package/src/app-keybindings.ts +1 -1
  126. package/src/apps/autoresearch-dashboard.ts +7 -3
  127. package/src/apps/cleanse-board.ts +2 -2
  128. package/src/apps/cleanse-picker.ts +1 -1
  129. package/src/apps/debug/log-viewer.ts +188 -72
  130. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  131. package/src/apps/debug/raw-sse.ts +39 -5
  132. package/src/apps/git/diff-pane.ts +26 -5
  133. package/src/apps/git/git-tui.ts +2 -2
  134. package/src/apps/git/help.ts +29 -20
  135. package/src/apps/if-bench-board.ts +2 -2
  136. package/src/apps/ps-top.ts +2 -2
  137. package/src/apps/setup-model-picker.ts +1 -1
  138. package/src/autocomplete.d.ts +5 -2
  139. package/src/autocomplete.ts +67 -43
  140. package/src/charts/chart-plan.d.ts +674 -0
  141. package/src/charts/chart-plan.ts +5081 -0
  142. package/src/charts/chart-svg.d.ts +59 -0
  143. package/src/charts/chart-svg.ts +4657 -0
  144. package/src/charts/table-data.d.ts +175 -0
  145. package/src/charts/table-data.ts +1478 -0
  146. package/src/chat/advisor-message.ts +11 -0
  147. package/src/chat/assistant-message.d.ts +5 -0
  148. package/src/chat/assistant-message.ts +219 -50
  149. package/src/chat/chat-transcript-builder.d.ts +2 -0
  150. package/src/chat/chat-transcript-builder.ts +5 -3
  151. package/src/chat/compaction-summary-message.d.ts +1 -0
  152. package/src/chat/compaction-summary-message.ts +8 -0
  153. package/src/chat/execution-shared.d.ts +6 -5
  154. package/src/chat/execution-shared.ts +42 -25
  155. package/src/chat/fence-figure.d.ts +42 -0
  156. package/src/chat/fence-figure.ts +80 -0
  157. package/src/chat/figure-markdown.d.ts +42 -0
  158. package/src/chat/figure-markdown.ts +174 -0
  159. package/src/chat/messages.d.ts +1 -1
  160. package/src/chat/messages.ts +1 -1
  161. package/src/chat/read-tool-group.ts +6 -4
  162. package/src/chat/skill-message.ts +6 -0
  163. package/src/chat/svg-figure.d.ts +33 -0
  164. package/src/chat/svg-figure.ts +399 -0
  165. package/src/chat/svg-source.d.ts +49 -0
  166. package/src/chat/svg-source.ts +188 -0
  167. package/src/chat/table-chart.d.ts +89 -0
  168. package/src/chat/table-chart.ts +294 -0
  169. package/src/chat/tool-execution.d.ts +2 -0
  170. package/src/chat/tool-execution.ts +80 -6
  171. package/src/chat/user-message.d.ts +15 -2
  172. package/src/chat/user-message.ts +55 -3
  173. package/src/chrome/shared.ts +53 -0
  174. package/src/chrome/transcript-container.d.ts +6 -0
  175. package/src/chrome/transcript-container.ts +93 -13
  176. package/src/components/box.d.ts +1 -0
  177. package/src/components/box.ts +5 -0
  178. package/src/components/disclosure.d.ts +1 -0
  179. package/src/components/disclosure.ts +8 -0
  180. package/src/components/editor.d.ts +15 -4
  181. package/src/components/editor.ts +294 -46
  182. package/src/components/form.d.ts +1 -0
  183. package/src/components/form.ts +8 -0
  184. package/src/components/image.d.ts +12 -44
  185. package/src/components/image.ts +47 -151
  186. package/src/components/key-value-list.d.ts +1 -0
  187. package/src/components/key-value-list.ts +4 -0
  188. package/src/components/layout/row.d.ts +1 -0
  189. package/src/components/layout/row.ts +7 -0
  190. package/src/components/layout/stack.d.ts +1 -0
  191. package/src/components/layout/stack.ts +7 -0
  192. package/src/components/markdown.d.ts +36 -3
  193. package/src/components/markdown.ts +433 -318
  194. package/src/components/scroll-view.d.ts +1 -0
  195. package/src/components/scroll-view.ts +5 -0
  196. package/src/components/section.d.ts +1 -0
  197. package/src/components/section.ts +5 -0
  198. package/src/components/settings-list.ts +2 -0
  199. package/src/components/table.d.ts +1 -0
  200. package/src/components/table.ts +4 -0
  201. package/src/components/text.d.ts +1 -0
  202. package/src/components/text.ts +7 -0
  203. package/src/components/tree-view.d.ts +7 -0
  204. package/src/components/tree-view.ts +38 -8
  205. package/src/components/truncated-text.d.ts +1 -0
  206. package/src/components/truncated-text.ts +5 -0
  207. package/src/desktop-notify.ts +1 -1
  208. package/src/fuzzy.d.ts +29 -1
  209. package/src/fuzzy.ts +115 -30
  210. package/src/key-hint-format.d.ts +1 -1
  211. package/src/key-hint-format.ts +1 -1
  212. package/src/keybinding-matchers.d.ts +3 -0
  213. package/src/keybinding-matchers.ts +6 -2
  214. package/src/keybindings.d.ts +1 -1
  215. package/src/keybindings.ts +1 -1
  216. package/src/kitty-graphics.d.ts +2 -2
  217. package/src/kitty-graphics.ts +4 -4
  218. package/src/loop-watchdog.d.ts +8 -7
  219. package/src/loop-watchdog.ts +18 -13
  220. package/src/multiplexers/cmux.d.ts +16 -0
  221. package/src/multiplexers/cmux.ts +29 -0
  222. package/src/multiplexers/herdr.d.ts +26 -0
  223. package/src/multiplexers/herdr.ts +64 -0
  224. package/src/multiplexers/notify.d.ts +13 -0
  225. package/src/multiplexers/notify.ts +24 -0
  226. package/src/multiplexers/orca.d.ts +12 -0
  227. package/src/multiplexers/orca.ts +16 -0
  228. package/src/multiplexers/screen.d.ts +9 -0
  229. package/src/multiplexers/screen.ts +15 -0
  230. package/src/multiplexers/tmux.d.ts +29 -0
  231. package/src/multiplexers/tmux.ts +40 -0
  232. package/src/multiplexers/types.d.ts +100 -0
  233. package/src/multiplexers/types.ts +110 -0
  234. package/src/multiplexers/wmux.d.ts +12 -0
  235. package/src/multiplexers/wmux.ts +16 -0
  236. package/src/multiplexers/zellij.d.ts +12 -0
  237. package/src/multiplexers/zellij.ts +24 -0
  238. package/src/native/backend.d.ts +3 -2
  239. package/src/native/backend.ts +174 -18
  240. package/src/native/blobs.d.ts +7 -3
  241. package/src/native/blobs.ts +47 -12
  242. package/src/native/encode.d.ts +3 -4
  243. package/src/native/encode.ts +58 -31
  244. package/src/native/overlay.d.ts +5 -0
  245. package/src/native/overlay.ts +13 -0
  246. package/src/native/reconcile.ts +4 -0
  247. package/src/overlays/agent-hub-projection.d.ts +6 -5
  248. package/src/overlays/agent-hub-projection.ts +124 -13
  249. package/src/overlays/agent-hub-types.d.ts +9 -0
  250. package/src/overlays/agent-hub-types.ts +6 -0
  251. package/src/overlays/agent-hub.ts +46 -31
  252. package/src/overlays/agent-transcript-viewer.ts +91 -23
  253. package/src/overlays/agents-hub.ts +28 -8
  254. package/src/overlays/annotation-overlay.ts +293 -90
  255. package/src/overlays/annotation-types.ts +2 -0
  256. package/src/overlays/ask-dialog.ts +7 -10
  257. package/src/overlays/btw-history-panel.ts +12 -1
  258. package/src/overlays/copy-selector.ts +30 -8
  259. package/src/overlays/extensions/state-manager.ts +52 -12
  260. package/src/overlays/history-search.ts +100 -7
  261. package/src/overlays/hook-selector.ts +12 -5
  262. package/src/overlays/jobs-panel.ts +28 -46
  263. package/src/overlays/model-browser.ts +341 -110
  264. package/src/overlays/model-hub.ts +300 -49
  265. package/src/overlays/move-overlay.ts +28 -14
  266. package/src/overlays/oauth-selector.ts +54 -70
  267. package/src/overlays/plan-review-overlay.ts +194 -70
  268. package/src/overlays/plugin-settings.ts +11 -5
  269. package/src/overlays/report-panel.ts +2 -2
  270. package/src/overlays/rewind-selector.ts +86 -17
  271. package/src/overlays/session-observer-registry.d.ts +2 -0
  272. package/src/overlays/session-observer-registry.ts +10 -2
  273. package/src/overlays/session-selector.ts +64 -26
  274. package/src/overlays/settings-defs.ts +4 -3
  275. package/src/overlays/settings-selector.ts +33 -8
  276. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  277. package/src/overlays/stats-notice.ts +1 -1
  278. package/src/overlays/tree-selector.ts +41 -20
  279. package/src/overlays/usage-dashboard.ts +100 -50
  280. package/src/overlays/usage-display.ts +10 -4
  281. package/src/prompt/attachment-chips.ts +15 -11
  282. package/src/prompt/composer-attachments.ts +6 -4
  283. package/src/prompt/composer-cache.ts +4 -6
  284. package/src/prompt/composer.ts +6 -37
  285. package/src/prompt/custom-editor.ts +54 -26
  286. package/src/prompt/image-source.ts +2 -1
  287. package/src/prompt/interactive-context-helpers.ts +30 -18
  288. package/src/prompt/macos-spelling.ts +7 -0
  289. package/src/prompt/magic-keywords.ts +11 -2
  290. package/src/prompt/model-mention-autocomplete.ts +14 -14
  291. package/src/prompt/tips.txt +6 -5
  292. package/src/prompt/welcome.ts +235 -411
  293. package/src/prompt/word-completion.ts +38 -5
  294. package/src/render/composite.d.ts +2 -0
  295. package/src/render/composite.ts +64 -0
  296. package/src/render/output-pane.d.ts +1 -0
  297. package/src/render/output-pane.ts +6 -0
  298. package/src/render/render-utils.d.ts +1 -0
  299. package/src/render/render-utils.ts +11 -0
  300. package/src/render/tool-card.d.ts +6 -0
  301. package/src/render/tool-card.ts +15 -0
  302. package/src/render/width-aware-text.d.ts +2 -0
  303. package/src/render/width-aware-text.ts +5 -0
  304. package/src/setup/scenes/model.ts +2 -1
  305. package/src/setup/scenes/splash.ts +116 -53
  306. package/src/setup/scenes/types.ts +1 -0
  307. package/src/status-line/component.ts +292 -78
  308. package/src/status-line/host.ts +4 -1
  309. package/src/status-line/segments.ts +40 -22
  310. package/src/status-line/types.ts +15 -3
  311. package/src/terminal-capabilities.d.ts +27 -35
  312. package/src/terminal-capabilities.ts +75 -189
  313. package/src/terminal-multiplexer.d.ts +127 -14
  314. package/src/terminal-multiplexer.ts +112 -33
  315. package/src/terminal.d.ts +22 -6
  316. package/src/terminal.ts +139 -37
  317. package/src/theme/active-symbols.d.ts +1 -1
  318. package/src/theme/active-symbols.ts +1 -1
  319. package/src/theme/schema-validation.ts +1 -0
  320. package/src/theme/schema.d.ts +11 -2
  321. package/src/theme/schema.ts +14 -1
  322. package/src/theme/session-color.ts +1 -7
  323. package/src/theme/symbols.d.ts +1 -1
  324. package/src/theme/symbols.ts +6 -1
  325. package/src/theme/theme-class.d.ts +7 -0
  326. package/src/theme/theme-class.ts +26 -1
  327. package/src/theme/theme-schema.json +5 -1
  328. package/src/theme/theme.d.ts +1 -1
  329. package/src/theme/theme.ts +8 -4
  330. package/src/theme/tui-adapters.d.ts +3 -0
  331. package/src/theme/tui-adapters.ts +68 -0
  332. package/src/tmux.d.ts +11 -4
  333. package/src/tmux.ts +47 -11
  334. package/src/tools/ast-edit.ts +59 -35
  335. package/src/tools/ast-grep.ts +35 -16
  336. package/src/tools/bash.ts +92 -42
  337. package/src/tools/daemon.d.ts +1 -1
  338. package/src/tools/daemon.ts +1 -1
  339. package/src/tools/default-renderer.ts +35 -19
  340. package/src/tools/edit.ts +142 -120
  341. package/src/tools/eval-format/index.ts +28 -2
  342. package/src/tools/eval-format/javascript.ts +7 -5
  343. package/src/tools/eval.d.ts +22 -3
  344. package/src/tools/eval.ts +364 -208
  345. package/src/tools/fetch.ts +5 -6
  346. package/src/tools/glob.d.ts +4 -0
  347. package/src/tools/glob.ts +33 -65
  348. package/src/tools/grep.ts +81 -48
  349. package/src/tools/index.ts +2 -0
  350. package/src/tools/lsp.ts +81 -59
  351. package/src/tools/mcp.ts +111 -68
  352. package/src/tools/native-view.d.ts +47 -0
  353. package/src/tools/native-view.ts +102 -1
  354. package/src/tools/output-meta.d.ts +11 -1
  355. package/src/tools/output-meta.ts +23 -3
  356. package/src/tools/read.ts +7 -5
  357. package/src/tools/renderer.d.ts +37 -2
  358. package/src/tools/renderer.ts +42 -2
  359. package/src/tools/streaming-output.d.ts +20 -1
  360. package/src/tools/streaming-output.ts +149 -50
  361. package/src/tools/task.d.ts +0 -4
  362. package/src/tools/task.ts +112 -59
  363. package/src/tools/todo.ts +119 -30
  364. package/src/tools/wait.ts +34 -24
  365. package/src/tools/web-search.ts +1 -1
  366. package/src/tools/write.d.ts +9 -2
  367. package/src/tools/write.ts +147 -95
  368. package/src/tools/yield.d.ts +22 -0
  369. package/src/tools/yield.ts +225 -0
  370. package/src/tui.d.ts +31 -0
  371. package/src/tui.ts +309 -160
  372. package/src/utils.d.ts +6 -0
  373. package/src/utils.ts +19 -0
  374. package/src/vim.d.ts +19 -5
  375. package/src/vim.ts +333 -18
  376. package/src/windows-input-mode.ts +8 -0
@@ -1,45 +1,124 @@
1
- /** True when this process is running inside a Herdr pane. */
2
- export function isInsideHerdr(env: NodeJS.ProcessEnv = Bun.env): boolean {
3
- // HERDR_ENV=1 is canonical. Identity vars survive env-sanitizing launchers
4
- // that drop HERDR_ENV. Do not use HERDR_SOCKET_PATH, HERDR_BIN_PATH,
5
- // HERDR_SESSION, HERDR_CONFIG_PATH, or HERDR_CLIENT_SOCKET_PATH here: they
6
- // are client-side and can be set outside a Herdr pane, matching the
7
- // CMUX_SOCKET_PATH warning below.
8
- if (env.HERDR_ENV === "1") return true;
9
- if (env.HERDR_PANE_ID || env.HERDR_TAB_ID || env.HERDR_WORKSPACE_ID) return true;
10
- return false;
1
+ import { cmuxMultiplexer } from "./multiplexers/cmux";
2
+ import { herdrMultiplexer } from "./multiplexers/herdr";
3
+ import { orcaMultiplexer } from "./multiplexers/orca";
4
+ import { screenMultiplexer } from "./multiplexers/screen";
5
+ import { tmuxMultiplexer } from "./multiplexers/tmux";
6
+ import type { TerminalMultiplexerModule, TerminalMultiplexerNotificationRequest } from "./multiplexers/types";
7
+ import { wmuxMultiplexer } from "./multiplexers/wmux";
8
+ import { zellijMultiplexer } from "./multiplexers/zellij";
9
+
10
+ /**
11
+ * Every multiplexer omp recognizes, in classification order within each
12
+ * precedence tier. Adding a multiplexer means adding its module here.
13
+ */
14
+ const registry = [
15
+ herdrMultiplexer,
16
+ tmuxMultiplexer,
17
+ screenMultiplexer,
18
+ zellijMultiplexer,
19
+ cmuxMultiplexer,
20
+ wmuxMultiplexer,
21
+ orcaMultiplexer,
22
+ ] as const satisfies readonly TerminalMultiplexerModule[];
23
+
24
+ /** Terminal multiplexers omp recognizes. */
25
+ export type TerminalMultiplexer = (typeof registry)[number]["id"];
26
+
27
+ const TERMINAL_MULTIPLEXERS: readonly TerminalMultiplexerModule<TerminalMultiplexer>[] = registry;
28
+ const MULTIPLEXERS_BY_ID = Object.fromEntries(
29
+ TERMINAL_MULTIPLEXERS.map(multiplexer => [multiplexer.id, multiplexer]),
30
+ ) as Record<TerminalMultiplexer, TerminalMultiplexerModule<TerminalMultiplexer>>;
31
+ const SESSION_TIER = TERMINAL_MULTIPLEXERS.filter(multiplexer => multiplexer.precedence === "session");
32
+ const OUTER_APP_TIER = TERMINAL_MULTIPLEXERS.filter(multiplexer => multiplexer.precedence === "outerApp");
33
+ const NOTIFICATION_TIER_ORDER = ["pane", "surface", "inBand"] as const;
34
+ // Innermost target first: a pane inside a surface must receive its own signal
35
+ // before the containing surface, followed by in-band rewrites. Registry order
36
+ // breaks ties within a tier (sort is stable).
37
+ const NOTIFIERS = TERMINAL_MULTIPLEXERS.flatMap(multiplexer =>
38
+ multiplexer.notifier ? [{ multiplexer, notifier: multiplexer.notifier }] : [],
39
+ ).sort((a, b) => NOTIFICATION_TIER_ORDER.indexOf(a.notifier.tier) - NOTIFICATION_TIER_ORDER.indexOf(b.notifier.tier));
40
+
41
+ /** Every environment variable multiplexer classification reads, including the TERM fallback. */
42
+ export const TERMINAL_MULTIPLEXER_ENV_KEYS: readonly string[] = [
43
+ ...TERMINAL_MULTIPLEXERS.flatMap(multiplexer => multiplexer.sessionEnvKeys),
44
+ "TERM",
45
+ ];
46
+
47
+ /**
48
+ * Whether an explicit session marker identifies the current provider.
49
+ *
50
+ * TERM is intentionally excluded: it is a classification fallback, not proof
51
+ * that a particular multiplexer session owns the current grid.
52
+ */
53
+ export function hasTerminalMultiplexerSession(
54
+ multiplexer: TerminalMultiplexer,
55
+ env: NodeJS.ProcessEnv = Bun.env,
56
+ ): boolean {
57
+ return MULTIPLEXERS_BY_ID[multiplexer].isInside(env);
11
58
  }
12
59
 
13
- /** Terminal multiplexers zeta recognizes as owning the screen grid. */
14
- export type TerminalMultiplexer = "herdr" | "tmux" | "screen" | "zellij" | "cmux" | "wmux";
60
+ /**
61
+ * Every multiplexer whose explicit session markers are present, in registry
62
+ * order. Nested sessions all appear; TERM is not consulted.
63
+ */
64
+ export function terminalMultiplexerSessions(
65
+ env: NodeJS.ProcessEnv = Bun.env,
66
+ ): TerminalMultiplexerModule<TerminalMultiplexer>[] {
67
+ return TERMINAL_MULTIPLEXERS.filter(multiplexer => multiplexer.isInside(env));
68
+ }
15
69
 
16
70
  /**
17
- * Classify which terminal multiplexer owns the current screen grid, or `null`
18
- * for a direct terminal. Single source of truth for both the render-path gate
71
+ * The multiplexer TERM names, whether or not its session markers survived.
72
+ * This is classification's TERM fallback; session markers are not consulted.
73
+ */
74
+ export function terminalMultiplexerForTerm(
75
+ env: NodeJS.ProcessEnv = Bun.env,
76
+ ): TerminalMultiplexerModule<TerminalMultiplexer> | undefined {
77
+ const term = env.TERM?.toLowerCase() ?? "";
78
+ return TERMINAL_MULTIPLEXERS.find(
79
+ multiplexer => multiplexer.termPrefix !== undefined && term.startsWith(multiplexer.termPrefix),
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Registry entry for the multiplexer hosting the current process, or
85
+ * `undefined` for a direct terminal. Same precedence as
86
+ * {@link classifyTerminalMultiplexer}.
87
+ */
88
+ export function classifyTerminalMultiplexerModule(
89
+ env: NodeJS.ProcessEnv = Bun.env,
90
+ ): TerminalMultiplexerModule<TerminalMultiplexer> | undefined {
91
+ return (
92
+ SESSION_TIER.find(multiplexer => multiplexer.isInside(env)) ??
93
+ terminalMultiplexerForTerm(env) ??
94
+ OUTER_APP_TIER.find(multiplexer => multiplexer.isInside(env))
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Classify which terminal multiplexer hosts the current process, or `null` for
100
+ * a direct terminal. Single source of truth for both the render-path gate
19
101
  * (`isInsideTerminalMultiplexer`) and the debug snapshot label.
20
102
  *
21
- * TMUX/STY/ZELLIJ, Herdr, and the CMUX/WMUX workspace/surface/remote-transport
22
- * markers are authoritative session signals. TERM can also survive when those
23
- * are stripped (`sudo` without -E, `su`, env-sanitizing launchers/ssh). Do not
24
- * use CMUX_SOCKET_PATH / WMUX_CLI / WMUX_PIPE here: they are CLI socket/path
25
- * overrides and can be set outside a CMUX/WMUX terminal. wmux is a Windows
26
- * multiplexer (Electron + xterm.js) modeled on cmux/herdr that repaints its
27
- * pane in place and exports WMUX=1 plus a native WMUX_SURFACE_ID.
103
+ * Session markers are authoritative. TERM can also survive when those markers
104
+ * are stripped (`sudo` without -E, `su`, env-sanitizing launchers/ssh), so a
105
+ * `tmux`/`screen` TERM prefix comes next. Outer applications such as Orca rank
106
+ * last so a multiplexer running inside them wins.
28
107
  */
29
108
  export function classifyTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): TerminalMultiplexer | null {
30
- if (isInsideHerdr(env)) return "herdr";
31
- if (env.TMUX) return "tmux";
32
- if (env.STY) return "screen";
33
- if (env.ZELLIJ) return "zellij";
34
- if (env.CMUX_WORKSPACE_ID || env.CMUX_SURFACE_ID || env.CMUX_REMOTE_TRANSPORT) return "cmux";
35
- if (env.WMUX === "1" || env.WMUX_SURFACE_ID) return "wmux";
36
- const term = env.TERM?.toLowerCase() ?? "";
37
- if (term.startsWith("tmux")) return "tmux";
38
- if (term.startsWith("screen")) return "screen";
39
- return null;
109
+ return classifyTerminalMultiplexerModule(env)?.id ?? null;
40
110
  }
41
111
 
42
- /** True when a terminal multiplexer owns the current screen grid. */
112
+ /** True when the classified multiplexer owns the current screen grid. */
43
113
  export function isInsideTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): boolean {
44
- return classifyTerminalMultiplexer(env) !== null;
114
+ return classifyTerminalMultiplexerModule(env)?.ownsScreenGrid ?? false;
115
+ }
116
+
117
+ /**
118
+ * Offer a notification to every active session's notifier, innermost tier
119
+ * first. Returns whether one delivered it; otherwise the terminal fallback
120
+ * applies unchanged.
121
+ */
122
+ export function routeTerminalMultiplexerNotification(request: TerminalMultiplexerNotificationRequest): boolean {
123
+ return NOTIFIERS.some(({ multiplexer, notifier }) => multiplexer.isInside(request.env) && notifier.send(request));
45
124
  }
package/src/terminal.d.ts CHANGED
@@ -128,13 +128,21 @@ export type TspHelloHandler = (hello: TspHello | null) => void;
128
128
  export type CursorShape = "default" | "block" | "underline" | "bar";
129
129
  export declare const CURSOR_SHAPE_CODES: Record<CursorShape, number>;
130
130
  export interface Terminal {
131
+ /**
132
+ * Start the terminal with input, resize, and host-disconnect handlers.
133
+ * Unless input is deferred, this sends capability probes; a terminal that
134
+ * cannot parse one may leave concealed bytes on the cursor row, so the
135
+ * caller repaints after starting.
136
+ */
131
137
  start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void, options?: TerminalStartOptions): void;
132
138
  /**
133
139
  * Take ownership of stdin after a `deferInput` start: enable raw mode,
134
140
  * attach input handlers, and run the capability probes start() skipped.
135
141
  * Bytes the user typed in cooked mode meanwhile are replayed through
136
- * `onInput`. No-op when input was never deferred. Optional so custom
137
- * Terminals built against older pi-tui versions keep working.
142
+ * `onInput`. No-op when input was never deferred. Like {@link start}, the
143
+ * probes may leave concealed bytes on the cursor row, so the caller
144
+ * repaints afterwards. Optional so custom Terminals built against older
145
+ * pi-tui versions keep working.
138
146
  */
139
147
  enableInput?(): void;
140
148
  stop(): void;
@@ -222,6 +230,12 @@ export interface Terminal {
222
230
  * `status` is the DECRPM value when the terminal answered DECRQM.
223
231
  */
224
232
  onPrivateModeReport?(callback: PrivateModeReportHandler): void;
233
+ /**
234
+ * Report whether DA1 advertises SIXEL (attribute 4), replaying the latest
235
+ * advertisement to late subscribers. A missing attribute does not rule out
236
+ * support discovered through another graphics query.
237
+ */
238
+ onSixelSupport?(callback: (supported: boolean) => void): void;
225
239
  /**
226
240
  * Register a callback fired once the startup Glyph Protocol handshake
227
241
  * resolves (see {@link GlyphProtocolReportHandler}). A subscriber that
@@ -290,10 +304,11 @@ export declare class ProcessTerminal implements Terminal {
290
304
  onAppearanceReport(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): () => void;
291
305
  /**
292
306
  * Re-query the terminal background through the startup DA1-sentinel FIFO,
293
- * pending/queued gating, parsing, dedup, and appearance callbacks. Inside
294
- * tmux, only this explicit path first passes an OSC 11 query to the outer
295
- * terminal, waits briefly for tmux to consume the response into its cache,
296
- * then reads that cache with a direct query. The outer query deliberately has
307
+ * pending/queued gating, parsing, dedup, and appearance callbacks. Inside a
308
+ * multiplexer that answers OSC 11 from a cache (tmux), only this explicit
309
+ * path first passes an OSC 11 query to the outer terminal, waits briefly for
310
+ * the multiplexer to consume the response into its cache, then reads that
311
+ * cache with a direct query. The outer query deliberately has
297
312
  * no DA1 sentinel: multiplexers can decode a fragmented DA1 response as a key
298
313
  * sequence and leak the remaining bytes into the editor. Startup and Mode 2031
299
314
  * probes remain direct. Suppressed while inactive, headless, or after teardown.
@@ -301,6 +316,7 @@ export declare class ProcessTerminal implements Terminal {
301
316
  refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
302
317
  onPrivateModeReport(callback: PrivateModeReportHandler): void;
303
318
  onGlyphProtocolReport(callback: GlyphProtocolReportHandler): void;
319
+ onSixelSupport(callback: (supported: boolean) => void): void;
304
320
  onTspHello(callback: TspHelloHandler): void;
305
321
  get tspProbePending(): boolean;
306
322
  get tspExpected(): boolean;
package/src/terminal.ts CHANGED
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { TtyWriter } from "@linxiraos/pi-natives";
4
4
  import { $env, isBunTestRuntime, isTerminalHeadless, isWsl } from "@linxiraos/pi-utils/env";
5
5
  import * as logger from "@linxiraos/pi-utils/logger";
6
+ import { popLoopPhase, pushLoopPhase } from "@linxiraos/pi-utils/loop-phase";
6
7
  import * as postmortem from "@linxiraos/pi-utils/postmortem";
7
8
  import { restoreTerminalStderr, suppressTerminalStderr } from "@linxiraos/pi-utils/stderr-guard";
8
9
  import { TSP_VERSION } from "@linxiraos/pi-wire";
@@ -27,7 +28,10 @@ import {
27
28
  setTerminalGlyphProtocol,
28
29
  TERMINAL,
29
30
  } from "./terminal-capabilities";
30
- import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
31
+ import { tmuxMultiplexer } from "./multiplexers/tmux";
32
+ import type { TerminalMultiplexerBackgroundColorCache } from "./multiplexers/types";
33
+ import { terminalMultiplexerSessions } from "./terminal-multiplexer";
34
+ import { setTmuxPaneApcTitles, tmuxSessionSpeaksTsp } from "./tmux";
31
35
  import { setHangulCompatibilityJamoWidth } from "./utils";
32
36
  import { translateWindowsAltGrSequence } from "./windows-altgr";
33
37
  import { Win32InputModeDecoder, Win32PasteMarkerNormalizer } from "./windows-input-mode";
@@ -48,6 +52,9 @@ const WIN32_RELAYED_ESCAPE_TIMEOUT_MS = 75;
48
52
  */
49
53
  const TSP_OSC_PREFIX = `\x1b]877;${TSP_PREFIX.slice(2)}`;
50
54
 
55
+ /** How long a TSP hello waits for its reply inside tmux, which answers DA1 itself. */
56
+ const TSP_TMUX_HELLO_TIMEOUT_MS = 1000;
57
+
51
58
  /**
52
59
  * The TSP message a reassembled input string holds, in APC form, or
53
60
  * `undefined` while its terminator has not arrived. An OSC 877 message may end
@@ -501,7 +508,12 @@ export const CURSOR_SHAPE_CODES: Record<CursorShape, number> = {
501
508
  bar: 6,
502
509
  };
503
510
  export interface Terminal {
504
- // Start the terminal with input, resize, and host-disconnect handlers.
511
+ /**
512
+ * Start the terminal with input, resize, and host-disconnect handlers.
513
+ * Unless input is deferred, this sends capability probes; a terminal that
514
+ * cannot parse one may leave concealed bytes on the cursor row, so the
515
+ * caller repaints after starting.
516
+ */
505
517
  start(
506
518
  onInput: (data: string) => void,
507
519
  onResize: () => void,
@@ -513,8 +525,10 @@ export interface Terminal {
513
525
  * Take ownership of stdin after a `deferInput` start: enable raw mode,
514
526
  * attach input handlers, and run the capability probes start() skipped.
515
527
  * Bytes the user typed in cooked mode meanwhile are replayed through
516
- * `onInput`. No-op when input was never deferred. Optional so custom
517
- * Terminals built against older pi-tui versions keep working.
528
+ * `onInput`. No-op when input was never deferred. Like {@link start}, the
529
+ * probes may leave concealed bytes on the cursor row, so the caller
530
+ * repaints afterwards. Optional so custom Terminals built against older
531
+ * pi-tui versions keep working.
518
532
  */
519
533
  enableInput?(): void;
520
534
 
@@ -646,6 +660,12 @@ export interface Terminal {
646
660
  * `status` is the DECRPM value when the terminal answered DECRQM.
647
661
  */
648
662
  onPrivateModeReport?(callback: PrivateModeReportHandler): void;
663
+ /**
664
+ * Report whether DA1 advertises SIXEL (attribute 4), replaying the latest
665
+ * advertisement to late subscribers. A missing attribute does not rule out
666
+ * support discovered through another graphics query.
667
+ */
668
+ onSixelSupport?(callback: (supported: boolean) => void): void;
649
669
  /**
650
670
  * Register a callback fired once the startup Glyph Protocol handshake
651
671
  * resolves (see {@link GlyphProtocolReportHandler}). A subscriber that
@@ -707,9 +727,18 @@ function parseOsc99KeyValues(section: string): Map<string, string> {
707
727
  }
708
728
  return values;
709
729
  }
730
+ /**
731
+ * Brackets the startup capability probes. A terminal that cannot parse a probe
732
+ * prints it as text at the cursor (Apple Terminal drops `ESC _`/`ESC \` and
733
+ * prints APC payloads, and abandons DECRQM at its `$`, printing the final `p`).
734
+ * DECSC, autowrap off, and SGR 8 (conceal) keep that text invisible and on the
735
+ * cursor row, so it can neither wrap nor scroll the screen; DECRC puts the
736
+ * cursor back. The screen owner repaints the row afterwards (see
737
+ * {@link Terminal.start} / {@link Terminal.enableInput}).
738
+ */
739
+ const PROBE_GUARD_BEGIN = "\x1b7\x1b[?7l\x1b[8m";
740
+ const PROBE_GUARD_END = "\x1b[28m\x1b[?7h\x1b8";
710
741
  const XTERM_SCROLL_TO_BOTTOM_MODES = [1010, 1011] as const;
711
- type Osc11QueryRoute = "direct" | "tmux";
712
- const TMUX_OSC11_CACHE_REFRESH_DELAY_MS = 100;
713
742
 
714
743
  function isXtermScrollToBottomMode(mode: number): boolean {
715
744
  return mode === 1010 || mode === 1011;
@@ -830,10 +859,10 @@ export class ProcessTerminal implements Terminal {
830
859
  #appearance: TerminalAppearance | undefined;
831
860
  #osc11Pending = false;
832
861
  #osc11ActiveToken?: TerminalAppearanceRequestToken;
833
- #osc11QueuedQuery?: { route: Osc11QueryRoute; token?: TerminalAppearanceRequestToken };
862
+ #osc11QueuedQuery?: { cache?: TerminalMultiplexerBackgroundColorCache; token?: TerminalAppearanceRequestToken };
834
863
  #nextAppearanceRequestToken = 1;
835
864
  #osc11ResponseBuffer = "";
836
- #osc11TmuxRefreshTimer?: Timer;
865
+ #osc11CacheRefreshTimer?: Timer;
837
866
  #osc99PendingId: string | undefined;
838
867
  #osc99ResponseBuffer = "";
839
868
  #osc99Capabilities = new Map<string, string>();
@@ -845,9 +874,15 @@ export class ProcessTerminal implements Terminal {
845
874
  #glyphProtocolReplyBuffer = "";
846
875
  #glyphProtocolResult: boolean | undefined;
847
876
  #glyphProtocolCallbacks: GlyphProtocolReportHandler[] = [];
877
+ #sixelSupport: boolean | undefined;
878
+ #sixelSupportCallbacks: Array<(supported: boolean) => void> = [];
848
879
  #tspPending = false;
849
880
  #tspResult: TspHello | null | undefined;
850
881
  #tspCallbacks: TspHelloHandler[] = [];
882
+ /** Hello deadline inside tmux, where no DA1 sentinel can end the probe. */
883
+ #tspTimeout: NodeJS.Timeout | undefined;
884
+ /** This tmux pane's APC titles are off for TSP and must be restored. */
885
+ #tspTmuxPaneGated = false;
851
886
  #tspReplyBuffer = "";
852
887
  #privateCsiResponseBuffer = "";
853
888
  #da1SentinelOwners: Da1SentinelOwner[] = [];
@@ -931,10 +966,11 @@ export class ProcessTerminal implements Terminal {
931
966
 
932
967
  /**
933
968
  * Re-query the terminal background through the startup DA1-sentinel FIFO,
934
- * pending/queued gating, parsing, dedup, and appearance callbacks. Inside
935
- * tmux, only this explicit path first passes an OSC 11 query to the outer
936
- * terminal, waits briefly for tmux to consume the response into its cache,
937
- * then reads that cache with a direct query. The outer query deliberately has
969
+ * pending/queued gating, parsing, dedup, and appearance callbacks. Inside a
970
+ * multiplexer that answers OSC 11 from a cache (tmux), only this explicit
971
+ * path first passes an OSC 11 query to the outer terminal, waits briefly for
972
+ * the multiplexer to consume the response into its cache, then reads that
973
+ * cache with a direct query. The outer query deliberately has
938
974
  * no DA1 sentinel: multiplexers can decode a fragmented DA1 response as a key
939
975
  * sequence and leak the remaining bytes into the editor. Startup and Mode 2031
940
976
  * probes remain direct. Suppressed while inactive, headless, or after teardown.
@@ -945,7 +981,10 @@ export class ProcessTerminal implements Terminal {
945
981
  if (token >= this.#nextAppearanceRequestToken) {
946
982
  this.#nextAppearanceRequestToken = token + 1;
947
983
  }
948
- this.#queryBackgroundColor(isInsideTmux() ? "tmux" : "direct", token);
984
+ const cache = terminalMultiplexerSessions().find(
985
+ multiplexer => multiplexer.backgroundColorCache,
986
+ )?.backgroundColorCache;
987
+ this.#queryBackgroundColor(cache, token);
949
988
  return token;
950
989
  }
951
990
 
@@ -960,6 +999,11 @@ export class ProcessTerminal implements Terminal {
960
999
  if (this.#glyphProtocolResult !== undefined) callback(this.#glyphProtocolResult);
961
1000
  }
962
1001
 
1002
+ onSixelSupport(callback: (supported: boolean) => void): void {
1003
+ this.#sixelSupportCallbacks.push(callback);
1004
+ if (this.#sixelSupport !== undefined) callback(this.#sixelSupport);
1005
+ }
1006
+
963
1007
  onTspHello(callback: TspHelloHandler): void {
964
1008
  this.#tspCallbacks.push(callback);
965
1009
  if (this.#tspResult !== undefined) callback(this.#tspResult);
@@ -1099,6 +1143,8 @@ export class ProcessTerminal implements Terminal {
1099
1143
  // events that lose modifier information. Must run after setRawMode(true)
1100
1144
  // since that resets console mode flags.
1101
1145
  this.#enableWindowsVTInput();
1146
+ // Every response-eliciting probe goes out inside the leak guard.
1147
+ this.#safeWrite(PROBE_GUARD_BEGIN);
1102
1148
  // Query and enable Kitty keyboard protocol
1103
1149
  // The query handler intercepts input temporarily, then installs the user's handler
1104
1150
  // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
@@ -1161,6 +1207,7 @@ export class ProcessTerminal implements Terminal {
1161
1207
  for (const mode of XTERM_SCROLL_TO_BOTTOM_MODES) {
1162
1208
  this.#queryPrivateMode(mode);
1163
1209
  }
1210
+ this.#safeWrite(PROBE_GUARD_END);
1164
1211
  }
1165
1212
 
1166
1213
  /**
@@ -1241,6 +1288,7 @@ export class ProcessTerminal implements Terminal {
1241
1288
 
1242
1289
  // DA1 (Primary Device Attributes) response: \x1b[?...c
1243
1290
  const da1ResponsePattern = /^\x1b\[\?[\d;]*c$/;
1291
+ const da1SixelAttributePattern = /;4(?:;|c$)/u;
1244
1292
 
1245
1293
  // Private CSI partial: \x1b[?<digits/semicolons>... — incomplete probe response
1246
1294
  // that the StdinBuffer flushed before the terminator arrived (split across
@@ -1384,6 +1432,19 @@ export class ProcessTerminal implements Terminal {
1384
1432
  // outstanding sentinel — a reply that arrives after the FIFO drains (slow
1385
1433
  // SSH/PTY links) must never reach the composer as literal text (#8542).
1386
1434
  if (da1ResponsePattern.test(sequence)) {
1435
+ // Publish graphics support without forwarding DA1 bytes to application
1436
+ // input or changing ownership of the existing probe sentinel.
1437
+ const supportsSixel = da1SixelAttributePattern.test(sequence);
1438
+ if (supportsSixel !== this.#sixelSupport) {
1439
+ this.#sixelSupport = supportsSixel;
1440
+ for (const callback of this.#sixelSupportCallbacks) {
1441
+ try {
1442
+ callback(supportsSixel);
1443
+ } catch {
1444
+ // Capability subscribers must not interrupt sentinel handling.
1445
+ }
1446
+ }
1447
+ }
1387
1448
  const owner = this.#da1SentinelOwners.shift();
1388
1449
  if (!owner) {
1389
1450
  // Late/unowned reply: nothing to resolve, just drop the bytes.
@@ -1406,7 +1467,7 @@ export class ProcessTerminal implements Terminal {
1406
1467
  ) {
1407
1468
  const query = this.#osc11QueuedQuery;
1408
1469
  this.#osc11QueuedQuery = undefined;
1409
- this.#startOsc11Query(query.route, query.token);
1470
+ this.#startOsc11Query(query.cache, query.token);
1410
1471
  }
1411
1472
  break;
1412
1473
  }
@@ -1624,36 +1685,39 @@ export class ProcessTerminal implements Terminal {
1624
1685
  * DA1 avoids indefinite hangs: if DA1 response arrives before OSC 11,
1625
1686
  * the terminal does not support OSC 11.
1626
1687
  */
1627
- #queryBackgroundColor(route: Osc11QueryRoute = "direct", token?: TerminalAppearanceRequestToken): void {
1688
+ #queryBackgroundColor(
1689
+ cache?: TerminalMultiplexerBackgroundColorCache,
1690
+ token?: TerminalAppearanceRequestToken,
1691
+ ): void {
1628
1692
  if (this.#dead) return;
1629
1693
  // Queue if an OSC 11 query is in flight or its DA1 sentinel hasn't been
1630
1694
  // consumed yet. Starting a new query while a DA1 is outstanding would
1631
1695
  // increment the sentinel counter, and the old DA1 arrival would then
1632
1696
  // prematurely clear the new query's pending state. Preserve a requested
1633
- // tmux passthrough route when coalescing direct and explicit queries, and
1634
- // retain the latest explicit request identity across automatic queries.
1697
+ // cache refresh when coalescing direct and explicit queries, and retain
1698
+ // the latest explicit request identity across automatic queries.
1635
1699
  if (this.#osc11Pending || this.#da1SentinelOwners.some(o => o.kind === "osc11")) {
1636
1700
  const queued = this.#osc11QueuedQuery;
1637
- this.#osc11QueuedQuery = {
1638
- route: queued?.route === "tmux" || route === "tmux" ? "tmux" : "direct",
1639
- token: token ?? queued?.token,
1640
- };
1701
+ this.#osc11QueuedQuery = { cache: queued?.cache ?? cache, token: token ?? queued?.token };
1641
1702
  return;
1642
1703
  }
1643
- this.#startOsc11Query(route, token);
1704
+ this.#startOsc11Query(cache, token);
1644
1705
  }
1645
1706
 
1646
- #startOsc11Query(route: Osc11QueryRoute, token?: TerminalAppearanceRequestToken): void {
1707
+ #startOsc11Query(
1708
+ cache: TerminalMultiplexerBackgroundColorCache | undefined,
1709
+ token?: TerminalAppearanceRequestToken,
1710
+ ): void {
1647
1711
  this.#osc11Pending = true;
1648
1712
  this.#osc11ActiveToken = token;
1649
1713
  this.#osc11ResponseBuffer = "";
1650
- if (route === "tmux") {
1651
- this.#safeWrite(wrapTmuxPassthrough("\x1b]11;?\x07"));
1652
- this.#osc11TmuxRefreshTimer = setTimeout(() => {
1653
- this.#osc11TmuxRefreshTimer = undefined;
1714
+ if (cache) {
1715
+ this.#safeWrite(cache.passthrough("\x1b]11;?\x07"));
1716
+ this.#osc11CacheRefreshTimer = setTimeout(() => {
1717
+ this.#osc11CacheRefreshTimer = undefined;
1654
1718
  if (this.#dead || !this.#osc11Pending) return;
1655
1719
  this.#startDirectOsc11Query();
1656
- }, TMUX_OSC11_CACHE_REFRESH_DELAY_MS);
1720
+ }, cache.settleMs);
1657
1721
  return;
1658
1722
  }
1659
1723
  this.#startDirectOsc11Query();
@@ -1776,6 +1840,9 @@ export class ProcessTerminal implements Terminal {
1776
1840
  #shouldQueryTspSupport(): boolean {
1777
1841
  const override = $env.PI_TUI_NATIVE;
1778
1842
  if (override === "0") return false;
1843
+ // tmux makes APC bodies the pane title unless the hosting terminal asked
1844
+ // for TSP through `@tern-tsp`: no override may force the probe there.
1845
+ if (tmuxMultiplexer.isInside($env)) return tmuxSessionSpeaksTsp($env);
1779
1846
  if (override === "1") return true;
1780
1847
  // Multiplexers swallow APC, so the reply could never arrive.
1781
1848
  if (isInsideTerminalMultiplexer($env)) return false;
@@ -1793,16 +1860,37 @@ export class ProcessTerminal implements Terminal {
1793
1860
  return;
1794
1861
  }
1795
1862
  this.#tspPending = true;
1863
+ if (tmuxMultiplexer.isInside($env)) {
1864
+ // tmux answers DA1 itself, ahead of any reply: wait for the reply instead.
1865
+ this.#tspTmuxPaneGated = true;
1866
+ setTmuxPaneApcTitles(false, $env);
1867
+ this.#tspTimeout = setTimeout(() => {
1868
+ this.#tspTimeout = undefined;
1869
+ this.#resolveTspSupport(null);
1870
+ }, TSP_TMUX_HELLO_TIMEOUT_MS);
1871
+ this.#safeWrite(encodeTspHelloQuery());
1872
+ return;
1873
+ }
1796
1874
  this.#da1SentinelOwners.push({ kind: "tsp" });
1797
1875
  this.#safeWrite(`${encodeTspHelloQuery()}\x1b[c`);
1798
1876
  }
1799
1877
 
1878
+ /** Hand APC titles back to tmux once this pane stops speaking TSP. */
1879
+ #endTspTmuxPane(): void {
1880
+ clearTimeout(this.#tspTimeout);
1881
+ this.#tspTimeout = undefined;
1882
+ if (!this.#tspTmuxPaneGated) return;
1883
+ this.#tspTmuxPaneGated = false;
1884
+ setTmuxPaneApcTitles(true, $env);
1885
+ }
1886
+
1800
1887
  #handleTspMessage(sequence: string): void {
1801
1888
  const message = parseTspMessage(sequence);
1802
- if (message?.verb === "r") {
1803
- if (message.reply.r === "hello") this.#resolveTspSupport(message.reply);
1889
+ if (message?.verb === "r" && message.reply.r === "hello") {
1890
+ this.#resolveTspSupport(message.reply);
1804
1891
  return;
1805
1892
  }
1893
+ // Events and every other reply (`blobs`) go to the native backend.
1806
1894
  this.#inputHandler?.(sequence);
1807
1895
  }
1808
1896
 
@@ -1811,6 +1899,12 @@ export class ProcessTerminal implements Terminal {
1811
1899
  this.#tspPending = false;
1812
1900
  const result = hello !== null && hello.v === TSP_VERSION ? hello : null;
1813
1901
  if (hello !== null && result === null) logger.warn("TSP: unsupported protocol version", { v: hello.v });
1902
+ if (result === null) {
1903
+ this.#endTspTmuxPane();
1904
+ } else {
1905
+ clearTimeout(this.#tspTimeout);
1906
+ this.#tspTimeout = undefined;
1907
+ }
1814
1908
  this.#tspResult = result;
1815
1909
  for (const cb of this.#tspCallbacks) {
1816
1910
  try {
@@ -2176,9 +2270,9 @@ export class ProcessTerminal implements Terminal {
2176
2270
  clearTimeout(this.#mode2031DebounceTimer);
2177
2271
  this.#mode2031DebounceTimer = undefined;
2178
2272
  }
2179
- if (this.#osc11TmuxRefreshTimer) {
2180
- clearTimeout(this.#osc11TmuxRefreshTimer);
2181
- this.#osc11TmuxRefreshTimer = undefined;
2273
+ if (this.#osc11CacheRefreshTimer) {
2274
+ clearTimeout(this.#osc11CacheRefreshTimer);
2275
+ this.#osc11CacheRefreshTimer = undefined;
2182
2276
  }
2183
2277
  this.#appearanceCallbacks = [];
2184
2278
  this.#appearanceReportCallbacks = [];
@@ -2195,8 +2289,11 @@ export class ProcessTerminal implements Terminal {
2195
2289
  this.#glyphProtocolResult = undefined;
2196
2290
  this.#glyphProtocolReplyBuffer = "";
2197
2291
  this.#glyphProtocolCallbacks = [];
2292
+ this.#sixelSupport = undefined;
2293
+ this.#sixelSupportCallbacks = [];
2198
2294
  setTerminalGlyphProtocol(false);
2199
2295
  this.#tspPending = false;
2296
+ this.#endTspTmuxPane();
2200
2297
  this.#tspResult = undefined;
2201
2298
  this.#tspReplyBuffer = "";
2202
2299
  this.#tspCallbacks = [];
@@ -2366,11 +2463,14 @@ export class ProcessTerminal implements Terminal {
2366
2463
  this.#trackStdoutBacklog(pending);
2367
2464
  return;
2368
2465
  }
2369
- // A console-sharing child process may have flipped the console codepage
2370
- // away from UTF-8; repair it before any bytes hit WriteFile so no frame
2371
- // is ever translated through an OEM codepage. See ensureWindowsConsoleUtf8.
2372
- if (process.platform === "win32") ensureWindowsConsoleUtf8();
2466
+ // Without the pump, writes (and the codepage guard's console calls) block
2467
+ // the event loop until the terminal drains; label them for the watchdog.
2468
+ pushLoopPhase("ui.terminal-write");
2373
2469
  try {
2470
+ // A console-sharing child process may have flipped the console codepage
2471
+ // away from UTF-8; repair it before any bytes hit WriteFile so no frame
2472
+ // is ever translated through an OEM codepage. See ensureWindowsConsoleUtf8.
2473
+ if (process.platform === "win32") ensureWindowsConsoleUtf8();
2374
2474
  // Windows ConPTY drops viewport tracking when a single write exceeds
2375
2475
  // ~32-64 KB: the host UI's scroll position stays parked at wherever
2376
2476
  // the write began, even though every byte landed in scrollback. Split
@@ -2399,6 +2499,8 @@ export class ProcessTerminal implements Terminal {
2399
2499
  this.#trackStdoutBacklog(process.stdout.writableLength ?? 0);
2400
2500
  } catch (err) {
2401
2501
  this.#markTerminalDisconnected("stdout failed", err);
2502
+ } finally {
2503
+ popLoopPhase();
2402
2504
  }
2403
2505
  }
2404
2506
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Active theme's symbol lookup without importing `./theme` (which pulls the
3
3
  * native addon through `@linxiraos/pi-natives`). Key-hint formatting runs on
4
- * CLI paths that must stay addon-free (`omp --version`, help), so it reads the
4
+ * CLI paths that must stay addon-free (`zetacode --version`, help), so it reads the
5
5
  * active theme through this mirror; `./theme` publishes every assignment here.
6
6
  */
7
7
  import { type SymbolKey } from "./symbols";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Active theme's symbol lookup without importing `./theme` (which pulls the
3
3
  * native addon through `@linxiraos/pi-natives`). Key-hint formatting runs on
4
- * CLI paths that must stay addon-free (`omp --version`, help), so it reads the
4
+ * CLI paths that must stay addon-free (`zetacode --version`, help), so it reads the
5
5
  * active theme through this mirror; `./theme` publishes every assignment here.
6
6
  */
7
7
  import { SYMBOL_PRESETS, type SymbolKey } from "./symbols";
@@ -13,6 +13,7 @@ const themeColorsSchema = type({
13
13
  muted: "string | number",
14
14
  dim: "string | number",
15
15
  text: "string | number",
16
+ "assistantMessageText?": "string | number",
16
17
  thinkingText: "string | number",
17
18
  selectedBg: "string | number",
18
19
  userMessageBg: "string | number",