@pellux/goodvibes-tui 2.0.20 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +16 -16
  3. package/docs/README.md +4 -4
  4. package/docs/foundation-artifacts/operator-contract.json +59 -4
  5. package/package.json +16 -6
  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-registry.ts +77 -35
  52. package/src/input/commands/acp-runtime.ts +19 -9
  53. package/src/input/commands/channel-runtime.ts +2 -2
  54. package/src/input/commands/ci-runtime.ts +1 -1
  55. package/src/input/commands/codebase-runtime.ts +1 -1
  56. package/src/input/commands/config.ts +7 -1
  57. package/src/input/commands/context-window.ts +25 -19
  58. package/src/input/commands/control-room-runtime.ts +18 -18
  59. package/src/input/commands/conversation-runtime.ts +0 -1
  60. package/src/input/commands/cost-runtime.ts +29 -20
  61. package/src/input/commands/diff-runtime.ts +35 -181
  62. package/src/input/commands/eval.ts +1 -1
  63. package/src/input/commands/experience-runtime.ts +3 -7
  64. package/src/input/commands/fresh-conversation.ts +86 -0
  65. package/src/input/commands/git-runtime.ts +5 -22
  66. package/src/input/commands/graph-runtime.ts +1 -1
  67. package/src/input/commands/guidance-runtime.ts +1 -1
  68. package/src/input/commands/health-runtime.ts +8 -9
  69. package/src/input/commands/hooks-runtime.ts +2 -2
  70. package/src/input/commands/hosted-runtime.ts +3 -3
  71. package/src/input/commands/incident-runtime.ts +3 -3
  72. package/src/input/commands/intelligence-runtime.ts +6 -9
  73. package/src/input/commands/knowledge.ts +2 -2
  74. package/src/input/commands/legacy-panel-command.ts +57 -0
  75. package/src/input/commands/local-auth-runtime.ts +8 -9
  76. package/src/input/commands/local-provider-runtime.ts +7 -4
  77. package/src/input/commands/local-runtime.ts +12 -15
  78. package/src/input/commands/local-setup-review.ts +1 -1
  79. package/src/input/commands/local-setup.ts +13 -19
  80. package/src/input/commands/memory.ts +1 -1
  81. package/src/input/commands/notify-runtime.ts +9 -0
  82. package/src/input/commands/operator-runtime.ts +10 -12
  83. package/src/input/commands/permissions-provenance.ts +3 -3
  84. package/src/input/commands/planning-runtime.ts +7 -7
  85. package/src/input/commands/platform-sandbox-runtime.ts +2 -2
  86. package/src/input/commands/platform-services-runtime.ts +1 -1
  87. package/src/input/commands/policy-dispatch.ts +2 -2
  88. package/src/input/commands/policy.ts +3 -3
  89. package/src/input/commands/provider.ts +1 -1
  90. package/src/input/commands/remote-runtime.ts +1 -1
  91. package/src/input/commands/review-runtime.ts +39 -94
  92. package/src/input/commands/rewind-runtime.ts +81 -108
  93. package/src/input/commands/runtime-services.ts +15 -17
  94. package/src/input/commands/services-runtime.ts +2 -2
  95. package/src/input/commands/session-content.ts +21 -5
  96. package/src/input/commands/session-workflow.ts +13 -38
  97. package/src/input/commands/settings-sync-runtime.ts +2 -2
  98. package/src/input/commands/shell-core.ts +19 -4
  99. package/src/input/commands/skills-runtime.ts +2 -2
  100. package/src/input/commands/status-runtime.ts +13 -0
  101. package/src/input/commands/subscription-runtime.ts +1 -1
  102. package/src/input/commands/tasks-runtime.ts +2 -8
  103. package/src/input/commands/work-plan-runtime.ts +4 -4
  104. package/src/input/commands/workstream-attempts.ts +24 -47
  105. package/src/input/commands/workstream-runtime.ts +2 -2
  106. package/src/input/commands/worktree-runtime.ts +3 -3
  107. package/src/input/commands.ts +3 -1
  108. package/src/input/composer-editor.ts +44 -0
  109. package/src/input/concealed-input.ts +1 -1
  110. package/src/input/config-modal-types.ts +20 -8
  111. package/src/input/config-modal.ts +106 -105
  112. package/src/input/confirm-dialog-types.ts +28 -0
  113. package/src/input/confirm-dialog.ts +70 -0
  114. package/src/input/delete-key-policy.ts +3 -3
  115. package/src/input/feed-context-factory.ts +15 -16
  116. package/src/input/handler-command-route.ts +7 -42
  117. package/src/input/handler-content-actions.ts +16 -15
  118. package/src/input/handler-feed-routes.ts +26 -211
  119. package/src/input/handler-feed.ts +68 -95
  120. package/src/input/handler-interactions.ts +5 -3
  121. package/src/input/handler-modal-routes.ts +215 -216
  122. package/src/input/handler-modal-stack.ts +81 -11
  123. package/src/input/handler-modal-token-routes.ts +4 -0
  124. package/src/input/handler-onboarding.ts +0 -1
  125. package/src/input/handler-picker-routes.ts +65 -107
  126. package/src/input/handler-session-view-route.ts +149 -0
  127. package/src/input/handler-shortcuts.ts +16 -154
  128. package/src/input/handler-types.ts +4 -2
  129. package/src/input/handler-ui-state.ts +45 -15
  130. package/src/input/handler-work-tree-route.ts +123 -0
  131. package/src/input/handler.ts +25 -70
  132. package/src/input/keybindings.ts +16 -71
  133. package/src/input/masked-entry-modal.ts +117 -0
  134. package/src/input/mcp-workspace.ts +48 -17
  135. package/src/input/model-picker-filter.ts +8 -0
  136. package/src/input/model-picker-open.ts +99 -0
  137. package/src/input/model-picker-types.ts +1 -1
  138. package/src/input/model-picker.ts +92 -9
  139. package/src/input/notifications-modal.ts +123 -0
  140. package/src/input/overlay-filter.ts +26 -0
  141. package/src/input/profile-picker-modal.ts +37 -8
  142. package/src/input/search.ts +3 -3
  143. package/src/input/selection-modal-queue.ts +2 -0
  144. package/src/input/selection-modal.ts +28 -16
  145. package/src/input/session-picker-modal.ts +33 -8
  146. package/src/input/settings-modal-activation.ts +7 -2
  147. package/src/input/settings-modal-data.ts +11 -6
  148. package/src/input/settings-modal-types.ts +1 -1
  149. package/src/input/settings-modal.ts +20 -20
  150. package/src/input/submission-intent.ts +1 -1
  151. package/src/input/submission-router.ts +4 -9
  152. package/src/input/surface-modal-host.ts +110 -0
  153. package/src/input/theme-settings-actions.ts +82 -0
  154. package/src/input/usage-modal.ts +183 -0
  155. package/src/input/views.ts +97 -0
  156. package/src/main.ts +175 -165
  157. package/src/permissions/hunk-selection.ts +1 -1
  158. package/src/permissions/prompt-card.ts +511 -0
  159. package/src/permissions/prompt.ts +59 -622
  160. package/src/renderer/agents-modal.ts +310 -0
  161. package/src/renderer/autocomplete-overlay.ts +42 -220
  162. package/src/renderer/back-to-bottom.ts +80 -0
  163. package/src/renderer/block-actions-overlay.ts +30 -48
  164. package/src/renderer/bookmark-modal.ts +48 -79
  165. package/src/renderer/changes-modal.ts +325 -0
  166. package/src/renderer/code-block.ts +184 -106
  167. package/src/renderer/command-palette.ts +93 -0
  168. package/src/renderer/composer-fragments.ts +4 -4
  169. package/src/renderer/composer.ts +146 -0
  170. package/src/renderer/compositor.ts +26 -160
  171. package/src/renderer/config-modal.ts +185 -81
  172. package/src/renderer/confirm-dialog.ts +50 -0
  173. package/src/renderer/context-inspector.ts +116 -154
  174. package/src/renderer/context-status-hint.ts +4 -4
  175. package/src/renderer/conversation-layout.ts +20 -0
  176. package/src/renderer/conversation-overlays.ts +94 -84
  177. package/src/renderer/conversation-surface.ts +19 -38
  178. package/src/renderer/diff-tint.ts +98 -0
  179. package/src/renderer/file-picker-overlay.ts +63 -90
  180. package/src/renderer/footer-targets.ts +38 -0
  181. package/src/renderer/git-status.ts +3 -4
  182. package/src/renderer/header-line.ts +140 -117
  183. package/src/renderer/help-overlay.ts +133 -219
  184. package/src/renderer/history-search-overlay.ts +15 -12
  185. package/src/renderer/lane-graph/bead.ts +800 -0
  186. package/src/renderer/lane-graph/glyphs.ts +134 -0
  187. package/src/renderer/lane-graph/layout.ts +248 -0
  188. package/src/renderer/lane-graph/paint.ts +537 -0
  189. package/src/renderer/lane-graph/semantic-memo.ts +77 -0
  190. package/src/renderer/layout-engine.ts +2 -57
  191. package/src/renderer/layout.ts +7 -13
  192. package/src/renderer/markdown-inline.ts +2 -1
  193. package/src/renderer/markdown-table.ts +17 -12
  194. package/src/renderer/markdown.ts +92 -34
  195. package/src/renderer/masked-entry-modal.ts +78 -0
  196. package/src/renderer/mcp-workspace.ts +166 -203
  197. package/src/renderer/model-workspace.ts +381 -436
  198. package/src/renderer/notifications-modal.ts +116 -0
  199. package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
  200. package/src/renderer/process-view.ts +192 -0
  201. package/src/renderer/profile-picker-modal.ts +58 -110
  202. package/src/renderer/search-overlay.ts +6 -3
  203. package/src/renderer/selection-modal-overlay.ts +87 -327
  204. package/src/renderer/semantic-diff.ts +2 -2
  205. package/src/renderer/session-chips.ts +102 -0
  206. package/src/renderer/session-picker-modal.ts +177 -297
  207. package/src/renderer/settings-modal-connections.ts +14 -23
  208. package/src/renderer/settings-modal-context.ts +367 -0
  209. package/src/renderer/settings-modal-helpers.ts +6 -11
  210. package/src/renderer/settings-modal.ts +322 -726
  211. package/src/renderer/shell-surface.ts +277 -175
  212. package/src/renderer/shortcuts-overlay.ts +217 -0
  213. package/src/renderer/status-line.ts +475 -0
  214. package/src/renderer/surface-compose.ts +174 -0
  215. package/src/renderer/surface-kit-extra.ts +303 -0
  216. package/src/renderer/surface-kit-list.ts +366 -0
  217. package/src/renderer/surface-kit-parts.ts +360 -0
  218. package/src/renderer/surface-kit.ts +568 -0
  219. package/src/renderer/syntax-highlighter.ts +280 -197
  220. package/src/renderer/syntax-theme.ts +51 -0
  221. package/src/renderer/system-message.ts +0 -1
  222. package/src/renderer/term-caps.ts +42 -1
  223. package/src/renderer/terminal-bg-probe.ts +116 -19
  224. package/src/renderer/terminal-palette-probe.ts +322 -0
  225. package/src/renderer/terminal-palette-reader.ts +183 -0
  226. package/src/renderer/terminal-palette.ts +58 -0
  227. package/src/renderer/theme-mode-config.ts +16 -1
  228. package/src/renderer/theme.ts +234 -207
  229. package/src/renderer/thinking.ts +0 -1
  230. package/src/renderer/throbber.ts +188 -0
  231. package/src/renderer/toast-center.ts +119 -0
  232. package/src/renderer/ui-factory.ts +39 -602
  233. package/src/renderer/ui-primitives.ts +8 -25
  234. package/src/renderer/usage-modal.ts +353 -0
  235. package/src/renderer/user-message.ts +58 -0
  236. package/src/renderer/voice-capture-chip.ts +53 -0
  237. package/src/runtime/bootstrap-command-context.ts +0 -5
  238. package/src/runtime/bootstrap-command-parts.ts +38 -89
  239. package/src/runtime/bootstrap-core.ts +13 -15
  240. package/src/runtime/bootstrap-hook-bridge.ts +12 -21
  241. package/src/runtime/bootstrap-notifier-sync.ts +17 -1
  242. package/src/runtime/bootstrap-shell.ts +16 -50
  243. package/src/runtime/bootstrap.ts +14 -14
  244. package/src/runtime/client/fleet-union.ts +5 -5
  245. package/src/runtime/client/hosted-exit.ts +2 -2
  246. package/src/runtime/client/hosted-sessions.ts +1 -1
  247. package/src/runtime/client/spine-adoption.ts +1 -1
  248. package/src/runtime/context-accounting-source.ts +5 -4
  249. package/src/runtime/index.ts +1 -0
  250. package/src/runtime/notification-dispatch.ts +102 -29
  251. package/src/runtime/recovery-offer-wiring.ts +4 -1
  252. package/src/runtime/recovery-prompt.ts +31 -15
  253. package/src/runtime/runtime-services-types.ts +10 -6
  254. package/src/runtime/services.ts +17 -12
  255. package/src/runtime/session-continuity-hints.ts +2 -5
  256. package/src/runtime/session-inbound-inputs.ts +1 -1
  257. package/src/runtime/session-resume-liveness-confirm.ts +14 -2
  258. package/src/runtime/startup-typeahead-gate.ts +103 -0
  259. package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
  260. package/src/runtime/store/domains/index.ts +0 -8
  261. package/src/runtime/store/selectors/index.ts +0 -28
  262. package/src/runtime/store/state.ts +0 -8
  263. package/src/runtime/ui-services.ts +4 -6
  264. package/src/runtime/usage-tracker.ts +348 -0
  265. package/src/runtime/wrfc-persistence.ts +4 -4
  266. package/src/shell/blocking-input.ts +92 -2
  267. package/src/shell/recovery-input-helpers.ts +1 -1
  268. package/src/shell/session-views.ts +539 -0
  269. package/src/shell/status-report.ts +86 -0
  270. package/src/shell/transcript-scroll.ts +68 -0
  271. package/src/shell/ui-openers.ts +84 -172
  272. package/src/shell/view-openers.ts +205 -0
  273. package/src/tools/wrfc-agent-guard.ts +22 -2
  274. package/src/utils/format-duration.ts +4 -8
  275. package/src/utils/splash-lines.ts +9 -1
  276. package/src/utils/terminal-width.ts +56 -0
  277. package/src/verification/verification-ledger.ts +18 -16
  278. package/src/{panels → views}/agent-inspector-shared.ts +2 -2
  279. package/src/{panels → views}/builtin-modals.ts +22 -21
  280. package/src/views/builtin-views.ts +135 -0
  281. package/src/{panels → views}/confirm-state.ts +9 -9
  282. package/src/{panels → views}/diff-review-model.ts +10 -3
  283. package/src/{panels → views}/eval-registry.ts +1 -1
  284. package/src/{panels → views}/fleet-acts.ts +49 -85
  285. package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
  286. package/src/{panels → views}/fleet-gateway.ts +5 -5
  287. package/src/{panels → views}/fleet-observed-render.ts +16 -16
  288. package/src/{panels → views}/fleet-read-model.ts +9 -9
  289. package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
  290. package/src/{panels → views}/fleet-spawn.ts +33 -49
  291. package/src/{panels → views}/fleet-steer.ts +12 -12
  292. package/src/{panels → views}/fleet-stop.ts +4 -4
  293. package/src/{panels → views}/fleet-transcript.ts +26 -27
  294. package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
  295. package/src/{panels → views}/hosted-session-feed.ts +6 -6
  296. package/src/views/modal-surface-registry.ts +41 -0
  297. package/src/{panels → views}/modals/devices-modal.ts +2 -2
  298. package/src/{panels → views}/modals/hooks-modal.ts +13 -13
  299. package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
  300. package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
  301. package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
  302. package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
  303. package/src/{panels → views}/modals/memory-modal.ts +8 -8
  304. package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
  305. package/src/{panels → views}/modals/pairing-modal.ts +9 -9
  306. package/src/{panels → views}/modals/planning-modal.ts +15 -20
  307. package/src/{panels → views}/modals/plugins-modal.ts +6 -6
  308. package/src/{panels → views}/modals/policy-modal.ts +13 -5
  309. package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
  310. package/src/{panels → views}/modals/remote-modal.ts +3 -3
  311. package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
  312. package/src/{panels → views}/modals/security-modal.ts +22 -16
  313. package/src/{panels → views}/modals/services-modal.ts +3 -3
  314. package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
  315. package/src/{panels → views}/modals/skills-modal.ts +9 -9
  316. package/src/{panels → views}/modals/subscription-modal.ts +3 -3
  317. package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
  318. package/src/views/notifications-feed.ts +314 -0
  319. package/src/{panels → views}/polish-core.ts +38 -36
  320. package/src/{panels → views}/polish-tables.ts +35 -26
  321. package/src/{panels → views}/polish.ts +101 -95
  322. package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
  323. package/src/{panels → views}/provider-health-routes.ts +14 -14
  324. package/src/{panels → views}/search-focus.ts +8 -8
  325. package/src/{panels → views}/session-maintenance.ts +11 -11
  326. package/src/views/skills-discovery.ts +159 -0
  327. package/src/views/view-deps.ts +80 -0
  328. package/src/views/view-panel-adapter.ts +53 -0
  329. package/src/{panels → views}/workstream-graph-render.ts +1 -1
  330. package/src/core/conversation-fold.ts +0 -101
  331. package/src/core/conversation-turn-structure.ts +0 -464
  332. package/src/core/conversation-user-receipts.ts +0 -42
  333. package/src/input/commands/operator-panel-runtime.ts +0 -186
  334. package/src/input/panel-integration-actions.ts +0 -33
  335. package/src/input/panel-mouse-geometry.ts +0 -97
  336. package/src/input/panel-paste-flood-guard.ts +0 -86
  337. package/src/panels/base-panel.ts +0 -301
  338. package/src/panels/builtin/agent.ts +0 -27
  339. package/src/panels/builtin/development.ts +0 -92
  340. package/src/panels/builtin/knowledge.ts +0 -12
  341. package/src/panels/builtin/operations.ts +0 -177
  342. package/src/panels/builtin/session.ts +0 -81
  343. package/src/panels/builtin/shared.ts +0 -357
  344. package/src/panels/builtin-panels.ts +0 -28
  345. package/src/panels/cost-tracker-panel.ts +0 -746
  346. package/src/panels/diff-panel.ts +0 -797
  347. package/src/panels/diff-review-panel.ts +0 -395
  348. package/src/panels/expandable-list-panel.ts +0 -189
  349. package/src/panels/fleet-deep-link.ts +0 -31
  350. package/src/panels/fleet-panel.ts +0 -799
  351. package/src/panels/git-panel.ts +0 -800
  352. package/src/panels/hosted-session-panel.ts +0 -221
  353. package/src/panels/index.ts +0 -21
  354. package/src/panels/local-auth-panel.ts +0 -478
  355. package/src/panels/notifications-feed.ts +0 -127
  356. package/src/panels/notifications-panel.ts +0 -160
  357. package/src/panels/panel-confirm-overlay.ts +0 -72
  358. package/src/panels/panel-manager.ts +0 -791
  359. package/src/panels/plugins-panel.ts +0 -371
  360. package/src/panels/scrollable-list-panel.ts +0 -498
  361. package/src/panels/skills-panel.ts +0 -454
  362. package/src/panels/token-budget-panel.ts +0 -688
  363. package/src/panels/types.ts +0 -191
  364. package/src/panels/wrfc-panel-format.ts +0 -133
  365. package/src/renderer/diff-view.ts +0 -134
  366. package/src/renderer/fleet-tab-strip.ts +0 -56
  367. package/src/renderer/footer-tips.ts +0 -53
  368. package/src/renderer/fullscreen-primitives.ts +0 -146
  369. package/src/renderer/fullscreen-workspace.ts +0 -194
  370. package/src/renderer/hint-grammar.ts +0 -52
  371. package/src/renderer/modal-factory.ts +0 -480
  372. package/src/renderer/overlay-box.ts +0 -154
  373. package/src/renderer/overlay-viewport.ts +0 -104
  374. package/src/renderer/panel-composite.ts +0 -145
  375. package/src/renderer/panel-workspace-bar.ts +0 -54
  376. package/src/renderer/process-indicator.ts +0 -163
  377. package/src/renderer/tab-strip.ts +0 -222
  378. package/src/renderer/text-layout.ts +0 -31
  379. package/src/renderer/tool-call.ts +0 -276
  380. package/src/renderer/tool-result-expanded-lines.ts +0 -48
  381. package/src/renderer/tool-result-summary.ts +0 -219
  382. package/src/runtime/diagnostics/panels/index.ts +0 -21
  383. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  384. package/src/runtime/perf/panel-contracts.ts +0 -32
  385. package/src/runtime/store/domains/panels.ts +0 -117
  386. /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
  387. /package/src/{panels → views}/modals/modal-theme.ts +0 -0
  388. /package/src/{panels → views}/provider-health-domains.ts +0 -0
