@pellux/goodvibes-tui 2.0.21 → 2.1.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 (391) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -29
  3. package/docs/README.md +8 -5
  4. package/docs/foundation-artifacts/operator-contract.json +1 -1
  5. package/package.json +28 -19
  6. package/src/cli/management.ts +5 -2
  7. package/src/cli/tui-startup.ts +34 -39
  8. package/src/core/agent-view-render.ts +157 -0
  9. package/src/core/approval-alert.ts +49 -11
  10. package/src/core/bookmark-navigation.ts +7 -1
  11. package/src/core/budget-breach-notifier.ts +27 -8
  12. package/src/core/compaction-receipt.ts +1 -1
  13. package/src/core/context-usage.ts +1 -1
  14. package/src/core/conversation-compaction-render.ts +3 -3
  15. package/src/core/conversation-display-only.ts +86 -0
  16. package/src/core/conversation-event-navigation.ts +27 -0
  17. package/src/core/conversation-line-cache.ts +216 -447
  18. package/src/core/conversation-render-context.ts +20 -27
  19. package/src/core/conversation-rendering.ts +157 -575
  20. package/src/core/conversation-splash-state.ts +6 -1
  21. package/src/core/conversation-types.ts +35 -0
  22. package/src/core/conversation-usage.ts +12 -0
  23. package/src/core/conversation.ts +130 -131
  24. package/src/core/long-task-notifier.ts +100 -31
  25. package/src/core/notices.ts +75 -0
  26. package/src/core/pairing-tailscale-gateway.ts +1 -1
  27. package/src/core/process-output-log.ts +143 -0
  28. package/src/core/rewind-receipt.ts +1 -1
  29. package/src/core/session-focus.ts +190 -0
  30. package/src/core/session-resume-core.ts +23 -81
  31. package/src/core/stream-event-wiring.ts +31 -5
  32. package/src/core/system-message-noise.ts +2 -2
  33. package/src/core/system-message-router.ts +16 -18
  34. package/src/core/turn-event-wiring.ts +272 -33
  35. package/src/core/work-tree-focus.ts +290 -0
  36. package/src/core/work-tree-fold-store.ts +143 -0
  37. package/src/core/work-tree-model.ts +799 -0
  38. package/src/core/work-tree-render.ts +384 -0
  39. package/src/core/work-tree-sources.ts +184 -0
  40. package/src/core/work-tree-wiring.ts +176 -0
  41. package/src/core/workstream-notification.ts +36 -6
  42. package/src/core/wrfc-notice-titles.ts +37 -0
  43. package/src/input/agents-modal-text.ts +119 -0
  44. package/src/input/agents-modal.ts +699 -0
  45. package/src/input/bookmark-modal.ts +33 -9
  46. package/src/input/changes-git.ts +251 -0
  47. package/src/input/changes-modal.ts +629 -0
  48. package/src/input/changes-model.ts +125 -0
  49. package/src/input/command-palette-types.ts +41 -0
  50. package/src/input/command-palette.ts +272 -0
  51. package/src/input/command-reference.ts +2 -2
  52. package/src/input/command-registry.ts +77 -35
  53. package/src/input/commands/acp-runtime.ts +19 -9
  54. package/src/input/commands/channel-runtime.ts +2 -2
  55. package/src/input/commands/ci-runtime.ts +1 -1
  56. package/src/input/commands/codebase-runtime.ts +1 -1
  57. package/src/input/commands/config.ts +7 -1
  58. package/src/input/commands/context-window.ts +25 -19
  59. package/src/input/commands/control-room-runtime.ts +18 -18
  60. package/src/input/commands/conversation-runtime.ts +0 -1
  61. package/src/input/commands/cost-runtime.ts +29 -20
  62. package/src/input/commands/diff-runtime.ts +35 -181
  63. package/src/input/commands/eval.ts +1 -1
  64. package/src/input/commands/experience-runtime.ts +3 -7
  65. package/src/input/commands/fresh-conversation.ts +86 -0
  66. package/src/input/commands/git-runtime.ts +5 -22
  67. package/src/input/commands/graph-runtime.ts +1 -1
  68. package/src/input/commands/guidance-runtime.ts +1 -1
  69. package/src/input/commands/health-runtime.ts +8 -9
  70. package/src/input/commands/hooks-runtime.ts +2 -2
  71. package/src/input/commands/hosted-runtime.ts +3 -3
  72. package/src/input/commands/incident-runtime.ts +3 -3
  73. package/src/input/commands/intelligence-runtime.ts +6 -9
  74. package/src/input/commands/knowledge.ts +2 -2
  75. package/src/input/commands/legacy-panel-command.ts +57 -0
  76. package/src/input/commands/local-auth-runtime.ts +8 -9
  77. package/src/input/commands/local-provider-runtime.ts +7 -4
  78. package/src/input/commands/local-runtime.ts +12 -15
  79. package/src/input/commands/local-setup-review.ts +1 -1
  80. package/src/input/commands/local-setup.ts +13 -19
  81. package/src/input/commands/memory.ts +1 -1
  82. package/src/input/commands/notify-runtime.ts +9 -0
  83. package/src/input/commands/operator-runtime.ts +10 -12
  84. package/src/input/commands/permissions-provenance.ts +3 -3
  85. package/src/input/commands/planning-runtime.ts +7 -7
  86. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  87. package/src/input/commands/platform-services-runtime.ts +1 -1
  88. package/src/input/commands/policy-dispatch.ts +2 -2
  89. package/src/input/commands/policy.ts +3 -3
  90. package/src/input/commands/provider.ts +1 -1
  91. package/src/input/commands/remote-runtime.ts +1 -1
  92. package/src/input/commands/review-runtime.ts +39 -94
  93. package/src/input/commands/rewind-runtime.ts +81 -108
  94. package/src/input/commands/runtime-services.ts +15 -17
  95. package/src/input/commands/services-runtime.ts +2 -2
  96. package/src/input/commands/session-content.ts +21 -5
  97. package/src/input/commands/session-workflow.ts +13 -38
  98. package/src/input/commands/settings-sync-runtime.ts +2 -2
  99. package/src/input/commands/shell-core.ts +19 -4
  100. package/src/input/commands/skills-runtime.ts +2 -2
  101. package/src/input/commands/status-runtime.ts +13 -0
  102. package/src/input/commands/subscription-runtime.ts +1 -1
  103. package/src/input/commands/tasks-runtime.ts +2 -8
  104. package/src/input/commands/work-plan-runtime.ts +4 -4
  105. package/src/input/commands/workstream-attempts.ts +24 -47
  106. package/src/input/commands/workstream-runtime.ts +2 -2
  107. package/src/input/commands/worktree-runtime.ts +3 -3
  108. package/src/input/commands.ts +3 -1
  109. package/src/input/composer-editor.ts +44 -0
  110. package/src/input/concealed-input.ts +1 -1
  111. package/src/input/config-modal-types.ts +20 -8
  112. package/src/input/config-modal.ts +106 -105
  113. package/src/input/confirm-dialog-types.ts +28 -0
  114. package/src/input/confirm-dialog.ts +70 -0
  115. package/src/input/delete-key-policy.ts +3 -3
  116. package/src/input/feed-context-factory.ts +15 -16
  117. package/src/input/handler-command-route.ts +7 -42
  118. package/src/input/handler-content-actions.ts +16 -15
  119. package/src/input/handler-feed-routes.ts +26 -211
  120. package/src/input/handler-feed.ts +68 -95
  121. package/src/input/handler-interactions.ts +5 -3
  122. package/src/input/handler-modal-routes.ts +215 -216
  123. package/src/input/handler-modal-stack.ts +81 -11
  124. package/src/input/handler-modal-token-routes.ts +4 -0
  125. package/src/input/handler-onboarding.ts +0 -1
  126. package/src/input/handler-picker-routes.ts +65 -107
  127. package/src/input/handler-session-view-route.ts +149 -0
  128. package/src/input/handler-shortcuts.ts +16 -154
  129. package/src/input/handler-types.ts +4 -2
  130. package/src/input/handler-ui-state.ts +45 -15
  131. package/src/input/handler-work-tree-route.ts +123 -0
  132. package/src/input/handler.ts +25 -70
  133. package/src/input/keybindings.ts +16 -71
  134. package/src/input/masked-entry-modal.ts +117 -0
  135. package/src/input/mcp-workspace.ts +48 -17
  136. package/src/input/model-picker-filter.ts +8 -0
  137. package/src/input/model-picker-open.ts +99 -0
  138. package/src/input/model-picker-types.ts +1 -1
  139. package/src/input/model-picker.ts +92 -9
  140. package/src/input/notifications-modal.ts +123 -0
  141. package/src/input/overlay-filter.ts +26 -0
  142. package/src/input/profile-picker-modal.ts +37 -8
  143. package/src/input/search.ts +3 -3
  144. package/src/input/selection-modal-queue.ts +2 -0
  145. package/src/input/selection-modal.ts +28 -16
  146. package/src/input/session-picker-modal.ts +33 -8
  147. package/src/input/settings-modal-activation.ts +7 -2
  148. package/src/input/settings-modal-data.ts +11 -6
  149. package/src/input/settings-modal-types.ts +1 -1
  150. package/src/input/settings-modal.ts +20 -20
  151. package/src/input/submission-intent.ts +1 -1
  152. package/src/input/submission-router.ts +4 -9
  153. package/src/input/surface-modal-host.ts +110 -0
  154. package/src/input/theme-settings-actions.ts +82 -0
  155. package/src/input/usage-modal.ts +183 -0
  156. package/src/input/views.ts +97 -0
  157. package/src/main.ts +175 -165
  158. package/src/permissions/hunk-selection.ts +1 -1
  159. package/src/permissions/prompt-card.ts +511 -0
  160. package/src/permissions/prompt.ts +59 -622
  161. package/src/renderer/agents-modal.ts +310 -0
  162. package/src/renderer/autocomplete-overlay.ts +42 -220
  163. package/src/renderer/back-to-bottom.ts +80 -0
  164. package/src/renderer/block-actions-overlay.ts +30 -48
  165. package/src/renderer/bookmark-modal.ts +48 -79
  166. package/src/renderer/changes-modal.ts +325 -0
  167. package/src/renderer/code-block.ts +184 -106
  168. package/src/renderer/command-palette.ts +93 -0
  169. package/src/renderer/composer-fragments.ts +4 -4
  170. package/src/renderer/composer.ts +146 -0
  171. package/src/renderer/compositor.ts +26 -160
  172. package/src/renderer/config-modal.ts +185 -81
  173. package/src/renderer/confirm-dialog.ts +50 -0
  174. package/src/renderer/context-inspector.ts +116 -154
  175. package/src/renderer/context-status-hint.ts +4 -4
  176. package/src/renderer/conversation-layout.ts +20 -0
  177. package/src/renderer/conversation-overlays.ts +94 -84
  178. package/src/renderer/conversation-surface.ts +19 -38
  179. package/src/renderer/diff-tint.ts +98 -0
  180. package/src/renderer/file-picker-overlay.ts +63 -90
  181. package/src/renderer/footer-targets.ts +38 -0
  182. package/src/renderer/git-status.ts +3 -4
  183. package/src/renderer/header-line.ts +140 -117
  184. package/src/renderer/help-overlay.ts +133 -219
  185. package/src/renderer/history-search-overlay.ts +15 -12
  186. package/src/renderer/lane-graph/bead.ts +800 -0
  187. package/src/renderer/lane-graph/glyphs.ts +134 -0
  188. package/src/renderer/lane-graph/layout.ts +248 -0
  189. package/src/renderer/lane-graph/paint.ts +537 -0
  190. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  191. package/src/renderer/layout-engine.ts +2 -57
  192. package/src/renderer/layout.ts +7 -13
  193. package/src/renderer/markdown-inline.ts +2 -1
  194. package/src/renderer/markdown-table.ts +17 -12
  195. package/src/renderer/markdown.ts +92 -34
  196. package/src/renderer/masked-entry-modal.ts +78 -0
  197. package/src/renderer/mcp-workspace.ts +166 -203
  198. package/src/renderer/model-workspace.ts +381 -436
  199. package/src/renderer/notifications-modal.ts +116 -0
  200. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  201. package/src/renderer/process-view.ts +192 -0
  202. package/src/renderer/profile-picker-modal.ts +58 -110
  203. package/src/renderer/search-overlay.ts +6 -3
  204. package/src/renderer/selection-modal-overlay.ts +87 -327
  205. package/src/renderer/semantic-diff.ts +2 -2
  206. package/src/renderer/session-chips.ts +102 -0
  207. package/src/renderer/session-picker-modal.ts +177 -297
  208. package/src/renderer/settings-modal-connections.ts +14 -23
  209. package/src/renderer/settings-modal-context.ts +367 -0
  210. package/src/renderer/settings-modal-helpers.ts +6 -11
  211. package/src/renderer/settings-modal.ts +322 -726
  212. package/src/renderer/shell-surface.ts +277 -175
  213. package/src/renderer/shortcuts-overlay.ts +217 -0
  214. package/src/renderer/status-line.ts +475 -0
  215. package/src/renderer/surface-compose.ts +174 -0
  216. package/src/renderer/surface-kit-extra.ts +303 -0
  217. package/src/renderer/surface-kit-list.ts +366 -0
  218. package/src/renderer/surface-kit-parts.ts +360 -0
  219. package/src/renderer/surface-kit.ts +568 -0
  220. package/src/renderer/syntax-highlighter.ts +280 -197
  221. package/src/renderer/syntax-theme.ts +51 -0
  222. package/src/renderer/system-message.ts +0 -1
  223. package/src/renderer/term-caps.ts +42 -1
  224. package/src/renderer/terminal-bg-probe.ts +116 -19
  225. package/src/renderer/terminal-palette-probe.ts +322 -0
  226. package/src/renderer/terminal-palette-reader.ts +183 -0
  227. package/src/renderer/terminal-palette.ts +58 -0
  228. package/src/renderer/theme-mode-config.ts +16 -1
  229. package/src/renderer/theme.ts +234 -207
  230. package/src/renderer/thinking.ts +0 -1
  231. package/src/renderer/throbber.ts +188 -0
  232. package/src/renderer/toast-center.ts +119 -0
  233. package/src/renderer/ui-factory.ts +39 -602
  234. package/src/renderer/ui-primitives.ts +8 -25
  235. package/src/renderer/usage-modal.ts +353 -0
  236. package/src/renderer/user-message.ts +58 -0
  237. package/src/renderer/voice-capture-chip.ts +53 -0
  238. package/src/runtime/bootstrap-command-context.ts +0 -5
  239. package/src/runtime/bootstrap-command-parts.ts +38 -89
  240. package/src/runtime/bootstrap-core.ts +13 -15
  241. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  242. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  243. package/src/runtime/bootstrap-shell.ts +16 -50
  244. package/src/runtime/bootstrap.ts +14 -14
  245. package/src/runtime/client/fleet-union.ts +5 -5
  246. package/src/runtime/client/hosted-exit.ts +2 -2
  247. package/src/runtime/client/hosted-sessions.ts +1 -1
  248. package/src/runtime/client/spine-adoption.ts +1 -1
  249. package/src/runtime/context-accounting-source.ts +5 -4
  250. package/src/runtime/index.ts +1 -0
  251. package/src/runtime/notification-dispatch.ts +102 -29
  252. package/src/runtime/recovery-offer-wiring.ts +4 -1
  253. package/src/runtime/recovery-prompt.ts +31 -15
  254. package/src/runtime/runtime-services-types.ts +10 -6
  255. package/src/runtime/services.ts +17 -12
  256. package/src/runtime/session-continuity-hints.ts +2 -5
  257. package/src/runtime/session-inbound-inputs.ts +1 -1
  258. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  259. package/src/runtime/startup-typeahead-gate.ts +103 -0
  260. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  261. package/src/runtime/store/domains/index.ts +0 -8
  262. package/src/runtime/store/selectors/index.ts +0 -28
  263. package/src/runtime/store/state.ts +0 -8
  264. package/src/runtime/ui-services.ts +4 -6
  265. package/src/runtime/usage-tracker.ts +348 -0
  266. package/src/runtime/wrfc-persistence.ts +4 -4
  267. package/src/shell/blocking-input.ts +92 -2
  268. package/src/shell/recovery-input-helpers.ts +1 -1
  269. package/src/shell/session-views.ts +539 -0
  270. package/src/shell/status-report.ts +86 -0
  271. package/src/shell/transcript-scroll.ts +68 -0
  272. package/src/shell/ui-openers.ts +84 -172
  273. package/src/shell/view-openers.ts +205 -0
  274. package/src/tools/wrfc-agent-guard.ts +22 -2
  275. package/src/utils/format-duration.ts +4 -8
  276. package/src/utils/splash-lines.ts +9 -1
  277. package/src/utils/terminal-width.ts +56 -0
  278. package/src/verification/live-verifier.ts +0 -10
  279. package/src/version.ts +1 -1
  280. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  281. package/src/{panels → views}/builtin-modals.ts +22 -21
  282. package/src/views/builtin-views.ts +135 -0
  283. package/src/{panels → views}/confirm-state.ts +9 -9
  284. package/src/{panels → views}/diff-review-model.ts +10 -3
  285. package/src/{panels → views}/eval-registry.ts +1 -1
  286. package/src/{panels → views}/fleet-acts.ts +49 -85
  287. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  288. package/src/{panels → views}/fleet-gateway.ts +5 -5
  289. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  290. package/src/{panels → views}/fleet-read-model.ts +9 -9
  291. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  292. package/src/{panels → views}/fleet-spawn.ts +33 -49
  293. package/src/{panels → views}/fleet-steer.ts +12 -12
  294. package/src/{panels → views}/fleet-stop.ts +4 -4
  295. package/src/{panels → views}/fleet-transcript.ts +26 -27
  296. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  297. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  298. package/src/views/modal-surface-registry.ts +41 -0
  299. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  300. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  301. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  302. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  303. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  304. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  305. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  306. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  307. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  308. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  309. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  310. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  311. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  312. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  313. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  314. package/src/{panels → views}/modals/security-modal.ts +22 -16
  315. package/src/{panels → views}/modals/services-modal.ts +3 -3
  316. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  317. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  318. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  319. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  320. package/src/views/notifications-feed.ts +314 -0
  321. package/src/{panels → views}/polish-core.ts +38 -36
  322. package/src/{panels → views}/polish-tables.ts +35 -26
  323. package/src/{panels → views}/polish.ts +101 -95
  324. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  325. package/src/{panels → views}/provider-health-routes.ts +14 -14
  326. package/src/{panels → views}/search-focus.ts +8 -8
  327. package/src/{panels → views}/session-maintenance.ts +11 -11
  328. package/src/views/skills-discovery.ts +159 -0
  329. package/src/views/view-deps.ts +80 -0
  330. package/src/views/view-panel-adapter.ts +53 -0
  331. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  332. package/src/core/conversation-fold.ts +0 -101
  333. package/src/core/conversation-turn-structure.ts +0 -464
  334. package/src/core/conversation-user-receipts.ts +0 -42
  335. package/src/input/commands/operator-panel-runtime.ts +0 -186
  336. package/src/input/panel-integration-actions.ts +0 -33
  337. package/src/input/panel-mouse-geometry.ts +0 -97
  338. package/src/input/panel-paste-flood-guard.ts +0 -86
  339. package/src/panels/base-panel.ts +0 -301
  340. package/src/panels/builtin/agent.ts +0 -27
  341. package/src/panels/builtin/development.ts +0 -92
  342. package/src/panels/builtin/knowledge.ts +0 -12
  343. package/src/panels/builtin/operations.ts +0 -177
  344. package/src/panels/builtin/session.ts +0 -81
  345. package/src/panels/builtin/shared.ts +0 -357
  346. package/src/panels/builtin-panels.ts +0 -28
  347. package/src/panels/cost-tracker-panel.ts +0 -746
  348. package/src/panels/diff-panel.ts +0 -797
  349. package/src/panels/diff-review-panel.ts +0 -395
  350. package/src/panels/expandable-list-panel.ts +0 -189
  351. package/src/panels/fleet-deep-link.ts +0 -31
  352. package/src/panels/fleet-panel.ts +0 -799
  353. package/src/panels/git-panel.ts +0 -800
  354. package/src/panels/hosted-session-panel.ts +0 -221
  355. package/src/panels/index.ts +0 -21
  356. package/src/panels/local-auth-panel.ts +0 -478
  357. package/src/panels/notifications-feed.ts +0 -127
  358. package/src/panels/notifications-panel.ts +0 -160
  359. package/src/panels/panel-confirm-overlay.ts +0 -72
  360. package/src/panels/panel-manager.ts +0 -791
  361. package/src/panels/plugins-panel.ts +0 -371
  362. package/src/panels/scrollable-list-panel.ts +0 -498
  363. package/src/panels/skills-panel.ts +0 -454
  364. package/src/panels/token-budget-panel.ts +0 -688
  365. package/src/panels/types.ts +0 -191
  366. package/src/panels/wrfc-panel-format.ts +0 -133
  367. package/src/renderer/diff-view.ts +0 -134
  368. package/src/renderer/fleet-tab-strip.ts +0 -56
  369. package/src/renderer/footer-tips.ts +0 -53
  370. package/src/renderer/fullscreen-primitives.ts +0 -146
  371. package/src/renderer/fullscreen-workspace.ts +0 -194
  372. package/src/renderer/hint-grammar.ts +0 -52
  373. package/src/renderer/modal-factory.ts +0 -480
  374. package/src/renderer/overlay-box.ts +0 -154
  375. package/src/renderer/overlay-viewport.ts +0 -104
  376. package/src/renderer/panel-composite.ts +0 -145
  377. package/src/renderer/panel-workspace-bar.ts +0 -54
  378. package/src/renderer/process-indicator.ts +0 -163
  379. package/src/renderer/tab-strip.ts +0 -222
  380. package/src/renderer/text-layout.ts +0 -31
  381. package/src/renderer/tool-call.ts +0 -276
  382. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  383. package/src/renderer/tool-result-summary.ts +0 -219
  384. package/src/runtime/diagnostics/panels/index.ts +0 -21
  385. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  386. package/src/runtime/perf/panel-contracts.ts +0 -32
  387. package/src/runtime/store/domains/panels.ts +0 -117
  388. package/src/verification/verification-ledger.ts +0 -707
  389. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  390. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  391. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * syntax-theme.ts, the one role -> colour map for code.
