@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
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-views.ts, agents and background processes opened full screen.
|
|
3
|
+
*
|
|
4
|
+
* Owns the view state (core/session-focus.ts: which session shows, its
|
|
5
|
+
* parent chain, the chips, the ctrl+x confirm), polls background process
|
|
6
|
+
* output into timestamped lines (core/process-output-log.ts), and hands the
|
|
7
|
+
* frame what changes inside a view:
|
|
8
|
+
*
|
|
9
|
+
* header the breadcrumb (main › engineer, the agent in its lane color)
|
|
10
|
+
* and that session's right side (its model; pid · uptime · port)
|
|
11
|
+
* chips one row under the header listing every session, when there is
|
|
12
|
+
* more than one
|
|
13
|
+
* body the agent's transcript as a lane graph, or the process output,
|
|
14
|
+
* with a thin bar in the session's color (dimmed) down column 0
|
|
15
|
+
* footer the composer's bar and placeholder, the status line's keys
|
|
16
|
+
* (what the next Esc does first), and main kept in sight
|
|
17
|
+
*
|
|
18
|
+
* Main keeps running underneath all of it: a view never interrupts main, and
|
|
19
|
+
* leaving one never stops what it shows. Stopping is ctrl+x twice.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
23
|
+
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
24
|
+
import type { AgentManager, ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
|
|
25
|
+
import type { ProcessNode, SteerResult } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
26
|
+
import type { ConversationManager } from '../core/conversation.ts';
|
|
27
|
+
import { SessionFocus, sameTarget, type SessionMark, type SessionTarget } from '../core/session-focus.ts';
|
|
28
|
+
import { ProcessOutputLog } from '../core/process-output-log.ts';
|
|
29
|
+
import { renderAgentView } from '../core/agent-view-render.ts';
|
|
30
|
+
import { renderHeaderLine } from '../renderer/header-line.ts';
|
|
31
|
+
import { renderSessionChips, type SessionChip } from '../renderer/session-chips.ts';
|
|
32
|
+
import { processMatches, processMaxScroll, processRows, renderProcessView, type ProcessRow } from '../renderer/process-view.ts';
|
|
33
|
+
import { laneColor } from '../renderer/lane-graph/paint.ts';
|
|
34
|
+
import type { ShellFooterView } from '../renderer/shell-surface.ts';
|
|
35
|
+
import type { SessionViewControls } from '../input/handler-session-view-route.ts';
|
|
36
|
+
import { activeTokens } from '../renderer/theme.ts';
|
|
37
|
+
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
38
|
+
import { interpolateColor } from '../utils/terminal-width.ts';
|
|
39
|
+
import { copyToClipboard } from '../utils/clipboard.ts';
|
|
40
|
+
|
|
41
|
+
export interface SessionViewsDeps {
|
|
42
|
+
readonly conversation: Pick<ConversationManager, 'laneColorOf' | 'getWorkTreeSources' | 'getTreeGlyphSet'>;
|
|
43
|
+
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'getConversationSnapshot'>;
|
|
44
|
+
readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
|
|
45
|
+
readonly fleetNodes: () => readonly ProcessNode[];
|
|
46
|
+
readonly steer: (agentId: string, text: string) => SteerResult;
|
|
47
|
+
/** Stop an agent and the agents it started. */
|
|
48
|
+
readonly killAgent: (agentId: string) => readonly string[];
|
|
49
|
+
readonly mainBusy: () => boolean;
|
|
50
|
+
/** The model main is serving (an agent with no model of its own runs on it). */
|
|
51
|
+
readonly mainModel: () => string;
|
|
52
|
+
/** The composer's text right now (the view's Esc hint depends on it). */
|
|
53
|
+
readonly promptText: () => string;
|
|
54
|
+
readonly requestRender: () => void;
|
|
55
|
+
readonly now?: () => number;
|
|
56
|
+
/** Process output poll interval (ms); 0 disables the timer (tests poll by hand). */
|
|
57
|
+
readonly pollMs?: number;
|
|
58
|
+
readonly copy?: (text: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What the frame draws while a view is open. */
|
|
62
|
+
export interface SessionViewFrame {
|
|
63
|
+
readonly header: Line;
|
|
64
|
+
readonly body: (height: number) => Line[];
|
|
65
|
+
readonly footer: ShellFooterView;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const NOTICE_MS = 4000;
|
|
69
|
+
const STEER_FORGET_MS = 10 * 60_000;
|
|
70
|
+
// The input area says only what the input area is; the keys are the status line's.
|
|
71
|
+
const NO_STDIN_REASON = 'No input here: GoodVibes cannot write to this process\'s stdin';
|
|
72
|
+
|
|
73
|
+
type AgentRecord = NonNullable<ReturnType<AgentManager['getStatus']>>;
|
|
74
|
+
|
|
75
|
+
function agentMark(status: AgentRecord['status']): SessionMark {
|
|
76
|
+
switch (status) {
|
|
77
|
+
case 'running': return 'run';
|
|
78
|
+
case 'pending': return 'wait';
|
|
79
|
+
case 'completed': return 'ok';
|
|
80
|
+
case 'failed': return 'err';
|
|
81
|
+
case 'cancelled': return 'cancel';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A process's mark from whether it ended (never parsed from the status text: a timeout or a kill does not read "done"). */
|
|
86
|
+
function processMark(p: { readonly done: boolean; readonly status: string }): SessionMark {
|
|
87
|
+
if (!p.done) return 'run';
|
|
88
|
+
return /^done \(exit 0\)$/.test(p.status) ? 'ok' : 'err';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shortCommand(cmd: string): string {
|
|
92
|
+
const one = cmd.replace(/\s+/g, ' ').trim();
|
|
93
|
+
return one.length > 28 ? `${one.slice(0, 27)}…` : one;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class SessionViews implements SessionViewControls {
|
|
97
|
+
readonly focus: SessionFocus;
|
|
98
|
+
readonly log = new ProcessOutputLog();
|
|
99
|
+
private scrollBy = new Map<string, number>();
|
|
100
|
+
/** The most each view could scroll at its last frame. */
|
|
101
|
+
private maxScrollBy = new Map<string, number>();
|
|
102
|
+
private readonly collapse = new Map<string, Map<string, boolean>>();
|
|
103
|
+
private readonly steers: Array<{ readonly agentId: string; readonly text: string; readonly at: number }> = [];
|
|
104
|
+
private notice: { readonly text: string; readonly tone: 'error' | 'info'; readonly until: number } | null = null;
|
|
105
|
+
private search: { query: string; editing: boolean; current: number } | null = null;
|
|
106
|
+
private rowsCache: { key: string; rows: ProcessRow[] } | null = null;
|
|
107
|
+
private agentCache: { key: string; lines: Line[] } | null = null;
|
|
108
|
+
private lastPage = 10;
|
|
109
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
110
|
+
|
|
111
|
+
constructor(private readonly deps: SessionViewsDeps) {
|
|
112
|
+
// A WRFC chain owner is named for what it is, as its lane in main is: its
|
|
113
|
+
// template ("engineer") would read as the chain's own engineer phase.
|
|
114
|
+
const agentName = (r: AgentRecord): string => (r.wrfcRole === 'owner' ? 'WRFC chain' : r.template || 'agent');
|
|
115
|
+
this.focus = new SessionFocus({
|
|
116
|
+
liveAgents: () => deps.agentManager.list()
|
|
117
|
+
.filter((a) => a.status === 'running' || a.status === 'pending')
|
|
118
|
+
.map((a) => ({ id: a.id, name: agentName(a), mark: agentMark(a.status) })),
|
|
119
|
+
liveProcesses: () => deps.processManager.list()
|
|
120
|
+
.filter((p) => !p.done)
|
|
121
|
+
.map((p) => ({ id: p.id, name: shortCommand(p.cmd), mark: 'run' as const })),
|
|
122
|
+
agent: (id) => {
|
|
123
|
+
const r = deps.agentManager.getStatus(id);
|
|
124
|
+
return r ? { name: agentName(r), mark: agentMark(r.status) } : null;
|
|
125
|
+
},
|
|
126
|
+
process: (id) => {
|
|
127
|
+
const p = deps.processManager.list().find((e) => e.id === id);
|
|
128
|
+
return p ? { name: shortCommand(p.cmd), mark: processMark(p) } : null;
|
|
129
|
+
},
|
|
130
|
+
parentAgent: (id) => {
|
|
131
|
+
const parent = deps.fleetNodes().find((n) => n.id === id)?.parentId;
|
|
132
|
+
return parent && deps.agentManager.getStatus(parent) ? parent : null;
|
|
133
|
+
},
|
|
134
|
+
mainBusy: deps.mainBusy,
|
|
135
|
+
now: deps.now,
|
|
136
|
+
});
|
|
137
|
+
this.focus.onChange(() => { this.search = null; this.agentCache = null; });
|
|
138
|
+
const pollMs = deps.pollMs ?? 500;
|
|
139
|
+
if (pollMs > 0) {
|
|
140
|
+
this.timer = setInterval(() => {
|
|
141
|
+
const ended = this.noteEnded();
|
|
142
|
+
// New output repaints the process view; a process ending repaints
|
|
143
|
+
// wherever you are, since the chips and the status line count it.
|
|
144
|
+
if ((this.poll() && this.kind === 'process') || ended) deps.requestRender();
|
|
145
|
+
}, pollMs);
|
|
146
|
+
(this.timer as { unref?: () => void }).unref?.();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dispose(): void {
|
|
151
|
+
if (this.timer) clearInterval(this.timer);
|
|
152
|
+
this.timer = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private now(): number { return this.deps.now?.() ?? Date.now(); }
|
|
156
|
+
|
|
157
|
+
private readonly endedSeen = new Set<string>();
|
|
158
|
+
|
|
159
|
+
/** True when a process ended since the last call (by any means: exit, timeout, kill). */
|
|
160
|
+
noteEnded(): boolean {
|
|
161
|
+
let ended = false;
|
|
162
|
+
const known = new Set<string>();
|
|
163
|
+
for (const p of this.deps.processManager.list()) {
|
|
164
|
+
known.add(p.id);
|
|
165
|
+
if (p.done && !this.endedSeen.has(p.id)) { this.endedSeen.add(p.id); ended = true; }
|
|
166
|
+
}
|
|
167
|
+
for (const id of this.endedSeen) if (!known.has(id)) this.endedSeen.delete(id);
|
|
168
|
+
return ended;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Read new process output; true when anything arrived. */
|
|
172
|
+
poll(): boolean {
|
|
173
|
+
const records = new Map<string, { stdout: readonly string[]; stderr: readonly string[]; done: boolean }>();
|
|
174
|
+
for (const p of this.deps.processManager.list()) {
|
|
175
|
+
const rec = this.deps.processManager.getStatus(p.id);
|
|
176
|
+
if (rec) records.set(p.id, rec);
|
|
177
|
+
}
|
|
178
|
+
return this.log.poll(records, this.now());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── SessionViewControls ────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
get kind(): 'main' | 'agent' | 'process' { return this.focus.current.kind; }
|
|
184
|
+
get active(): boolean { return this.focus.active; }
|
|
185
|
+
get searchEditing(): boolean { return this.search?.editing === true; }
|
|
186
|
+
|
|
187
|
+
open(target: SessionTarget): boolean {
|
|
188
|
+
const ok = this.focus.open(target);
|
|
189
|
+
if (ok) this.deps.requestRender();
|
|
190
|
+
return ok;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
chipsVisible(): boolean { return this.focus.chipsVisible(); }
|
|
194
|
+
cycle(delta: 1 | -1): void { this.focus.cycle(delta); }
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Esc inside a view, one layer per press: close the process search, then
|
|
198
|
+
* (scrolled back) return to the live bottom, then go up one level. Nothing
|
|
199
|
+
* here ever stops the agent, the process or main.
|
|
200
|
+
*/
|
|
201
|
+
escape(): void {
|
|
202
|
+
if (this.search) { this.search = null; return; }
|
|
203
|
+
if (this.scrolledBack()) { this.follow(); return; }
|
|
204
|
+
this.focus.back();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
pressStop(): void {
|
|
208
|
+
const target = this.focus.current;
|
|
209
|
+
if (target.kind === 'main') return;
|
|
210
|
+
const name = this.focus.nameOf(target);
|
|
211
|
+
if (!this.stoppable(target)) { this.say(`${name} is not running: there is nothing to stop.`, 'info'); return; }
|
|
212
|
+
const press = this.focus.pressStop();
|
|
213
|
+
if (press === 'armed') return; // the status line asks for the confirming press
|
|
214
|
+
if (target.kind === 'agent') {
|
|
215
|
+
const stopped = this.deps.killAgent(target.id);
|
|
216
|
+
this.say(stopped.length > 0 ? `Stopped ${name}${stopped.length > 1 ? ` and ${stopped.length - 1} agent${stopped.length === 2 ? '' : 's'} it started` : ''}.` : `${name} could not be stopped.`, stopped.length > 0 ? 'info' : 'error');
|
|
217
|
+
} else {
|
|
218
|
+
this.say(this.deps.processManager.stop(target.id) ? `Stopped ${name}.` : `${name} could not be stopped.`, 'info');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
scroll(rows: number): void {
|
|
223
|
+
const key = this.scrollKey();
|
|
224
|
+
// Clamped to what the last frame could show, so a scroll past the top
|
|
225
|
+
// never leaves the view "scrolled back" with nothing further up.
|
|
226
|
+
const max = this.maxScrollBy.get(key) ?? Number.POSITIVE_INFINITY;
|
|
227
|
+
this.scrollBy.set(key, Math.max(0, Math.min(max, (this.scrollBy.get(key) ?? 0) + rows)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The next Esc returns the scrolled-back view to its live bottom (no search open, an empty composer). */
|
|
231
|
+
escGoesToBottom(): boolean {
|
|
232
|
+
return this.scrolledBack() && !this.search && this.deps.promptText().length === 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The open view is scrolled away from its live bottom (the back-to-bottom pill shows). */
|
|
236
|
+
scrolledBack(): boolean {
|
|
237
|
+
return this.active && (this.scrollBy.get(this.scrollKey()) ?? 0) > 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pageRows(): number { return Math.max(1, this.lastPage - 2); }
|
|
241
|
+
follow(): void { this.scrollBy.set(this.scrollKey(), 0); }
|
|
242
|
+
|
|
243
|
+
steer(text: string): boolean {
|
|
244
|
+
const target = this.focus.current;
|
|
245
|
+
if (target.kind !== 'agent') return false;
|
|
246
|
+
const name = this.focus.nameOf(target);
|
|
247
|
+
const result = this.deps.steer(target.id, text);
|
|
248
|
+
if (!result.queued) {
|
|
249
|
+
this.say(`${name} did not take the message: ${result.reason ?? 'refused'}.`, 'error');
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
this.steers.push({ agentId: target.id, text, at: this.now() });
|
|
253
|
+
this.follow();
|
|
254
|
+
this.say(`Sent to ${name}: it arrives at its next turn. Main keeps running.`, 'info');
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
startSearch(): void { this.search = { query: this.search?.query ?? '', editing: true, current: -1 }; }
|
|
259
|
+
searchType(text: string): void { if (this.search) { this.search.query += text; this.search.current = -1; this.searchNext(); } }
|
|
260
|
+
searchBackspace(): void { if (this.search) { this.search.query = this.search.query.slice(0, -1); this.search.current = -1; } }
|
|
261
|
+
|
|
262
|
+
searchNext(): void {
|
|
263
|
+
const target = this.focus.current;
|
|
264
|
+
if (!this.search || !this.search.query || target.kind !== 'process') return;
|
|
265
|
+
const matches = processMatches(this.log.lines(target.id), this.search.query);
|
|
266
|
+
if (matches.length === 0) { this.search.current = -1; return; }
|
|
267
|
+
// Newest match first, then each older one, wrapping back to the newest.
|
|
268
|
+
const at = matches.indexOf(this.search.current);
|
|
269
|
+
this.search.current = at > 0 ? matches[at - 1]! : matches[matches.length - 1]!;
|
|
270
|
+
// Bring the match into view, near the middle of the screen.
|
|
271
|
+
const rows = this.rows(target.id, this.lastWidth);
|
|
272
|
+
const rowIdx = rows.findIndex((r) => r.source === this.search!.current);
|
|
273
|
+
if (rowIdx >= 0) this.scrollBy.set(this.scrollKey(), Math.max(0, rows.length - rowIdx - Math.floor(this.lastPage / 2)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
copyOutput(): void {
|
|
277
|
+
const target = this.focus.current;
|
|
278
|
+
if (target.kind !== 'process') return;
|
|
279
|
+
const text = this.log.lines(target.id).map((l) => l.text).join('\n');
|
|
280
|
+
(this.deps.copy ?? copyToClipboard)(text);
|
|
281
|
+
this.say(`Copied ${this.log.lines(target.id).length} lines of output.`, 'info');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
restart(): void {
|
|
285
|
+
this.say('Restart is not available: the process manager does not keep what it needs to start this process again.', 'info');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
refuseText(): void {
|
|
289
|
+
this.say('No input here: this process\'s stdin is not reachable. / searches, y copies, esc goes back.', 'info');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Frame ──────────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
private lastWidth = 80;
|
|
295
|
+
|
|
296
|
+
/** Rows the header takes: the header row, plus the chips row when it shows. */
|
|
297
|
+
headerRows(): number { return this.chipsVisible() ? 2 : 1; }
|
|
298
|
+
|
|
299
|
+
/** The chips row, or null when there is only main. */
|
|
300
|
+
chips(width: number): Line | null {
|
|
301
|
+
const sessions = this.focus.sessions();
|
|
302
|
+
if (sessions.length < 2) return null;
|
|
303
|
+
const t = activeTokens();
|
|
304
|
+
const chips: SessionChip[] = sessions.map((s) => {
|
|
305
|
+
const color = this.colorOf(s.target);
|
|
306
|
+
const markFg = s.mark === 'ok' ? t.success : s.mark === 'err' ? t.error : s.mark === 'cancel' || (s.mark === 'idle') ? t.textFaint : s.mark === 'wait' ? t.warning : color;
|
|
307
|
+
const glyph = s.target.kind === 'process' && s.mark === 'run' ? '▶'
|
|
308
|
+
: s.mark === 'run' ? '◐' : s.mark === 'idle' ? '●' : s.mark === 'ok' ? '✓' : s.mark === 'err' ? '✕' : s.mark === 'cancel' ? '○' : '◌';
|
|
309
|
+
return { mark: glyph, markFg, name: s.name, color, current: sameTarget(s.target, this.focus.current) };
|
|
310
|
+
});
|
|
311
|
+
return renderSessionChips(width, chips);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The session's own color: brand for main, the lane color for an agent, success for a process. */
|
|
315
|
+
colorOf(target: SessionTarget): string {
|
|
316
|
+
const t = activeTokens();
|
|
317
|
+
if (target.kind === 'main') return t.brand;
|
|
318
|
+
if (target.kind === 'process') return t.success;
|
|
319
|
+
return laneColor(this.colorIndexOf(target.id));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private colorIndexOf(agentId: string): number {
|
|
323
|
+
return this.deps.conversation.laneColorOf(agentId) ?? 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** What the frame draws inside a view, or null in main. */
|
|
327
|
+
frame(width: number, version?: string): SessionViewFrame | null {
|
|
328
|
+
this.lastWidth = width;
|
|
329
|
+
const target = this.focus.current;
|
|
330
|
+
if (target.kind === 'agent') {
|
|
331
|
+
const record = this.deps.agentManager.getStatus(target.id);
|
|
332
|
+
if (!record) { this.focus.back(); return null; }
|
|
333
|
+
return this.agentFrame(width, record, version);
|
|
334
|
+
}
|
|
335
|
+
if (target.kind === 'process') {
|
|
336
|
+
const rec = this.deps.processManager.getStatus(target.id);
|
|
337
|
+
if (!rec) { this.focus.back(); return null; }
|
|
338
|
+
return this.processFrame(width, target.id, rec, version);
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private crumbs(): Array<{ text: string; fg: string; bold?: boolean }> {
|
|
344
|
+
const t = activeTokens();
|
|
345
|
+
const path = this.focus.breadcrumb();
|
|
346
|
+
return path.map((p, i) => i === path.length - 1
|
|
347
|
+
? { text: this.focus.nameOf(p), fg: this.colorOf(p), bold: true }
|
|
348
|
+
: { text: this.focus.nameOf(p), fg: t.textMuted });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The Esc key's action as the status line states it. */
|
|
352
|
+
private escHint(): readonly [string, string] {
|
|
353
|
+
if (this.search) return ['esc', 'close search'];
|
|
354
|
+
if (this.deps.promptText().length > 0) return ['esc', 'clear input'];
|
|
355
|
+
if (this.scrolledBack()) return ['esc', 'back to bottom'];
|
|
356
|
+
return ['esc', `back to ${this.focus.nameOf(this.focus.parentOf(this.focus.current))}`];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private footerNotice(name: string): ShellFooterView['notice'] {
|
|
360
|
+
if (this.focus.stopArmed()) return { text: `Press ctrl+x again to stop ${name}`, tone: 'error' };
|
|
361
|
+
if (this.notice && this.notice.until > this.now()) return { text: this.notice.text, tone: this.notice.tone };
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private mainTrail(): ShellFooterView['trail'] {
|
|
366
|
+
const t = activeTokens();
|
|
367
|
+
return this.deps.mainBusy() ? { text: '◐ main · working', fg: t.textMuted } : { text: '● main · idle', fg: t.textFaint };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private stoppable(target: SessionTarget): boolean {
|
|
371
|
+
if (target.kind === 'agent') {
|
|
372
|
+
const s = this.deps.agentManager.getStatus(target.id)?.status;
|
|
373
|
+
return s === 'running' || s === 'pending';
|
|
374
|
+
}
|
|
375
|
+
if (target.kind === 'process') return this.deps.processManager.getStatus(target.id)?.done === false;
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private queuedSteers(agentId: string, messages: readonly ConversationMessageSnapshot[], running: boolean): string[] {
|
|
380
|
+
const now = this.now();
|
|
381
|
+
const seen = (text: string): boolean => messages.some((m) => m.role === 'user' && typeof m.content === 'string' && m.content.includes(text));
|
|
382
|
+
for (let i = this.steers.length - 1; i >= 0; i--) {
|
|
383
|
+
const s = this.steers[i]!;
|
|
384
|
+
if (s.agentId !== agentId) continue;
|
|
385
|
+
if (!running || now - s.at > STEER_FORGET_MS || seen(s.text)) this.steers.splice(i, 1);
|
|
386
|
+
}
|
|
387
|
+
return this.steers.filter((s) => s.agentId === agentId).map((s) => s.text);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private agentFrame(width: number, record: AgentRecord, version?: string): SessionViewFrame {
|
|
391
|
+
const t = activeTokens();
|
|
392
|
+
const name = this.focus.nameOf(this.focus.current);
|
|
393
|
+
const colorIndex = this.colorIndexOf(record.id);
|
|
394
|
+
const color = laneColor(colorIndex);
|
|
395
|
+
const parent = this.focus.nameOf(this.focus.parentOf(this.focus.current));
|
|
396
|
+
const running = record.status === 'running' || record.status === 'pending';
|
|
397
|
+
const header = renderHeaderLine(width, '', undefined, undefined, version, undefined, {
|
|
398
|
+
crumbs: this.crumbs(),
|
|
399
|
+
right: record.model ?? this.deps.mainModel(),
|
|
400
|
+
})[0]!;
|
|
401
|
+
const keys: Array<readonly [string, string]> = [this.escHint()];
|
|
402
|
+
if (running) keys.push(['ctrl+x', 'stop agent']);
|
|
403
|
+
const cost = this.deps.fleetNodes().find((n) => n.id === record.id);
|
|
404
|
+
const footer: ShellFooterView = {
|
|
405
|
+
barColor: color,
|
|
406
|
+
placeholder: running ? `Message ${name}. This steers it; main keeps running.` : `${name} has finished; a message will not reach it.`,
|
|
407
|
+
keys,
|
|
408
|
+
trail: this.mainTrail(),
|
|
409
|
+
notice: this.footerNotice(name),
|
|
410
|
+
cost: cost && cost.costState !== 'unpriced' && typeof cost.costUsd === 'number' ? `${name} $${cost.costUsd.toFixed(2)}` : null,
|
|
411
|
+
};
|
|
412
|
+
const body = (height: number): Line[] => {
|
|
413
|
+
this.lastPage = height;
|
|
414
|
+
const info = this.deps.conversation.getWorkTreeSources().agent?.(record.id);
|
|
415
|
+
const messages = this.deps.agentManager.getConversationSnapshot(record.id);
|
|
416
|
+
const steers = this.queuedSteers(record.id, messages, running);
|
|
417
|
+
const now = this.now();
|
|
418
|
+
const frame = Math.floor(now / 150);
|
|
419
|
+
const key = [width, record.id, record.status, record.toolCallCount, messages.length, messages[messages.length - 1]?.content.length ?? 0, steers.length, running ? frame : 0, running ? Math.floor(now / 1000) : 0, colorIndex].join('|');
|
|
420
|
+
if (!this.agentCache || this.agentCache.key !== key) {
|
|
421
|
+
const collapse = this.collapse.get(record.id) ?? new Map<string, boolean>();
|
|
422
|
+
this.collapse.set(record.id, collapse);
|
|
423
|
+
const lines = renderAgentView({
|
|
424
|
+
width,
|
|
425
|
+
agent: info ?? { id: record.id, name, task: record.task, status: record.status, startedAt: record.startedAt, completedAt: record.completedAt, toolCallCount: record.toolCallCount, error: record.error, messages: [] },
|
|
426
|
+
messages,
|
|
427
|
+
colorIndex,
|
|
428
|
+
parentName: parent,
|
|
429
|
+
sources: this.deps.conversation.getWorkTreeSources(),
|
|
430
|
+
collapse,
|
|
431
|
+
glyphSet: this.deps.conversation.getTreeGlyphSet(),
|
|
432
|
+
frame,
|
|
433
|
+
now,
|
|
434
|
+
queuedSteers: steers,
|
|
435
|
+
});
|
|
436
|
+
this.agentCache = { key, lines };
|
|
437
|
+
}
|
|
438
|
+
return this.window(this.agentCache.lines, height, width, interpolateColor(color, t.background || t.backgroundBase, 0.35));
|
|
439
|
+
};
|
|
440
|
+
return { header, body, footer };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private rows(id: string, width: number): ProcessRow[] {
|
|
444
|
+
const lines = this.log.lines(id);
|
|
445
|
+
const key = `${id}|${width}|${lines.length}|${this.log.dropped(id)}`;
|
|
446
|
+
if (!this.rowsCache || this.rowsCache.key !== key) this.rowsCache = { key, rows: processRows(lines, width) };
|
|
447
|
+
return this.rowsCache.rows;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private processFrame(width: number, id: string, rec: NonNullable<ReturnType<ProcessManager['getStatus']>>, version?: string): SessionViewFrame {
|
|
451
|
+
const t = activeTokens();
|
|
452
|
+
const name = this.focus.nameOf(this.focus.current);
|
|
453
|
+
const color = t.success;
|
|
454
|
+
const now = this.now();
|
|
455
|
+
const port = this.log.port(id);
|
|
456
|
+
const up = rec.done ? `ran ${formatElapsed((rec.completedAt ?? now) - rec.startTime)}` : `up ${formatElapsed(now - rec.startTime)}`;
|
|
457
|
+
const header = renderHeaderLine(width, '', undefined, undefined, version, undefined, {
|
|
458
|
+
crumbs: this.crumbs(),
|
|
459
|
+
right: [`pid ${rec.pid}`, up, port !== undefined ? `:${port}` : ''].filter(Boolean).join(' · '),
|
|
460
|
+
rightFg: t.textMuted,
|
|
461
|
+
})[0]!;
|
|
462
|
+
const keys: Array<readonly [string, string]> = [this.escHint()];
|
|
463
|
+
if (!rec.done) keys.push(['ctrl+x', 'stop (asks first)']);
|
|
464
|
+
keys.push(['/', 'search'], ['y', 'copy']);
|
|
465
|
+
const promptEmpty = this.deps.promptText().length === 0;
|
|
466
|
+
const footer: ShellFooterView = {
|
|
467
|
+
barColor: color,
|
|
468
|
+
disabledReason: promptEmpty ? NO_STDIN_REASON : undefined,
|
|
469
|
+
keys,
|
|
470
|
+
trail: null,
|
|
471
|
+
notice: this.footerNotice(name),
|
|
472
|
+
noContext: true,
|
|
473
|
+
};
|
|
474
|
+
const ended = rec.done
|
|
475
|
+
? {
|
|
476
|
+
text: rec.timedOut ? `stopped at its timeout · ${up}` : rec.signal ? `ended by ${rec.signal} · ${up}` : `exited with code ${rec.exitCode ?? '?'} · ${up}`,
|
|
477
|
+
ok: rec.exitCode === 0,
|
|
478
|
+
}
|
|
479
|
+
: null;
|
|
480
|
+
const body = (height: number): Line[] => {
|
|
481
|
+
this.lastPage = height;
|
|
482
|
+
this.poll();
|
|
483
|
+
const rows = this.rows(id, width);
|
|
484
|
+
const key = this.scrollKey();
|
|
485
|
+
const inner = SessionViews.innerRows(height);
|
|
486
|
+
const max = processMaxScroll(rows.length, inner);
|
|
487
|
+
const scroll = Math.min(this.scrollBy.get(key) ?? 0, max);
|
|
488
|
+
this.scrollBy.set(key, scroll);
|
|
489
|
+
this.maxScrollBy.set(key, max);
|
|
490
|
+
const lines = renderProcessView({
|
|
491
|
+
width, height: inner, rows, scrollFromBottom: scroll, color, ended, dropped: this.log.dropped(id),
|
|
492
|
+
search: this.search ? { query: this.search.query, editing: this.search.editing, current: this.search.current } : null,
|
|
493
|
+
});
|
|
494
|
+
return this.window(lines, height, width, interpolateColor(color, t.background || t.backgroundBase, 0.35));
|
|
495
|
+
};
|
|
496
|
+
return { header, body, footer };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Rows the view's own content gets: the body less the blank row under the
|
|
501
|
+
* chips and the blank row above the composer (the concept's agent and
|
|
502
|
+
* process screens leave both empty, with no bar).
|
|
503
|
+
*/
|
|
504
|
+
private static innerRows(height: number): number {
|
|
505
|
+
return height >= 3 ? height - 2 : height;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The body: a blank row, then the bottom rows of `all` (less the scroll)
|
|
510
|
+
* with the view's bar down column 0 on every one of them, then a blank row.
|
|
511
|
+
* The bar runs the full height of the block between the two blank rows,
|
|
512
|
+
* the way the concept's agent and process screens draw it.
|
|
513
|
+
*/
|
|
514
|
+
private window(all: readonly Line[], height: number, width: number, barColor: string): Line[] {
|
|
515
|
+
const blank = (): Line => Array.from({ length: width }, () => ({ char: ' ', fg: '', bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false }));
|
|
516
|
+
const inner = SessionViews.innerRows(height);
|
|
517
|
+
const key = this.scrollKey();
|
|
518
|
+
const maxScroll = Math.max(0, all.length - inner);
|
|
519
|
+
const scroll = Math.min(this.scrollBy.get(key) ?? 0, maxScroll);
|
|
520
|
+
if (this.kind === 'agent') { this.scrollBy.set(key, scroll); this.maxScrollBy.set(key, maxScroll); }
|
|
521
|
+
const end = this.kind === 'agent' ? all.length - scroll : all.length;
|
|
522
|
+
const shown = all.slice(Math.max(0, end - inner), end).map((l) => l.slice(0, width));
|
|
523
|
+
while (shown.length < inner) shown.push(blank());
|
|
524
|
+
for (const line of shown) {
|
|
525
|
+
if (line.length > 0) line[0] = { char: '┃', fg: barColor, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
526
|
+
}
|
|
527
|
+
return inner < height ? [blank(), ...shown, blank()] : shown;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private scrollKey(): string {
|
|
531
|
+
const c = this.focus.current;
|
|
532
|
+
return c.kind === 'main' ? 'main' : `${c.kind}:${c.id}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private say(text: string, tone: 'error' | 'info'): void {
|
|
536
|
+
this.notice = { text, tone, until: this.now() + NOTICE_MS };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-report.ts, what /status prints.
|
|
3
|
+
*
|
|
4
|
+
* The main screen keeps only the composer and one status line; the rows that
|
|
5
|
+
* used to sit under the composer (token totals, context use, working
|
|
6
|
+
* directory, model and provider, tool count, notification mode, the session
|
|
7
|
+
* spine and the web surface address) are reported here in full, next to the
|
|
8
|
+
* safety states that also stay visible on the composer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
12
|
+
import { permissionModeLabel } from '../core/permission-mode.ts';
|
|
13
|
+
|
|
14
|
+
export interface StatusReportSource {
|
|
15
|
+
readonly workingDirectory: string;
|
|
16
|
+
readonly branch?: string;
|
|
17
|
+
readonly dirty?: boolean;
|
|
18
|
+
readonly model: string;
|
|
19
|
+
readonly provider?: string;
|
|
20
|
+
/** Failover marker while serving differs from the configured model. */
|
|
21
|
+
readonly modelNote?: string;
|
|
22
|
+
readonly permissionMode?: string;
|
|
23
|
+
readonly toolCount: number;
|
|
24
|
+
readonly notifyMode?: string;
|
|
25
|
+
readonly usage: { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number };
|
|
26
|
+
/** Formatted cost ("$0.246", "you $0.25 · fleet $0.47"), null when unpriced. */
|
|
27
|
+
readonly cost: string | null;
|
|
28
|
+
readonly contextTokens: number;
|
|
29
|
+
readonly contextWindow: number;
|
|
30
|
+
/** Where the window came from ('catalog: abacus', 'consensus of 4 providers', 'family default'). */
|
|
31
|
+
readonly contextWindowSource?: string;
|
|
32
|
+
/** Compaction threshold as a fraction. */
|
|
33
|
+
readonly compactFraction: number;
|
|
34
|
+
readonly sessionSpine?: 'online' | 'offline';
|
|
35
|
+
readonly webSurfaceUrl?: string;
|
|
36
|
+
readonly autoApprove: boolean;
|
|
37
|
+
readonly keepAwake: boolean;
|
|
38
|
+
/** Short microphone state, null when no microphone is open. */
|
|
39
|
+
readonly microphone: string | null;
|
|
40
|
+
readonly runningAgents: number;
|
|
41
|
+
readonly runningProcesses: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function n(value: number): string {
|
|
45
|
+
return abbreviateCount(value, { bSuffix: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatStatusReport(s: StatusReportSource): string {
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
const row = (label: string, value: string): void => { lines.push(` ${label.padEnd(12)} ${value}`); };
|
|
51
|
+
lines.push('Session');
|
|
52
|
+
row('directory', `${s.workingDirectory}${s.branch ? ` (${s.branch}${s.dirty ? ', uncommitted changes' : ''})` : ''}`);
|
|
53
|
+
row('model', `${s.model}${s.provider ? ` (${s.provider})` : ''}${s.modelNote ? ` · ${s.modelNote}` : ''}`);
|
|
54
|
+
row('mode', permissionModeLabel(s.permissionMode));
|
|
55
|
+
row('tools', String(s.toolCount));
|
|
56
|
+
if (s.notifyMode) row('notify', s.notifyMode);
|
|
57
|
+
lines.push('Tokens');
|
|
58
|
+
const total = s.usage.input + s.usage.output + s.usage.cacheRead + s.usage.cacheWrite;
|
|
59
|
+
row('input', s.usage.input > 0 ? n(s.usage.input) : '—');
|
|
60
|
+
row('output', n(s.usage.output));
|
|
61
|
+
row('cache read', n(s.usage.cacheRead));
|
|
62
|
+
row('cache write', n(s.usage.cacheWrite));
|
|
63
|
+
row('total', n(total));
|
|
64
|
+
row('cost', s.cost ?? 'n/a (model not priced)');
|
|
65
|
+
lines.push('Context');
|
|
66
|
+
if (s.contextWindow > 0) {
|
|
67
|
+
const pct = Math.round(Math.min(1, s.contextTokens / s.contextWindow) * 100);
|
|
68
|
+
row('used', `${s.contextTokens > 0 ? n(s.contextTokens) : '—'} / ${n(s.contextWindow)} (${pct}%)`);
|
|
69
|
+
if (s.contextWindowSource) row('window from', s.contextWindowSource);
|
|
70
|
+
row('compacts at', `${Math.round(s.compactFraction * 100)}%`);
|
|
71
|
+
} else {
|
|
72
|
+
row('used', `${s.contextTokens > 0 ? n(s.contextTokens) : '—'} / unknown (nothing states this model's context window)`);
|
|
73
|
+
if (s.contextWindowSource) row('window from', s.contextWindowSource);
|
|
74
|
+
}
|
|
75
|
+
lines.push('Surfaces');
|
|
76
|
+
row('spine', s.sessionSpine ?? 'local only');
|
|
77
|
+
row('web', s.webSurfaceUrl ?? 'off');
|
|
78
|
+
lines.push('Safety');
|
|
79
|
+
row('auto-approve', s.autoApprove ? 'ON: every change is approved automatically' : 'off');
|
|
80
|
+
row('sleep', s.keepAwake ? 'disabled (keep-awake is on)' : 'allowed');
|
|
81
|
+
row('microphone', s.microphone ?? 'closed');
|
|
82
|
+
lines.push('Background');
|
|
83
|
+
row('agents', String(s.runningAgents));
|
|
84
|
+
row('processes', String(s.runningProcesses));
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|