@@ -1,34 +1,34 @@
1
1
  import { type Line, type Cell, createStyledCell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
2
2
  import { UIFactory } from './ui-factory.ts';
3
- import { getDisplayWidth } from '../utils/terminal-width.ts';
3
+ import { getDisplayWidth, wrapPreservingIndent } from '../utils/terminal-width.ts';
4
4
  import { LAYOUT } from './layout.ts';
5
5
  import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
6
- import { UI_TONES } from './ui-primitives.ts';
6
+ import { activeTokens } from './theme.ts';
7
+ import { syntaxStyles } from './syntax-theme.ts';
7
8
 
8
9
  /**
9
- * Regex-fallback tokenizer theme. VS Code Dark+ inspired, but the
10
- * semantic-token hues (string/number/keyword/type/function/operator/
11
- * property/comment) are pinned to the same values as the tree-sitter
12
- * Vaporwave palette in syntax-highlighter.ts's TOKEN_STYLES map, so a code
13
- * block that starts on the regex fallback and gets replaced by the async
14
- * tree-sitter parse doesn't visibly shift color mid-stream.
15
- * `accent` and `bg` are chrome (the language-label header bar and the body
16
- * background), not syntax tokens, and keep their original VS Code Dark+
17
- * values, folded here from two separate literals (one in the header, one
18
- * duplicated on the footer line) into a single named source.
10
+ * Regex-fallback tokenizer colours. The syntax hues come from the same role
11
+ * map as the tree-sitter path (syntax-theme.ts), so a code block that starts
12
+ * on the regex fallback and is replaced by the async tree-sitter parse does
13
+ * not shift colour mid-stream. Text with no role uses the theme's text colour
14
+ * (never the terminal default, which can vanish on the themed body fill).
19
15
  */
20
- const FALLBACK_THEME = {
21
- string: '#00ff88',
22
- number: '#ffcc00',
23
- keyword: '#d000ff',
24
- type: '#ff6b9d',
25
- function: UI_TONES.accent.brand,
26
- operator: '#ffffff',
27
- property: '#87ceeb',
28
- comment: '#666666',
29
- bg: '#0d0d0d',
30
- accent: '#4ec9b0',
31
- } as const;
16
+ function fallbackTheme() {
17
+ const styles = syntaxStyles();
18
+ const tokens = activeTokens();
19
+ return {
20
+ string: styles.string.fg,
21
+ number: styles.number.fg,
22
+ keyword: styles.keyword.fg,
23
+ type: styles.type.fg,
24
+ function: styles.function.fg,
25
+ operator: styles.operator.fg,
26
+ property: styles.property.fg,
27
+ comment: styles.comment.fg,
28
+ plain: styles.plain.fg,
29
+ punctuation: tokens.textMuted,
30
+ };
31
+ }
32
32
 
33
33
  // ─── Language Keyword Maps ───────────────────────────────────────────────────
34
34
 
@@ -81,13 +81,14 @@ type SyntaxToken = { text: string; fg: string; bold?: boolean; italic?: boolean
81
81
  // ─── Tokenizers ──────────────────────────────────────────────────────────────
82
82
 
83
83
  function tokenizeTsJs(line: string): SyntaxToken[] {
84
+ const th = fallbackTheme();
84
85
  const tokens: SyntaxToken[] = [];
85
86
  let i = 0;
86
87
 
87
88
  while (i < line.length) {
88
89
  // Line comment
89
90
  if (line.slice(i, i + 2) === '//') {
90
- tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
91
+ tokens.push({ text: line.slice(i), fg: th.comment, italic: true });
91
92
  break;
92
93
  }
93
94
  // String (single, double, template)
@@ -98,7 +99,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
98
99
  if (line[j] === '\\') j++;
99
100
  j++;
100
101
  }
101
- tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
102
+ tokens.push({ text: line.slice(i, j + 1), fg: th.string });
102
103
  i = j + 1;
103
104
  continue;
104
105
  }
@@ -106,7 +107,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
106
107
  if (/[0-9]/.test(line[i])) {
107
108
  let j = i;
108
109
  while (j < line.length && /[0-9._xXbBoO]/.test(line[j])) j++;
109
- tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
110
+ tokens.push({ text: line.slice(i, j), fg: th.number });
110
111
  i = j;
111
112
  continue;
112
113
  }
@@ -116,13 +117,13 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
116
117
  while (j < line.length && /[\w$]/.test(line[j])) j++;
117
118
  const word = line.slice(i, j);
118
119
  if (TS_JS_KEYWORDS.has(word)) {
119
- tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
120
+ tokens.push({ text: word, fg: th.keyword });
120
121
  } else if (TS_TYPES.has(word)) {
121
- tokens.push({ text: word, fg: FALLBACK_THEME.type });
122
+ tokens.push({ text: word, fg: th.type });
122
123
  } else if (line[j] === '(') {
123
- tokens.push({ text: word, fg: FALLBACK_THEME.function });
124
+ tokens.push({ text: word, fg: th.function });
124
125
  } else {
125
- tokens.push({ text: word, fg: '' });
126
+ tokens.push({ text: word, fg: th.plain });
126
127
  }
127
128
  i = j;
128
129
  continue;
@@ -130,7 +131,7 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
130
131
  // Operators and punctuation
131
132
  const ch = line[i];
132
133
  const isOp = '=<>!&|+-*/%^~?:'.includes(ch);
133
- tokens.push({ text: ch, fg: isOp ? FALLBACK_THEME.operator : '' });
134
+ tokens.push({ text: ch, fg: isOp ? th.operator : '' });
134
135
  i++;
135
136
  }
136
137
 
@@ -138,26 +139,27 @@ function tokenizeTsJs(line: string): SyntaxToken[] {
138
139
  }
139
140
 
140
141
  function tokenizePython(line: string): SyntaxToken[] {
142
+ const th = fallbackTheme();
141
143
  const tokens: SyntaxToken[] = [];
142
144
  let i = 0;
143
145
 
144
146
  while (i < line.length) {
145
147
  if (line[i] === '#') {
146
- tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
148
+ tokens.push({ text: line.slice(i), fg: th.comment, italic: true });
147
149
  break;
148
150
  }
149
151
  if (line[i] === '"' || line[i] === "'") {
150
152
  const q = line[i];
151
153
  let j = i + 1;
152
154
  while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
153
- tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
155
+ tokens.push({ text: line.slice(i, j + 1), fg: th.string });
154
156
  i = j + 1;
155
157
  continue;
156
158
  }
157
159
  if (/[0-9]/.test(line[i])) {
158
160
  let j = i;
159
161
  while (j < line.length && /[0-9._]/.test(line[j])) j++;
160
- tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
162
+ tokens.push({ text: line.slice(i, j), fg: th.number });
161
163
  i = j;
162
164
  continue;
163
165
  }
@@ -166,44 +168,45 @@ function tokenizePython(line: string): SyntaxToken[] {
166
168
  while (j < line.length && /[\w]/.test(line[j])) j++;
167
169
  const word = line.slice(i, j);
168
170
  if (PYTHON_KEYWORDS.has(word)) {
169
- tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
171
+ tokens.push({ text: word, fg: th.keyword });
170
172
  } else if (/^[A-Z]/.test(word)) {
171
- tokens.push({ text: word, fg: FALLBACK_THEME.type });
173
+ tokens.push({ text: word, fg: th.type });
172
174
  } else if (line[j] === '(') {
173
- tokens.push({ text: word, fg: FALLBACK_THEME.function });
175
+ tokens.push({ text: word, fg: th.function });
174
176
  } else {
175
- tokens.push({ text: word, fg: '' });
177
+ tokens.push({ text: word, fg: th.plain });
176
178
  }
177
179
  i = j;
178
180
  continue;
179
181
  }
180
- tokens.push({ text: line[i], fg: '' });
182
+ tokens.push({ text: line[i], fg: th.plain });
181
183
  i++;
182
184
  }
183
185
  return tokens;
184
186
  }
185
187
 
186
188
  function tokenizeBash(line: string): SyntaxToken[] {
189
+ const th = fallbackTheme();
187
190
  const tokens: SyntaxToken[] = [];
188
191
  let i = 0;
189
192
 
190
193
  while (i < line.length) {
191
194
  if (line[i] === '#') {
192
- tokens.push({ text: line.slice(i), fg: FALLBACK_THEME.comment, italic: true });
195
+ tokens.push({ text: line.slice(i), fg: th.comment, italic: true });
193
196
  break;
194
197
  }
195
198
  if (line[i] === '"' || line[i] === "'") {
196
199
  const q = line[i];
197
200
  let j = i + 1;
198
201
  while (j < line.length && line[j] !== q) { if (line[j] === '\\') j++; j++; }
199
- tokens.push({ text: line.slice(i, j + 1), fg: FALLBACK_THEME.string });
202
+ tokens.push({ text: line.slice(i, j + 1), fg: th.string });
200
203
  i = j + 1;
201
204
  continue;
202
205
  }
203
206
  if (line[i] === '$') {
204
207
  let j = i + 1;
205
208
  while (j < line.length && /[\w{}_]/.test(line[j])) j++;
206
- tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.property });
209
+ tokens.push({ text: line.slice(i, j), fg: th.property });
207
210
  i = j;
208
211
  continue;
209
212
  }
@@ -212,20 +215,21 @@ function tokenizeBash(line: string): SyntaxToken[] {
212
215
  while (j < line.length && /[\w-]/.test(line[j])) j++;
213
216
  const word = line.slice(i, j);
214
217
  if (BASH_KEYWORDS.has(word)) {
215
- tokens.push({ text: word, fg: FALLBACK_THEME.keyword, bold: true });
218
+ tokens.push({ text: word, fg: th.keyword });
216
219
  } else {
217
- tokens.push({ text: word, fg: '' });
220
+ tokens.push({ text: word, fg: th.plain });
218
221
  }
219
222
  i = j;
220
223
  continue;
221
224
  }
222
- tokens.push({ text: line[i], fg: '' });
225
+ tokens.push({ text: line[i], fg: th.plain });
223
226
  i++;
224
227
  }
