@pellux/goodvibes-tui 2.0.20 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +59 -4
- package/package.json +16 -6
- 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-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/verification-ledger.ts +18 -16
- 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/{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,799 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// fleet-panel.ts
|
|
3
|
-
//
|
|
4
|
-
//, FleetPanel: the live unified observability tree. Renders the
|
|
5
|
-
// registered process fleet (agents incl. WRFC roles, WRFC chains/subtasks,
|
|
6
|
-
// workflow-tool FSMs, watchers, background processes) as a depth-first tree
|
|
7
|
-
// with a selected-row detail region. Session tabs (Part C): Enter on an
|
|
8
|
-
// attachable node (agent/wrfc-chain) opens a tab with that process's
|
|
9
|
-
// transcript/member summary, see fleet-tabs.ts + fleet-transcript.ts. This
|
|
10
|
-
// file owns input dispatch and layout; fleet-deep-link.ts owns the
|
|
11
|
-
// cross-panel jump target (item 4).
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
import { readFile } from 'node:fs/promises';
|
|
15
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
16
|
-
import type { SteerResult } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
17
|
-
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
18
|
-
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
19
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
20
|
-
import { PanelConfirmOverlay } from './panel-confirm-overlay.ts';
|
|
21
|
-
import { isPanelSearchBackspace, isPanelSearchCancel, isPanelSearchCommit, isPanelSearchPrintable } from './search-focus.ts';
|
|
22
|
-
import { buildKeyboardHints, buildPanelWorkspace, buildSearchInputLine, DEFAULT_PANEL_PALETTE, getPanelWorkspaceContentBudget, type PanelPalette } from './polish.ts';
|
|
23
|
-
import {
|
|
24
|
-
isBlockedOnUserNode,
|
|
25
|
-
isRunningProcessState,
|
|
26
|
-
isTerminalProcessState,
|
|
27
|
-
type FleetReadModel,
|
|
28
|
-
type FleetTreeRow,
|
|
29
|
-
} from './fleet-read-model.ts';
|
|
30
|
-
import type { FleetActs } from './fleet-acts.ts';
|
|
31
|
-
import type { FleetSpawn } from './fleet-spawn.ts';
|
|
32
|
-
import {
|
|
33
|
-
activeFleetTab,
|
|
34
|
-
appendSteerText,
|
|
35
|
-
attachFleetTab,
|
|
36
|
-
detachActiveFleetTab,
|
|
37
|
-
EMPTY_FLEET_TABS_STATE,
|
|
38
|
-
isAttachableFleetKind,
|
|
39
|
-
stepFleetTab,
|
|
40
|
-
type FleetTab,
|
|
41
|
-
type FleetTabsState,
|
|
42
|
-
type SteerBadge,
|
|
43
|
-
} from './fleet-tabs.ts';
|
|
44
|
-
import { liveSteerableLabels, reconcileSteerBadges as reconcileSteerBadgesPure, renderSteerBadgeLine, steerRefusalMessage } from './fleet-steer.ts';
|
|
45
|
-
import { FleetStopTracker, toggleFleetPause, buildFleetTreeHints, fleetKillConfirmArgs } from './fleet-stop.ts';
|
|
46
|
-
import { resolveFleetDeepLinkIndex } from './fleet-deep-link.ts';
|
|
47
|
-
import { parseAgentLedger, renderFleetAgentTranscript, renderFleetChainSummary, renderFleetLedgerFallback, renderFleetTranscriptLoading } from './fleet-transcript.ts';
|
|
48
|
-
import { renderFleetTabStrip } from '../renderer/fleet-tab-strip.ts';
|
|
49
|
-
import { renderFleetDetailLines, renderFleetRowLine } from './fleet-panel-format.ts';
|
|
50
|
-
|
|
51
|
-
const C = DEFAULT_PANEL_PALETTE;
|
|
52
|
-
|
|
53
|
-
export interface FleetActionCallbacks {
|
|
54
|
-
/** Graceful interruption (AgentManager.cancel / trigger-schedule disable, via the registry). */
|
|
55
|
-
readonly interrupt: (id: string) => boolean;
|
|
56
|
-
/** Re-arm a `paused` node (disabled trigger/schedule or automation job), interrupt()'s inverse. Honest false when not paused / non-resumable. */
|
|
57
|
-
readonly resume: (id: string) => boolean;
|
|
58
|
-
/** Hard stop, optionally cascading to killable descendants. Returns the node ids acted on. */
|
|
59
|
-
readonly kill: (id: string, opts: { readonly cascade: boolean }) => readonly string[];
|
|
60
|
-
/**
|
|
61
|
-
* Fetch an agent's conversation history, live (running) or
|
|
62
|
-
* frozen (just-completed, still in the SDK's retention ring). Empty array
|
|
63
|
-
* means "unavailable" (evicted, or never registered), FleetPanel degrades
|
|
64
|
-
* to the on-disk ledger fallback in that case, never fabricating content.
|
|
65
|
-
*/
|
|
66
|
-
readonly getConversationSnapshot: (agentId: string) => readonly ConversationMessageSnapshot[];
|
|
67
|
-
/** Resolve the on-disk `<agentId>.jsonl` event-ledger path for the degraded fallback view. */
|
|
68
|
-
readonly resolveSessionLogPath: (agentId: string) => string;
|
|
69
|
-
/**
|
|
70
|
-
* Queue a human message for a live in-process agent (or a
|
|
71
|
-
* wrfc-subtask's current live member agent), delivered at the target's
|
|
72
|
-
* next turn boundary, never mid-token. Honest refusal
|
|
73
|
-
* (`{queued:false,reason}`) for anything that cannot take mid-run input
|
|
74
|
-
* (terminal nodes, non-agent kinds, the wrfc-chain coordinator itself).
|
|
75
|
-
*/
|
|
76
|
-
readonly steer: (id: string, text: string) => SteerResult;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const NOOP_ACTIONS: FleetActionCallbacks = {
|
|
80
|
-
interrupt: (_id: string) => false,
|
|
81
|
-
resume: (_id: string) => false,
|
|
82
|
-
kill: (_id: string, _opts: { readonly cascade: boolean }) => [],
|
|
83
|
-
getConversationSnapshot: (_agentId: string) => [],
|
|
84
|
-
resolveSessionLogPath: (agentId: string) => agentId,
|
|
85
|
-
steer: (_id: string, _text: string) => ({ queued: false, reason: 'no live registry' }),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
89
|
-
private readonly unsub: () => void;
|
|
90
|
-
private readonly unsubConsumed: () => void;
|
|
91
|
-
private readonly actions: FleetActionCallbacks;
|
|
92
|
-
private readonly confirmOverlay: PanelConfirmOverlay;
|
|
93
|
-
private follow = false;
|
|
94
|
-
/** Tree view mode: the live fleet, or the session archive of finished subtrees. */
|
|
95
|
-
private viewMode: 'active' | 'archived' = 'active';
|
|
96
|
-
private tabsState: FleetTabsState = EMPTY_FLEET_TABS_STATE;
|
|
97
|
-
private tickTimer: ReturnType<typeof setInterval> | null = null;
|
|
98
|
-
/**
|
|
99
|
-
* The node id the cursor is anchored to, independent of its row index.
|
|
100
|
-
* `selectedIndex` is a plain offset into `getItems()`'s current (rebuilt-every-
|
|
101
|
-
* snapshot) order, so a stale index silently drifts; `reanchorSelection()`
|
|
102
|
-
* re-locates this id each snapshot and repoints selectedIndex. null means "no
|
|
103
|
-
* anchor yet" (base class index-0 default until first navigation/tick).
|
|
104
|
-
*/
|
|
105
|
-
private selectedNodeId: string | null = null;
|
|
106
|
-
|
|
107
|
-
/** d1: the "stopping…" write-window overlay tracker (see fleet-stop.ts). */
|
|
108
|
-
private readonly stopTracker = new FleetStopTracker();
|
|
109
|
-
|
|
110
|
-
public constructor(
|
|
111
|
-
private readonly readModel: FleetReadModel,
|
|
112
|
-
actions: Partial<FleetActionCallbacks> = {},
|
|
113
|
-
private readonly configManager: ConfigManager | null = null,
|
|
114
|
-
// The waiting-on-human acts (pick / conflict / discard). Null in a runtime
|
|
115
|
-
// with no daemon gateway wired, those keys then fall through honestly.
|
|
116
|
-
private readonly acts: FleetActs | null = null,
|
|
117
|
-
// The ACP spawn affordance ('n' hosts a discovered third-party agent); null with no gateway wired.
|
|
118
|
-
private readonly spawn: FleetSpawn | null = null,
|
|
119
|
-
) {
|
|
120
|
-
super('fleet', 'Fleet', '⊟', 'runtime-ops');
|
|
121
|
-
this.showSelectionGutter = true; // visible ▸ focus indicator
|
|
122
|
-
this.actions = { ...NOOP_ACTIONS, ...actions };
|
|
123
|
-
this.confirmOverlay = new PanelConfirmOverlay(() => this.markDirty());
|
|
124
|
-
this.unsub = readModel.subscribe(() => {
|
|
125
|
-
this.reanchorSelection();
|
|
126
|
-
this.applyFollow();
|
|
127
|
-
this.reconcileSteerBadges();
|
|
128
|
-
this.markDirty();
|
|
129
|
-
});
|
|
130
|
-
// The honest "the agent actually consumed this steer at
|
|
131
|
-
// its turn boundary" signal, see fleet-tabs.ts's SteerBadgeStatus doc.
|
|
132
|
-
// A read-model without a runtimeBus dep never invokes this (graceful
|
|
133
|
-
// no-op), same degrade as steer()/steerable without a messageBus dep.
|
|
134
|
-
this.unsubConsumed = readModel.subscribeConsumed((event) => {
|
|
135
|
-
let changed = false;
|
|
136
|
-
for (const tab of this.tabsState.tabs) {
|
|
137
|
-
const badge = tab.steerBadge;
|
|
138
|
-
if (!badge || badge.messageId !== event.messageId) continue;
|
|
139
|
-
// Consumed-wins: this real signal can still upgrade an already
|
|
140
|
-
// 'dropped' (inferred) badge, as long as it hasn't linger-cleared yet.
|
|
141
|
-
if (badge.status === 'queued' || badge.status === 'dropped') {
|
|
142
|
-
tab.steerBadge = { messageId: badge.messageId, status: 'consumed', queuedAt: badge.queuedAt, resolvedAt: Date.now() };
|
|
143
|
-
changed = true;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (changed) this.markDirty();
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
public override onActivate(): void {
|
|
151
|
-
super.onActivate();
|
|
152
|
-
// Time-derived fields (elapsed, follow target) stay live even when no
|
|
153
|
-
// registry tick fires while this panel is on screen (cockpit-panel.ts
|
|
154
|
-
// STALL_TICK_MS precedent). Also re-anchors defensively in case the
|
|
155
|
-
// underlying list changed without a subscribe notification.
|
|
156
|
-
if (this.tickTimer === null) {
|
|
157
|
-
this.tickTimer = this.registerTimer(setInterval(() => {
|
|
158
|
-
this.reanchorSelection();
|
|
159
|
-
this.applyFollow();
|
|
160
|
-
this.reconcileSteerBadges();
|
|
161
|
-
this.markDirty();
|
|
162
|
-
}, 1000));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
public override onDeactivate(): void {
|
|
167
|
-
super.onDeactivate();
|
|
168
|
-
if (this.tickTimer !== null) {
|
|
169
|
-
this.clearTimer(this.tickTimer);
|
|
170
|
-
this.tickTimer = null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
public override onDestroy(): void {
|
|
175
|
-
super.onDestroy();
|
|
176
|
-
this.unsub();
|
|
177
|
-
this.unsubConsumed();
|
|
178
|
-
for (const tab of this.tabsState.tabs) tab.lineCache.clear();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** True while a session tab (not the root tree) is focused. Replaces the old isDetailFocused() seam. */
|
|
182
|
-
public isTabActive(): boolean {
|
|
183
|
-
return this.tabsState.activeTabIndex > 0;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* True while the steer composer on the active tab is open,
|
|
188
|
-
* every character of a burst (paste, or several fast-typed chars in one
|
|
189
|
-
* stdin chunk) must land in the draft one at a time rather than being
|
|
190
|
-
* routed away as panel hotkeys or exploded to the main composer. See
|
|
191
|
-
* Panel.isCapturingTextBurst's doc comment and git-panel.ts's
|
|
192
|
-
* `commitMessage` precedent.
|
|
193
|
-
*/
|
|
194
|
-
public override isCapturingTextBurst(): boolean {
|
|
195
|
-
const tab = activeFleetTab(this.tabsState);
|
|
196
|
-
return tab !== null && tab.steerDraft !== null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Introspection for tests: the current tab-bar state (read-only). */
|
|
200
|
-
public getTabsState(): FleetTabsState {
|
|
201
|
-
return this.tabsState;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
public isFollowing(): boolean {
|
|
205
|
-
return this.follow;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Ctrl+X (panel-close) collision resolution: the global
|
|
210
|
-
* shortcut route (handler-shortcuts.ts) calls this BEFORE closing the
|
|
211
|
-
* panel. Detaching the active tab consumes the key; on the root tree tab
|
|
212
|
-
* there is nothing to detach, so this returns false and Ctrl+X falls
|
|
213
|
-
* through to its ordinary panel-close behavior.
|
|
214
|
-
*/
|
|
215
|
-
public interceptPanelClose(): boolean {
|
|
216
|
-
if (this.tabsState.activeTabIndex === 0) return false;
|
|
217
|
-
this.tabsState = detachActiveFleetTab(this.tabsState);
|
|
218
|
-
this.markDirty();
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
protected getItems(): readonly FleetTreeRow[] {
|
|
223
|
-
return this.viewMode === 'archived'
|
|
224
|
-
? this.readModel.getArchivedSnapshot().rows
|
|
225
|
-
: this.readModel.getSnapshot().rows;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
protected override getPalette(): PanelPalette {
|
|
229
|
-
return C;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
protected override getEmptyStateMessage(): string {
|
|
233
|
-
return this.viewMode === 'archived'
|
|
234
|
-
? ' Archive is empty: press a on a finished process (or A for all) to move it here.'
|
|
235
|
-
: ' No processes tracked yet.';
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* (cross-restart honesty): a restart starts from an empty `AgentManager` Map
|
|
240
|
-
* (no bridge from a prior registry), so a completed process from a previous
|
|
241
|
-
* session can never reappear here, a real, permanent limitation, documented
|
|
242
|
-
* rather than implying "nothing has ever run" or that a restart brings it back.
|
|
243
|
-
*/
|
|
244
|
-
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
245
|
-
return [
|
|
246
|
-
{
|
|
247
|
-
command: 'previous sessions',
|
|
248
|
-
summary: "not shown here: this list resets on TUI restart; each agent's .goodvibes/tui/sessions/<id>.jsonl event log still persists on disk",
|
|
249
|
-
},
|
|
250
|
-
];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private applyFollow(): void {
|
|
254
|
-
if (!this.follow) return;
|
|
255
|
-
const rows = this.getItems();
|
|
256
|
-
if (rows.length === 0) return;
|
|
257
|
-
let bestIdx = -1;
|
|
258
|
-
let bestStarted = -Infinity;
|
|
259
|
-
rows.forEach((row, idx) => {
|
|
260
|
-
if (!isRunningProcessState(row.node.state)) return;
|
|
261
|
-
const started = row.node.startedAt ?? -Infinity;
|
|
262
|
-
if (started >= bestStarted) {
|
|
263
|
-
bestStarted = started;
|
|
264
|
-
bestIdx = idx;
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
if (bestIdx >= 0) {
|
|
268
|
-
this.selectedIndex = bestIdx;
|
|
269
|
-
this.selectedNodeId = rows[bestIdx]!.node.id;
|
|
270
|
-
this.needsRender = true;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** Record the current selection's node id as the anchor for future snapshot updates. */
|
|
275
|
-
private captureSelectionAnchor(): void {
|
|
276
|
-
const item = this.getSelectedItem();
|
|
277
|
-
this.selectedNodeId = item ? item.node.id : null;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Re-locate `selectedNodeId` in the current snapshot and repoint
|
|
282
|
-
* `selectedIndex` at it, so the selection follows a specific process across
|
|
283
|
-
* adds/removes/reorders instead of drifting onto whatever row occupies the old
|
|
284
|
-
* index. When the anchored node is gone (pruned), or no anchor exists yet,
|
|
285
|
-
* falls back to clampSelection() (nearest valid index) and re-anchors there.
|
|
286
|
-
*/
|
|
287
|
-
private reanchorSelection(): void {
|
|
288
|
-
const rows = this.getItems();
|
|
289
|
-
if (this.selectedNodeId !== null) {
|
|
290
|
-
const idx = rows.findIndex((row) => row.node.id === this.selectedNodeId);
|
|
291
|
-
if (idx >= 0) {
|
|
292
|
-
this.selectedIndex = idx;
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
this.clampSelection();
|
|
297
|
-
this.captureSelectionAnchor();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** Find a node's live ProcessNode in the current snapshot, or null (pruned/never registered). */
|
|
301
|
-
private findLiveNode(nodeId: string): FleetTreeRow['node'] | null {
|
|
302
|
-
return this.getItems().find((row) => row.node.id === nodeId)?.node ?? null;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** item 4 (see fleet-deep-link.ts). */
|
|
306
|
-
public receiveDeepLink(target: { readonly id: string; readonly kind?: string }): void {
|
|
307
|
-
const idx = resolveFleetDeepLinkIndex(this.getItems(), target);
|
|
308
|
-
if (idx < 0) { this.setError('node no longer present.'); return; }
|
|
309
|
-
// A successful reveal must be VISIBLE: with a session tab active, render()
|
|
310
|
-
// routes to the tab view and the selection change would be silently
|
|
311
|
-
// swallowed (batch refutation finding 1), return to the tree first
|
|
312
|
-
// (tabs kept; only the active view switches).
|
|
313
|
-
this.tabsState = { tabs: this.tabsState.tabs, activeTabIndex: 0 };
|
|
314
|
-
this.clearError(); this.selectedIndex = idx; this.selectedNodeId = target.id; this.markDirty();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Focus the next node blocked on the operator (awaiting approval/input),
|
|
319
|
-
* cycling in display order and wrapping. Resolves against the LIVE fleet
|
|
320
|
-
* (blocked nodes never live in the archive) and returns to the tree view from
|
|
321
|
-
* any attached tab so the reveal is visible (receiveDeepLink's contract).
|
|
322
|
-
* Honest no-op message when nothing waits; always consumes 'b' (returns true).
|
|
323
|
-
*/
|
|
324
|
-
private jumpToNextBlocked(): boolean {
|
|
325
|
-
const snapshot = this.readModel.getSnapshot();
|
|
326
|
-
const blockedIds = snapshot.blockedNodeIds;
|
|
327
|
-
if (blockedIds.length === 0) {
|
|
328
|
-
this.setError('Nothing is blocked on you right now.');
|
|
329
|
-
this.markDirty();
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
// Pick the blocked id strictly after the current anchor (wrapping), so
|
|
333
|
-
// repeated presses cycle through every waiting node. Anchor on whatever row
|
|
334
|
-
// is actually selected right now (the base class defaults the cursor to
|
|
335
|
-
// row 0 before any explicit navigation sets selectedNodeId), so the first
|
|
336
|
-
// press already advances past a blocked node the cursor happens to sit on.
|
|
337
|
-
const anchorId = this.getSelectedItem()?.node.id ?? this.selectedNodeId;
|
|
338
|
-
const currentPos = anchorId === null || anchorId === undefined ? -1 : blockedIds.indexOf(anchorId);
|
|
339
|
-
const nextId = blockedIds[(currentPos + 1) % blockedIds.length]!;
|
|
340
|
-
// Reveal in the live tree: leave the archive view and any active session tab.
|
|
341
|
-
this.viewMode = 'active';
|
|
342
|
-
this.tabsState = { tabs: this.tabsState.tabs, activeTabIndex: 0 };
|
|
343
|
-
const idx = snapshot.rows.findIndex((row) => row.node.id === nextId);
|
|
344
|
-
if (idx >= 0) {
|
|
345
|
-
this.selectedIndex = idx;
|
|
346
|
-
this.selectedNodeId = nextId;
|
|
347
|
-
}
|
|
348
|
-
this.clearError();
|
|
349
|
-
this.markDirty();
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* The "dropped inference" reconciliation: called on every read-model
|
|
355
|
-
* snapshot update and on the panel's 1s tick. See fleet-steer.ts's
|
|
356
|
-
* reconcileSteerBadges doc for why this is needed (the SDK emits no
|
|
357
|
-
* cancelled/expired signal for a queued steer).
|
|
358
|
-
*/
|
|
359
|
-
private reconcileSteerBadges(): void {
|
|
360
|
-
if (this.tabsState.tabs.length === 0) return;
|
|
361
|
-
const changed = reconcileSteerBadgesPure(this.tabsState.tabs, (id) => this.findLiveNode(id), Date.now());
|
|
362
|
-
if (changed) this.markDirty();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** Kick off (once) the async ledger-fallback load for a tab whose conversation snapshot is unavailable. */
|
|
366
|
-
private ensureLedgerLoaded(tab: FleetTab): void {
|
|
367
|
-
if (tab.ledgerLoadStarted) return;
|
|
368
|
-
tab.ledgerLoadStarted = true;
|
|
369
|
-
const path = this.actions.resolveSessionLogPath(tab.agentId);
|
|
370
|
-
readFile(path, 'utf-8')
|
|
371
|
-
.then((raw) => {
|
|
372
|
-
tab.ledgerEntries = parseAgentLedger(raw);
|
|
373
|
-
this.markDirty();
|
|
374
|
-
})
|
|
375
|
-
.catch(() => {
|
|
376
|
-
tab.ledgerEntries = [];
|
|
377
|
-
this.markDirty();
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
public override handleInput(key: string): boolean {
|
|
382
|
-
if (this.lastError !== null) this.clearError();
|
|
383
|
-
|
|
384
|
-
// The candidate picker (a flagged pick decision) owns the view + input while active.
|
|
385
|
-
if (this.acts?.pickModeActive()) return this.acts.handlePickInput(key);
|
|
386
|
-
// The observed-agent steer composer (a drill-in) likewise owns EVERY key until Enter/Esc.
|
|
387
|
-
if (this.acts?.observedSteerActive()) return this.acts.handleObservedSteerInput(key);
|
|
388
|
-
// The ACP spawn picker (host a third-party agent) likewise owns view + input.
|
|
389
|
-
if (this.spawn?.spawnModeActive()) return this.spawn.handleSpawnInput(key);
|
|
390
|
-
|
|
391
|
-
// Confirm overlay owns input first (K-armed kill confirm).
|
|
392
|
-
if (this.confirmOverlay.handleInput(key)) return true;
|
|
393
|
-
|
|
394
|
-
// Once the steer composer is open on the active tab, it
|
|
395
|
-
// owns EVERY key (same full-priority-while-composing contract as
|
|
396
|
-
// git-panel.ts's commitMessage entry) until Enter (submit) or Esc
|
|
397
|
-
// (cancel), a burst/paste and single hotkeys like 'j'/'s'/'[' all land
|
|
398
|
-
// in the draft, never as tree/tab navigation (focus rule).
|
|
399
|
-
const composingTab = activeFleetTab(this.tabsState);
|
|
400
|
-
if (composingTab && composingTab.steerDraft !== null) {
|
|
401
|
-
return this.handleSteerInput(composingTab, key);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Tab switching works from ANY view (root tree or an attached tab) once
|
|
405
|
-
// at least one tab is open, [ / ] step through Tree, tab 1, tab 2, ...
|
|
406
|
-
// Ctrl+X detach is handled by interceptPanelClose(), not handleInput (it
|
|
407
|
-
// must win a global-shortcut race, see that method's doc comment).
|
|
408
|
-
if ((key === '[' || key === ']') && this.tabsState.tabs.length > 0) {
|
|
409
|
-
this.tabsState = stepFleetTab(this.tabsState, key === ']' ? 1 : -1);
|
|
410
|
-
this.markDirty();
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Jump-to-blocked works from ANYWHERE in the panel (root tree OR an attached
|
|
415
|
-
// session tab): it steps to the next node waiting on the operator and
|
|
416
|
-
// reveals it in the live tree so the selection is actually visible.
|
|
417
|
-
if (key === 'b') {
|
|
418
|
-
return this.jumpToNextBlocked();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (this.tabsState.activeTabIndex > 0) {
|
|
422
|
-
if (key === 's') {
|
|
423
|
-
return this.tryOpenSteerDraft(activeFleetTab(this.tabsState)!);
|
|
424
|
-
}
|
|
425
|
-
// A session tab is focused, the tree isn't visible, so tree-only keys
|
|
426
|
-
// (navigate/i/K/f/Enter) don't apply here.
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const selected = this.getSelectedItem();
|
|
431
|
-
|
|
432
|
-
// Waiting-on-human acts on the selected row: Enter on a flagged pick/conflict
|
|
433
|
-
// row, D on a worktree-owning row. Returns false (falls through to attach /
|
|
434
|
-
// ordinary handling) for any other row, so Enter still opens an agent tab.
|
|
435
|
-
if (this.acts && selected && this.acts.handleTreeKey(key, selected.node)) {
|
|
436
|
-
this.captureSelectionAnchor();
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Host a third-party ACP agent: lists discovered agents, then a known dir,
|
|
441
|
-
// then spawns it as an acp-agent row. Quiet when none are discovered.
|
|
442
|
-
if (key === 'n' && this.spawn) { void this.spawn.begin(); return true; }
|
|
443
|
-
|
|
444
|
-
// Archive controls: v toggles the live fleet <-> session archive view;
|
|
445
|
-
// a archives the selected finished subtree (or restores it from the
|
|
446
|
-
// archive view); A archives every fully-finished root subtree.
|
|
447
|
-
if (key === 'v') {
|
|
448
|
-
this.viewMode = this.viewMode === 'active' ? 'archived' : 'active';
|
|
449
|
-
this.markDirty();
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
|
-
if (key === 'a') {
|
|
453
|
-
if (!selected) return false;
|
|
454
|
-
if (this.viewMode === 'archived') {
|
|
455
|
-
const restored = this.readModel.unarchive(selected.node.id);
|
|
456
|
-
if (restored === 0) this.setError('Nothing restored for this node.');
|
|
457
|
-
this.markDirty();
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
if (!isTerminalProcessState(selected.node.state)) {
|
|
461
|
-
this.setError('Only finished processes can be archived.');
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
const result = this.readModel.archive(selected.node.id);
|
|
465
|
-
if (!result.archived) this.setError(result.reason ?? 'Could not archive this process.');
|
|
466
|
-
this.markDirty();
|
|
467
|
-
return true;
|
|
468
|
-
}
|
|
469
|
-
if (key === 'A' && this.viewMode === 'active') {
|
|
470
|
-
const count = this.readModel.archiveFinished();
|
|
471
|
-
if (count === 0) this.setError('No fully-finished processes to archive.');
|
|
472
|
-
this.markDirty();
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (key === 'i') {
|
|
477
|
-
// Guard: only consume on a real, non-terminal node (cockpit-panel.ts
|
|
478
|
-
// precedent) so the key falls through when there is nothing to act on.
|
|
479
|
-
if (!selected || isTerminalProcessState(selected.node.state)) return false;
|
|
480
|
-
// Not every kind can be interrupted (only 'agent' nodes ever are, see
|
|
481
|
-
// the SDK's fleet adapters: schedule/trigger/watcher/workflow/
|
|
482
|
-
// wrfc-chain/wrfc-subtask/background-process all report
|
|
483
|
-
// capabilities.interruptible: false unconditionally). Consume the key
|
|
484
|
-
// and say so rather than silently calling interrupt() on a node the
|
|
485
|
-
// registry has no interrupt route for.
|
|
486
|
-
if (!selected.node.capabilities.interruptible) {
|
|
487
|
-
this.setError(`${selected.node.kind} does not support interrupt.`);
|
|
488
|
-
return true;
|
|
489
|
-
}
|
|
490
|
-
this.actions.interrupt(selected.node.id);
|
|
491
|
-
this.stopTracker.mark(selected.node.id);
|
|
492
|
-
this.markDirty();
|
|
493
|
-
return true;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (key === 'K') {
|
|
497
|
-
if (!selected || isTerminalProcessState(selected.node.state)) return false;
|
|
498
|
-
// Observed foreign agents have killable:false, the refusal below reads
|
|
499
|
-
// honestly; goodvibes only observes them, so stop is never offered.
|
|
500
|
-
if (!selected.node.capabilities.killable) {
|
|
501
|
-
this.setError(`${selected.node.kind} does not support kill.`);
|
|
502
|
-
return true;
|
|
503
|
-
}
|
|
504
|
-
this.confirmOverlay.arm(fleetKillConfirmArgs(selected.node, this.getItems(), {
|
|
505
|
-
kill: (id) => this.actions.kill(id, { cascade: true }),
|
|
506
|
-
markStopping: (id) => this.stopTracker.mark(id),
|
|
507
|
-
}));
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// d2: 'p' toggles pause<->resume by the selected node's state (see
|
|
512
|
-
// toggleFleetPause in fleet-stop.ts).
|
|
513
|
-
if (key === 'p') {
|
|
514
|
-
if (!selected) return false;
|
|
515
|
-
return toggleFleetPause(selected.node, {
|
|
516
|
-
interrupt: (id) => this.actions.interrupt(id),
|
|
517
|
-
resume: (id) => this.actions.resume(id),
|
|
518
|
-
setError: (m) => this.setError(m),
|
|
519
|
-
markDirty: () => this.markDirty(),
|
|
520
|
-
tracker: this.stopTracker,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (key === 'f') {
|
|
525
|
-
this.follow = !this.follow;
|
|
526
|
-
if (this.follow) this.applyFollow();
|
|
527
|
-
this.markDirty();
|
|
528
|
-
return true;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (key === 'enter' || key === 'return') {
|
|
532
|
-
if (!selected) return false;
|
|
533
|
-
const node = selected.node;
|
|
534
|
-
if (!isAttachableFleetKind(node.kind)) {
|
|
535
|
-
this.setError(`${node.kind} has no transcript to attach.`);
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
this.tabsState = attachFleetTab(this.tabsState, node);
|
|
539
|
-
this.markDirty();
|
|
540
|
-
return true;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (key === 's') {
|
|
544
|
-
// An earlier replay fix: 's' from the TREE was silently dead, steering
|
|
545
|
-
// required an undiscoverable Enter-attach first. Attach-and-steer in
|
|
546
|
-
// one press for a steerable node; honest refusal otherwise.
|
|
547
|
-
if (!selected) return false;
|
|
548
|
-
const node = selected.node;
|
|
549
|
-
// Observed foreign agents steer as a DRILL-IN only; that 's' is handled by
|
|
550
|
-
// acts.handleTreeKey above (opens the composer in the row's detail), so it
|
|
551
|
-
// never reaches this attach path.
|
|
552
|
-
// Closure replay note: a node that FINISHED in the hint→keypress race
|
|
553
|
-
// window must say so, "does not support steer" is misleading for an
|
|
554
|
-
// agent whose only problem is being done.
|
|
555
|
-
if (isTerminalProcessState(node.state)) {
|
|
556
|
-
this.setError(`${node.kind} already finished: nothing to steer.`);
|
|
557
|
-
return true;
|
|
558
|
-
}
|
|
559
|
-
if (!node.capabilities.steerable || !isAttachableFleetKind(node.kind)) {
|
|
560
|
-
this.setError(`${node.kind} does not support steer.`);
|
|
561
|
-
return true;
|
|
562
|
-
}
|
|
563
|
-
this.tabsState = attachFleetTab(this.tabsState, node);
|
|
564
|
-
this.markDirty();
|
|
565
|
-
return this.tryOpenSteerDraft(activeFleetTab(this.tabsState)!);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const consumed = super.handleInput(key);
|
|
569
|
-
if (consumed) this.captureSelectionAnchor();
|
|
570
|
-
return consumed;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* The `s` gate, mirrors the i/K capability-gating template
|
|
575
|
-
* exactly (guard on the live node existing, non-terminal, and the specific
|
|
576
|
-
* capability; consume the key and say so rather than silently no-op-ing).
|
|
577
|
-
*/
|
|
578
|
-
private tryOpenSteerDraft(tab: FleetTab): boolean {
|
|
579
|
-
const node = this.findLiveNode(tab.nodeId);
|
|
580
|
-
if (!node || isTerminalProcessState(node.state)) {
|
|
581
|
-
this.setError(`${node?.kind ?? tab.kind} already finished: nothing to steer.`);
|
|
582
|
-
return true;
|
|
583
|
-
}
|
|
584
|
-
if (!node.capabilities.steerable) {
|
|
585
|
-
this.setError(`${node.kind} does not support steering.`);
|
|
586
|
-
return true;
|
|
587
|
-
}
|
|
588
|
-
tab.steerDraft = '';
|
|
589
|
-
this.markDirty();
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** Dispatch for the open steer composer, mirrors git-panel.ts's handleCommitMessageInput. */
|
|
594
|
-
private handleSteerInput(tab: FleetTab, key: string): boolean {
|
|
595
|
-
if (isPanelSearchCancel(key)) {
|
|
596
|
-
tab.steerDraft = null;
|
|
597
|
-
this.markDirty();
|
|
598
|
-
return true;
|
|
599
|
-
}
|
|
600
|
-
if (isPanelSearchCommit(key)) {
|
|
601
|
-
const text = (tab.steerDraft ?? '').trim();
|
|
602
|
-
if (text.length === 0) {
|
|
603
|
-
tab.steerDraft = null;
|
|
604
|
-
this.markDirty();
|
|
605
|
-
return true;
|
|
606
|
-
}
|
|
607
|
-
// Clear the draft ONLY on a confirmed queue. A refusal keeps the composer
|
|
608
|
-
// open with the text intact (submitSteer explains why + suggests live targets)
|
|
609
|
-
// so the operator never loses what they typed to a just-idle agent.
|
|
610
|
-
if (this.submitSteer(tab, text)) tab.steerDraft = null;
|
|
611
|
-
this.markDirty();
|
|
612
|
-
return true;
|
|
613
|
-
}
|
|
614
|
-
if (isPanelSearchBackspace(key)) {
|
|
615
|
-
tab.steerDraft = (tab.steerDraft ?? '').slice(0, -1);
|
|
616
|
-
this.markDirty();
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
|
-
// Ordinary typing AND a pasted multi-line block's line-break characters
|
|
620
|
-
// (delivered one at a time through the same burst pipeline, see
|
|
621
|
-
// isCapturingTextBurst) both flow through appendSteerText, which
|
|
622
|
-
// normalizes \r/\n to a collapsed space instead of corrupting the
|
|
623
|
-
// one-line field or silently dropping the content.
|
|
624
|
-
if (key.length === 1 && (isPanelSearchPrintable(key) || key === '\r' || key === '\n')) {
|
|
625
|
-
tab.steerDraft = appendSteerText(tab.steerDraft ?? '', key);
|
|
626
|
-
this.markDirty();
|
|
627
|
-
return true;
|
|
628
|
-
}
|
|
629
|
-
return true; // absorb every other key while composing
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Submit a composed steer message. Returns whether it was queued so the caller
|
|
634
|
-
* clears the draft only on a confirmed send (WO item 4): a refusal (target
|
|
635
|
-
* went idle while composing) PRESERVES the typed text and suggests which agents
|
|
636
|
-
* ARE steerable, instead of silently discarding the draft. On a successful queue
|
|
637
|
-
* the ⧗ badge line is the immediate honest acknowledgment.
|
|
638
|
-
*/
|
|
639
|
-
private submitSteer(tab: FleetTab, text: string): boolean {
|
|
640
|
-
const result = this.actions.steer(tab.nodeId, text);
|
|
641
|
-
if (result.queued) {
|
|
642
|
-
// queuedAt drives reconcileSteerBadges' TTL-expiry fallback (fleet-steer.ts).
|
|
643
|
-
tab.steerBadge = { messageId: result.messageId, status: 'queued', queuedAt: Date.now() };
|
|
644
|
-
this.markDirty();
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
647
|
-
const siblings = liveSteerableLabels(this.readModel.getSnapshot().rows.map((row) => row.node), tab.nodeId);
|
|
648
|
-
this.setError(steerRefusalMessage(result.reason, siblings));
|
|
649
|
-
this.markDirty();
|
|
650
|
-
return false;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
protected renderItem(row: FleetTreeRow, _index: number, _selected: boolean, width: number): Line {
|
|
654
|
-
// Display-only overrides (stopping wins, then blocked-on-user) are derived
|
|
655
|
-
// here from panel state and passed into the pure row renderer.
|
|
656
|
-
const stopping = this.stopTracker.isStopping(row.node.id);
|
|
657
|
-
const blocked = !stopping && isBlockedOnUserNode(row.node);
|
|
658
|
-
return renderFleetRowLine(row, width, stopping, blocked, this.steerBadgeForNode(row.node.id), C);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/** A node's steer badge, looked up via its attached tab (if any), null when the node has no tab or no active/recent steer. */
|
|
662
|
-
private steerBadgeForNode(nodeId: string): SteerBadge | null {
|
|
663
|
-
return this.tabsState.tabs.find((t) => t.nodeId === nodeId)?.steerBadge ?? null;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
private renderDetail(row: FleetTreeRow, width: number): Line[] {
|
|
667
|
-
const node = row.node;
|
|
668
|
-
// Mirror the tree row's display-only overrides (stopping wins, then blocked)
|
|
669
|
-
// so the detail's literal state text never contradicts the row glyph.
|
|
670
|
-
const stopping = this.stopTracker.isStopping(node.id);
|
|
671
|
-
const blocked = !stopping && isBlockedOnUserNode(node);
|
|
672
|
-
// The drill-in observed-steer draft (when the composer is open on this row)
|
|
673
|
-
// renders an input line in the observed detail. For a workstream row, lazily
|
|
674
|
-
// fetch its task graph so the edges/pool posture renders in-panel.
|
|
675
|
-
this.acts?.ensureGraphFor(node);
|
|
676
|
-
return renderFleetDetailLines(node, width, stopping, blocked, C, this.acts?.observedSteerDraftFor(node.id) ?? null, this.acts?.graphFor(node.id) ?? null);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/** Renders the active session tab's content (transcript / chain summary / ledger fallback) in place of the tree. */
|
|
680
|
-
private renderTabView(width: number, height: number): Line[] {
|
|
681
|
-
const palette = this.getPalette();
|
|
682
|
-
const tab = activeFleetTab(this.tabsState);
|
|
683
|
-
if (!tab) return this.renderTreeView(width, height); // defensive; should be unreachable
|
|
684
|
-
const stripLine = renderFleetTabStrip(this.tabsState, width);
|
|
685
|
-
|
|
686
|
-
const snapshotRows = this.readModel.getSnapshot().rows;
|
|
687
|
-
const liveNode = snapshotRows.find((row) => row.node.id === tab.nodeId)?.node ?? null;
|
|
688
|
-
// The steer composer only ever applies to 'agent' tabs
|
|
689
|
-
// (wrfc-chain has no conversation loop of its own, see
|
|
690
|
-
// fleet-tabs.ts's FleetAttachableKind doc); the live-node lookup is
|
|
691
|
-
// shared with the isTerminal computation below either way.
|
|
692
|
-
const canSteer = liveNode !== null && !isTerminalProcessState(liveNode.state) && liveNode.capabilities.steerable;
|
|
693
|
-
|
|
694
|
-
// While composing, Enter/Esc replace the tab-switch/detach
|
|
695
|
-
// hints (mirrors git-panel.ts's renderCommitCompose footer) so the
|
|
696
|
-
// footer never advertises a key the composer itself has absorbed.
|
|
697
|
-
const hints: Array<{ keys: string; label: string }> = tab.steerDraft !== null
|
|
698
|
-
? [{ keys: 'Enter', label: 'send' }, { keys: 'Esc', label: 'cancel' }]
|
|
699
|
-
: [
|
|
700
|
-
{ keys: '[ ]', label: 'switch tab' },
|
|
701
|
-
{ keys: 'Ctrl+X', label: 'detach' },
|
|
702
|
-
...(canSteer ? [{ keys: 's', label: 'steer' }] : []),
|
|
703
|
-
];
|
|
704
|
-
|
|
705
|
-
const errorLine = this.renderErrorLine(width);
|
|
706
|
-
const composerLines: Line[] = [];
|
|
707
|
-
if (tab.steerDraft !== null) {
|
|
708
|
-
composerLines.push(buildSearchInputLine(
|
|
709
|
-
width,
|
|
710
|
-
'Steer: ',
|
|
711
|
-
`${tab.steerDraft}_`,
|
|
712
|
-
palette,
|
|
713
|
-
{ active: true, bg: palette.inputBg, valueColor: palette.info },
|
|
714
|
-
));
|
|
715
|
-
} else if (tab.steerBadge && !this.stopTracker.isStopping(tab.nodeId)) {
|
|
716
|
-
// A queued-steer promise ("delivers on its next turn") must not render
|
|
717
|
-
// while a stop is in flight for this node, there may be no next turn
|
|
718
|
-
// (refutation finding 4). The badge reconciles to dropped/consumed once
|
|
719
|
-
// the node's terminal state lands.
|
|
720
|
-
composerLines.push(renderSteerBadgeLine(tab.steerBadge, width, palette, liveNode?.label));
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const footerLines = [
|
|
724
|
-
...(errorLine ? [errorLine] : []),
|
|
725
|
-
...composerLines,
|
|
726
|
-
buildKeyboardHints(width, hints, palette),
|
|
727
|
-
];
|
|
728
|
-
const contentBudget = getPanelWorkspaceContentBudget(width, height, { footerLines, palette });
|
|
729
|
-
const contentHeight = Math.max(1, contentBudget - (stripLine ? 1 : 0));
|
|
730
|
-
|
|
731
|
-
let contentLines: Line[];
|
|
732
|
-
|
|
733
|
-
if (tab.kind === 'wrfc-chain') {
|
|
734
|
-
const memberRows = snapshotRows.filter((row) => row.node.parentId === tab.nodeId);
|
|
735
|
-
// d3: a completed chain prunes its wrapper node, so an empty member
|
|
736
|
-
// list can mean "finished", not "not started yet". Absent (pruned) OR
|
|
737
|
-
// terminal => the honest "completed" wording; present + non-terminal with
|
|
738
|
-
// no members yet => the "not started" wording.
|
|
739
|
-
const chainDoneOrAbsent = liveNode === null || isTerminalProcessState(liveNode.state);
|
|
740
|
-
contentLines = renderFleetChainSummary(memberRows, width, chainDoneOrAbsent);
|
|
741
|
-
} else {
|
|
742
|
-
const isTerminal = liveNode ? isTerminalProcessState(liveNode.state) : true;
|
|
743
|
-
const snapshot = this.actions.getConversationSnapshot(tab.agentId);
|
|
744
|
-
const result = renderFleetAgentTranscript(snapshot, isTerminal, tab.lineCache, width, contentHeight, this.configManager);
|
|
745
|
-
if (result.mode === 'unavailable') {
|
|
746
|
-
this.ensureLedgerLoaded(tab);
|
|
747
|
-
contentLines = tab.ledgerEntries !== null
|
|
748
|
-
? renderFleetLedgerFallback(tab.ledgerEntries, width, contentHeight)
|
|
749
|
-
: renderFleetTranscriptLoading(width);
|
|
750
|
-
} else {
|
|
751
|
-
contentLines = result.lines;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const sections = [
|
|
756
|
-
...(stripLine ? [{ lines: [stripLine] }] : []),
|
|
757
|
-
{ lines: contentLines },
|
|
758
|
-
];
|
|
759
|
-
return buildPanelWorkspace(width, height, { title: `Fleet: ${tab.label}`, sections, footerLines, palette });
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
private renderTreeView(width: number, height: number): Line[] {
|
|
763
|
-
const selected = this.getSelectedItem();
|
|
764
|
-
const footer: Line[] = [];
|
|
765
|
-
if (this.confirmOverlay.pending) {
|
|
766
|
-
footer.push(...(this.confirmOverlay.renderLines(width) ?? []));
|
|
767
|
-
} else if (selected) {
|
|
768
|
-
footer.push(...this.renderDetail(selected, width));
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Capability-gated tree hints, only advertise i/K/p when the selected node
|
|
772
|
-
// can actually accept them (most kinds are never interruptible; nothing is
|
|
773
|
-
// killable once terminal), incl. the d2 state-dependent p pause/resume chip.
|
|
774
|
-
// Mirrors the handleInput guards above. See buildFleetTreeHints (fleet-stop.ts).
|
|
775
|
-
// Blocked count is always taken from the LIVE fleet (blocked nodes never
|
|
776
|
-
// live in the archive), so the 'b' jump hint shows even from the archive view.
|
|
777
|
-
const blockedCount = this.readModel.getSnapshot().blockedNodeIds.length;
|
|
778
|
-
const hints = buildFleetTreeHints(selected?.node, this.follow, this.tabsState.tabs.length > 0, this.viewMode, blockedCount);
|
|
779
|
-
|
|
780
|
-
// Tab strip renders only when tabs exist, omitting it entirely with no
|
|
781
|
-
// tabs attached keeps the pre-session-tab root-tree rendering byte-identical.
|
|
782
|
-
const stripLine = renderFleetTabStrip(this.tabsState, width);
|
|
783
|
-
const header = stripLine ? [stripLine] : undefined;
|
|
784
|
-
|
|
785
|
-
return this.renderList(width, height, {
|
|
786
|
-
title: this.viewMode === 'archived' ? 'Fleet: Archived' : 'Fleet',
|
|
787
|
-
header,
|
|
788
|
-
footer,
|
|
789
|
-
hints,
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
public override render(width: number, height: number): Line[] {
|
|
794
|
-
if (this.acts?.pickModeActive()) return this.acts.renderPickMode(width, height, C);
|
|
795
|
-
if (this.spawn?.spawnModeActive()) return this.spawn.renderSpawnMode(width, height, C);
|
|
796
|
-
if (this.tabsState.activeTabIndex > 0) return this.renderTabView(width, height);
|
|
797
|
-
return this.renderTreeView(width, height);
|
|
798
|
-
}
|
|
799
|
-
}
|