@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
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents-modal.ts, the Agents modal (/agents, F2, ctrl+o, ctrl+p "agents" and
|
|
3
|
+
* every old console name: fleet, cockpit, tasks, wrfc, inspector, ops, …).
|
|
4
|
+
*
|
|
5
|
+
* One list of everything running or finished this session (agents, WRFC
|
|
6
|
+
* chains and their members, workflows, watchers, schedules, background
|
|
7
|
+
* processes, third-party agents hosted over ACP, observed foreign sessions)
|
|
8
|
+
* plus the daemon-hosted conversation this terminal is attached to. The right
|
|
9
|
+
* side tails the selected one live.
|
|
10
|
+
*
|
|
11
|
+
* Keys on the list: ↑↓ move; Enter opens the selected agent (or background
|
|
12
|
+
* process) full screen, outside the modal, with its own composer (or acts on
|
|
13
|
+
* a row waiting on you: pick a winner, resolve a conflict); other kinds with
|
|
14
|
+
* a transcript open full height inside the modal; s steers
|
|
15
|
+
* (inline input); x stops it and its descendants (asks first); i interrupts;
|
|
16
|
+
* p pauses or resumes; n hosts a third-party agent; D discards a worktree;
|
|
17
|
+
* b jumps to the next thing waiting on you; f follows the newest running
|
|
18
|
+
* agent; v switches to the archive, a archives or restores, A archives
|
|
19
|
+
* everything finished; / filters. In the full view ctrl+x stops (asks first),
|
|
20
|
+
* [ ] steps between opened agents, and Esc goes back up a level and never
|
|
21
|
+
* interrupts anything.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFile } from 'node:fs/promises';
|
|
25
|
+
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
26
|
+
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
27
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
28
|
+
import type { ProcessNode, SteerResult } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
29
|
+
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
30
|
+
import type { SurfaceModal, SurfaceModalHost } from './surface-modal-host.ts';
|
|
31
|
+
import type { KitHint, SurfaceLayer } from '../renderer/surface-kit.ts';
|
|
32
|
+
import type { KitRow } from '../renderer/surface-kit-list.ts';
|
|
33
|
+
import { activeTokens } from '../renderer/theme.ts';
|
|
34
|
+
import { renderAgentsModal, type AgentsDetail, type AgentsModalView, type AgentsText } from '../renderer/agents-modal.ts';
|
|
35
|
+
import { formatUsd } from '../renderer/usage-modal.ts';
|
|
36
|
+
import {
|
|
37
|
+
fleetAttentionText, fleetNodeAttention, fleetStallMarker, isBlockedOnUserNode, isRunningProcessState, isTerminalProcessState,
|
|
38
|
+
type FleetReadModel, type FleetTreeRow,
|
|
39
|
+
} from '../views/fleet-read-model.ts';
|
|
40
|
+
import type { FleetActs } from '../views/fleet-acts.ts';
|
|
41
|
+
import type { FleetSpawn } from '../views/fleet-spawn.ts';
|
|
42
|
+
import {
|
|
43
|
+
activeFleetTab, attachFleetTab, EMPTY_FLEET_TABS_STATE, isAttachableFleetKind, stepFleetTab, type FleetTab, type FleetTabsState,
|
|
44
|
+
} from '../views/fleet-session-tabs.ts';
|
|
45
|
+
import { liveSteerableLabels, reconcileSteerBadges, steerBadgeGlyph, steerRefusalMessage } from '../views/fleet-steer.ts';
|
|
46
|
+
import { FleetStopTracker, fleetKillConfirmArgs, fleetStateDisplay, toggleFleetPause } from '../views/fleet-stop.ts';
|
|
47
|
+
import { formatFleetCost, renderFleetDetailLines } from '../views/fleet-format.ts';
|
|
48
|
+
import { hasFleetCost } from '../views/fleet-read-model.ts';
|
|
49
|
+
import { parseAgentLedger, renderFleetAgentTranscript, renderFleetChainSummary, renderFleetLedgerFallback, renderFleetTranscriptLoading } from '../views/fleet-transcript.ts';
|
|
50
|
+
import { isObservedExternalNode } from '../views/fleet-observed-render.ts';
|
|
51
|
+
import type { HostedSessionFeed } from '../views/hosted-session-feed.ts';
|
|
52
|
+
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
53
|
+
import { isTextBackspace } from './delete-key-policy.ts';
|
|
54
|
+
import type { ConfirmOptions } from './confirm-dialog.ts';
|
|
55
|
+
import type { ViewTarget } from './views.ts';
|
|
56
|
+
import { hostedBody, hostedHeaderTexts, hostedRowTexts, transcriptTail } from './agents-modal-text.ts';
|
|
57
|
+
|
|
58
|
+
/** What the modal can do to a process (the process registry, through the fleet read model). */
|
|
59
|
+
export interface FleetActionCallbacks {
|
|
60
|
+
readonly interrupt: (id: string) => boolean;
|
|
61
|
+
readonly resume: (id: string) => boolean;
|
|
62
|
+
readonly kill: (id: string, opts: { readonly cascade: boolean }) => readonly string[];
|
|
63
|
+
/** An agent's conversation, live or just finished; empty when unavailable. */
|
|
64
|
+
readonly getConversationSnapshot: (agentId: string) => readonly ConversationMessageSnapshot[];
|
|
65
|
+
/** The agent's on-disk event log, the fallback when its conversation is gone. */
|
|
66
|
+
readonly resolveSessionLogPath: (agentId: string) => string;
|
|
67
|
+
readonly steer: (id: string, text: string) => SteerResult;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AgentsModalDeps {
|
|
71
|
+
readonly readModel: FleetReadModel;
|
|
72
|
+
readonly actions: FleetActionCallbacks;
|
|
73
|
+
readonly acts?: FleetActs | null;
|
|
74
|
+
readonly spawn?: FleetSpawn | null;
|
|
75
|
+
readonly configManager?: ConfigManager | null;
|
|
76
|
+
readonly hosted?: HostedSessionFeed;
|
|
77
|
+
/** Send a line to the hosted session (/hosted say). */
|
|
78
|
+
readonly steerHosted?: (text: string) => void;
|
|
79
|
+
/** This session's own cost (the "you" half of the header), or null when unpriced. */
|
|
80
|
+
readonly sessionCost?: () => number | null;
|
|
81
|
+
readonly confirm?: (options: ConfirmOptions) => Promise<boolean>;
|
|
82
|
+
readonly requestRender: () => void;
|
|
83
|
+
/** Tick for elapsed times and steer badges (ms; 0 disables, for tests). */
|
|
84
|
+
readonly tickMs?: number;
|
|
85
|
+
/** Open an agent or background process full screen (shell/session-views.ts); false when it is not known here. */
|
|
86
|
+
readonly openSessionView?: (target: { readonly kind: 'agent' | 'process'; readonly id: string }) => boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const HOSTED_ID = 'hosted-session';
|
|
90
|
+
|
|
91
|
+
type ListEntry = { readonly kind: 'node'; readonly row: FleetTreeRow } | { readonly kind: 'hosted' };
|
|
92
|
+
|
|
93
|
+
/** One key in the vocabulary the fleet controllers (acts, spawn) take. */
|
|
94
|
+
function keyName(token: InputToken): string | null {
|
|
95
|
+
if (token.type === 'text') return token.value.length === 1 ? token.value : null;
|
|
96
|
+
if (token.type === 'key') return token.logicalName ?? null;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class AgentsModal implements SurfaceModal {
|
|
101
|
+
readonly name = 'agents';
|
|
102
|
+
view: 'active' | 'archived' = 'active';
|
|
103
|
+
query = '';
|
|
104
|
+
filtering = false;
|
|
105
|
+
selectedId: string | null = null;
|
|
106
|
+
follow = false;
|
|
107
|
+
tabs: FleetTabsState = EMPTY_FLEET_TABS_STATE;
|
|
108
|
+
/** The full-height view of the hosted session is open. */
|
|
109
|
+
hostedFull = false;
|
|
110
|
+
steer: { readonly id: string; draft: string } | null = null;
|
|
111
|
+
status: AgentsText | null = null;
|
|
112
|
+
readonly stopTracker = new FleetStopTracker();
|
|
113
|
+
private readonly unsubs: Array<() => void> = [];
|
|
114
|
+
private tickTimer: ReturnType<typeof setInterval> | null = null;
|
|
115
|
+
|
|
116
|
+
constructor(private readonly deps: AgentsModalDeps) {
|
|
117
|
+
this.unsubs.push(deps.readModel.subscribe(() => { this.onFleetChange(); deps.requestRender(); }));
|
|
118
|
+
this.unsubs.push(deps.readModel.subscribeConsumed((event) => {
|
|
119
|
+
for (const tab of this.tabs.tabs) {
|
|
120
|
+
const badge = tab.steerBadge;
|
|
121
|
+
if (badge && badge.messageId === event.messageId && (badge.status === 'queued' || badge.status === 'dropped')) {
|
|
122
|
+
tab.steerBadge = { messageId: badge.messageId, status: 'consumed', queuedAt: badge.queuedAt, resolvedAt: Date.now() };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
deps.requestRender();
|
|
126
|
+
}));
|
|
127
|
+
if (deps.hosted) this.unsubs.push(deps.hosted.subscribe(() => deps.requestRender()));
|
|
128
|
+
const tickMs = deps.tickMs ?? 1_000;
|
|
129
|
+
if (tickMs > 0) this.tickTimer = setInterval(() => { this.onFleetChange(); deps.requestRender(); }, tickMs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onClose(): void {
|
|
133
|
+
for (const unsub of this.unsubs) unsub();
|
|
134
|
+
this.unsubs.length = 0;
|
|
135
|
+
if (this.tickTimer !== null) clearInterval(this.tickTimer);
|
|
136
|
+
this.tickTimer = null;
|
|
137
|
+
for (const tab of this.tabs.tabs) tab.lineCache.clear();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private onFleetChange(): void {
|
|
141
|
+
this.applyFollow();
|
|
142
|
+
if (this.tabs.tabs.length > 0) reconcileSteerBadges(this.tabs.tabs, (id) => this.findNode(id), Date.now());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Data ───────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
private snapshotRows(): readonly FleetTreeRow[] {
|
|
148
|
+
return this.view === 'archived' ? this.deps.readModel.getArchivedSnapshot().rows : this.deps.readModel.getSnapshot().rows;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private findNode(id: string): ProcessNode | null {
|
|
152
|
+
return this.deps.readModel.getSnapshot().rows.find((row) => row.node.id === id)?.node ?? null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private hostedAttached(): boolean {
|
|
156
|
+
return this.view === 'active' && Boolean(this.deps.hosted?.getState().record);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private matches(row: FleetTreeRow): boolean {
|
|
160
|
+
const q = this.query.trim().toLowerCase();
|
|
161
|
+
if (!q) return true;
|
|
162
|
+
const node = row.node;
|
|
163
|
+
return [node.label, node.task ?? '', node.kind, node.id, node.currentActivity?.text ?? '', node.headline?.text ?? '']
|
|
164
|
+
.some((text) => text.toLowerCase().includes(q));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The list in display order, grouped: hosted, running (whole subtrees), finished. */
|
|
168
|
+
private groups(): Array<{ readonly name: string; readonly entries: ListEntry[] }> {
|
|
169
|
+
const rows = this.snapshotRows().filter((row) => this.matches(row));
|
|
170
|
+
if (this.view === 'archived') return rows.length > 0 ? [{ name: 'Archived', entries: rows.map((row) => ({ kind: 'node', row })) }] : [];
|
|
171
|
+
const groups: Array<{ name: string; entries: ListEntry[] }> = [];
|
|
172
|
+
const hostedMatches = !this.query.trim() || 'hosted'.includes(this.query.trim().toLowerCase()) || (this.deps.hosted?.getState().record?.title ?? '').toLowerCase().includes(this.query.trim().toLowerCase());
|
|
173
|
+
if (this.hostedAttached() && hostedMatches) groups.push({ name: 'Hosted', entries: [{ kind: 'hosted' }] });
|
|
174
|
+
// A root and everything under it stays together, in the group of its liveliest node.
|
|
175
|
+
const all = this.snapshotRows();
|
|
176
|
+
const rootOf = new Map<string, string>();
|
|
177
|
+
let currentRoot = '';
|
|
178
|
+
for (const row of all) {
|
|
179
|
+
if (row.depth === 0) currentRoot = row.node.id;
|
|
180
|
+
rootOf.set(row.node.id, currentRoot);
|
|
181
|
+
}
|
|
182
|
+
const liveRoots = new Set<string>();
|
|
183
|
+
for (const row of all) if (!isTerminalProcessState(row.node.state)) liveRoots.add(rootOf.get(row.node.id) ?? row.node.id);
|
|
184
|
+
const running = rows.filter((row) => liveRoots.has(rootOf.get(row.node.id) ?? row.node.id));
|
|
185
|
+
const finished = rows.filter((row) => !liveRoots.has(rootOf.get(row.node.id) ?? row.node.id));
|
|
186
|
+
if (running.length > 0) groups.push({ name: 'Running', entries: running.map((row) => ({ kind: 'node', row })) });
|
|
187
|
+
if (finished.length > 0) groups.push({ name: 'Finished', entries: finished.map((row) => ({ kind: 'node', row })) });
|
|
188
|
+
return groups;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private entries(): ListEntry[] {
|
|
192
|
+
return this.groups().flatMap((group) => group.entries);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private entryId(entry: ListEntry): string {
|
|
196
|
+
return entry.kind === 'hosted' ? HOSTED_ID : entry.row.node.id;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private selectedEntry(): ListEntry | null {
|
|
200
|
+
const entries = this.entries();
|
|
201
|
+
if (entries.length === 0) return null;
|
|
202
|
+
return entries.find((entry) => this.entryId(entry) === this.selectedId) ?? entries[0]!;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
selectedNode(): ProcessNode | null {
|
|
206
|
+
const entry = this.selectedEntry();
|
|
207
|
+
return entry?.kind === 'node' ? entry.row.node : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private move(delta: number): void {
|
|
211
|
+
const entries = this.entries();
|
|
212
|
+
if (entries.length === 0) return;
|
|
213
|
+
const current = entries.findIndex((entry) => this.entryId(entry) === this.selectedId);
|
|
214
|
+
const next = Math.max(0, Math.min(entries.length - 1, (current < 0 ? 0 : current) + delta));
|
|
215
|
+
this.selectedId = this.entryId(entries[next]!);
|
|
216
|
+
this.follow = false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private applyFollow(): void {
|
|
220
|
+
if (!this.follow) return;
|
|
221
|
+
let best: FleetTreeRow | null = null;
|
|
222
|
+
for (const row of this.deps.readModel.getSnapshot().rows) {
|
|
223
|
+
if (!isRunningProcessState(row.node.state)) continue;
|
|
224
|
+
if (!best || (row.node.startedAt ?? 0) >= (best.node.startedAt ?? 0)) best = row;
|
|
225
|
+
}
|
|
226
|
+
if (best) this.selectedId = best.node.id;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Select a specific process (a deep link from a notification, the work plan, /agents --target). */
|
|
230
|
+
reveal(target: ViewTarget): boolean {
|
|
231
|
+
const rows = this.deps.readModel.getSnapshot().rows;
|
|
232
|
+
// A tool target (the keybindings modal's Tools tab) names a tool, not a
|
|
233
|
+
// process: select the first process whose current step is that tool.
|
|
234
|
+
const row = target.kind === 'tool'
|
|
235
|
+
? rows.find((r) => r.node.currentActivity?.toolName === target.id)
|
|
236
|
+
: rows.find((r) => r.node.id === target.id && (!target.kind || r.node.kind === target.kind));
|
|
237
|
+
this.view = 'active';
|
|
238
|
+
this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 };
|
|
239
|
+
this.hostedFull = false;
|
|
240
|
+
this.query = '';
|
|
241
|
+
if (!row) {
|
|
242
|
+
this.status = target.kind === 'tool'
|
|
243
|
+
? { text: `Nothing is running ${target.id} right now.`, tone: 'muted' }
|
|
244
|
+
: { text: `${target.id} is no longer running here.`, tone: 'warning' };
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
this.selectedId = row.node.id;
|
|
248
|
+
this.status = null;
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Open the hosted session's full view (after /hosted new or attach). */
|
|
253
|
+
showHosted(): void {
|
|
254
|
+
this.view = 'active';
|
|
255
|
+
this.selectedId = HOSTED_ID;
|
|
256
|
+
this.hostedFull = this.hostedAttached();
|
|
257
|
+
this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private level(): 'list' | 'full' | 'picker' {
|
|
261
|
+
if (this.deps.acts?.pickModeActive() || this.deps.spawn?.spawnModeActive()) return 'picker';
|
|
262
|
+
if (this.hostedFull || this.tabs.activeTabIndex > 0) return 'full';
|
|
263
|
+
return 'list';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Actions ────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
private say(text: string, tone: AgentsText['tone'] = 'muted'): void {
|
|
269
|
+
this.status = { text, tone };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private openFull(host: SurfaceModalHost): void {
|
|
273
|
+
const entry = this.selectedEntry();
|
|
274
|
+
if (!entry) return;
|
|
275
|
+
if (entry.kind === 'hosted') { this.hostedFull = true; return; }
|
|
276
|
+
const node = entry.row.node;
|
|
277
|
+
if (this.deps.acts?.handleTreeKey('enter', node)) return;
|
|
278
|
+
// An agent or a background process opens full screen, outside the modal.
|
|
279
|
+
const target = node.kind === 'agent' ? { kind: 'agent' as const, id: node.id }
|
|
280
|
+
: node.kind === 'background-process' ? { kind: 'process' as const, id: node.id } : null;
|
|
281
|
+
if (target && this.deps.openSessionView?.(target)) {
|
|
282
|
+
host.close(this, 'done');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!isAttachableFleetKind(node.kind)) {
|
|
286
|
+
this.say(`${node.kind} has no transcript to open; its details are on the right.`, 'faint');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.tabs = attachFleetTab(this.tabs, node);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private beginSteer(): void {
|
|
293
|
+
const entry = this.selectedEntry();
|
|
294
|
+
if (!entry) return;
|
|
295
|
+
if (entry.kind === 'hosted') {
|
|
296
|
+
if (!this.deps.steerHosted) { this.say('Steering the hosted session is not available here.', 'error'); return; }
|
|
297
|
+
this.steer = { id: HOSTED_ID, draft: '' };
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const node = entry.row.node;
|
|
301
|
+
if (isObservedExternalNode(node)) { this.deps.acts?.handleTreeKey('s', node); return; }
|
|
302
|
+
if (isTerminalProcessState(node.state)) { this.say(`${node.label} already finished: nothing to steer.`, 'faint'); return; }
|
|
303
|
+
if (!node.capabilities.steerable || !isAttachableFleetKind(node.kind)) { this.say(`${node.kind} does not take steering.`, 'faint'); return; }
|
|
304
|
+
this.steer = { id: node.id, draft: '' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private submitSteer(): void {
|
|
308
|
+
const steer = this.steer;
|
|
309
|
+
if (!steer) return;
|
|
310
|
+
const text = steer.draft.trim();
|
|
311
|
+
if (!text) { this.steer = null; return; }
|
|
312
|
+
if (steer.id === HOSTED_ID) {
|
|
313
|
+
this.steer = null;
|
|
314
|
+
this.deps.steerHosted?.(text);
|
|
315
|
+
this.say('Sent to the hosted session.');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const result = this.deps.actions.steer(steer.id, text);
|
|
319
|
+
if (!result.queued) {
|
|
320
|
+
// The draft stays so nothing typed is lost.
|
|
321
|
+
const siblings = liveSteerableLabels(this.deps.readModel.getSnapshot().rows.map((row) => row.node), steer.id);
|
|
322
|
+
this.say(steerRefusalMessage(result.reason, siblings), 'error');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const node = this.findNode(steer.id);
|
|
326
|
+
if (node) {
|
|
327
|
+
const keep = this.tabs.activeTabIndex;
|
|
328
|
+
this.tabs = attachFleetTab(this.tabs, node);
|
|
329
|
+
if (this.level() !== 'full' || keep === 0) this.tabs = { tabs: this.tabs.tabs, activeTabIndex: keep };
|
|
330
|
+
const tab = this.tabs.tabs.find((t) => t.nodeId === steer.id);
|
|
331
|
+
if (tab) tab.steerBadge = { messageId: result.messageId, status: 'queued', queuedAt: Date.now() };
|
|
332
|
+
}
|
|
333
|
+
this.steer = null;
|
|
334
|
+
this.say(`Steer queued for ${node?.label ?? steer.id}: it arrives at its next turn (⧗ until then).`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** x / ctrl+x: stop the node and its descendants, after asking. */
|
|
338
|
+
private stopSelected(nodeId?: string): void {
|
|
339
|
+
const node = nodeId ? this.findNode(nodeId) : this.selectedNode();
|
|
340
|
+
if (!node || isTerminalProcessState(node.state)) { if (node) this.say(`${node.label} already finished.`, 'faint'); return; }
|
|
341
|
+
if (!node.capabilities.killable) { this.say(`${node.kind} cannot be stopped from here.`, 'faint'); return; }
|
|
342
|
+
const args = fleetKillConfirmArgs(node, this.snapshotRows(), {
|
|
343
|
+
kill: (id) => this.deps.actions.kill(id, { cascade: true }),
|
|
344
|
+
markStopping: (id) => this.stopTracker.mark(id),
|
|
345
|
+
});
|
|
346
|
+
if (!this.deps.confirm) { this.say('Stopping needs a confirm dialog, which is not available here.', 'error'); return; }
|
|
347
|
+
void this.deps.confirm({
|
|
348
|
+
title: `Stop ${node.label}?`,
|
|
349
|
+
body: `This stops ${args.label} and everything it started. It cannot be resumed.\nPress ctrl+x again to stop it; Esc keeps it running.`,
|
|
350
|
+
confirmLabel: 'Stop',
|
|
351
|
+
tone: 'danger',
|
|
352
|
+
confirmChord: { key: 'x', ctrl: true },
|
|
353
|
+
}).then((ok) => {
|
|
354
|
+
if (ok) { args.onConfirm(); this.say(`Stopping ${node.label}…`, 'warning'); }
|
|
355
|
+
this.deps.requestRender();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private interruptSelected(): void {
|
|
360
|
+
const node = this.selectedNode();
|
|
361
|
+
if (!node || isTerminalProcessState(node.state)) return;
|
|
362
|
+
if (!node.capabilities.interruptible) { this.say(`${node.kind} does not support interrupt.`, 'faint'); return; }
|
|
363
|
+
this.deps.actions.interrupt(node.id);
|
|
364
|
+
this.stopTracker.mark(node.id);
|
|
365
|
+
this.say(`Interrupting ${node.label}…`, 'warning');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private jumpToBlocked(): void {
|
|
369
|
+
const blocked = this.deps.readModel.getSnapshot().blockedNodeIds;
|
|
370
|
+
if (blocked.length === 0) { this.say('Nothing is waiting on you right now.', 'faint'); return; }
|
|
371
|
+
const at = this.selectedId ? blocked.indexOf(this.selectedId) : -1;
|
|
372
|
+
this.view = 'active';
|
|
373
|
+
this.query = '';
|
|
374
|
+
this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 };
|
|
375
|
+
this.hostedFull = false;
|
|
376
|
+
this.selectedId = blocked[(at + 1) % blocked.length]!;
|
|
377
|
+
this.status = null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private archiveSelected(all: boolean): void {
|
|
381
|
+
if (all) {
|
|
382
|
+
if (this.view !== 'active') return;
|
|
383
|
+
const count = this.deps.readModel.archiveFinished();
|
|
384
|
+
this.say(count > 0 ? `Archived ${count} finished process${count === 1 ? '' : 'es'}.` : 'Nothing has fully finished to archive.', 'faint');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const node = this.selectedNode();
|
|
388
|
+
if (!node) return;
|
|
389
|
+
if (this.view === 'archived') {
|
|
390
|
+
const restored = this.deps.readModel.unarchive(node.id);
|
|
391
|
+
this.say(restored > 0 ? `Restored ${node.label}.` : 'Nothing restored for this one.', 'faint');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (!isTerminalProcessState(node.state)) { this.say('Only finished processes can be archived.', 'faint'); return; }
|
|
395
|
+
const result = this.deps.readModel.archive(node.id);
|
|
396
|
+
this.say(result.archived ? `Archived ${node.label}.` : result.reason ?? 'Could not archive this one.', 'faint');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Keys ───────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
escape(): boolean {
|
|
402
|
+
if (this.steer) { this.steer = null; return true; }
|
|
403
|
+
if (this.deps.acts?.observedSteerActive()) { this.deps.acts.handleObservedSteerInput('escape'); return true; }
|
|
404
|
+
if (this.deps.acts?.pickModeActive()) { this.deps.acts.handlePickInput('escape'); return true; }
|
|
405
|
+
if (this.deps.spawn?.spawnModeActive()) { this.deps.spawn.handleSpawnInput('escape'); return true; }
|
|
406
|
+
if (this.filtering) { this.filtering = false; return true; }
|
|
407
|
+
if (this.hostedFull) { this.hostedFull = false; return true; }
|
|
408
|
+
if (this.tabs.activeTabIndex > 0) { this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 0 }; return true; }
|
|
409
|
+
if (this.query) { this.query = ''; return true; }
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
handleToken(token: InputToken, host: SurfaceModalHost): void {
|
|
414
|
+
if (this.steer) { this.handleSteerToken(token); return; }
|
|
415
|
+
// F2 and ctrl+o open this modal; pressed again here, they close it.
|
|
416
|
+
if (token.type === 'key' && ((token.logicalName === 'f2' && !token.ctrl) || (token.ctrl && token.logicalName === 'o'))) {
|
|
417
|
+
host.close(this, 'done');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const key = keyName(token);
|
|
421
|
+
if (this.deps.acts?.observedSteerActive()) {
|
|
422
|
+
if (token.type === 'text') for (const ch of token.value) this.deps.acts.handleObservedSteerInput(ch);
|
|
423
|
+
else if (key) this.deps.acts.handleObservedSteerInput(key);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (this.deps.acts?.pickModeActive()) { if (key) this.deps.acts.handlePickInput(key); return; }
|
|
427
|
+
if (this.deps.spawn?.spawnModeActive()) { if (key) this.deps.spawn.handleSpawnInput(key); return; }
|
|
428
|
+
if (this.filtering) { this.handleFilterToken(token); return; }
|
|
429
|
+
if (this.level() === 'full') { this.handleFullToken(token); return; }
|
|
430
|
+
if (token.type === 'key') {
|
|
431
|
+
const name = token.logicalName ?? '';
|
|
432
|
+
if (token.ctrl && name === 'x') { this.stopSelected(); return; }
|
|
433
|
+
if (name === 'up') this.move(-1);
|
|
434
|
+
else if (name === 'down') this.move(1);
|
|
435
|
+
else if (name === 'pageup') this.move(-10);
|
|
436
|
+
else if (name === 'pagedown') this.move(10);
|
|
437
|
+
else if (name === 'enter') this.openFull(host);
|
|
438
|
+
else if (isTextBackspace(name) && this.query) this.query = this.query.slice(0, -1);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (token.type !== 'text' || token.value.length !== 1) return;
|
|
442
|
+
this.status = null;
|
|
443
|
+
const node = this.selectedNode();
|
|
444
|
+
switch (token.value) {
|
|
445
|
+
case '/': this.filtering = true; break;
|
|
446
|
+
case 'j': this.move(1); break;
|
|
447
|
+
case 'k': this.move(-1); break;
|
|
448
|
+
case 's': this.beginSteer(); break;
|
|
449
|
+
case 'x': case 'K': this.stopSelected(); break;
|
|
450
|
+
case 'i': this.interruptSelected(); break;
|
|
451
|
+
case 'p':
|
|
452
|
+
if (node) toggleFleetPause(node, { interrupt: (id) => this.deps.actions.interrupt(id), resume: (id) => this.deps.actions.resume(id), setError: (m) => this.say(m, 'faint'), markDirty: () => {}, tracker: this.stopTracker });
|
|
453
|
+
break;
|
|
454
|
+
case 'n':
|
|
455
|
+
if (this.deps.spawn) void this.deps.spawn.begin().then(() => this.deps.requestRender());
|
|
456
|
+
else this.say('Hosting a third-party agent needs the daemon, which is not connected.', 'faint');
|
|
457
|
+
break;
|
|
458
|
+
case 'D':
|
|
459
|
+
if (node && !this.deps.acts?.handleTreeKey('D', node)) this.say('This row does not own a worktree.', 'faint');
|
|
460
|
+
break;
|
|
461
|
+
case 'b': this.jumpToBlocked(); break;
|
|
462
|
+
case 'f': this.follow = !this.follow; this.applyFollow(); this.say(this.follow ? 'Following the newest running agent.' : 'Stopped following.', 'faint'); break;
|
|
463
|
+
case 'v': this.view = this.view === 'active' ? 'archived' : 'active'; this.selectedId = null; break;
|
|
464
|
+
case 'a': this.archiveSelected(false); break;
|
|
465
|
+
case 'A': this.archiveSelected(true); break;
|
|
466
|
+
case ']':
|
|
467
|
+
if (this.tabs.tabs.length > 0) this.tabs = { tabs: this.tabs.tabs, activeTabIndex: 1 };
|
|
468
|
+
break;
|
|
469
|
+
case '[':
|
|
470
|
+
if (this.tabs.tabs.length > 0) this.tabs = { tabs: this.tabs.tabs, activeTabIndex: this.tabs.tabs.length };
|
|
471
|
+
break;
|
|
472
|
+
default: break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private handleFilterToken(token: InputToken): void {
|
|
477
|
+
if (token.type === 'text') {
|
|
478
|
+
this.query += [...token.value].filter((ch) => ch >= ' ').join('');
|
|
479
|
+
this.selectedId = null;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (token.type !== 'key') return;
|
|
483
|
+
const name = token.logicalName ?? '';
|
|
484
|
+
if (isTextBackspace(name)) { this.query = this.query.slice(0, -1); return; }
|
|
485
|
+
if (name === 'enter') { this.filtering = false; return; }
|
|
486
|
+
if (name === 'up' || name === 'down') { this.filtering = false; this.move(name === 'up' ? -1 : 1); }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private handleFullToken(token: InputToken): void {
|
|
490
|
+
if (token.type === 'key') {
|
|
491
|
+
const name = token.logicalName ?? '';
|
|
492
|
+
if (token.ctrl && name === 'x') {
|
|
493
|
+
const tab = activeFleetTab(this.tabs);
|
|
494
|
+
if (tab) this.stopSelected(tab.nodeId);
|
|
495
|
+
else this.say('The hosted session ends with /hosted kill; detaching keeps or ends it by its policy.', 'faint');
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (token.type !== 'text' || token.value.length !== 1) return;
|
|
500
|
+
if (token.value === 's') {
|
|
501
|
+
const tab = activeFleetTab(this.tabs);
|
|
502
|
+
if (tab) this.selectedId = tab.nodeId;
|
|
503
|
+
this.beginSteer();
|
|
504
|
+
} else if ((token.value === ']' || token.value === '[') && !this.hostedFull) {
|
|
505
|
+
const next = stepFleetTab(this.tabs, token.value === ']' ? 1 : -1);
|
|
506
|
+
if (next.activeTabIndex > 0) this.tabs = next;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private handleSteerToken(token: InputToken): void {
|
|
511
|
+
const steer = this.steer!;
|
|
512
|
+
if (token.type === 'text') {
|
|
513
|
+
steer.draft += token.value.replace(/[\r\n]+/g, ' ').replace(/[\x00-\x1f\x7f]/g, '');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (token.type !== 'key') return;
|
|
517
|
+
const name = token.logicalName ?? '';
|
|
518
|
+
if (name === 'enter') this.submitSteer();
|
|
519
|
+
else if (isTextBackspace(name)) steer.draft = steer.draft.slice(0, -1);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── View ───────────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
private badgeFor(nodeId: string): FleetTab['steerBadge'] {
|
|
525
|
+
return this.tabs.tabs.find((tab) => tab.nodeId === nodeId)?.steerBadge ?? null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private nodeRow(row: FleetTreeRow, selected: boolean): KitRow {
|
|
529
|
+
const t = activeTokens();
|
|
530
|
+
const node = row.node;
|
|
531
|
+
const stopping = this.stopTracker.isStopping(node.id);
|
|
532
|
+
const blocked = !stopping && isBlockedOnUserNode(node);
|
|
533
|
+
const disp = fleetStateDisplay(node.state, stopping, blocked);
|
|
534
|
+
const running = isRunningProcessState(node.state) || node.state === 'awaiting-approval';
|
|
535
|
+
const mark = stopping || blocked ? disp.glyph : running ? '◐' : node.state === 'done' ? '✓' : node.state === 'failed' || node.state === 'killed' ? '✕' : '○';
|
|
536
|
+
const markFg = stopping || blocked ? t.warning : running ? t.brand : node.state === 'done' ? t.success : node.state === 'failed' || node.state === 'killed' ? t.error : t.textFaint;
|
|
537
|
+
const badge = this.badgeFor(node.id);
|
|
538
|
+
const attention = fleetNodeAttention(node);
|
|
539
|
+
const stall = fleetStallMarker(node);
|
|
540
|
+
const activity = stopping ? 'stopping…'
|
|
541
|
+
: blocked ? fleetAttentionText(attention ?? { reason: 'approval' })
|
|
542
|
+
: [node.currentActivity?.toolName ? `${node.currentActivity.toolName}: ${node.currentActivity.text}` : node.headline?.text ?? node.currentActivity?.text ?? node.task ?? '', stall ?? ''].filter(Boolean).join(' · ');
|
|
543
|
+
const attempt = node.attemptGroup ? ` attempt ${node.attemptGroup.index + 1}/${node.attemptGroup.total}${node.attemptGroup.held ? ' held' : ''}` : '';
|
|
544
|
+
const kindTag = node.kind === 'agent' ? '' : node.kind === 'acp-agent' ? 'hosted' : node.kind.replace(/-/g, ' ');
|
|
545
|
+
const desc = [kindTag, `${activity}${attempt}`.trim(), badge ? steerBadgeGlyph(badge.status) : ''].filter(Boolean).join(' · ');
|
|
546
|
+
const cost = hasFleetCost(node.costUsd, node.costState) ? formatFleetCost(node.costUsd, node.costState) : '';
|
|
547
|
+
const prefix = row.depth > 0 ? `${'│ '.repeat(row.depth - 1)}└ ` : '';
|
|
548
|
+
return { label: `${prefix}${node.label}`, desc: desc || undefined, right: [formatElapsed(node.elapsedMs), cost].filter(Boolean).join(' · '), mark, markFg, selected };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private listRows(): KitRow[] {
|
|
552
|
+
const t = activeTokens();
|
|
553
|
+
const selected = this.selectedEntry();
|
|
554
|
+
const rows: KitRow[] = [];
|
|
555
|
+
for (const group of this.groups()) {
|
|
556
|
+
rows.push({ header: group.name, headerRight: group.name === 'Hosted' ? 'on the daemon' : undefined });
|
|
557
|
+
for (const entry of group.entries) {
|
|
558
|
+
const isSelected = selected !== null && this.entryId(entry) === this.entryId(selected);
|
|
559
|
+
if (entry.kind === 'hosted') {
|
|
560
|
+
const state = this.deps.hosted!.getState();
|
|
561
|
+
const record = state.record!;
|
|
562
|
+
rows.push({ label: record.title.trim() || record.id, desc: `${state.streaming ? 'live' : 'not streaming'} · ${record.status}`, right: `${record.turnCount} turns`, mark: '◈', markFg: t.brandEnd, selected: isSelected });
|
|
563
|
+
} else {
|
|
564
|
+
rows.push(this.nodeRow(entry.row, isSelected));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return rows;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private detail(): AgentsDetail | null {
|
|
572
|
+
const entry = this.selectedEntry();
|
|
573
|
+
if (!entry) return null;
|
|
574
|
+
if (entry.kind === 'hosted') {
|
|
575
|
+
const state = this.deps.hosted!.getState();
|
|
576
|
+
return { title: state.record!.title.trim() || 'Hosted session', meta: 'a conversation running inside the daemon', blocks: [{ kind: 'text', lines: hostedHeaderTexts(state) }], tail: hostedRowTexts(state.rows.slice(-40)) };
|
|
577
|
+
}
|
|
578
|
+
const node = entry.row.node;
|
|
579
|
+
const stopping = this.stopTracker.isStopping(node.id);
|
|
580
|
+
const blocked = !stopping && isBlockedOnUserNode(node);
|
|
581
|
+
this.deps.acts?.ensureGraphFor(node);
|
|
582
|
+
const observedDraft = this.deps.acts?.observedSteerDraftFor(node.id) ?? null;
|
|
583
|
+
const graph = this.deps.acts?.graphFor(node.id) ?? null;
|
|
584
|
+
const meta = [node.kind === 'acp-agent' ? 'hosted over ACP' : node.kind, node.model, node.provider].filter(Boolean).join(' · ');
|
|
585
|
+
const tail = node.kind === 'agent' || node.kind === 'acp-agent' ? transcriptTail(this.deps.actions.getConversationSnapshot(node.id), 30) : [];
|
|
586
|
+
return { title: node.label, meta, blocks: [{ kind: 'lines', build: (width) => renderFleetDetailLines(node, width, stopping, blocked, undefined, observedDraft, graph) }], tail };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private fullBody(width: number, height: number): readonly Line[] {
|
|
590
|
+
if (this.hostedFull) return [];
|
|
591
|
+
const tab = activeFleetTab(this.tabs);
|
|
592
|
+
if (!tab) return [];
|
|
593
|
+
const live = this.findNode(tab.nodeId);
|
|
594
|
+
if (tab.kind === 'wrfc-chain') {
|
|
595
|
+
const members = this.deps.readModel.getSnapshot().rows.filter((row) => row.node.parentId === tab.nodeId);
|
|
596
|
+
return renderFleetChainSummary(members, width, live === null || isTerminalProcessState(live.state));
|
|
597
|
+
}
|
|
598
|
+
const isTerminal = live ? isTerminalProcessState(live.state) : true;
|
|
599
|
+
const snapshot = this.deps.actions.getConversationSnapshot(tab.nodeId);
|
|
600
|
+
const result = renderFleetAgentTranscript(snapshot, isTerminal, tab.lineCache, width, height, this.deps.configManager ?? null);
|
|
601
|
+
if (result.mode !== 'unavailable') return result.lines;
|
|
602
|
+
if (!tab.ledgerLoadStarted) {
|
|
603
|
+
tab.ledgerLoadStarted = true;
|
|
604
|
+
readFile(this.deps.actions.resolveSessionLogPath(tab.nodeId), 'utf-8')
|
|
605
|
+
.then((raw) => { tab.ledgerEntries = parseAgentLedger(raw); })
|
|
606
|
+
.catch(() => { tab.ledgerEntries = []; })
|
|
607
|
+
.finally(() => this.deps.requestRender());
|
|
608
|
+
}
|
|
609
|
+
return tab.ledgerEntries !== null ? renderFleetLedgerFallback(tab.ledgerEntries, width, height) : renderFleetTranscriptLoading(width);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private sub(): string {
|
|
613
|
+
const snapshot = this.deps.readModel.getSnapshot();
|
|
614
|
+
const failed = snapshot.rows.filter((row) => row.node.state === 'failed').length;
|
|
615
|
+
const blocked = snapshot.blockedNodeIds.length;
|
|
616
|
+
const parts = [`${snapshot.runningCount} running`];
|
|
617
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
618
|
+
if (blocked > 0) parts.push(`${blocked} waiting on you`);
|
|
619
|
+
const you = this.deps.sessionCost?.();
|
|
620
|
+
if (you !== undefined) parts.push(`you ${you === null ? 'unpriced' : formatUsd(you)}`);
|
|
621
|
+
parts.push(`fleet ${snapshot.totalCost === null ? '$0.00' : formatUsd(snapshot.totalCost)}`);
|
|
622
|
+
return parts.join(' · ');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private hints(level: 'list' | 'full' | 'picker'): KitHint[] {
|
|
626
|
+
if (this.steer) return [['⏎', 'send'], ['esc', 'cancel']];
|
|
627
|
+
if (this.deps.acts?.observedSteerActive()) return [['⏎', 'send to the session'], ['esc', 'cancel']];
|
|
628
|
+
if (level === 'picker') return [['↑↓', 'choose'], ['⏎', this.deps.acts?.pickModeActive() ? 'pick (asks first)' : 'next'], ['esc', 'back']];
|
|
629
|
+
if (this.filtering) return [['⏎', 'done'], ['esc', 'stop filtering']];
|
|
630
|
+
if (level === 'full') {
|
|
631
|
+
if (this.hostedFull) return [['s', 'say'], ['esc', 'back']];
|
|
632
|
+
return [['s', 'steer'], ['ctrl+x', 'stop (asks first)'], ['[ ]', 'other agents'], ['esc', 'back']];
|
|
633
|
+
}
|
|
634
|
+
const node = this.selectedNode();
|
|
635
|
+
const attention = node ? fleetNodeAttention(node) : null;
|
|
636
|
+
const enter = attention?.reason === 'pick' ? 'pick a winner' : attention?.reason === 'conflict' ? 'resolve conflict' : 'open full screen';
|
|
637
|
+
const hints: KitHint[] = [['⏎', enter], ['s', 'steer'], ['ctrl+x', 'stop'], ['n', 'new agent']];
|
|
638
|
+
if (node && !isTerminalProcessState(node.state) && node.capabilities.interruptible) hints.push(['i', 'interrupt']);
|
|
639
|
+
if (node && (node.state === 'paused' ? node.capabilities.resumable : node.capabilities.pausable && !isTerminalProcessState(node.state))) hints.push(['p', node.state === 'paused' ? 'resume' : 'pause']);
|
|
640
|
+
const blocked = this.deps.readModel.getSnapshot().blockedNodeIds.length;
|
|
641
|
+
if (blocked > 0) hints.push(['b', `waiting on you (${blocked})`]);
|
|
642
|
+
hints.push(['v', this.view === 'archived' ? 'live' : 'archive'], ['a', this.view === 'archived' ? 'restore' : 'archive one'], ['f', this.follow ? 'following' : 'follow']);
|
|
643
|
+
return hints;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private buildView(): AgentsModalView {
|
|
647
|
+
const level = this.level();
|
|
648
|
+
const tab = activeFleetTab(this.tabs);
|
|
649
|
+
const input = this.steer
|
|
650
|
+
? { label: this.steer.id === HOSTED_ID ? 'say: ' : `steer ${this.findNode(this.steer.id)?.label ?? ''}: `, draft: this.steer.draft }
|
|
651
|
+
: this.deps.acts?.observedSteerActive() && this.selectedNode()
|
|
652
|
+
? { label: 'steer the foreign session: ', draft: this.deps.acts.observedSteerDraftFor(this.selectedNode()!.id) ?? '' }
|
|
653
|
+
: null;
|
|
654
|
+
const pick = this.deps.acts?.pickView() ?? null;
|
|
655
|
+
const spawn = this.deps.spawn?.spawnView() ?? null;
|
|
656
|
+
const shown = this.entries().length;
|
|
657
|
+
const total = this.snapshotRows().length + (this.hostedAttached() ? 1 : 0);
|
|
658
|
+
const hostedState = this.deps.hosted?.getState();
|
|
659
|
+
return {
|
|
660
|
+
level,
|
|
661
|
+
crumbs: level === 'full' ? [this.hostedFull ? (hostedState?.record?.title || 'hosted session') : (this.findNode(tab!.nodeId)?.label ?? tab!.label)]
|
|
662
|
+
: level === 'picker' ? [pick ? 'pick a winner' : spawn?.title ?? 'host an agent']
|
|
663
|
+
: this.view === 'archived' ? ['archive'] : [],
|
|
664
|
+
sub: level === 'list' ? this.sub() : '',
|
|
665
|
+
hints: this.hints(level),
|
|
666
|
+
status: this.status,
|
|
667
|
+
input,
|
|
668
|
+
filter: { query: this.query, active: this.filtering, count: this.query ? `${shown} of ${total}` : `${total} shown` },
|
|
669
|
+
rows: level === 'list' ? this.listRows() : [],
|
|
670
|
+
detail: level === 'list' ? this.detail() : null,
|
|
671
|
+
full: level === 'full' ? {
|
|
672
|
+
meta: this.hostedFull ? 'hosted on the daemon' : [tab!.kind, this.findNode(tab!.nodeId)?.state ?? 'finished'].join(' · '),
|
|
673
|
+
header: this.hostedFull && hostedState ? hostedHeaderTexts(hostedState) : [],
|
|
674
|
+
body: this.hostedFull && hostedState ? (width, height) => hostedBody(hostedState.rows, width, height) : (width, height) => this.fullBody(width, height),
|
|
675
|
+
} : null,
|
|
676
|
+
picker: level === 'picker'
|
|
677
|
+
? pick
|
|
678
|
+
? { intro: `Best of N: ${pick.title}. Choose the winner; Enter shows its diff and asks before merging.${pick.proposal ? ` The judge proposes ${pick.proposal} (advisory).` : ''}`, options: pick.candidates, side: pick.diff ? diffSide(pick.diff) : [{ text: 'This candidate has no diff to preview.', tone: 'faint' }] }
|
|
679
|
+
: { intro: spawn?.creating ? 'Hosting the agent…' : spawn?.intro ?? '', options: spawn?.options ?? [], side: [] }
|
|
680
|
+
: null,
|
|
681
|
+
scrollOwner: this,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
render(screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
686
|
+
return renderAgentsModal(this.buildView(), screenWidth, screenHeight);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** The files a candidate diff touches, for the picker's side column. */
|
|
691
|
+
function diffSide(diff: string): AgentsText[] {
|
|
692
|
+
const files = diff.split('\n').filter((line) => line.startsWith('diff --git ')).map((line) => line.replace(/^diff --git a\/.+? b\//, ''));
|
|
693
|
+
const added = diff.split('\n').filter((line) => line.startsWith('+') && !line.startsWith('+++')).length;
|
|
694
|
+
const removed = diff.split('\n').filter((line) => line.startsWith('-') && !line.startsWith('---')).length;
|
|
695
|
+
return [
|
|
696
|
+
{ text: `${files.length} file${files.length === 1 ? '' : 's'} · +${added} −${removed}`, tone: 'text', bold: true },
|
|
697
|
+
...files.map((file) => ({ text: file, tone: 'muted' as const })),
|
|
698
|
+
];
|
|
699
|
+
}
|