225
228
  return tokens;
226
229
  }
227
230
 
228
231
  function tokenizeJson(line: string): SyntaxToken[] {
232
+ const th = fallbackTheme();
229
233
  const tokens: SyntaxToken[] = [];
230
234
  let i = 0;
231
235
 
@@ -237,9 +241,9 @@ function tokenizeJson(line: string): SyntaxToken[] {
237
241
  // JSON key: followed by :
238
242
  const rest = line.slice(j + 1).trimStart();
239
243
  if (rest.startsWith(':')) {
240
- tokens.push({ text: str, fg: FALLBACK_THEME.property });
244
+ tokens.push({ text: str, fg: th.property });
241
245
  } else {
242
- tokens.push({ text: str, fg: FALLBACK_THEME.string });
246
+ tokens.push({ text: str, fg: th.string });
243
247
  }
244
248
  i = j + 1;
245
249
  continue;
@@ -247,32 +251,33 @@ function tokenizeJson(line: string): SyntaxToken[] {
247
251
  if (/[0-9-]/.test(line[i])) {
248
252
  let j = i;
249
253
  while (j < line.length && /[0-9.eE+-]/.test(line[j])) j++;
250
- tokens.push({ text: line.slice(i, j), fg: FALLBACK_THEME.number });
254
+ tokens.push({ text: line.slice(i, j), fg: th.number });
251
255
  i = j;
252
256
  continue;
253
257
  }
254
258
  const boolNull = ['true', 'false', 'null'].find(k => line.startsWith(k, i));
255
259
  if (boolNull) {
256
- tokens.push({ text: boolNull, fg: FALLBACK_THEME.keyword, bold: true });
260
+ tokens.push({ text: boolNull, fg: th.keyword });
257
261
  i += boolNull.length;
258
262
  continue;
259
263
  }
260
- tokens.push({ text: line[i], fg: '244' });
264
+ tokens.push({ text: line[i], fg: th.punctuation });
261
265
  i++;
262
266
  }
263
267
  return tokens;
264
268
  }
265
269
 
266
270
  function tokenizeYaml(line: string): SyntaxToken[] {
271
+ const th = fallbackTheme();
267
272
  const tokens: SyntaxToken[] = [];
268
273
  if (line.trimStart().startsWith('#')) {
269
- return [{ text: line, fg: FALLBACK_THEME.comment, italic: true }];
274
+ return [{ text: line, fg: th.comment, italic: true }];
270
275
  }
271
276
  const keyMatch = line.match(/^(\s*)([^:]+)(:)(\s*.*)/);
272
277
  if (keyMatch) {
273
- if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: '' });
274
- tokens.push({ text: keyMatch[2], fg: FALLBACK_THEME.property });
275
- tokens.push({ text: keyMatch[3], fg: '244' });
278
+ if (keyMatch[1]) tokens.push({ text: keyMatch[1], fg: th.plain });
279
+ tokens.push({ text: keyMatch[2], fg: th.property });
280
+ tokens.push({ text: keyMatch[3], fg: th.punctuation });
276
281
  if (keyMatch[4]) {
277
282
  const val = keyMatch[4];
278
283
  const trimVal = val.trimStart();
@@ -280,16 +285,41 @@ function tokenizeYaml(line: string): SyntaxToken[] {
280
285
  const isStr = /^['"]/.test(trimVal);
281
286
  const isBool = trimVal === 'true' || trimVal === 'false' || trimVal === 'null' || trimVal === 'yes' || trimVal === 'no';
282
287
  const isNum = /^-?[0-9]/.test(trimVal);
283
- const valFg = isStr ? FALLBACK_THEME.string : isBool ? FALLBACK_THEME.keyword : isNum ? FALLBACK_THEME.number : '';
288
+ const valFg = isStr ? th.string : isBool ? th.keyword : isNum ? th.number : th.plain;
284
289
  tokens.push({ text: val, fg: valFg });
285
290
  }
286
291
  return tokens;
287
292
  }
288
- return [{ text: line, fg: '' }];
293
+ return [{ text: line, fg: th.plain }];
289
294
  }
290
295
 
291
296
  function tokenizePlain(line: string): SyntaxToken[] {
292
- return [{ text: line, fg: '' }];
297
+ const th = fallbackTheme();
298
+ return [{ text: line, fg: th.plain }];
299
+ }
300
+
301
+ // ─── Line highlighting outside a code block ─────────────────────────────────
302
+
303
+ /**
304
+ * Syntax colors for lines of code in `lang` (a fence tag or a file extension),
305
+ * one token list per line: the tree-sitter result once it is cached, the regex
306
+ * tokenizer until then. The Changes modal colors diff lines with this.
307
+ */
308
+ export function highlightCodeLines(codeLines: readonly string[], lang: string): SyntaxToken[][] {
309
+ const hl = lang ? _sharedHighlighter.highlight(codeLines.join('\n'), lang) : null;
310
+ const language = detectLanguage(lang);
311
+ return codeLines.map((line, i) => {
312
+ const fromTree = hl?.[i];
313
+ if (fromTree && fromTree.length > 0) return fromTree as HLToken[];
314
+ switch (language) {
315
+ case 'ts': return tokenizeTsJs(line);
316
+ case 'python': return tokenizePython(line);
317
+ case 'bash': return tokenizeBash(line);
318
+ case 'json': return tokenizeJson(line);
319
+ case 'yaml': return tokenizeYaml(line);
320
+ default: return tokenizePlain(line);
321
+ }
322
+ });
293
323
  }
294
324
 
295
325
  // ─── Main Renderer ───────────────────────────────────────────────────────────
@@ -302,6 +332,17 @@ function tokenizePlain(line: string): SyntaxToken[] {
302
332
  */
303
333
  const _sharedHighlighter = new SyntaxHighlighter();
304
334
 
335
+ /**
336
+ * The shared highlighter's state, for anything that caches drawn code: a line
337
+ * drawn with the regex placeholder (a miss) is stale once the generation moves
338
+ * on, and onSyntaxHighlightReady says when to repaint.
339
+ */
340
+ export function syntaxHighlightGeneration(): number { return _sharedHighlighter.generation; }
341
+ export function syntaxHighlightMisses(): number { return _sharedHighlighter.missCount; }
342
+ export function onSyntaxHighlightReady(listener: () => void): () => void { return _sharedHighlighter.onReady(listener); }
343
+ /** Resolves once no parse is in flight: a frame drawn after it no longer depends on timing or on what drew before. */
344
+ export function settleSyntaxHighlighting(): Promise<void> { return _sharedHighlighter.settle(); }
345
+
305
346
  /**
306
347
  * renderCodeBlock - Render lines of code with syntax highlighting and line numbers.
307
348
  * Returns Line[] for the cell-based pipeline.
@@ -314,12 +355,18 @@ export function renderCodeBlock(
314
355
  ): Line[] {
315
356
  const lines: Line[] = [];
316
357
  const language = detectLanguage(lang);
317
- const leftMargin = LAYOUT.LEFT_MARGIN;
358
+ // The fill starts at column 3 like every other fill (user messages, the
359
+ // composer), so code text lands on column 5 after the 2-column padding.
360
+ const leftMargin = LAYOUT.LEFT_MARGIN - 1;
318
361
  const showLineNumbers = opts.showLineNumbers ?? true;
319
362
  const lineNumW = showLineNumbers ? String(codeLines.length).length + 1 : 0; // e.g. "10 "
320
- const contentStartX = showLineNumbers ? leftMargin + lineNumW + 1 : leftMargin;
321
- const BG = FALLBACK_THEME.bg;
322
- const LINE_NUM_FG = '238';
363
+ // Text keeps 2 columns from both edges of the code fill (the Measurements
364
+ // table's padding rule), and the fill has a blank row above and below it.
365
+ const PAD = 2;
366
+ const contentStartX = showLineNumbers ? leftMargin + PAD + lineNumW + 1 : leftMargin + PAD;
367
+ const palette = activeTokens();
368
+ const BG = palette.backgroundCode;
369
+ const LINE_NUM_FG = palette.textFaint;
323
370
  const effectiveWidth = width - LAYOUT.RIGHT_MARGIN;
324
371
 
325
372
  // Try tree-sitter highlight cache first (populated asynchronously).
@@ -340,20 +387,32 @@ export function renderCodeBlock(
340
387
  };
341
388
 
342
389
  // Header bar: language label
343
- const langLabel = lang ? ` ${lang} ` : ' code ';
344
- const headerLine = createEmptyLine(width);
345
- const headerStr = langLabel.padEnd(effectiveWidth - leftMargin);
346
- let hx = leftMargin;
347
- for (const ch of headerStr) {
348
- if (hx >= effectiveWidth) break;
349
- headerLine[hx] = createStyledCell(ch, { fg: '#1a1a1a', bg: FALLBACK_THEME.accent, bold: true });
350
- hx++;
351
- }
352
- lines.push(headerLine);
353
-
354
- // Code lines
390
+ // No header bar: the language (when the fence names one) sits muted on the
391
+ // right of the first code row, and only when the code leaves room for it.
392
+ const langLabel = lang;
393
+ const padLine = createEmptyLine(width);
394
+ for (let px = leftMargin; px < effectiveWidth; px++) padLine[px] = createStyledCell(' ', { bg: BG });
395
+ lines.push(padLine);
396
+
397
+ // Code lines. A line wider than the text column wraps the way an opened
398
+ // read or command output does (wrapPreservingIndent: indentation kept,
399
+ // continuation rows indented like the first): text is never cut at the
400
+ // fill's edge. Each character keeps its token's style across the wrap, and
401
+ // a continuation row leaves the line-number column blank.
402
+ const textEnd = effectiveWidth - PAD;
403
+ const textWidth = Math.max(8, textEnd - contentStartX);
404
+ const bodyRow = (): Cell[] => {
405
+ const line: Cell[] = createEmptyLine(width);
406
+ // Paint only the code-block body band so body rows match the header/footer width.
407
+ for (let x = leftMargin; x < effectiveWidth; x++) line[x] = createStyledCell(' ', { bg: BG });
408
+ return line;
409
+ };
355
410
  for (let i = 0; i < codeLines.length; i++) {
356
- const rawLine = codeLines[i];
411
+ // Control characters (a tab included) take one blank column, as they always have.
412
+ const rawLine = Array.from(codeLines[i] ?? '').map((ch) => {
413
+ const code = ch.charCodeAt(0);
414
+ return code < 32 || code === 127 ? ' ' : ch;
415
+ }).join('');
357
416
  const lineNum = String(i + 1).padStart(lineNumW);
358
417
 
359
418
  // Select token source: tree-sitter (accurate) or regex (fallback)
@@ -361,43 +420,62 @@ export function renderCodeBlock(
361
420
  hlLines && i < hlLines.length && hlLines[i].length > 0
362
421
  ? (hlLines[i] as HLToken[])
363
422
  : regexTokenize(rawLine);
364
-
365
- const line: Cell[] = createEmptyLine(width);
366
- // Paint only the code-block body band so body rows match the header/footer width.
367
- for (let x = leftMargin; x < effectiveWidth; x++) {
368
- line[x] = createStyledCell(' ', { bg: BG });
423
+ // Each character's style, when the tokens spell the line (plain otherwise).
424
+ const chars = Array.from(rawLine);
425
+ const styles: Array<Pick<SyntaxToken, 'fg' | 'bold' | 'italic'>> = [];
426
+ const spelled = tokens.map((tk) => tk.text).join('').replace(/[\x00-\x1f\x7f]/g, ' ');
427
+ if (spelled === rawLine) {
428
+ for (const token of tokens) for (const _ of Array.from(token.text)) styles.push({ fg: token.fg, bold: token.bold, italic: token.italic });
429
+ } else {
430
+ for (let k = 0; k < chars.length; k++) styles.push({ fg: palette.text });
369
431
  }
370
432
 
371
- let cx = leftMargin;
372
- if (showLineNumbers) {
373
- for (const ch of lineNum) {
374
- if (cx >= contentStartX) break;
375
- line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG, dim: true });
433
+ const parts = getDisplayWidth(rawLine.replace(/\s+$/, '')) <= textWidth ? [rawLine] : wrapPreservingIndent(rawLine, textWidth);
434
+ let cursor = 0;
435
+ parts.forEach((part, partIndex) => {
436
+ const line = bodyRow();
437
+ let cx = leftMargin + PAD;
438
+ if (showLineNumbers) {
439
+ const label = partIndex === 0 ? lineNum : ' '.repeat(lineNumW);
440
+ for (const ch of label) {
441
+ if (cx >= contentStartX) break;
442
+ line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG });
443
+ }
444
+ line[cx++] = createStyledCell(' ', { bg: BG });
376
445
  }
377
- line[cx++] = createStyledCell(' ', { bg: BG });
378
- }
379
-
380
- // Syntax tokens
381
- for (const token of tokens) {
382
- for (const ch of token.text) {
383
- if (cx >= effectiveWidth) break;
446
+ // A continuation row: the wrap dropped the spaces it broke at and
447
+ // re-added the indent, so the next source character is the first
448
+ // non-space after the cursor.
449
+ const pc = Array.from(part);
450
+ let lead = 0;
451
+ if (partIndex > 0) {
452
+ while (pc[lead] === ' ') lead++;
453
+ while (chars[cursor] === ' ') cursor++;
454
+ }
455
+ cx += lead;
456
+ for (let k = lead; k < pc.length; k++) {
457
+ const ch = pc[k]!;
458
+ const style = styles[cursor] ?? { fg: palette.text };
459
+ cursor++;
384
460
  const cw = getDisplayWidth(ch);
385
- const code = ch.charCodeAt(0);
386
- if (code < 32 || code === 127) {
387
- cx++;
388
- continue;
389
- }
390
- line[cx] = createStyledCell(ch, { fg: token.fg, bg: BG, bold: token.bold, italic: token.italic });
391
- // Bound the wide-glyph placeholder against the body's own right edge
392
- // (effectiveWidth), not the full line width, otherwise a 2-column
393
- // glyph landing on the last body column spills its placeholder cell
394
- // into the reserved right-margin band the header/footer stop at.
395
- if (cw === 2 && cx + 1 < effectiveWidth) line[cx + 1] = { ...line[cx], char: '' };
461
+ // A wide glyph that does not fit before the text edge ends the row
462
+ // rather than drawing half of itself into the padding.
463
+ if (cx + cw > textEnd) break;
464
+ line[cx] = createStyledCell(ch, { fg: style.fg, bg: BG, bold: style.bold, italic: style.italic });
465
+ // Bound the wide-glyph placeholder against the text edge so it never
466
+ // spills into the padding or the reserved right-margin band.
467
+ if (cw === 2 && cx + 1 < textEnd) line[cx + 1] = { ...line[cx], char: '' };
396
468
  cx += cw;
397
469
  }
398
- }
399
-
400
- lines.push(line);
470
+ if (i === 0 && partIndex === 0 && langLabel) {
471
+ const labelX = textEnd - getDisplayWidth(langLabel);
472
+ if (labelX > cx + 1) {
473
+ let lx = labelX;
474
+ for (const ch of langLabel) line[lx++] = createStyledCell(ch, { fg: palette.textMuted, bg: BG });
475
+ }
476
+ }
477
+ lines.push(line);
478
+ });
401
479
  }
402
480
 
403
481
  // Footer line
@@ -0,0 +1,93 @@
1
+ /**
2
+ * renderCommandPalette, the command palette drawn with the surface kit.
3
+ *
4
+ * ✦ Commands esc
5
+ *
6
+ * ▏Type a command, setting, model or session 156 commands
7
+ *
8
+ * ✦ suggested ┌ preview ────────────┐
9
+ * Changes /diff Changes
10
+ * Switch model opus 5.5 /model /diff [path]
11
+ * ✦ session Show the working-tree
12
+ * Resume a session /resume diff. …
13
+ *
14
+ * ↑↓ move ⏎ run tab fill composer 120 more ↓
15
+ *
16
+ * The preview shows what the selected command will do before it runs:
17
+ * its full description, usage, aliases, category and what Enter will do.
18
+ */
19
+
20
+ import type { PaletteEntry, PaletteView } from '../input/command-palette-types.ts';
21
+ import { activeTokens } from './theme.ts';
22
+ import { beginModal, finishModal, scrollCountText, searchRow, type SurfaceLayer } from './surface-kit.ts';
23
+ import { drawList, type KitRow } from './surface-kit-list.ts';
24
+ import { inset, insetLines, type KitInset } from './surface-kit-parts.ts';
25
+
26
+ function rowsFor(palette: PaletteView): KitRow[] {
27
+ const selected = palette.getSelected();
28
+ const rows: KitRow[] = [];
29
+ const searching = palette.query.trim().length > 0;
30
+ for (const section of palette.sections()) {
31
+ if (!searching) rows.push({ header: section.group });
32
+ for (const entry of section.entries) {
33
+ rows.push({
34
+ label: entry.title,
35
+ desc: palette.describe(entry.id),
36
+ right: entry.right ?? `/${entry.id}`,
37
+ selected: entry === selected,
38
+ });
39
+ }
40
+ }
41
+ if (rows.length === 0) rows.push({ label: `Nothing matches "${palette.query}".`, muted: true });
42
+ return rows;
43
+ }
44
+
45
+ function drawPreview(palette: PaletteView, entry: PaletteEntry | null, canvas: Parameters<typeof insetLines>[0], p: KitInset): void {
46
+ const t = activeTokens();
47
+ if (!entry) {
48
+ insetLines(canvas, p, p.l, p.top, [{ text: 'Type to search every command; old view names such as fleet or git find their new homes.', style: { fg: t.textMuted } }]);
49
+ return;
50
+ }
51
+ const blank = { text: '', style: {} };
52
+ const live = palette.describe(entry.id);
53
+ insetLines(canvas, p, p.l, p.top, [
54
+ { text: entry.title, style: { fg: t.text, bold: true } },
55
+ { text: `/${entry.id}${entry.argsHint ? ` ${entry.argsHint}` : ''}`, style: { fg: t.brand } },
56
+ ...(live ? [{ text: live, style: { fg: t.textMuted } }] : []),
57
+ blank,
58
+ ...(entry.description ? [{ text: entry.description, style: { fg: t.textMuted } }, blank] : []),
59
+ {
60
+ text: entry.needsArgs
61
+ ? `Enter fills the composer: it needs ${entry.argsHint ?? 'arguments'}.`
62
+ : 'Enter runs it now; tab fills the composer to add arguments.',
63
+ style: { fg: t.textFaint },
64
+ },
65
+ blank,
66
+ ...(entry.aliases.length > 0 ? [{ text: `Also: ${entry.aliases.map((a) => `/${a}`).join(' ')}`, style: { fg: t.textFaint } }] : []),
67
+ ...(entry.keywords.length > 0 ? [{ text: `Finds: ${entry.keywords.join(', ')}`, style: { fg: t.textFaint } }] : []),
68
+ { text: entry.category, style: { fg: t.textFaint } },
69
+ ]);
70
+ }
71
+
72
+ export function renderCommandPalette(palette: PaletteView, screenWidth: number, screenHeight: number): SurfaceLayer {
73
+ const f = beginModal(screenWidth, screenHeight, {
74
+ title: 'Commands',
75
+ hints: [['↑↓', 'move'], ['⏎', 'run'], ['tab', 'fill composer']],
76
+ });
77
+ const total = palette.entries.length;
78
+ const shown = palette.flat().length;
79
+ searchRow(f, f.top, palette.query, 'Type a command, setting, model or session', palette.query ? `${shown} of ${total}` : `${total} commands`);
80
+
81
+ const body = f.top + 2;
82
+ const inner = f.r - f.l + 1;
83
+ // A preview when there is room for it; below that the list takes the width.
84
+ const sideBySide = inner >= 70;
85
+ const x1 = sideBySide ? f.l + Math.floor(inner * 0.55) - 1 : f.r;
86
+ const result = drawList(f.canvas, { rows: rowsFor(palette), top: body, bottom: f.bottom, x0: f.l, x1, scrollKey: { owner: palette, name: 'list' } });
87
+ if (sideBySide) {
88
+ const p = inset(f.canvas, x1 + 3, body, f.r + 2 - (x1 + 3) + 1, f.bottom - body + 1);
89
+ drawPreview(palette, palette.getSelected(), f.canvas, p);
90
+ }
91
+ f.hintRight = scrollCountText(result.above, result.below);
92
+ return finishModal(f);
93
+ }
@@ -23,9 +23,9 @@ export function renderQueuedMessageList(width: number, items: readonly { readonl
23
23
  const bodyBg = activeTheme().collapsedBodyBg;
24
24
  const lines: Line[] = [];
25
25
  const header = `${items.length} queued: /queue edit·delete until delivered`;
26
- lines.push(...renderConversationFragment(header, width, { prefix: ' ⧗ ', prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg, dim: true }));
26
+ lines.push(...renderConversationFragment(header, width, { prefix: ' ⧗ ', prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg }));
27
27
  items.forEach((item, index) => {
28
- lines.push(...renderConversationFragment(item.text, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg, dim: true }));
28
+ lines.push(...renderConversationFragment(item.text, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg }));
29
29
  });
30
30
  return lines;
31
31
  }
@@ -54,7 +54,7 @@ export function renderMemoryProvenanceChip(width: number, count: number, entries
54
54
  const bodyBg = activeTheme().collapsedBodyBg;
55
55
  const noun = count === 1 ? 'memory' : 'memories';
56
56
  const header = expanded ? `used ${count} ${noun}: Alt+M to hide` : `used ${count} ${noun}: Alt+M to list`;
57
- const lines: Line[] = renderConversationFragment(header, width, { prefix: ' ◆ ', prefixFg: t.state.info, text: t.fg.dim, bodyBg, dim: true });
57
+ const lines: Line[] = renderConversationFragment(header, width, { prefix: ' ◆ ', prefixFg: t.state.info, text: t.fg.dim, bodyBg });
58
58
  if (expanded) {
59
59
  entries.forEach((entry, index) => {
60
60
  const body = entry.record === undefined
@@ -62,7 +62,7 @@ export function renderMemoryProvenanceChip(width: number, count: number, entries
62
62
  : entry.record === null
63
63
  ? 'no longer available'
64
64
  : `${entry.record.summary} · ${entry.record.cls}`;
65
- lines.push(...renderConversationFragment(body, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.info, text: t.fg.dim, bodyBg, dim: true }));
65
+ lines.push(...renderConversationFragment(body, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.info, text: t.fg.dim, bodyBg }));
66
66
  });
67
67
  }
68
68
  return lines;