@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.
- package/CHANGELOG.md +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-reference.ts +2 -2
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- package/src/verification/verification-ledger.ts +0 -707
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /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
|
-
}
|