@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,68 @@
1
+ /**
2
+ * transcript-scroll.ts, where the main transcript is scrolled to.
3
+ *
4
+ * top the first transcript line on screen
5
+ * locked parked at the live bottom: each frame follows the tail
6
+ *
7
+ * Scrolling up (the mouse wheel, PageUp, Up past the input history) unlocks
8
+ * it; reaching the bottom again, Esc (handler-modal-stack.ts), or submitting
9
+ * input locks it again. While it is unlocked the back-to-bottom pill shows
10
+ * above the input area (back-to-bottom.ts) and Esc returns to the bottom
11
+ * instead of interrupting the running turn.
12
+ */
13
+
14
+ export class TranscriptScroll {
15
+ /** The first transcript line on screen. */
16
+ top = 0;
17
+ /** Parked at the live bottom, following the tail. */
18
+ locked = true;
19
+ /** The clamp the last frame computed (overlay- and footer-aware); null before the first frame. */
20
+ private lastMax: number | null = null;
21
+
22
+ /** Away from the live bottom: the pill shows and Esc goes back down. */
23
+ get scrolledBack(): boolean {
24
+ return !this.locked;
25
+ }
26
+
27
+ /**
28
+ * Scroll by `delta` lines (negative is up). `fallbackMax` is used only
29
+ * before the first frame has computed the real clamp.
30
+ */
31
+ scrollBy(delta: number, fallbackMax: () => number): void {
32
+ const max = this.lastMax ?? fallbackMax();
33
+ this.top = Math.max(0, Math.min(this.top + delta, max));
34
+ this.locked = this.top >= max;
35
+ }
36
+
37
+ /** Back to the live bottom: the next frame follows the tail again. */
38
+ toBottom(): void {
39
+ this.locked = true;
40
+ }
41
+
42
+ /** Show `line` at the top (a bookmark, a search hit); unlocks unless that is the bottom. */
43
+ jumpTo(line: number, max?: number): void {
44
+ this.locked = false;
45
+ this.top = Math.max(0, max === undefined ? line : Math.min(line, max));
46
+ }
47
+
48
+ /** Follow the tail while locked (the stream path). */
49
+ followTail(lineCount: number, viewportHeight: number): void {
50
+ if (!this.locked) return;
51
+ this.top = Math.max(0, lineCount - viewportHeight);
52
+ }
53
+
54
+ /**
55
+ * Take the frame's clamp. Unlocked but already at the bottom (a jump that
56
+ * landed there, output that shrank) is the live bottom: lock again.
57
+ * Returns true when that re-lock happened.
58
+ */
59
+ settle(nextTop: number, maxScroll: number): boolean {
60
+ this.top = nextTop;
61
+ this.lastMax = maxScroll;
62
+ if (!this.locked && nextTop >= maxScroll) {
63
+ this.locked = true;
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ }
@@ -1,9 +1,11 @@
1
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
+ import { openModelPickerNow, type ModelPickerOpenDeps } from '../input/model-picker-open.ts';
2
3
  import { getProviderIdFromModel } from '@pellux/goodvibes-sdk/platform/providers';
3
- import type { ConversationManager } from '../core/conversation';
4
4
  import type { CommandContext } from '../input/command-registry.ts';
5
5
  import type { InputHandler } from '../input/handler.ts';
6
- import type { PanelManager } from '../panels/panel-manager.ts';
6
+ import type { ShellViews } from '../views/builtin-views.ts';
7
+ import type { ViewPanelAdapter } from '../views/view-panel-adapter.ts';
8
+ import { wireViewOpeners } from './view-openers.ts';
7
9
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
8
10
  import type { MutableRuntimeState } from '@/runtime/index.ts';
9
11
  import type { FeatureFlagManager } from '@/runtime/index.ts';
@@ -13,11 +15,14 @@ import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
13
15
  import type { MemoryEmbeddingProviderRegistry } from '@pellux/goodvibes-sdk/platform/state';
14
16
  import type { ServiceInspectionQuery } from '@/runtime/index.ts';
15
17
  import type { EmbeddingProviderPickerEntry, ModelPickerTargetInfo } from '../input/model-picker.ts';
16
- import type { SelectionItem } from '../input/selection-modal.ts';
18
+ import { CommandPalette, buildPaletteEntries } from '../input/command-palette.ts';
19
+ import { confirmThrough } from '../input/confirm-dialog.ts';
20
+ import { bridgeNotificationFeedToToasts, getSharedToastCenter } from '../renderer/toast-center.ts';
21
+ import { getSharedNotificationFeed } from '../views/notifications-feed.ts';
17
22
  import { categorizeBuiltinCommands } from '../input/commands.ts';
18
23
  import { syncServiceSettingToPlatform } from './service-settings-sync.ts';
19
- import { setActiveThemeMode } from '../renderer/theme.ts';
20
- import { THEME_MODE_CONFIG_KEY, coerceThemeModeSetting } from '../renderer/theme-mode-config.ts';
24
+ import { setActiveThemeMode, setActiveThemeName } from '../renderer/theme.ts';
25
+ import { THEME_MODE_CONFIG_KEY, THEME_NAME_CONFIG_KEY, coerceThemeModeSetting } from '../renderer/theme-mode-config.ts';
21
26
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
22
27
  import { buildFirstOpenItems, decodeFirstOpenChoice, selfRecordWorkspaceRegistration } from '../cli/tui-startup.ts';
23
28
  import type { SettingsDaemonCredentialWriter } from '../input/settings-modal-secrets.ts';
@@ -27,8 +32,10 @@ import type { WorkspaceTrustLevel } from '@pellux/goodvibes-sdk/platform/runtime
27
32
  type WireShellUiOpenersOptions = {
28
33
  commandContext: CommandContext;
29
34
  input: InputHandler;
30
- panelManager: PanelManager;
31
- conversation: ConversationManager;
35
+ /** The read models and config-modal surfaces behind the built-in modals. */
36
+ views: ShellViews;
37
+ /** The operator API's panels.list / panels.open, answered by the modal views. */
38
+ viewPanelAdapter: ViewPanelAdapter;
32
39
  configManager: ConfigManager;
33
40
  providerRegistry: ProviderRegistry;
34
41
  runtime: MutableRuntimeState;
@@ -125,8 +132,8 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
125
132
  const {
126
133
  commandContext,
127
134
  input,
128
- panelManager,
129
- conversation,
135
+ views,
136
+ viewPanelAdapter,
130
137
  configManager,
131
138
  providerRegistry,
132
139
  runtime,
@@ -284,70 +291,39 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
284
291
  ];
285
292
  };
286
293
 
287
- commandContext.openModelPicker = () => {
288
- // Picker-open re-check: re-verify each provider's live model list (TTL-
289
- // respecting) so a freshly-opened picker reflects models the provider
290
- // started or stopped serving. Fire-and-forget; a completed refresh re-renders
291
- // so the list updates in place without blocking the open.
292
- void providerRegistry.refreshLiveModelDiscovery?.().then((reports) => {
293
- if (reports.some((report) => report.added.length > 0 || report.removed.length > 0)) render();
294
- }).catch(() => {});
295
- void (async () => {
296
- // getSelectableModels() is catalog-driven and can list models whose
297
- // `provider` id (e.g. 'google', sourced from the pricing catalog) was never
298
- // handed to providerRegistry.register()/registerRuntimeProvider(). Selecting
299
- // such a model fails hard at turn time with ProviderNotFoundError
300
- // ("Provider 'X' is not registered."). Filter to runtime-registered
301
- // providers only, so the picker never offers a model that cannot work.
302
- const models = providerRegistry.getSelectableModels().filter((m) => providerRegistry.has(m.provider));
303
- const configuredIds = new Set(getConfiguredProviderIds());
304
- input.modelPicker.configuredProviders = configuredIds;
305
- const providerIds = [...new Set(models.map((m) => m.provider))];
306
- const secretProviderIds = await resolveSecretProviderIds();
307
- input.modelPicker.configuredViaMap = buildConfiguredViaMap(providerIds, configuredIds, subscriptionManager, secretProviderIds);
308
- void getPinned().then((pinned) => {
309
- input.modelPicker.pinnedIds = new Set(pinned);
310
- });
311
- void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
312
- const embeddingEntries = await resolveEmbeddingProviderEntries();
313
- input.modelPicker.embeddingProviders = embeddingEntries;
314
- input.modalOpened('modelPicker');
315
- input.modelPicker.setTargetInfos(buildModelPickerTargets(embeddingEntries));
316
- input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
317
- render();
318
- })().catch((error: unknown) => {
319
- commandContext.print?.(`Model picker failed to open: ${summarizeError(error)}`);
320
- render();
321
- });
294
+ // The picker opens at once on the cached catalog; the credential-source
295
+ // reads, the embeddings probe and the live model re-check fill in after
296
+ // (input/model-picker-open.ts). getSelectableModels() and listModels() can
297
+ // name catalog providers this runtime never registered; both lists keep
298
+ // runtime-registered providers only, so the picker never offers a model
299
+ // that fails with "Provider not registered" at turn time.
300
+ const pickerOpenDeps: ModelPickerOpenDeps = {
301
+ picker: input.modelPicker,
302
+ modalOpened: () => input.modalOpened('modelPicker'),
303
+ render,
304
+ listModels: () => providerRegistry.getSelectableModels().filter((m) => providerRegistry.has(m.provider)),
305
+ listProviders: () => [...new Set(providerRegistry.listModels().map((m) => m.provider))].filter((id) => providerRegistry.has(id)),
306
+ currentModelId: getCurrentModelForPickerTarget,
307
+ currentProviderId: getCurrentProviderForPickerTarget,
308
+ configuredProviderIds: () => new Set(getConfiguredProviderIds()),
309
+ buildConfiguredVia: (ids, configured, secretIds) => buildConfiguredViaMap([...ids], new Set(configured), subscriptionManager, secretIds),
310
+ buildTargets: buildModelPickerTargets,
311
+ resolveSecretProviderIds,
312
+ resolveEmbeddingProviders: resolveEmbeddingProviderEntries,
313
+ refreshLiveModels: async () => ((await providerRegistry.refreshLiveModelDiscovery?.()) ?? []).some((r) => r.added.length > 0 || r.removed.length > 0),
314
+ prefetch: async () => {
315
+ input.modelPicker.pinnedIds = new Set(await getPinned());
316
+ await input.modelPicker.loadRecentModels();
317
+ },
318
+ onError: (error) => { commandContext.print?.(`Model picker could not load everything: ${summarizeError(error)}`); render(); },
322
319
  };
323
320
 
321
+ commandContext.openModelPicker = () => { void openModelPickerNow(pickerOpenDeps, 'models'); };
322
+
324
323
  commandContext.openModelPickerWithTarget = (target) => input.openModelPickerWithTarget(target);
325
324
  commandContext.openProviderModelPickerWithTarget = (target) => input.openProviderModelPickerWithTarget(target);
326
325
 
327
- commandContext.openProviderPicker = () => {
328
- void (async () => {
329
- // listModels() surfaces every catalog provider id, not just the ones
330
- // actually registered on this runtime (see openModelPicker above for the
331
- // same class of bug). Intersect against providerRegistry.has() so the
332
- // provider picker never offers a provider that will fail with
333
- // "Provider not registered" once a model under it is selected.
334
- const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))]
335
- .filter((providerId) => providerRegistry.has(providerId));
336
- const configuredIds = new Set(getConfiguredProviderIds());
337
- input.modelPicker.configuredProviders = configuredIds;
338
- const secretProviderIds = await resolveSecretProviderIds();
339
- input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager, secretProviderIds);
340
- const embeddingEntries = await resolveEmbeddingProviderEntries();
341
- input.modelPicker.embeddingProviders = embeddingEntries;
342
- input.modalOpened('modelPicker');
343
- input.modelPicker.setTargetInfos(buildModelPickerTargets(embeddingEntries));
344
- input.modelPicker.openProviders(providers, getCurrentProviderForPickerTarget());
345
- render();
346
- })().catch((error: unknown) => {
347
- commandContext.print?.(`Provider picker failed to open: ${summarizeError(error)}`);
348
- render();
349
- });
350
- };
326
+ commandContext.openProviderPicker = () => { void openModelPickerNow(pickerOpenDeps, 'providers'); };
351
327
 