3
+ *
4
+ * Both highlighters resolve through here: the tree-sitter path
5
+ * (syntax-highlighter.ts, NODE_TYPE_COLORS holds roles, not colours) and the
6
+ * regex fallback (code-block.ts). So a code block looks the same whichever
7
+ * path rendered it, and both follow the active theme.
8
+ */
9
+
10
+ import { activeTokens, type PaletteTokens } from './theme.ts';
11
+
12
+ /** A syntax colour role. `plain` is text with no specific role. */
13
+ export type SyntaxRole =
14
+ | 'keyword'
15
+ | 'string'
16
+ | 'number'
17
+ | 'comment'
18
+ | 'function'
19
+ | 'type'
20
+ | 'operator'
21
+ | 'property'
22
+ | 'builtin'
23
+ | 'plain';
24
+
25
+ export interface SyntaxStyle {
26
+ readonly fg: string;
27
+ readonly italic?: boolean;
28
+ }
29
+
30
+ const stylesByPalette = new WeakMap<PaletteTokens, Readonly<Record<SyntaxRole, SyntaxStyle>>>();
31
+
32
+ /** Role styles for a token table (memoized per table; defaults to the active theme). */
33
+ export function syntaxStyles(tokens: Readonly<PaletteTokens> = activeTokens()): Readonly<Record<SyntaxRole, SyntaxStyle>> {
34
+ const cached = stylesByPalette.get(tokens);
35
+ if (cached !== undefined) return cached;
36
+ const styles: Record<SyntaxRole, SyntaxStyle> = {
37
+ keyword: { fg: tokens.syntaxKeyword },
38
+ string: { fg: tokens.syntaxString },
39
+ number: { fg: tokens.syntaxNumber },
40
+ comment: { fg: tokens.syntaxComment, italic: true },
41
+ function: { fg: tokens.syntaxFunction },
42
+ type: { fg: tokens.syntaxType },
43
+ operator: { fg: tokens.syntaxOperator },
44
+ property: { fg: tokens.syntaxProperty },
45
+ builtin: { fg: tokens.syntaxBuiltin },
46
+ plain: { fg: tokens.text },
47
+ };
48
+ Object.freeze(styles);
49
+ stylesByPalette.set(tokens, styles);
50
+ return styles;
51
+ }
@@ -94,6 +94,5 @@ export function renderSystemMessage(
94
94
  return renderConversationNotice(content, width, {
95
95
  accent,
96
96
  text: textColor,
97
- dim: msgType === 'info',
98
97
  }, border.char);
99
98
  }
