@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
@@ -1,791 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // PanelManager, central manager for panel lifecycle, navigation, and split
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Panel, PanelRegistration, PanelCategory, PanelDeepLinkTarget } from './types.ts';
6
- export type { PanelDeepLinkTarget } from './types.ts';
7
- // Type-only, erased at runtime, routed through the `@/` alias (not a relative
8
- // path) so it stays out of the relative-import graph the architecture
9
- // cycle-checker walks, same discipline as the PanelManager import in types.ts.
10
- import type { ConfigModalSurface } from '@/input/config-modal-types.ts';
11
-
12
- // ---------------------------------------------------------------------------
13
- // Pane
14
- // ---------------------------------------------------------------------------
15
-
16
- export interface Pane {
17
- panels: Panel[];
18
- activeIndex: number;
19
- }
20
-
21
- export interface WorkspaceTab {
22
- readonly id: string;
23
- readonly name: string;
24
- readonly icon: string;
25
- readonly pane: 'top' | 'bottom';
26
- readonly active: boolean;
27
- readonly focused: boolean;
28
- }
29
-
30
- // ---------------------------------------------------------------------------
31
- // PanelManager
32
- // ---------------------------------------------------------------------------
33
-
34
- export class PanelManager {
35
- private registry: PanelRegistration[] = [];
36
- private retainedPanels = new Map<string, Panel>();
37
- /** Old/absorbed panel id -> merged target id (from the console panel merges). */
38
- private aliases = new Map<string, string>();
39
- /**
40
- * Retired panel id -> modal name (panels migrated to config-modal surfaces).
41
- * Unlike `aliases` (panel -> panel), a hit here means no panel is ever
42
- * constructed for this id, `open()` invokes `openModalCallback` instead
43
- * and returns a sentinel. This map and the open()-time check are the
44
- * sole mechanism for that redirect.
45
- */
46
- private modalRedirects = new Map<string, string>();
47
- /**
48
- * Modal name -> the surface the config-modal host renders. Built once
49
- * in builtin-modals.ts (closing over read-models) and looked up by
50
- * ui-openers' openModal callback. Distinct from `modalRedirects` (panel id ->
51
- * modal name): a redirect resolves a legacy panel id to a name; this map
52
- * resolves that name to the actual surface data/actions.
53
- */
54
- private modalSurfaces = new Map<string, ConfigModalSurface>();
55
- /**
56
- * Late-bound: the modal stack is constructed after PanelManager (same
57
- * ordering constraint as the openAgentDetail callback in
58
- * builtin/shared.ts), so this is injected via a setter rather than the
59
- * constructor.
60
- */
61
- private openModalCallback?: (modalName: string) => void;
62
- private _visible: boolean = false;
63
- private _splitRatio: number = 0.6;
64
-
65
- // Two panes for the top/bottom split within the panel area
66
- private topPane: Pane = { panels: [], activeIndex: 0 };
67
- private bottomPane: Pane = { panels: [], activeIndex: 0 };
68
- private _focusedPane: 'top' | 'bottom' = 'top';
69
- private _verticalSplitRatio: number = 0.5; // top gets 50% of panel height
70
- private _bottomPaneVisible: boolean = false;
71
-
72
- // Single source of truth for prompt-vs-panel keyboard focus. Previously this
73
- // lived as a `panelFocused` boolean scattered across InputHandler and the
74
- // input-routing seams; centralizing it here is what makes it impossible for
75
- // panel focus to disagree with workspace visibility (see getFocusTarget).
76
- private _focusTarget: 'prompt' | 'panel' = 'prompt';
77
-
78
- // Cache for getWorkspaceTabs(), invalidated on every panel lifecycle event
79
- private _cachedWorkspaceTabs: readonly WorkspaceTab[] | null = null;
80
-
81
- // Most-recently-opened panel ids (front = newest), for the picker's "Recent"
82
- // group. Session-scoped; capped to keep it a short list.
83
- private _recentlyOpened: string[] = [];
84
- private static readonly RECENT_CAP = 8;
85
-
86
- /** Record a panel id as most-recently-opened (front of the ring, deduped). */
87
- private _recordRecent(panelId: string): void {
88
- this._recentlyOpened = [panelId, ...this._recentlyOpened.filter((id) => id !== panelId)]
89
- .slice(0, PanelManager.RECENT_CAP);
90
- }
91
-
92
- /** Most-recently-opened panel ids, newest first. */
93
- getRecentlyOpened(): readonly string[] {
94
- return this._recentlyOpened;
95
- }
96
-
97
- // -------------------------------------------------------------------------
98
- // Registration
99
- // -------------------------------------------------------------------------
100
-
101
- registerType(registration: PanelRegistration): void {
102
- const existing = this.registry.findIndex(r => r.id === registration.id);
103
- // registry-time icon-uniqueness assertion. Tab-bar icons are a
104
- // single glyph; two panels sharing one silently made the workspace tab
105
- // strip ambiguous (the historical W/R/U/K/M/Q/Y/J/P collisions). Compare
106
- // against every OTHER registration (excluding a re-registration of the
107
- // same id, which is a legitimate update, e.g. tests replacing a factory).
108
- const iconOwner = this.registry.find(r => r.id !== registration.id && r.icon === registration.icon);
109
- if (iconOwner) {
110
- throw new Error(
111
- `Panel icon '${registration.icon}' for '${registration.id}' collides with already-registered panel '${iconOwner.id}'. Panel icons must be unique across the registry.`,
112
- );
113
- }
114
- if (existing >= 0) {
115
- this.registry[existing] = registration;
116
- } else {
117
- this.registry.push(registration);
118
- }
119
- }
120
-
121
- /**
122
- * Register a compat redirect so an absorbed panel's old id still resolves
123
- * after a console merge (docs, saved layouts, and muscle memory do not
124
- * break). Resolved by open/close/activateById/getPanel/getPaneOf.
125
- */
126
- registerAlias(aliasId: string, targetId: string): void {
127
- this.aliases.set(aliasId, targetId);
128
- }
129
-
130
- /**
131
- * Register a redirect so `open(panelId)` opens the named modal instead of
132
- * constructing a panel (MIGRATE-TO-MODAL surfaces). Checked in open()
133
- * before alias/registry resolution.
134
- */
135
- registerModalRedirect(panelId: string, modalName: string): void {
136
- this.modalRedirects.set(panelId, modalName);
137
- }
138
-
139
- /**
140
- * Inject the callback `open()` invokes when it hits a modal redirect.
141
- * Late-bound via setter because the modal stack is constructed after
142
- * PanelManager (mirrors the openAgentDetail callback pattern).
143
- */
144
- setOpenModalCallback(callback: (modalName: string) => void): void {
145
- this.openModalCallback = callback;
146
- }
147
-
148
- /**
149
- * The modal name `panelIdOrAlias` redirects to, if any, lets callers
150
- * (e.g. the `/panel` command) print "moved to the <name> modal" before or
151
- * instead of calling open().
152
- */
153
- getModalRedirect(panelIdOrAlias: string): string | undefined {
154
- return this.modalRedirects.get(panelIdOrAlias);
155
- }
156
-
157
- /**
158
- * Register the surface (data + actions) the config-modal host renders for a
159
- * modal name. Called from builtin-modals.ts alongside registerModalRedirect.
160
- */
161
- registerModalSurface(surface: ConfigModalSurface): void {
162
- this.modalSurfaces.set(surface.name, surface);
163
- }
164
-
165
- /** Resolve a modal name to its registered surface, if any. */
166
- getModalSurface(name: string): ConfigModalSurface | undefined {
167
- return this.modalSurfaces.get(name);
168
- }
169
-
170
- private _resolveId(panelId: string): string {
171
- return this.aliases.get(panelId) ?? panelId;
172
- }
173
-
174
- /**
175
- * Placeholder returned by open() for a modal-redirected id. Never pushed
176
- * into a pane, never retained, never rendered, it exists only so open()
177
- * can keep its non-null `Panel` return type without constructing the real
178
- * (deleted) panel view. `name` carries the modal name so a caller that
179
- * inspects the returned panel's `name` (rather than calling
180
- * getModalRedirect() beforehand) still gets an honest answer.
181
- */
182
- private _modalRedirectSentinel(panelId: string, modalName: string): Panel {
183
- return {
184
- id: panelId,
185
- name: modalName,
186
- icon: '·',
187
- category: 'session',
188
- onActivate: () => {},
189
- onDeactivate: () => {},
190
- onDestroy: () => {},
191
- render: () => [],
192
- isTransient: true,
193
- isPinned: false,
194
- needsRender: false,
195
- invalidate: () => {},
196
- markRendered: () => {},
197
- };
198
- }
199
-
200
- getRegisteredTypes(): PanelRegistration[] {
201
- return [...this.registry];
202
- }
203
-
204
- getTypesByCategory(): Map<PanelCategory, PanelRegistration[]> {
205
- const map = new Map<PanelCategory, PanelRegistration[]>();
206
- for (const reg of this.registry) {
207
- const list = map.get(reg.category) ?? [];
208
- list.push(reg);
209
- map.set(reg.category, list);
210
- }
211
- return map;
212
- }
213
-
214
- prewarmRegistered(): void {
215
- for (const registration of this.registry) {
216
- if (!registration.preload) continue;
217
- if (this.getPanel(registration.id) || this.retainedPanels.has(registration.id)) continue;
218
- const panel = registration.factory();
219
- this.retainedPanels.set(registration.id, panel);
220
- }
221
- }
222
-
223
- // -------------------------------------------------------------------------
224
- // Panel lifecycle, operates on a specific pane (defaults to focused)
225
- // -------------------------------------------------------------------------
226
-
227
- /** Invalidate the workspace tab cache. Call on every panel lifecycle mutation. */
228
- private _invalidateWorkspaceTabs(): void {
229
- this._cachedWorkspaceTabs = null;
230
- }
231
-
232
- open(panelIdOrAlias: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget): Panel {
233
- const modalName = this.modalRedirects.get(panelIdOrAlias);
234
- if (modalName !== undefined) {
235
- this.openModalCallback?.(modalName);
236
- return this._modalRedirectSentinel(panelIdOrAlias, modalName);
237
- }
238
- const panelId = this._resolveId(panelIdOrAlias);
239
- this._recordRecent(panelId);
240
- const existingPane = this._findPaneOf(panelId);
241
- if (existingPane) {
242
- this._visible = true;
243
- // Honor an explicitly requested pane so open(id, pane) never lies about
244
- // where the panel lands: relocate it if it currently lives in the other
245
- // pane (fixes `/panel open <id> top` and the panel-list T/B move keys).
246
- if (pane && pane !== existingPane) {
247
- this._moveBetweenPanes(existingPane, pane, panelId);
248
- return this._deliverDeepLink(this.getPanel(panelId)!, target);
249
- }
250
- this._activateByIdInPane(panelId, existingPane);
251
- this._focusedPane = existingPane;
252
- if (existingPane === 'bottom') this._bottomPaneVisible = true;
253
- return this._deliverDeepLink(this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!, target);
254
- }
255
-
256
- const targetPane = pane ?? this._focusedPane;
257
- const p = this._getPane(targetPane);
258
-
259
- const oldPanel = p.panels[p.activeIndex];
260
- if (oldPanel) oldPanel.onDeactivate();
261
-
262
- const panel = this._obtainPanel(panelId);
263
- p.panels.push(panel);
264
- p.activeIndex = p.panels.length - 1;
265
- this._visible = true;
266
- // If opening into bottom pane, also make it visible
267
- if (targetPane === 'bottom') {
268
- this._bottomPaneVisible = true;
269
- this._focusedPane = 'bottom';
270
- } else {
271
- this._focusedPane = 'top';
272
- }
273
- panel.onActivate();
274
- this._invalidateWorkspaceTabs();
275
- return this._deliverDeepLink(panel, target);
276
- }
277
-
278
- /**
279
- * Hand an optional deep-link target (item 4) to a panel that just
280
- * resolved from open(). Panels without a "node" concept simply don't
281
- * implement `receiveDeepLink`, the call is a no-op via optional chaining.
282
- */
283
- private _deliverDeepLink(panel: Panel, target: PanelDeepLinkTarget | undefined): Panel {
284
- if (target) panel.receiveDeepLink?.(target);
285
- return panel;
286
- }
287
-
288
- close(panelIdOrAlias: string): void {
289
- const panelId = this._resolveId(panelIdOrAlias);
290
- // Search both panes
291
- for (const which of ['top', 'bottom'] as const) {
292
- const p = this._getPane(which);
293
- const index = p.panels.findIndex(panel => panel.id === panelId);
294
- if (index < 0) continue;
295
-
296
- const panel = p.panels[index];
297
- const wasActive = index === p.activeIndex;
298
- if (wasActive) panel.onDeactivate();
299
- if (this._shouldRetain(panelId)) {
300
- this.retainedPanels.set(panelId, panel);
301
- } else {
302
- panel.onDestroy();
303
- }
304
- p.panels.splice(index, 1);
305
-
306
- if (p.panels.length === 0) {
307
- p.activeIndex = 0;
308
- if (which === 'bottom') {
309
- this._bottomPaneVisible = false;
310
- // Move focus to top if we were focused on empty bottom
311
- if (this._focusedPane === 'bottom') this._focusedPane = 'top';
312
- }
313
- } else {
314
- p.activeIndex = Math.min(p.activeIndex, p.panels.length - 1);
315
- if (wasActive) {
316
- const newActive = p.panels[p.activeIndex];
317
- if (newActive) newActive.onActivate();
318
- }
319
- }
320
-
321
- // Hide sidebar if no panels remain in either pane
322
- if (this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
323
- this._visible = false;
324
- }
325
- this._invalidateWorkspaceTabs();
326
- return;
327
- }
328
- }
329
-
330
- /**
331
- * Move a panel to a specific pane. If panelId is omitted, moves the active
332
- * panel from the currently focused pane.
333
- */
334
- moveToPane(dest: 'top' | 'bottom', panelId?: string): void {
335
- const srcPaneName = panelId
336
- ? this._findPaneOf(panelId) ?? this._focusedPane
337
- : this._focusedPane;
338
- if (srcPaneName === dest) return; // already there
339
- const dstPaneName = dest;
340
- this._moveBetweenPanes(srcPaneName, dstPaneName, panelId);
341
- }
342
-
343
- /**
344
- * Move a panel to the other pane. If panelId is omitted, moves the active
345
- * panel from the currently focused pane.
346
- */
347
- moveToOtherPane(panelId?: string): void {
348
- const srcPaneName = panelId
349
- ? this._findPaneOf(panelId) ?? this._focusedPane
350
- : this._focusedPane;
351
- const dstPaneName: 'top' | 'bottom' = srcPaneName === 'top' ? 'bottom' : 'top';
352
- this._moveBetweenPanes(srcPaneName, dstPaneName, panelId);
353
- }
354
-
355
- // -------------------------------------------------------------------------
356
- // Navigation, operates on focused pane
357
- // -------------------------------------------------------------------------
358
-
359
- nextPanel(): void {
360
- const p = this._getFocusedPane();
361
- if (p.panels.length === 0) return;
362
- const oldPanel = p.panels[p.activeIndex];
363
- if (oldPanel) oldPanel.onDeactivate();
364
- p.activeIndex = (p.activeIndex + 1) % p.panels.length;
365
- const newPanel = p.panels[p.activeIndex];
366
- if (newPanel) newPanel.onActivate();
367
- this._invalidateWorkspaceTabs();
368
- }
369
-
370
- nextWorkspaceTab(): void {
371
- this._cycleWorkspaceTab(1);
372
- }
373
-
374
- prevWorkspaceTab(): void {
375
- this._cycleWorkspaceTab(-1);
376
- }
377
-
378
- prevPanel(): void {
379
- const p = this._getFocusedPane();
380
- if (p.panels.length === 0) return;
381
- const oldPanel = p.panels[p.activeIndex];
382
- if (oldPanel) oldPanel.onDeactivate();
383
- p.activeIndex = (p.activeIndex - 1 + p.panels.length) % p.panels.length;
384
- const newPanel = p.panels[p.activeIndex];
385
- if (newPanel) newPanel.onActivate();
386
- this._invalidateWorkspaceTabs();
387
- }
388
-
389
- activateByIndex(index: number): void {
390
- const p = this._getFocusedPane();
391
- if (index < 0 || index >= p.panels.length) return;
392
- if (index === p.activeIndex) return;
393
- const oldPanel = p.panels[p.activeIndex];
394
- if (oldPanel) oldPanel.onDeactivate();
395
- p.activeIndex = index;
396
- const newPanel = p.panels[p.activeIndex];
397
- if (newPanel) newPanel.onActivate();
398
- this._invalidateWorkspaceTabs();
399
- }
400
-
401
- activateById(panelIdOrAlias: string): void {
402
- const panelId = this._resolveId(panelIdOrAlias);
403
- const which = this._findPaneOf(panelId);
404
- if (!which) return;
405
- this._activateByIdInPane(panelId, which);
406
- }
407
-
408
- // -------------------------------------------------------------------------
409
- // Pane focus control
410
- // -------------------------------------------------------------------------
411
-
412
- focusPane(pane: 'top' | 'bottom'): void {
413
- if (pane === 'bottom' && !this._bottomPaneVisible) return;
414
- this._focusedPane = pane;
415
- this._invalidateWorkspaceTabs();
416
- }
417
-
418
- getFocusedPane(): 'top' | 'bottom' {
419
- return this._focusedPane;
420
- }
421
-
422
- /** Get the currently active (focused) panel, or null if none. */
423
- getActivePanel(): Panel | null {
424
- const p = this._getFocusedPane();
425
- return p.panels[p.activeIndex] ?? null;
426
- }
427
-
428
- togglePaneFocus(): void {
429
- if (!this._bottomPaneVisible || this.bottomPane.panels.length === 0) return;
430
- this._focusedPane = this._focusedPane === 'top' ? 'bottom' : 'top';
431
- this._invalidateWorkspaceTabs();
432
- }
433
-
434
- // -------------------------------------------------------------------------
435
- // Keyboard focus ownership (prompt vs. panel workspace)
436
- // -------------------------------------------------------------------------
437
-
438
- /**
439
- * Which surface owns keyboard focus. Self-healing: focus can only rest on the
440
- * panel workspace while it is visible, non-empty, and has an active panel, so
441
- * panel focus can never disagree with workspace visibility. Any code that
442
- * asks for the focus target therefore reads a value that is always consistent
443
- * with what is actually on screen.
444
- */
445
- getFocusTarget(): 'prompt' | 'panel' {
446
- if (this._focusTarget === 'panel' && !this._workspaceIsFocusable()) {
447
- this._focusTarget = 'prompt';
448
- }
449
- return this._focusTarget;
450
- }
451
-
452
- /** True when the panel workspace currently owns keyboard focus. */
453
- isPanelFocused(): boolean {
454
- return this.getFocusTarget() === 'panel';
455
- }
456
-
457
- /**
458
- * Give keyboard focus to the panel workspace. No-op when there is nothing
459
- * focusable (no visible, non-empty pane with an active panel), this is the
460
- * guard that upholds the focus/visibility invariant on the write path.
461
- */
462
- focusPanels(): void {
463
- if (this._workspaceIsFocusable()) {
464
- this._focusTarget = 'panel';
465
- }
466
- }
467
-
468
- /** Return keyboard focus to the prompt. */
469
- focusPrompt(): void {
470
- this._focusTarget = 'prompt';
471
- }
472
-
473
- private _workspaceIsFocusable(): boolean {
474
- return this._visible && this.getAllOpen().length > 0 && this.getActivePanel() !== null;
475
- }
476
-
477
- // -------------------------------------------------------------------------
478
- // Pane visibility
479
- // -------------------------------------------------------------------------
480
-
481
- toggleBottomPane(): void {
482
- this._invalidateWorkspaceTabs();
483
- if (this._bottomPaneVisible) {
484
- this._bottomPaneVisible = false;
485
- if (this._focusedPane === 'bottom') this._focusedPane = 'top';
486
- } else {
487
- this._bottomPaneVisible = true;
488
- // If bottom pane is empty, populate it
489
- if (this.bottomPane.panels.length === 0) {
490
- if (this.topPane.panels.length > 1) {
491
- // Move last panel from top to bottom
492
- const panel = this.topPane.panels.pop()!;
493
- if (this.topPane.activeIndex >= this.topPane.panels.length) {
494
- this.topPane.activeIndex = Math.max(0, this.topPane.panels.length - 1);
495
- }
496
- this.bottomPane.panels.push(panel);
497
- this.bottomPane.activeIndex = 0;
498
- } else {
499
- // Open a predictable default panel in the bottom pane. purge:
500
- // 'panel-list' was deleted (dead weight over a 5-panel registry,
501
- // see the DELETE disposition), so the default is explicitly
502
- // 'fleet' rather than falling back to registry[0] (whatever
503
- // registers first, currently 'git', see risk 5 in the brief).
504
- const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
505
- if (defaultPanel) {
506
- this.open(defaultPanel.id, 'bottom');
507
- }
508
- }
509
- }
510
- this._focusedPane = 'bottom';
511
- }
512
- }
513
-
514
- isBottomPaneVisible(): boolean {
515
- return this._bottomPaneVisible && this.bottomPane.panels.length > 0;
516
- }
517
-
518
- // -------------------------------------------------------------------------
519
- // Pane state accessors
520
- // -------------------------------------------------------------------------
521
-
522
- getTopPane(): Readonly<Pane> {
523
- return this.topPane;
524
- }
525
-
526
- getBottomPane(): Readonly<Pane> {
527
- return this.bottomPane;
528
- }
529
-
530
- // -------------------------------------------------------------------------
531
- // Backward-compatible accessors (operate on focused pane)
532
- // -------------------------------------------------------------------------
533
-
534
- getOpen(): Panel[] {
535
- const p = this._getFocusedPane();
536
- return [...p.panels];
537
- }
538
-
539
- /**
540
- * Returns all panels across both panes (top then bottom).
541
- * Use this when you need to know if any panels exist at all.
542
- */
543
- getAllOpen(): Panel[] {
544
- return [...this.topPane.panels, ...this.bottomPane.panels];
545
- }
546
-
547
- getActive(): Panel | null {
548
- const p = this._getFocusedPane();
549
- if (p.panels.length === 0) return null;
550
- return p.panels[p.activeIndex] ?? null;
551
- }
552
-
553
- getPanel(panelIdOrAlias: string): Panel | null {
554
- const panelId = this._resolveId(panelIdOrAlias);
555
- return this.topPane.panels.find((panel) => panel.id === panelId)
556
- ?? this.bottomPane.panels.find((panel) => panel.id === panelId)
557
- ?? null;
558
- }
559
-
560
- getPaneOf(panelIdOrAlias: string): 'top' | 'bottom' | null {
561
- return this._findPaneOf(this._resolveId(panelIdOrAlias));
562
- }
563
-
564
- getWorkspaceTabs(): readonly WorkspaceTab[] {
565
- if (this._cachedWorkspaceTabs !== null) return this._cachedWorkspaceTabs;
566
- // `active` = the currently selected tab in its own pane (independent of focus).
567
- // `focused` = true only for the one tab in the globally focused pane that is active.
568
- const focusedPanelId = this.getActivePanel()?.id;
569
- const topActivePanelId = this.topPane.panels[this.topPane.activeIndex]?.id;
570
- const bottomActivePanelId = this.bottomPane.panels[this.bottomPane.activeIndex]?.id;
571
- const topTabs = this.topPane.panels.map((panel) => ({
572
- id: panel.id,
573
- name: panel.name,
574
- icon: panel.icon,
575
- pane: 'top' as const,
576
- active: panel.id === topActivePanelId,
577
- focused: panel.id === focusedPanelId,
578
- }));
579
- const bottomTabs = this.bottomPane.panels.map((panel) => ({
580
- id: panel.id,
581
- name: panel.name,
582
- icon: panel.icon,
583
- pane: 'bottom' as const,
584
- active: panel.id === bottomActivePanelId,
585
- focused: panel.id === focusedPanelId,
586
- }));
587
- const tabs = [...topTabs, ...bottomTabs] as WorkspaceTab[];
588
- this._cachedWorkspaceTabs = tabs;
589
- return tabs;
590
- }
591
-
592
- activateWorkspaceIndex(index: number): void {
593
- const tabs = this.getWorkspaceTabs();
594
- if (index < 0 || index >= tabs.length) return;
595
- const tab = tabs[index]!;
596
- this._focusedPane = tab.pane;
597
- if (tab.pane === 'bottom') this._bottomPaneVisible = true;
598
- this._activateByIdInPane(tab.id, tab.pane);
599
- this._invalidateWorkspaceTabs();
600
- }
601
-
602
- // -------------------------------------------------------------------------
603
- // Visibility
604
- // -------------------------------------------------------------------------
605
-
606
- toggle(): void {
607
- this._visible = !this._visible;
608
- // Auto-open a default panel if toggling visible with nothing open.
609
- // purge: explicitly 'fleet' rather than registry[0], see the
610
- // matching comment in toggleBottomPane() above.
611
- if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
612
- const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
613
- if (defaultPanel) this.open(defaultPanel.id);
614
- }
615
- }
616
-
617
- show(): void {
618
- this._visible = true;
619
- }
620
-
621
- hide(): void {
622
- this._visible = false;
623
- }
624
-
625
- isVisible(): boolean {
626
- return this._visible;
627
- }
628
-
629
- // -------------------------------------------------------------------------
630
- // Horizontal split control (left/right)
631
- // -------------------------------------------------------------------------
632
-
633
- getSplitRatio(): number {
634
- return this._splitRatio;
635
- }
636
-
637
- setSplitRatio(ratio: number): void {
638
- this._splitRatio = Math.max(0.3, Math.min(0.85, ratio));
639
- }
640
-
641
- widenLeft(): void {
642
- this.setSplitRatio(this._splitRatio + 0.05);
643
- }
644
-
645
- widenRight(): void {
646
- this.setSplitRatio(this._splitRatio - 0.05);
647
- }
648
-
649
- getLeftWidth(totalWidth: number): number {
650
- return Math.floor(totalWidth * this._splitRatio);
651
- }
652
-
653
- getRightWidth(totalWidth: number): number {
654
- return totalWidth - this.getLeftWidth(totalWidth);
655
- }
656
-
657
- // -------------------------------------------------------------------------
658
- // Vertical split control (top/bottom within panel area)
659
- // -------------------------------------------------------------------------
660
-
661
- getVerticalSplitRatio(): number {
662
- return this._verticalSplitRatio;
663
- }
664
-
665
- setVerticalSplitRatio(ratio: number): void {
666
- this._verticalSplitRatio = Math.max(0.2, Math.min(0.8, ratio));
667
- }
668
-
669
- // -------------------------------------------------------------------------
670
- // Cleanup
671
- // -------------------------------------------------------------------------
672
-
673
- destroyAll(): void {
674
- for (const panel of [...this.topPane.panels, ...this.bottomPane.panels, ...this.retainedPanels.values()]) {
675
- panel.onDestroy();
676
- }
677
- this.topPane = { panels: [], activeIndex: 0 };
678
- this.bottomPane = { panels: [], activeIndex: 0 };
679
- this.retainedPanels.clear();
680
- this.registry = [];
681
- this._recentlyOpened = [];
682
- this._focusedPane = 'top';
683
- this._focusTarget = 'prompt';
684
- this._bottomPaneVisible = false;
685
- this._visible = false;
686
- this._invalidateWorkspaceTabs();
687
- }
688
-
689
- // -------------------------------------------------------------------------
690
- // Private helpers
691
- // -------------------------------------------------------------------------
692
-
693
- private _getPane(which: 'top' | 'bottom'): Pane {
694
- return which === 'top' ? this.topPane : this.bottomPane;
695
- }
696
-
697
- private _getFocusedPane(): Pane {
698
- return this._getPane(this._focusedPane);
699
- }
700
-
701
- private _findPaneOf(panelId: string): 'top' | 'bottom' | null {
702
- if (this.topPane.panels.some(p => p.id === panelId)) return 'top';
703
- if (this.bottomPane.panels.some(p => p.id === panelId)) return 'bottom';
704
- return null;
705
- }
706
-
707
- private _moveBetweenPanes(srcPaneName: 'top' | 'bottom', dstPaneName: 'top' | 'bottom', panelId?: string): void {
708
- const src = this._getPane(srcPaneName);
709
- const dst = this._getPane(dstPaneName);
710
-
711
- const id = panelId ?? src.panels[src.activeIndex]?.id;
712
- if (!id) return;
713
-
714
- const index = src.panels.findIndex(p => p.id === id);
715
- if (index < 0) return;
716
-
717
- const panel = src.panels[index];
718
- const wasActive = index === src.activeIndex;
719
- if (wasActive) panel.onDeactivate();
720
- src.panels.splice(index, 1);
721
- src.activeIndex = Math.min(src.activeIndex, Math.max(0, src.panels.length - 1));
722
-
723
- if (wasActive && src.panels.length > 0) {
724
- src.panels[src.activeIndex]?.onActivate();
725
- }
726
-
727
- // Deactivate current active in dest
728
- const oldDstActive = dst.panels[dst.activeIndex];
729
- if (oldDstActive) oldDstActive.onDeactivate();
730
-
731
- dst.panels.push(panel);
732
- dst.activeIndex = dst.panels.length - 1;
733
- panel.onActivate();
734
-
735
- if (dstPaneName === 'bottom') {
736
- this._bottomPaneVisible = true;
737
- }
738
- this._focusedPane = dstPaneName;
739
- this._invalidateWorkspaceTabs();
740
- }
741
-
742
- private _cycleWorkspaceTab(direction: 1 | -1): void {
743
- const tabs = this.getWorkspaceTabs();
744
- if (tabs.length === 0) return;
745
- const currentIndex = tabs.findIndex((tab) => tab.focused);
746
- const nextIndex = currentIndex < 0
747
- ? 0
748
- : (currentIndex + direction + tabs.length) % tabs.length;
749
- this.activateWorkspaceIndex(nextIndex);
750
- }
751
-
752
- private _obtainPanel(panelId: string): Panel {
753
- const retained = this.retainedPanels.get(panelId);
754
- if (retained) {
755
- this.retainedPanels.delete(panelId);
756
- return retained;
757
- }
758
- const registration = this._getRegistration(panelId);
759
- if (!registration) {
760
- throw new Error(`No panel type registered with id: ${panelId}`);
761
- }
762
- return registration.factory();
763
- }
764
-
765
- private _getRegistration(panelId: string): PanelRegistration | undefined {
766
- const resolvedId = this._resolveId(panelId);
767
- return this.registry.find((registration) => registration.id === resolvedId);
768
- }
769
-
770
- private _shouldRetain(panelId: string): boolean {
771
- return this._getRegistration(panelId)?.retainOnClose === true;
772
- }
773
-
774
- private _activateByIdInPane(panelId: string, which: 'top' | 'bottom'): void {
775
- const p = this._getPane(which);
776
- const index = p.panels.findIndex(panel => panel.id === panelId);
777
- if (index < 0) return;
778
- if (index === p.activeIndex) {
779
- p.panels[index]?.onActivate();
780
- return;
781
- }
782
- if (index !== p.activeIndex) {
783
- const oldPanel = p.panels[p.activeIndex];
784
- if (oldPanel) oldPanel.onDeactivate();
785
- p.activeIndex = index;
786
- const newPanel = p.panels[p.activeIndex];
787
- if (newPanel) newPanel.onActivate();
788
- this._invalidateWorkspaceTabs();
789
- }
790
- }
791
- }