352
328
  commandContext.completeEmbeddingProviderSelection = (providerId: string) => {
353
329
  try {
@@ -382,6 +358,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
382
358
  if (!input.helpOverlayActive) input.modalOpened('help');
383
359
  input.helpOverlayActive = !input.helpOverlayActive;
384
360
  input.helpScrollOffset = 0;
361
+ input.overlayFilters.help.clear();
385
362
  render();
386
363
  };
387
364
 
@@ -398,6 +375,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
398
375
  if (!input.shortcutsOverlayActive) input.modalOpened('shortcuts');
399
376
  input.shortcutsOverlayActive = !input.shortcutsOverlayActive;
400
377
  input.shortcutsScrollOffset = 0;
378
+ input.overlayFilters.shortcuts.clear();
401
379
  render();
402
380
  };
403
381
 
@@ -427,6 +405,12 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
427
405
  onSettingApplied: (change) => {
428
406
  // forced dark/light applies immediately (rebuild palettes + full
429
407
  // repaint); auto only re-probes at startup, so it takes effect next launch.
408
+ if (String(change.key) === THEME_NAME_CONFIG_KEY) {
409
+ // Theme changes apply immediately: rebuild palettes + full repaint.
410
+ const applied = setActiveThemeName(change.value);
411
+ commandContext.requestFullRepaint?.();
412
+ return { message: `Theme: ${applied} (applied now)` };
413
+ }
430
414
  if (String(change.key) === THEME_MODE_CONFIG_KEY) {
431
415
  const next = coerceThemeModeSetting(change.value);
432
416
  if (next === 'dark' || next === 'light') {
@@ -446,15 +430,14 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
446
430
  render();
447
431
  };
448
432
 
449
- // (the purge): open a MIGRATE-TO-MODAL surface by name. Fed from both
450
- // ctx.openModal(name) (migrated command front-doors) and PanelManager's
451
- // open()-time modal-redirect callback (a legacy panel id resolving to a modal
452
- // name). Surfaces are registered in builtin-modals.ts; a name with no
453
- // registered surface degrades honestly to a print rather than a blank modal.
433
+ // Open a config-modal surface by name (or by an old name that redirects to
434
+ // one, e.g. 'accounts' opens 'providers-modal'). Surfaces are registered in
435
+ // builtin-modals.ts; a name with no registered surface degrades honestly to
436
+ // a print rather than a blank modal.
454
437
  // The stack name is the stable 'config' slot (one config modal at a time,
455
438
  // opening another swaps the surface), so Esc close/return and the modal-stack
456
439
  // machinery need only the single 'config' case (handler-ui-state.ts).
457
- // Some panel-id redirects (and migrated front-doors) resolve to a NATIVE
440
+ // Some legacy view-name redirects (and migrated front-doors) resolve to a NATIVE
458
441
  // modal that is not a ConfigModalSurface, e.g. `sessions` -> `sessionPicker`,
459
442
  // where 'sessionPicker' is the real session-picker modal opened by
460
443
  // commandContext.openSessionPicker below, NOT a registered config surface.
@@ -475,7 +458,8 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
475
458
  openNative();
476
459
  return;
477
460
  }
478
- const surface = panelManager.getModalSurface(name);
461
+ const registry = views.modalSurfaces;
462
+ const surface = registry.getModalSurface(name) ?? registry.getModalSurface(registry.getModalRedirect(name) ?? '');
479
463
  if (!surface) {
480
464
  commandContext.print(`'${name}' is not available yet in this build.`);
481
465
  render();
@@ -485,7 +469,6 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
485
469
  input.configModal.open(surface, render);
486
470
  render();
487
471
  };
488
- panelManager.setOpenModalCallback(commandContext.openModal);
489
472
 
490
473
  commandContext.openMcpWorkspace = () => {
491
474
  input.openMcpWorkspace(commandContext);
@@ -498,124 +481,53 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
498
481
  render();
499
482
  };
500
483
 
501
- commandContext.openPanelPicker = () => {
502
- // Focus ownership lives in PanelManager (focusTarget); read it there rather
503
- // than tracking a parallel input.panelFocused flag. Toggle semantics: if the
504
- // workspace is visible AND already focused, hide it; otherwise reveal and
505
- // focus it (opening a picker when nothing is open yet).
506
- if (panelManager.isVisible() && panelManager.getFocusTarget() === 'panel') {
507
- panelManager.hide();
508
- panelManager.focusPrompt();
509
- conversation.setSplashSuppressed(false);
510
- conversation.rebuildHistory();
511
- render();
512
- return;
513
- }
514
- if (panelManager.getAllOpen().length === 0) {
515
- // (the purge): 'panel-list' (a browse-all-panels picker PANEL)
516
- // was DELETE-disposition, a picker over a handful of panels is dead
517
- // weight now. Its replacement is this selection MODAL, built from the
518
- // live registry (PanelManager.getRegisteredTypes()) rather than a
519
- // hardcoded list, so it can never list a retired/deleted id.
520
- const items: SelectionItem[] = panelManager.getRegisteredTypes().map((entry) => ({
521
- id: entry.id,
522
- label: `${entry.icon} ${entry.name}`,
523
- detail: entry.description,
524
- category: entry.category,
525
- primaryAction: 'select',
526
- }));
527
- commandContext.openSelection?.('Open Panel', items, { allowSearch: true }, (result) => {
528
- if (!result) return;
529
- panelManager.open(result.item.id);
530
- panelManager.show();
531
- panelManager.focusPanels();
532
- conversation.setSplashSuppressed(true);
533
- conversation.rebuildHistory();
534
- render();
535
- });
536
- return;
537
- }
538
- panelManager.show();
539
- panelManager.focusPanels();
540
- conversation.setSplashSuppressed(true);
541
- conversation.rebuildHistory();
542
- render();
543
- };
544
-
545
484
  // Registry-driven surfaces built inside command handlers (/help) group by
546
485
  // the same categories as the palette and the generated reference.
547
486
  commandContext.getCommandCategories = () => getCommandCategoryMap();
548
487
 
488
+ input.surfaceModals.onChange = render;
489
+ commandContext.confirm = (options) => confirmThrough(input.surfaceModals, options);
490
+ const toasts = getSharedToastCenter();
491
+ toasts.onChange = render;
492
+ commandContext.showToast = (toast) => toasts.show(toast);
493
+ bridgeNotificationFeedToToasts(getSharedNotificationFeed(), toasts);
494
+
549
495
  commandContext.openCommandPalette = () => {
550
496
  // The palette is built live from the command registry, never a hardcoded
551
497
  // list, so it can never drift from the real command set. Category labels
552
498
  // come from the same source the generated docs use (getCommandCategoryMap).
553
499
  const registry = input.commandRegistry;
554
500
  if (!registry) return;
555
- const categoryByName = getCommandCategoryMap();
556
- const items: SelectionItem[] = registry
557
- .getAll()
558
- .slice()
559
- .sort((a, b) => a.name.localeCompare(b.name))
560
- .map((cmd) => {
561
- const argsHint = cmd.argsHint ?? cmd.usage;
562
- const detailParts: string[] = [];
563
- if (cmd.description) detailParts.push(cmd.description);
564
- if (argsHint) detailParts.push(`(args: ${argsHint})`);
565
- if ((cmd.aliases ?? []).length > 0) detailParts.push(`[aka ${(cmd.aliases ?? []).map((a) => `/${a}`).join(', ')}]`);
566
- return {
567
- id: cmd.name,
568
- label: `/${cmd.name}`,
569
- detail: detailParts.join(' '),
570
- category: categoryByName.get(cmd.name) ?? 'Other',
571
- primaryAction: 'select' as const,
572
- };
573
- });
574
- commandContext.openSelection?.(
575
- 'Command Palette',
576
- items,
577
- { allowSearch: true, primaryVerbLabel: 'Run' },
578
- (result) => {
579
- if (!result) return;
580
- const cmd = registry.get(result.item.id);
581
- if (!cmd) return;
582
- // Pre-fill the composer with the command (and a trailing space so the
583
- // inline args hint shows what the command wants). Keyboard-first: focus
584
- // returns to the composer with command mode armed so the user fills any
585
- // args and presses Enter, matching tab-completion's fill behavior.
586
- input.prompt = `/${cmd.name} `;
501
+ const top = input.surfaceModals.top();
502
+ if (top instanceof CommandPalette) return;
503
+ const palette = new CommandPalette({
504
+ entries: buildPaletteEntries(registry.getAll(), getCommandCategoryMap()),
505
+ describe: (id) => (id === 'model' ? commandContext.session.runtime.model || undefined : undefined),
506
+ onRun: (entry, mode) => {
507
+ if (mode === 'run' && commandContext.executeCommand) {
508
+ void commandContext.executeCommand(entry.id, []).finally(() => render());
509
+ return;
510
+ }
511
+ // Fill the composer with the command (and a trailing space so the
512
+ // inline args hint shows what the command wants), command mode armed,
513
+ // so the user types any arguments and presses Enter, matching
514
+ // tab-completion's fill behavior.
515
+ input.prompt = `/${entry.id} `;
587
516
  input.cursorPos = input.prompt.length;
588
517
  input.commandMode = true;
589
518
  input.autocomplete?.reset();
590
519
  commandContext.focusPrompt?.();
591
520
  render();
592
521
  },
593
- );
594
- };
595
-
596
- commandContext.focusPanels = () => {
597
- panelManager.focusPanels();
522
+ });
523
+ input.surfaceModals.push(palette);
598
524
  render();
599
525
  };
600
526
 
601
527
  commandContext.focusPrompt = () => {
602
- panelManager.focusPrompt();
603
528
  input.indicatorFocused = false;
604
529
  render();
605
530
  };
606
531
 
607
- commandContext.showPanel = (panelId, pane, target, opts) => {
608
- // forward the deep-link target so a jumped-to panel lands on the
609
- // right row (fleet --target); undefined for ordinary opens.
610
- panelManager.open(panelId, pane, target);
611
- panelManager.show();
612
- // focus rule 1a: every registered caller of showPanel is a slash
613
- // command (/panel open, /routes, /approval, /tasks, /ops-control, ...),
614
- // the command path leaves focus in the composer ("the user is
615
- // mid-command-flow") unless the caller explicitly asks to grab it.
616
- if (opts?.focus) panelManager.focusPanels();
617
- conversation.setSplashSuppressed(true);
618
- conversation.rebuildHistory();
619
- render();
620
- };
532
+ wireViewOpeners({ commandContext, input, views, viewPanelAdapter, render });
621
533
  }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * view-openers.ts, the openers for the built-in modals: Agents, Usage,
3
+ * Changes (and its previews), Notifications and the local-auth password
4
+ * prompt, plus ctx.openView, which turns any current or old view name
5
+ * ('fleet', 'cockpit', 'tokens', 'git', 'providers', …) into the modal that
6
+ * holds it now (input/views.ts).
7
+ *
8
+ * Every modal is a kit SurfaceModal pushed on the input handler's modal host.
9
+ * Opening one that is already open brings it to the top instead of stacking a
10
+ * second copy.
11
+ */
12
+
13
+ import type { CommandContext } from '../input/command-registry.ts';
14
+ import type { InputHandler } from '../input/handler.ts';
15
+ import type { SurfaceModal } from '../input/surface-modal-host.ts';
16
+ import type { ShellViews } from '../views/builtin-views.ts';
17
+ import type { ViewPanelAdapter } from '../views/view-panel-adapter.ts';
18
+ import { AgentsModal } from '../input/agents-modal.ts';
19
+ import { UsageModal } from '../input/usage-modal.ts';
20
+ import { ChangesModal, type ChangesDeps } from '../input/changes-modal.ts';
21
+ import { NotificationsModal } from '../input/notifications-modal.ts';
22
+ import { MaskedEntryModal } from '../input/masked-entry-modal.ts';
23
+ import { confirmThrough } from '../input/confirm-dialog.ts';
24
+ import { resolveViewName, type ViewTarget } from '../input/views.ts';
25
+ import { getSharedNotificationFeed } from '../views/notifications-feed.ts';
26
+ import { getSharedHostedSessionFeed } from '../views/hosted-session-feed.ts';
27
+ import { revertReviewHunk } from '../input/commands/review-runtime.ts';
28
+
29
+ export interface WireViewOpenersOptions {
30
+ readonly commandContext: CommandContext;
31
+ readonly input: InputHandler;
32
+ readonly views: ShellViews;
33
+ readonly viewPanelAdapter: ViewPanelAdapter;
34
+ readonly render: () => void;
35
+ }
36
+
37
+ /** Split a preview text into a diff and a note: `@@ label @@\n text` is a note, not a hunk. */
38
+ function previewParts(text: string): { diff: string | null; note: string | null } {
39
+ const first = text.split('\n', 1)[0] ?? '';
40
+ if (/^@@ (?!-)/.test(first)) return { diff: null, note: text.slice(first.length + 1).trim() || null };
41
+ return { diff: text, note: null };
42
+ }
43
+
44
+ export function wireViewOpeners(options: WireViewOpenersOptions): void {
45
+ const { commandContext, input, views, viewPanelAdapter, render } = options;
46
+ const host = input.surfaceModals;
47
+
48
+ const findOpen = <T extends SurfaceModal>(type: abstract new (...args: never[]) => T): T | undefined =>
49
+ host.modals().find((modal): modal is T => modal instanceof type);
50
+
51
+ const show = (modal: SurfaceModal): void => {
52
+ host.push(modal);
53
+ render();
54
+ };
55
+
56
+ // ── Agents ────────────────────────────────────────────────────────────────
57
+ commandContext.openAgents = (opts = {}) => {
58
+ const modal = findOpen(AgentsModal) ?? new AgentsModal({
59
+ readModel: views.fleet.readModel,
60
+ actions: views.fleet.actions,
61
+ acts: views.fleet.acts,
62
+ spawn: views.fleet.spawn,
63
+ configManager: views.configManager,
64
+ hosted: getSharedHostedSessionFeed(),
65
+ steerHosted: (text) => { void commandContext.executeCommand?.('hosted', ['say', ...text.split(/\s+/)]); },
66
+ sessionCost: () => {
67
+ const cost = views.usage.sessionCost();
68
+ return cost.priced ? cost.usd : null;
69
+ },
70
+ confirm: (confirmOptions) => confirmThrough(host, confirmOptions),
71
+ requestRender: render,
72
+ openSessionView: (target) => commandContext.openSessionView?.(target) ?? false,
73
+ });
74
+ if (opts.target) modal.reveal(opts.target);
75
+ if (opts.hosted) modal.showHosted();
76
+ show(modal);
77
+ };
78
+
79
+ // ── Usage ─────────────────────────────────────────────────────────────────
80
+ commandContext.openUsage = (opts = {}) => {
81
+ const existing = findOpen(UsageModal);
82
+ if (existing) {
83
+ if (opts.tab) existing.tab = opts.tab;
84
+ show(existing);
85
+ return;
86
+ }
87
+ show(new UsageModal({
88
+ tracker: views.usage,
89
+ tab: opts.tab,
90
+ compact: () => { void commandContext.executeCommand?.('compact', []).finally(render); },
91
+ requestRender: render,
92
+ }));
93
+ };
94
+ // A click on the footer's token or context row opens Usage.
95
+ input.onFooterTarget = (target) => {
96
+ if (target === 'usage') commandContext.openUsage?.();
97
+ };
98
+
99
+ // ── Changes ───────────────────────────────────────────────────────────────
100
+ const changesDeps: ChangesDeps = {
101
+ workingDirectory: views.workingDirectory,
102
+ getSessionFiles: views.getSessionFiles,
103
+ requestRender: render,
104
+ submitInput: (text) => commandContext.submitInput?.(text),
105
+ openInEditor: (path, line) => commandContext.openFileInEditor?.(path, line),
106
+ revertHunk: (hunk) => revertReviewHunk(commandContext, hunk),
107
+ confirm: (confirmOptions) => confirmThrough(host, confirmOptions),
108
+ };
109
+ // One workspace for the session: reviewed marks and attached comments survive closing it.
110
+ const workspace = new ChangesModal(changesDeps, 'workspace');
111
+ commandContext.openChanges = (opts = {}) => {
112
+ show(workspace);
113
+ void workspace.reload(opts.source ?? workspace.source);
114
+ };
115
+
116
+ const preview = (title: string, text: string | null, note: string | null): ChangesModal => {
117
+ const modal = new ChangesModal(changesDeps, 'preview');
118
+ const parts = text ? previewParts(text) : { diff: null, note: null };
119
+ modal.loadPreview(title, parts.diff, note ?? parts.note);
120
+ show(modal);
121
+ return modal;
122
+ };
123
+ commandContext.previewChanges = async (opts) => {
124
+ const modal = preview(opts.title, opts.diff ?? null, opts.note ?? null);
125
+ if (!opts.question) return false;
126
+ return modal.ask(opts.question);
127
+ };
128
+
129
+ // The fleet acts show candidate diffs and ask over them through this.
130
+ let fleetPreview: ChangesModal | null = null;
131
+ views.bridge.diffSurface = {
132
+ show: (title, diff) => {
133
+ if (fleetPreview) host.close(fleetPreview, 'done');
134
+ fleetPreview = preview(title, diff, null);
135
+ },
136
+ armConfirm: (arm) => {
137
+ const tone = arm.verb === 'Discard' ? 'danger' as const : 'warning' as const;
138
+ const asked = fleetPreview && host.modals().includes(fleetPreview)
139
+ ? fleetPreview.ask({ text: arm.label, confirmLabel: arm.verb, tone })
140
+ : confirmThrough(host, { title: `${arm.verb}?`, body: arm.label, confirmLabel: arm.verb, tone });
141
+ void asked.then((ok) => {
142
+ if (ok) void arm.onConfirm();
143
+ else arm.onCancel?.();
144
+ render();
145
+ });
146
+ },
147
+ close: () => {
148
+ if (fleetPreview) host.close(fleetPreview, 'done');
149
+ fleetPreview = null;
150
+ render();
151
+ },
152
+ };
153
+
154
+ // ── Notifications ─────────────────────────────────────────────────────────
155
+ const redirect = (name: string): string | undefined => views.modalSurfaces.getModalRedirect(name);
156
+ commandContext.openNotifications = () => {
157
+ show(findOpen(NotificationsModal) ?? new NotificationsModal({
158
+ feed: getSharedNotificationFeed(),
159
+ resolveSubject: (subject) => (resolveViewName(subject, redirect) ? () => { openView(subject); } : null),
160
+ }));
161
+ };
162
+
163
+ // ── Local-auth password prompt ────────────────────────────────────────────
164
+ const openMaskedEntry = (kind: 'add-user' | 'rotate-password', username?: string): void => {
165
+ show(new MaskedEntryModal({
166
+ kind,
167
+ username,
168
+ auth: views.localUserAuthManager,
169
+ onDone: (message) => { commandContext.print(message); render(); },
170
+ }));
171
+ };
172
+ views.bridge.openMaskedEntry = openMaskedEntry;
173
+ commandContext.openLocalAuthMaskedEntry = openMaskedEntry;
174
+
175
+ // ── Any view by name ──────────────────────────────────────────────────────
176
+ const openView = (name: string, target?: ViewTarget): boolean => {
177
+ const route = resolveViewName(name, redirect);
178
+ if (!route) return false;
179
+ switch (route.kind) {
180
+ case 'agents': commandContext.openAgents?.({ target, hosted: route.hosted }); break;
181
+ case 'usage': commandContext.openUsage?.({ tab: route.tab }); break;
182
+ case 'changes': commandContext.openChanges?.({ source: route.mode === 'git' ? 'working' : 'session' }); break;
183
+ case 'notifications': commandContext.openNotifications?.(); break;
184
+ case 'sessions': commandContext.openSessionPicker?.(); break;
185
+ case 'modal': commandContext.openModal?.(route.name); break;
186
+ }
187
+ return true;
188
+ };
189
+ commandContext.openView = openView;
190
+ viewPanelAdapter.setOpener((id) => openView(id));
191
+
192
+ // The older per-console callbacks commands still call, each onto its modal.
193
+ commandContext.openForensicsView = () => { openView('forensics'); };
194
+ commandContext.openIncidentView = () => { openView('incident'); };
195
+ commandContext.openPolicyView = () => { openView('policy'); };
196
+ commandContext.openHooksView = () => { openView('hooks'); };
197
+ commandContext.openCommunicationView = () => { openView('communication'); };
198
+ commandContext.openOrchestrationView = () => { openView('orchestration'); };
199
+ commandContext.openCockpitView = () => { openView('cockpit'); };
200
+ commandContext.openSecurityView = () => { openView('security'); };
201
+ commandContext.openKnowledgeView = () => { openView('knowledge'); };
202
+ commandContext.openMemoryView = () => { openView('memory'); };
203
+ commandContext.openRemoteView = () => { openView('remote'); };
204
+ commandContext.openSubscriptionView = () => { openView('subscription'); };
205
+ }