@@ -56,7 +56,7 @@ export function probeTermCaps(stdout: NodeJS.WriteStream): TermColorCaps {
56
56
  : 1;
57
57
 
58
58
  let capability: ColorCapability;
59
- if (depth >= 24) {
59
+ if (depth >= 24 || (depth >= 8 && advertisesTruecolor(process.env))) {
60
60
  capability = 'truecolor';
61
61
  } else if (depth >= 8) {
62
62
  capability = 'ansi256';
@@ -70,6 +70,47 @@ export function probeTermCaps(stdout: NodeJS.WriteStream): TermColorCaps {
70
70
  return { capability, syncedOutput };
71
71
  }
72
72
 
73
+ /** TERM values of terminals that draw 24-bit color without saying so in COLORTERM. */
74
+ const TRUECOLOR_TERMS: ReadonlySet<string> = new Set([
75
+ 'xterm-ghostty', 'xterm-kitty', 'alacritty', 'foot', 'foot-extra', 'wezterm', 'contour', 'rio', 'xterm-direct',
76
+ ]);
77
+
78
+ /** TERM_PROGRAM values of terminals that draw 24-bit color. */
79
+ const TRUECOLOR_PROGRAMS: ReadonlySet<string> = new Set([
80
+ 'ghostty', 'wezterm', 'iterm.app', 'vscode', 'hyper', 'tabby', 'rio', 'warpterminal',
81
+ ]);
82
+
83
+ /**
84
+ * Whether the environment says the terminal draws 24-bit color. The stream's
85
+ * own depth check only trusts COLORTERM and a short list, so it reports 256
86
+ * colors for Ghostty, kitty, Alacritty and foot, and a 256-color downsample
87
+ * turns every gradient into visible bands. This reads COLORTERM
88
+ * (truecolor / 24bit), a TERM ending in "-direct" (the terminfo convention for
89
+ * direct color), and TERM / TERM_PROGRAM values of terminals known to draw it.
90
+ */
91
+ function advertisesTruecolor(env: Readonly<Record<string, string | undefined>>): boolean {
92
+ const colorterm = (env['COLORTERM'] ?? '').toLowerCase();
93
+ if (colorterm === 'truecolor' || colorterm === '24bit') return true;
94
+ const term = (env['TERM'] ?? '').toLowerCase();
95
+ if (term.endsWith('-direct') || TRUECOLOR_TERMS.has(term)) return true;
96
+ return TRUECOLOR_PROGRAMS.has((env['TERM_PROGRAM'] ?? '').toLowerCase());
97
+ }
98
+
99
+ /**
100
+ * Whether the terminal can draw unicode (box drawing, beads, triangles), read
101
+ * from the environment: TERM=dumb cannot, and a locale naming a charset other
102
+ * than UTF-8 (including C and POSIX) cannot. No locale variables at all counts
103
+ * as capable, which is what modern terminal emulators report in practice.
104
+ * The conversation work tree draws ascii on a terminal this reports as limited.
105
+ */
106
+ export function probeUnicodeSupport(env: Readonly<Record<string, string | undefined>> = process.env): boolean {
107
+ if ((env['TERM'] ?? '') === 'dumb') return false;
108
+ const locale = env['LC_ALL'] || env['LC_CTYPE'] || env['LANG'] || '';
109
+ if (locale === '') return true;
110
+ if (locale === 'C' || locale === 'POSIX') return false;
111
+ return /utf-?8/i.test(locale);
112
+ }
113
+
73
114
  // ---------------------------------------------------------------------------
74
115
  // Color parsing helpers
75
116
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * terminal-bg-probe, OSC 11 terminal-background detection for `auto` theme mode.
3
3
  *
4
- *. On startup, when appearance is `auto` and stdout is a TTY, we ask
4
+ * On startup, when appearance is `auto` and stdout is a TTY, we ask
5
5
  * the terminal for its background colour with an OSC 11 query and classify the
6
6
  * reply as light or dark. Everything about this is best-effort and conservative:
7
7
  * any timeout, any unparseable reply, any ambiguity resolves to DARK, which is
@@ -27,15 +27,29 @@
27
27
  * leak into the input pipeline.
28
28
  *
29
29
  * tmux:
30
- * When $TMUX is set the query is wrapped in the tmux DCS passthrough envelope so
31
- * the outer terminal sees it. This only elicits a reply if the user has
32
- * `allow-passthrough` enabled; otherwise the query is swallowed and the timeout
33
- * fallback (dark) covers it silently.
30
+ * The query goes out as it is. tmux answers OSC 10 / 11 / 4 itself from the
31
+ * attached terminal's colours; the DCS passthrough envelope this used to use
32
+ * is discarded unless allow-passthrough is on (off by default). A query
33
+ * written while no client is attached is dropped by tmux, which is what the
34
+ * palette reader's retries are for.
35
+ *
36
+ * Palette (opt-in via `probePalette`):
37
+ * OSC 10 (foreground) and OSC 4;0..15 ride in the same write, after the OSC 11
38
+ * query. This probe's filter runs first and hands the OSC 11 body to the
39
+ * palette reader (terminal-palette-reader.ts), whose round consumes the other
40
+ * 17 replies within a 175 ms window (150 ms + 25 ms margin) and stores the
41
+ * result via terminal-palette.ts. A round with no colours is retried; a late
42
+ * palette re-themes and, in auto mode after a background timeout, settles
43
+ * light vs dark. With `probePalette` unset the bytes written and the
44
+ * filtering are exactly as described above.
34
45
  */
35
46
 
36
- import { setActiveThemeMode, type ThemeMode } from './theme.ts';
37
- import { resolveConfiguredThemeMode } from './theme-mode-config.ts';
47
+ import { refreshForTerminalPalette, setActiveThemeMode, setActiveThemeName, type ThemeMode } from './theme.ts';
48
+ import { resolveConfiguredThemeMode, resolveConfiguredThemeName } from './theme-mode-config.ts';
38
49
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
50
+ import { PALETTE_QUERIES, type PaletteProbeResolution } from './terminal-palette-probe.ts';
51
+ import { TerminalPaletteReader, type TerminalPaletteReaderOptions } from './terminal-palette-reader.ts';
52
+ import { setTerminalPalette } from './terminal-palette.ts';
39
53
 
40
54
  /** OSC 11 "query background colour" request, ST-terminated. */
41
55
  export const OSC11_QUERY = '\x1b]11;?\x1b\\';
@@ -172,6 +186,12 @@ export interface TerminalBackgroundProbeOptions {
172
186
  readonly onResolve: (result: ProbeResolution) => void;
173
187
  /** Probe window in ms (default DEFAULT_PROBE_TIMEOUT_MS). */
174
188
  readonly timeoutMs?: number;
189
+ /**
190
+ * Observer for the raw body of a complete OSC 11 reply (before
191
+ * classification). The palette probe uses it to record the background
192
+ * colour, since this filter consumes the reply ahead of it.
193
+ */
194
+ readonly onReplySpec?: (spec: string) => void;
175
195
  }
176
196
 
177
197
  /**
@@ -187,10 +207,12 @@ export class TerminalBackgroundProbe {
187
207
  private resolved = false;
188
208
  private timer: ReturnType<typeof setTimeout> | null = null;
189
209
  private readonly onResolve: (result: ProbeResolution) => void;
210
+ private readonly onReplySpec: ((spec: string) => void) | undefined;
190
211
  private readonly timeoutMs: number;
191
212
 
192
213
  constructor(options: TerminalBackgroundProbeOptions) {
193
214
  this.onResolve = options.onResolve;
215
+ this.onReplySpec = options.onReplySpec;
194
216
  this.timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
195
217
  }
196
218
 
@@ -235,6 +257,7 @@ export class TerminalBackgroundProbe {
235
257
  // keystrokes must be preserved here, not read back off this.buffer).
236
258
  const spec = this.buffer.slice(prefixIdx + OSC11_PREFIX.length, term.start);
237
259
  const remainder = this.buffer.slice(term.end);
260
+ this.onReplySpec?.(spec);
238
261
  const rgb = parseColorSpec(spec);
239
262
  if (rgb === null) {
240
263
  this.resolve('dark', 'unparseable');
@@ -267,14 +290,9 @@ export class TerminalBackgroundProbe {
267
290
  }
268
291
 
269
292
  // ---------------------------------------------------------------------------
270
- // tmux passthrough + install entry
293
+ // Install entry
271
294
  // ---------------------------------------------------------------------------
272
295
 
273
- /** Wrap a control sequence in the tmux DCS passthrough envelope (ESC doubled). */
274
- export function wrapForTmuxPassthrough(seq: string): string {
275
- return `\x1bPtmux;${seq.replace(/\x1b/g, '\x1b\x1b')}\x1b\\`;
276
- }
277
-
278
296
  /** The stdin filter the caller installs at the front of its data handler. */
279
297
  export interface ThemeProbeHandle {
280
298
  /** Filter a raw stdin chunk; returns the bytes to forward downstream. */
@@ -285,8 +303,6 @@ export interface InstallThemeProbeOptions {
285
303
  readonly configManager: Pick<ConfigManager, 'get'>;
286
304
  /** Whether stdout is a real TTY (probe only runs on a TTY in auto mode). */
287
305
  readonly isTTY: boolean;
288
- /** Process env (only TMUX is read). */
289
- readonly env: Record<string, string | undefined>;
290
306
  /** Writes the query bytes (caller wraps in its terminal-output guard). */
291
307
  readonly writeQuery: (bytes: string) => void;
292
308
  /** Called when a light reply wins within the window → repaint once. */
@@ -295,6 +311,23 @@ export interface InstallThemeProbeOptions {
295
311
  readonly timeoutMs?: number;
296
312
  /** Optional resolution observer (tests / diagnostics). */
297
313
  readonly onResolve?: (result: ProbeResolution) => void;
314
+ /**
315
+ * Also read the terminal palette (OSC 10 + OSC 4;0..15) in the same write as
316
+ * the OSC 11 query, and keep asking until it arrives (terminal-palette-reader.ts).
317
+ * Runs on a TTY in every appearance mode; the result lands in the
318
+ * terminal-palette store. Default false (query bytes unchanged).
319
+ */
320
+ readonly probePalette?: boolean;
321
+ /** Palette window override (tests). Default PALETTE_PROBE_TIMEOUT_MS. */
322
+ readonly paletteTimeoutMs?: number;
323
+ /** Optional palette round observer (tests / diagnostics). */
324
+ readonly onPaletteResolve?: (result: PaletteProbeResolution) => void;
325
+ /** Palette retry tuning (tests). */
326
+ readonly paletteRetry?: Pick<TerminalPaletteReaderOptions, 'retryDelayMs' | 'maxRounds' | 'minRoundSpacingMs' | 'now'>;
327
+ /** Where an Esc / Alt+] key held at a palette round's close goes (the input pipeline). */
328
+ readonly forwardInput?: (bytes: string) => void;
329
+ /** Subscribe to terminal resizes (a client attaching); retries an unanswered palette read. */
330
+ readonly subscribeResize?: (listener: () => void) => void;
298
331
  }
299
332
 
300
333
  /**
@@ -305,13 +338,32 @@ export interface InstallThemeProbeOptions {
305
338
  * - auto + !TTY → stay dark (headless/piped; probe cannot run).
306
339
  * Returns a handle whose filterInput() the caller wires at the front of its
307
340
  * stdin data handler. For non-auto and non-TTY cases the filter is a passthrough.
341
+ *
342
+ * The queries are always written as they are, never wrapped for tmux
343
+ * passthrough: tmux answers OSC 10, OSC 11 and OSC 4 itself from the attached
344
+ * terminal's colours, while a passthrough envelope is discarded unless the
345
+ * user turned allow-passthrough on (it is off by default), which left every
346
+ * tmux user without a palette.
308
347
  */
309
348
  export function installBackgroundThemeProbe(options: InstallThemeProbeOptions): ThemeProbeHandle {
310
349
  const pref = resolveConfiguredThemeMode(options.configManager);
350
+ // The configured theme applies before the first paint; the mode below
351
+ // (forced, or dark until the probe answers) completes the resolution.
352
+ setActiveThemeName(resolveConfiguredThemeName(options.configManager));
353
+ let backgroundTimedOut = false;
354
+ const palette = options.probePalette === true && options.isTTY
355
+ ? createPaletteReader(options, pref, () => backgroundTimedOut)
356
+ : null;
357
+ if (palette !== null) options.subscribeResize?.(() => palette.noteResize());
311
358
 
312
359
  if (pref === 'dark' || pref === 'light') {
313
360
  setActiveThemeMode(pref);
314
- return { filterInput: (chunk) => chunk };
361
+ if (palette === null) return { filterInput: (chunk) => chunk };
362
+ // Forced mode: no background classification, but the palette batch (which
363
+ // still carries OSC 11 for the background colour) goes out and the
364
+ // reader's filter consumes every reply.
365
+ palette.start();
366
+ return { filterInput: (chunk) => palette.feed(chunk) };
315
367
  }
316
368
 
317
369
  // pref === 'auto'
@@ -322,7 +374,9 @@ export function installBackgroundThemeProbe(options: InstallThemeProbeOptions):
322
374
 
323
375
  const probe = new TerminalBackgroundProbe({
324
376
  timeoutMs: options.timeoutMs,
377
+ onReplySpec: palette === null ? undefined : (spec) => palette.noteBackgroundSpec(spec),
325
378
  onResolve: (result) => {
379
+ backgroundTimedOut = result.reason === 'timeout';
326
380
  if (result.mode === 'light') {
327
381
  setActiveThemeMode('light');
328
382
  options.requestRepaint();
@@ -331,9 +385,52 @@ export function installBackgroundThemeProbe(options: InstallThemeProbeOptions):
331
385
  },
332
386
  });
333
387
 
334
- const query = options.env['TMUX'] ? wrapForTmuxPassthrough(OSC11_QUERY) : OSC11_QUERY;
335
- options.writeQuery(query);
388
+ if (palette === null) {
389
+ options.writeQuery(OSC11_QUERY);
390
+ probe.startTimeout();
391
+ return { filterInput: (chunk) => (probe.active ? probe.feed(chunk) : chunk) };
392
+ }
393
+
394
+ // One write: OSC 11 first (so the background reply arrives first and the
395
+ // background probe's timing is unchanged), then OSC 10 and OSC 4;0..15.
396
+ palette.start();
336
397
  probe.startTimeout();
398
+ return {
399
+ filterInput: (chunk) => {
400
+ const afterBackground = probe.active ? probe.feed(chunk) : chunk;
401
+ return palette.feed(afterBackground);
402
+ },
403
+ };
404
+ }
337
405
 
338
- return { filterInput: (chunk) => (probe.active ? probe.feed(chunk) : chunk) };
406
+ /**
407
+ * Palette reader whose result is stored beside the background probe's. In auto
408
+ * mode a palette that arrives after the background probe timed out (tmux with
409
+ * no client attached yet) still settles light vs dark from its background.
410
+ */
411
+ function createPaletteReader(
412
+ options: InstallThemeProbeOptions,
413
+ pref: ThemeMode | 'auto',
414
+ backgroundTimedOut: () => boolean,
415
+ ): TerminalPaletteReader {
416
+ return new TerminalPaletteReader({
417
+ ...options.paletteRetry,
418
+ timeoutMs: options.paletteTimeoutMs,
419
+ writeBatch: () => options.writeQuery(OSC11_QUERY + PALETTE_QUERIES),
420
+ onFlush: options.forwardInput,
421
+ onRoundResolve: (result) => options.onPaletteResolve?.(result),
422
+ onPalette: (result) => {
423
+ setTerminalPalette(result);
424
+ let repaint = refreshForTerminalPalette();
425
+ if (pref === 'auto' && backgroundTimedOut() && result.background !== undefined) {
426
+ const rgb = parseColorSpec(result.background);
427
+ if (rgb !== null && classifyBackgroundLuminance(rgb) === 'light') {
428
+ setActiveThemeMode('light');
429
+ repaint = true;
430
+ }
431
+ }
432
+ // The system theme is generated from this palette: re-resolve and repaint.
433
+ if (repaint) options.requestRepaint();
434
+ },
435
+ });
339
436
  }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * terminal-palette-probe, reads the terminal's foreground and 16 ANSI colours.
3
+ *
4
+ * The queries (OSC 10 foreground, OSC 4;0..15 palette) go out in the SAME write
5
+ * as the existing OSC 11 background query (see installBackgroundThemeProbe in
6
+ * terminal-bg-probe.ts), so the whole batch costs one write and one round trip.
7
+ * Terminals answer in request order: OSC 11 first, which keeps the background
8
+ * probe's timing exactly as before, then OSC 10, then OSC 4;0..15.
9
+ *
10
+ * Budget:
11
+ * The collection window is PALETTE_PROBE_TIMEOUT_MS = 175 ms, which is the
12
+ * background probe's 150 ms (DEFAULT_PROBE_TIMEOUT_MS) plus a 25 ms margin
13
+ * (PALETTE_PROBE_MARGIN_MS) for the 17 replies queued behind the OSC 11 one.
14
+ * Both windows start from the same write. Nothing blocks on it: first paint
15
+ * is not delayed, the only cost is that the stdin filter stays in front of the
16
+ * input pipeline for at most 25 ms longer than before. A terminal that
17
+ * answers all 18 queries ends the window early (typically single-digit ms).
18
+ *
19
+ * Stream safety mirrors the background probe: only matched OSC 4 / OSC 10 /
20
+ * OSC 11 replies are consumed, every other byte passes through in order, a
21
+ * reply split across chunks is held until complete, and on timeout any held
22
+ * reply fragment is discarded rather than flushed into the composer.
23
+ *
24
+ * This module deliberately imports nothing from terminal-bg-probe.ts (that file
25
+ * imports this one), so the two never form an import cycle.
26
+ */
27
+
28
+ import {
29
+ emptyTerminalPalette,
30
+ TERMINAL_PALETTE_ANSI_SLOTS,
31
+ type TerminalPalette,
32
+ } from './terminal-palette.ts';
33
+
34
+ /** OSC 10 "query default foreground colour", ST-terminated. */
35
+ const OSC10_QUERY = '\x1b]10;?\x1b\\';
36
+
37
+ /** OSC 4 "query palette entry N", ST-terminated. */
38
+ function osc4Query(index: number): string {
39
+ return `\x1b]4;${index};?\x1b\\`;
40
+ }
41
+
42
+ /** OSC 10 followed by OSC 4;0 .. OSC 4;15, appended after the OSC 11 query. */
43
+ export const PALETTE_QUERIES: string = OSC10_QUERY
44
+ + Array.from({ length: TERMINAL_PALETTE_ANSI_SLOTS }, (_, i) => osc4Query(i)).join('');
45
+
46
+ /** Extra time the palette window stays open past the background probe window. */
47
+ const PALETTE_PROBE_MARGIN_MS = 25;
48
+
49
+ /** Palette collection window: background probe window (150 ms) + margin (25 ms). */
50
+ export const PALETTE_PROBE_TIMEOUT_MS = 150 + PALETTE_PROBE_MARGIN_MS;
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Pure parsing
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** Scale a 1..4 digit hex channel to 8 bits against its own full scale. */
57
+ function channelTo8Bit(hex: string): number | null {
58
+ if (!/^[0-9a-fA-F]{1,4}$/.test(hex)) return null;
59
+ const max = 16 ** hex.length - 1;
60
+ return Math.round((parseInt(hex, 16) / max) * 255);
61
+ }
62
+
63
+ function toHexByte(value: number): string {
64
+ return value.toString(16).padStart(2, '0');
65
+ }
66
+
67
+ /**
68
+ * Parse an OSC colour reply body into `#rrggbb`. Accepts `rgb:R/G/B` with 1 to
69
+ * 4 hex digits per channel (each channel scaled by its own width, so `f`, `ff`,
70
+ * `fff`, `ffff` all give `ff`), `rgba:R/G/B/A` (alpha ignored), and `#rrggbb` /
71
+ * `#rrrrggggbbbb`. Returns null for anything else.
72
+ */
73
+ export function parseOscColorToHex(spec: string): string | null {
74
+ const trimmed = spec.trim();
75
+ let channels: string[];
76
+
77
+ if (trimmed.startsWith('rgb:') || trimmed.startsWith('rgba:')) {
78
+ const parts = trimmed.slice(trimmed.indexOf(':') + 1).split('/');
79
+ const expected = trimmed.startsWith('rgba:') ? 4 : 3;
80
+ if (parts.length !== expected) return null;
81
+ channels = parts.slice(0, 3);
82
+ } else if (trimmed.startsWith('#')) {
83
+ const hex = trimmed.slice(1);
84
+ if (hex.length !== 6 && hex.length !== 12) return null;
85
+ const per = hex.length / 3;
86
+ channels = [hex.slice(0, per), hex.slice(per, per * 2), hex.slice(per * 2)];
87
+ } else {
88
+ return null;
89
+ }
90
+
91
+ const values = channels.map(channelTo8Bit);
92
+ if (values.some((v) => v === null)) return null;
93
+ return `#${(values as number[]).map(toHexByte).join('')}`;
94
+ }
95
+
96
+ /** Start of a reply we own: ESC ] 4 ; N ; or ESC ] 10 ; or ESC ] 11 ; */
97
+ const REPLY_INTRODUCER = /\x1b\](?:4;(\d{1,3});|(1[01]);)/;
98
+
99
+ /** A buffer tail that could still grow into REPLY_INTRODUCER. */
100
+ const PARTIAL_INTRODUCER = /\x1b(?:\](?:4(?:;\d{0,3})?|1[01]?)?)?$/;
101
+
102
+ /** Length of a trailing partial introducer to hold back, or 0. */
103
+ function trailingPartialLen(buffer: string): number {
104
+ const esc = buffer.lastIndexOf('\x1b');
105
+ if (esc === -1) return 0;
106
+ const tail = buffer.slice(esc);
107
+ const m = PARTIAL_INTRODUCER.exec(tail);
108
+ return m !== null && m.index === 0 ? tail.length : 0;
109
+ }
110
+
111
+ /**
112
+ * OSC string terminator at/after `from`: BEL or ST (ESC \). Same rules as the
113
+ * background probe: a lone trailing ESC waits for the next byte; an ESC followed
114
+ * by anything other than `\` ends the body (malformed terminator).
115
+ */
116
+ function findTerminator(buffer: string, from: number): { start: number; end: number } | null {
117
+ for (let k = from; k < buffer.length; k++) {
118
+ const c = buffer[k];
119
+ if (c === '\x07') return { start: k, end: k + 1 };
120
+ if (c === '\x1b') {
121
+ if (k + 1 >= buffer.length) return null;
122
+ return buffer[k + 1] === '\\' ? { start: k, end: k + 2 } : { start: k, end: k + 1 };
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // TerminalPaletteProbe, the stateful stream filter
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export interface PaletteProbeResolution {
133
+ /** Collected colours; unanswered or unparseable slots are undefined. */
134
+ readonly palette: TerminalPalette;
135
+ /** 'complete' when all 18 replies arrived, 'timeout' when the window closed. */
136
+ readonly reason: 'complete' | 'timeout';
137
+ /** How many of the 18 queries got a reply (parseable or not). */
138
+ readonly replies: number;
139
+ }
140
+
141
+ export interface TerminalPaletteProbeOptions {
142
+ /** Called exactly once, on completion or timeout. */
143
+ readonly onResolve: (result: PaletteProbeResolution) => void;
144
+ /** Window in ms (default PALETTE_PROBE_TIMEOUT_MS). */
145
+ readonly timeoutMs?: number;
146
+ /**
147
+ * Receives a key held at the window's close that never became a reply: a
148
+ * lone ESC (Esc) or ESC ] (Alt+]). Without it they are dropped, which is
149
+ * tolerable for the startup window only; a retry round can open while the
150
+ * user is typing.
151
+ */
152
+ readonly onFlush?: (bytes: string) => void;
153
+ }
154
+
155
+ /** Total replies expected: background + foreground + 16 ANSI slots. */
156
+ const EXPECTED_REPLIES = 2 + TERMINAL_PALETTE_ANSI_SLOTS;
157
+
158
+ /**
159
+ * Stateful OSC 4 / 10 / 11 reply filter. feed() takes a raw stdin chunk and
160
+ * returns the bytes that continue down the input pipeline. The background
161
+ * probe, when it runs in front of this filter and consumes the OSC 11 reply,
162
+ * hands the reply body over with noteBackgroundSpec().
163
+ */
164
+ export class TerminalPaletteProbe {
165
+ /** True until resolved. */
166
+ public active = true;
167
+
168
+ private buffer = '';
169
+ private resolved = false;
170
+ private timer: ReturnType<typeof setTimeout> | null = null;
171
+ private readonly palette: TerminalPalette = emptyTerminalPalette();
172
+ private backgroundAnswered = false;
173
+ private foregroundAnswered = false;
174
+ private readonly ansiAnswered = new Array<boolean>(TERMINAL_PALETTE_ANSI_SLOTS).fill(false);
175
+ private readonly onResolve: (result: PaletteProbeResolution) => void;
176
+ private readonly onFlush: ((bytes: string) => void) | undefined;
177
+ private readonly timeoutMs: number;
178
+
179
+ constructor(options: TerminalPaletteProbeOptions) {
180
+ this.onResolve = options.onResolve;
181
+ this.onFlush = options.onFlush;
182
+ this.timeoutMs = options.timeoutMs ?? PALETTE_PROBE_TIMEOUT_MS;
183
+ }
184
+
185
+ /** Arm the window. Call right after writing the batched query. */
186
+ startTimeout(): void {
187
+ this.timer = setTimeout(() => this.resolve('timeout'), this.timeoutMs);
188
+ (this.timer as unknown as { unref?: () => void }).unref?.();
189
+ }
190
+
191
+ /** Record an OSC 11 reply body consumed upstream by the background probe. */
192
+ noteBackgroundSpec(spec: string): void {
193
+ if (!this.active) return;
194
+ this.recordBackground(spec);
195
+ if (this.isComplete()) this.resolve('complete');
196
+ }
197
+
198
+ /** Consume a raw stdin chunk; returns the passthrough bytes. */
199
+ feed(chunk: string): string {
200
+ if (!this.active) return chunk;
201
+ this.buffer += chunk;
202
+ let out = '';
203
+
204
+ for (;;) {
205
+ const m = REPLY_INTRODUCER.exec(this.buffer);
206
+ if (m === null) {
207
+ const keep = trailingPartialLen(this.buffer);
208
+ out += this.buffer.slice(0, this.buffer.length - keep);
209
+ this.buffer = keep > 0 ? this.buffer.slice(this.buffer.length - keep) : '';
210
+ break;
211
+ }
212
+
213
+ out += this.buffer.slice(0, m.index);
214
+ const bodyStart = m.index + m[0].length;
215
+ const term = findTerminator(this.buffer, bodyStart);
216
+ if (term === null) {
217
+ this.buffer = this.buffer.slice(m.index);
218
+ break;
219
+ }
220
+
221
+ const spec = this.buffer.slice(bodyStart, term.start);
222
+ if (m[1] !== undefined) this.recordAnsi(Number(m[1]), spec);
223
+ else if (m[2] === '10') this.recordForeground(spec);
224
+ else this.recordBackground(spec);
225
+ this.buffer = this.buffer.slice(term.end);
226
+
227
+ if (this.isComplete()) {
228
+ // Keep the post-reply bytes before resolve() clears the buffer.
229
+ const remainder = this.buffer;
230
+ this.resolve('complete');
231
+ out += remainder;
232
+ break;
233
+ }
234
+ }
235
+
236
+ return out;
237
+ }
238
+
239
+ private recordBackground(spec: string): void {
240
+ this.backgroundAnswered = true;
241
+ this.palette.background = parseOscColorToHex(spec) ?? undefined;
242
+ }
243
+
244
+ private recordForeground(spec: string): void {
245
+ this.foregroundAnswered = true;
246
+ this.palette.foreground = parseOscColorToHex(spec) ?? undefined;
247
+ }
248
+
249
+ private recordAnsi(index: number, spec: string): void {
250
+ // Replies for slots we never asked about are still consumed, not recorded.
251
+ if (index < 0 || index >= TERMINAL_PALETTE_ANSI_SLOTS) return;
252
+ this.ansiAnswered[index] = true;
253
+ this.palette.ansi[index] = parseOscColorToHex(spec) ?? undefined;
254
+ }
255
+
256
+ private replyCount(): number {
257
+ return (this.backgroundAnswered ? 1 : 0)
258
+ + (this.foregroundAnswered ? 1 : 0)
259
+ + this.ansiAnswered.filter(Boolean).length;
260
+ }
261
+
262
+ private isComplete(): boolean {
263
+ return this.replyCount() === EXPECTED_REPLIES;
264
+ }
265
+
266
+ private resolve(reason: PaletteProbeResolution['reason']): void {
267
+ if (this.resolved) return;
268
+ this.resolved = true;
269
+ this.active = false;
270
+ if (this.timer !== null) {
271
+ clearTimeout(this.timer);
272
+ this.timer = null;
273
+ }
274
+ // A held reply fragment is discarded, never flushed into the composer.
275
+ // The two holds that are real keys (Esc, Alt+]) are handed on.
276
+ const held = this.buffer;
277
+ this.buffer = '';
278
+ if (held === '\x1b' || held === '\x1b]') this.onFlush?.(held);
279
+ this.onResolve({
280
+ palette: { ...this.palette, ansi: [...this.palette.ansi] },
281
+ reason,
282
+ replies: this.replyCount(),
283
+ });
284
+ }
285
+ }
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // Late replies (outside any collection window)
289
+ // ---------------------------------------------------------------------------
290
+
291
+ /** One reply found by sweepPaletteReplies. */
292
+ export interface PaletteReply {
293
+ readonly slot: 'background' | 'foreground' | number;
294
+ /** `#rrggbb`, or undefined when the body did not parse. */
295
+ readonly hex: string | undefined;
296
+ }
297
+
298
+ /**
299
+ * Remove complete OSC 4 / 10 / 11 replies from a chunk that arrived outside a
300
+ * collection window (a reply slower than the window, over ssh or a busy
301
+ * multiplexer). Unlike the probe's own filter this never holds bytes back: a
302
+ * reply split across chunks passes through, so a lone Esc key is never
303
+ * delayed. Replies are complete in one read in practice.
304
+ */
305
+ export function sweepPaletteReplies(chunk: string): { readonly out: string; readonly replies: PaletteReply[] } {
306
+ if (!chunk.includes('\x1b]')) return { out: chunk, replies: [] };
307
+ const replies: PaletteReply[] = [];
308
+ let rest = chunk;
309
+ let out = '';
310
+ for (;;) {
311
+ const m = REPLY_INTRODUCER.exec(rest);
312
+ if (m === null) break;
313
+ const term = findTerminator(rest, m.index + m[0].length);
314
+ if (term === null) break;
315
+ const hex = parseOscColorToHex(rest.slice(m.index + m[0].length, term.start)) ?? undefined;
316
+ const slot = m[1] !== undefined ? Number(m[1]) : m[2] === '10' ? 'foreground' : 'background';
317
+ if (typeof slot !== 'number' || slot < TERMINAL_PALETTE_ANSI_SLOTS) replies.push({ slot, hex });
318
+ out += rest.slice(0, m.index);
319
+ rest = rest.slice(term.end);
320
+ }
321
+ return { out: out + rest, replies };
322
+ }