@pellux/goodvibes-tui 2.0.21 → 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 +58 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* conversation-turn-structure.ts, turns the flat message array into the
|
|
3
|
-
* structural render plan the transcript tree draws from.
|
|
4
|
-
*
|
|
5
|
-
* Three jobs, deliberately not conflated:
|
|
6
|
-
*
|
|
7
|
-
* 1. **Turn grouping.** Which assistant messages share one `● assistant`
|
|
8
|
-
* header (see computeAssistantTurns).
|
|
9
|
-
*
|
|
10
|
-
* 2. **Structural placement.** Where each row goes. A tool result is a child
|
|
11
|
-
* of the CALL that produced it, identified by callId, not a row appended
|
|
12
|
-
* wherever it happens to land in the message array. Two calls issued
|
|
13
|
-
* together run concurrently and settle in whatever order they finish; the
|
|
14
|
-
* later-finishing call's result must still render inside its own call's
|
|
15
|
-
* subtree, which is above any call issued afterwards. This is why the plan
|
|
16
|
-
* is a permutation of the message array rather than a walk of it.
|
|
17
|
-
*
|
|
18
|
-
* 3. **Connector geometry.** Each branch row's box-drawing connector and the
|
|
19
|
-
* `│` gutters of its still-open ancestors.
|
|
20
|
-
*
|
|
21
|
-
* ## Node identity
|
|
22
|
-
*
|
|
23
|
-
* Every planned row carries a stable string id, `m:<absIdx>` for a message,
|
|
24
|
-
* `c:<absIdx>:<callIndex>` for one tool call, each prefixed by its scope (see
|
|
25
|
-
* nesting below). Ids derive from append-only indices that are never
|
|
26
|
-
* renumbered, so a row inserted in the middle does not change any other row's
|
|
27
|
-
* id. The line cache keys on these ids (not on buffer position or plan order),
|
|
28
|
-
* so an insertion above a row reuses that row's cached lines.
|
|
29
|
-
*
|
|
30
|
-
* ## Nesting is a render-layer concern, not a transcript-model one
|
|
31
|
-
*
|
|
32
|
-
* `ConversationMessageSnapshot` is flat and stays flat. Deeper nesting needs
|
|
33
|
-
* no schema change: a subagent keeps its own ConversationManager, and
|
|
34
|
-
* `AgentManager.getConversationSnapshot(agentId)` already exposes it live (the
|
|
35
|
-
* same read `fleet-panel.ts` uses to draw an attached agent's transcript). So
|
|
36
|
-
* when a tool call spawned an agent, the plan SPLICES that agent's own plan in
|
|
37
|
-
* beneath the call, recursively, the identical operation to lifting a result
|
|
38
|
-
* into its call's subtree, applied to a nested snapshot. The parent
|
|
39
|
-
* transcript, the persisted format, and the message schema are untouched; only
|
|
40
|
-
* the render plan nests.
|
|
41
|
-
*
|
|
42
|
-
* Because child snapshots are read at plan time on every rebuild, a child's
|
|
43
|
-
* rows appear as they happen rather than when the child finishes, and a
|
|
44
|
-
* child's late row inserts into its own subtree.
|
|
45
|
-
*
|
|
46
|
-
* Depth is unbounded in principle and bounded in practice by MAX_NEST_DEPTH
|
|
47
|
-
* plus a per-path cycle guard, because a spawn graph that revisits an agent id
|
|
48
|
-
* would otherwise recurse forever. Both guards report themselves on the node
|
|
49
|
-
* (`truncated`) rather than silently dropping rows.
|
|
50
|
-
*
|
|
51
|
-
* ## Connector vocabulary
|
|
52
|
-
*
|
|
53
|
-
* Proper box drawing: `├` for a branch with siblings below it, `└` for the
|
|
54
|
-
* last sibling, `│` carried down the gutter for every ancestor whose subtree
|
|
55
|
-
* is still open. Connectors are a PURE FUNCTION of current structure,
|
|
56
|
-
* recomputed from scratch on every plan build, never cached, so a row that
|
|
57
|
-
* stops being last flips `└`→`├` on the next rebuild.
|
|
58
|
-
*
|
|
59
|
-
* That flip is one cell. Indentation width, content column, and row text are
|
|
60
|
-
* all independent of sibling status by construction: `indentCols` depends only
|
|
61
|
-
* on depth and width, and the connector occupies a column that exists whether
|
|
62
|
-
* it holds `├` or `└`. So a sibling arriving repaints a connector cell and
|
|
63
|
-
* moves no text. (The rejected alternative, a uniform glyph, cannot express
|
|
64
|
-
* "this subtree continues past here", which is what makes a deep tree
|
|
65
|
-
* scannable.)
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
69
|
-
|
|
70
|
-
type Message = ConversationMessageSnapshot;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Hard ceiling on nesting depth. A spawn chain deeper than this renders its
|
|
74
|
-
* ancestors and marks the deepest shown call as truncated rather than
|
|
75
|
-
* recursing without bound.
|
|
76
|
-
*/
|
|
77
|
-
export const MAX_NEST_DEPTH = 8;
|
|
78
|
-
|
|
79
|
-
/** Connector drawn at a branch row's own indent column. */
|
|
80
|
-
export type TreeConnector = '├' | '└';
|
|
81
|
-
|
|
82
|
-
export interface RenderNode {
|
|
83
|
-
/** Stable identity, survives insertions above it. Scope-prefixed. */
|
|
84
|
-
readonly id: string;
|
|
85
|
-
readonly kind: 'message' | 'toolcall';
|
|
86
|
-
/** Index of the message within ITS OWN snapshot array. */
|
|
87
|
-
readonly absIdx: number;
|
|
88
|
-
/** Position within the owning assistant message's toolCalls, 'toolcall' only. */
|
|
89
|
-
readonly callIndex?: number;
|
|
90
|
-
/** 0 = flush; >=1 = branch depth. */
|
|
91
|
-
readonly depth: number;
|
|
92
|
-
/** The resolved message this row renders (root or nested snapshot). */
|
|
93
|
-
readonly message: Message;
|
|
94
|
-
/**
|
|
95
|
-
* Scope prefix identifying which snapshot `absIdx` indexes into. Empty for
|
|
96
|
-
* the root transcript, so root collapse keys stay byte-identical to what
|
|
97
|
-
* they were before nesting existed.
|
|
98
|
-
*/
|
|
99
|
-
readonly scope: string;
|
|
100
|
-
/** Agent whose snapshot this row came from, when nested. */
|
|
101
|
-
readonly agentId?: string;
|
|
102
|
-
/** Connector for this row; undefined at depth 0. */
|
|
103
|
-
readonly connector?: TreeConnector;
|
|
104
|
-
/**
|
|
105
|
-
* Ancestor depths (1-based) whose subtree continues below this row, so the
|
|
106
|
-
* renderer draws `│` in those gutters. Excludes this row's own depth.
|
|
107
|
-
*/
|
|
108
|
-
readonly openAncestorDepths: readonly number[];
|
|
109
|
-
/** Set when recursion stopped here (depth ceiling or cycle), surfaced honestly. */
|
|
110
|
-
readonly truncated?: 'depth' | 'cycle';
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Which assistant messages share one header, and what that header must say.
|
|
115
|
-
* Keyed by the absolute index of every assistant message in the run.
|
|
116
|
-
*/
|
|
117
|
-
export interface AssistantTurnMembership {
|
|
118
|
-
readonly turnKey: string;
|
|
119
|
-
readonly headIdx: number;
|
|
120
|
-
readonly isHead: boolean;
|
|
121
|
-
readonly toolCallCount: number;
|
|
122
|
-
readonly sharedToolLabel: string | undefined;
|
|
123
|
-
readonly hasReasoning: boolean;
|
|
124
|
-
readonly memberIndexes: readonly number[];
|
|
125
|
-
readonly resultIndexes: readonly number[];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** True when an assistant message carries prose the user is meant to read. */
|
|
129
|
-
export function hasUserFacingProse(message: Message): boolean {
|
|
130
|
-
return message.role === 'assistant'
|
|
131
|
-
&& typeof message.content === 'string'
|
|
132
|
-
&& message.content.trim().length > 0;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function modelKeyOf(message: Extract<Message, { role: 'assistant' }>): string {
|
|
136
|
-
if (!message.model && !message.provider) return '';
|
|
137
|
-
return `${message.model ?? ''}|${message.provider ?? ''}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* True when `message` must start a fresh `● assistant` header.
|
|
142
|
-
*
|
|
143
|
-
* A new header starts when there is no open run (a user message intervened),
|
|
144
|
-
* when the message carries user-facing prose (prose closes the current group
|
|
145
|
-
* and whatever follows begins a fresh one), or when the model/provider
|
|
146
|
-
* changed (a mid-run model switch is real information and stays visible).
|
|
147
|
-
*
|
|
148
|
-
* A system message does NOT break a run: operational status noise interleaves
|
|
149
|
-
* freely with tool activity, and breaking on it would fragment nearly every
|
|
150
|
-
* group back into the per-message headers this change removes.
|
|
151
|
-
*/
|
|
152
|
-
function startsNewTurn(
|
|
153
|
-
message: Extract<Message, { role: 'assistant' }>,
|
|
154
|
-
run: { readonly modelKey: string } | null,
|
|
155
|
-
): boolean {
|
|
156
|
-
if (run === null) return true;
|
|
157
|
-
if (hasUserFacingProse(message)) return true;
|
|
158
|
-
const key = modelKeyOf(message);
|
|
159
|
-
return key !== '' && run.modelKey !== '' && key !== run.modelKey;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
interface OpenRun {
|
|
163
|
-
headIdx: number;
|
|
164
|
-
modelKey: string;
|
|
165
|
-
members: number[];
|
|
166
|
-
toolLabels: string[];
|
|
167
|
-
resultIndexes: number[];
|
|
168
|
-
hasReasoning: boolean;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function computeAssistantTurns(
|
|
172
|
-
messages: readonly Message[],
|
|
173
|
-
indexOffset = 0,
|
|
174
|
-
): ReadonlyMap<number, AssistantTurnMembership> {
|
|
175
|
-
const runs: OpenRun[] = [];
|
|
176
|
-
let open: OpenRun | null = null;
|
|
177
|
-
const callOwner = new Map<string, OpenRun>();
|
|
178
|
-
|
|
179
|
-
for (let i = 0; i < messages.length; i++) {
|
|
180
|
-
const message = messages[i]!;
|
|
181
|
-
const absIdx = indexOffset + i;
|
|
182
|
-
|
|
183
|
-
if (message.role === 'user') { open = null; continue; }
|
|
184
|
-
if (message.role === 'system') continue;
|
|
185
|
-
|
|
186
|
-
if (message.role === 'tool') {
|
|
187
|
-
const owner = message.callId ? callOwner.get(message.callId) : undefined;
|
|
188
|
-
if (owner) owner.resultIndexes.push(absIdx);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (startsNewTurn(message, open)) {
|
|
193
|
-
open = { headIdx: absIdx, modelKey: modelKeyOf(message), members: [], toolLabels: [], resultIndexes: [], hasReasoning: false };
|
|
194
|
-
runs.push(open);
|
|
195
|
-
}
|
|
196
|
-
open!.members.push(absIdx);
|
|
197
|
-
if (open!.modelKey === '') open!.modelKey = modelKeyOf(message);
|
|
198
|
-
if (message.reasoningContent || message.reasoningSummary) open!.hasReasoning = true;
|
|
199
|
-
for (const call of message.toolCalls ?? []) {
|
|
200
|
-
open!.toolLabels.push(call.name);
|
|
201
|
-
if (call.id !== undefined) callOwner.set(call.id, open!);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const membership = new Map<number, AssistantTurnMembership>();
|
|
206
|
-
for (const run of runs) {
|
|
207
|
-
const distinct = new Set(run.toolLabels);
|
|
208
|
-
const shared = distinct.size === 1 && run.toolLabels.length >= 2 ? run.toolLabels[0] : undefined;
|
|
209
|
-
const base = {
|
|
210
|
-
turnKey: `turn_${run.headIdx}`,
|
|
211
|
-
headIdx: run.headIdx,
|
|
212
|
-
toolCallCount: run.toolLabels.length,
|
|
213
|
-
sharedToolLabel: shared,
|
|
214
|
-
hasReasoning: run.hasReasoning,
|
|
215
|
-
memberIndexes: run.members,
|
|
216
|
-
resultIndexes: run.resultIndexes,
|
|
217
|
-
} as const;
|
|
218
|
-
for (const absIdx of run.members) membership.set(absIdx, { ...base, isHead: absIdx === run.headIdx });
|
|
219
|
-
for (const absIdx of run.resultIndexes) membership.set(absIdx, { ...base, isHead: false });
|
|
220
|
-
}
|
|
221
|
-
return membership;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** Resolve a spawned agent's live snapshot; return null when unavailable. */
|
|
225
|
-
export type AgentSnapshotResolver = (agentId: string) => readonly Message[] | null;
|
|
226
|
-
|
|
227
|
-
export interface RenderPlanOptions {
|
|
228
|
-
readonly resolveAgentSnapshot?: AgentSnapshotResolver | undefined;
|
|
229
|
-
readonly maxDepth?: number | undefined;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* The agent id a tool call spawned, when it spawned one.
|
|
234
|
-
*
|
|
235
|
-
* Read from the call's own result payload first (the spawn tool reports the id
|
|
236
|
-
* it allocated, see AgentInput.agentId in the SDK's agent schema), falling
|
|
237
|
-
* back to an explicit `agentId` argument on the call. Returns undefined for
|
|
238
|
-
* every ordinary tool call, which is the overwhelmingly common case, so this
|
|
239
|
-
* stays a cheap check.
|
|
240
|
-
*/
|
|
241
|
-
function spawnedAgentIdOf(
|
|
242
|
-
call: { readonly name: string; readonly arguments: Record<string, unknown> },
|
|
243
|
-
results: readonly Message[],
|
|
244
|
-
): string | undefined {
|
|
245
|
-
for (const result of results) {
|
|
246
|
-
if (result.role !== 'tool') continue;
|
|
247
|
-
const raw = result.content;
|
|
248
|
-
if (typeof raw !== 'string' || !raw.includes('agentId')) continue;
|
|
249
|
-
try {
|
|
250
|
-
const parsed: unknown = JSON.parse(raw);
|
|
251
|
-
if (parsed && typeof parsed === 'object') {
|
|
252
|
-
const id = (parsed as Record<string, unknown>).agentId;
|
|
253
|
-
if (typeof id === 'string' && id.length > 0) return id;
|
|
254
|
-
}
|
|
255
|
-
} catch {
|
|
256
|
-
// Non-JSON result, fall through to the argument check.
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const argId = call.arguments.agentId;
|
|
260
|
-
return typeof argId === 'string' && argId.length > 0 ? argId : undefined;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Internal tree node; flattened into RenderNode[] once sibling counts are known. */
|
|
264
|
-
interface TreeNode {
|
|
265
|
-
id: string;
|
|
266
|
-
kind: 'message' | 'toolcall';
|
|
267
|
-
absIdx: number;
|
|
268
|
-
callIndex?: number;
|
|
269
|
-
message: Message;
|
|
270
|
-
scope: string;
|
|
271
|
-
agentId?: string;
|
|
272
|
-
truncated?: 'depth' | 'cycle';
|
|
273
|
-
children: TreeNode[];
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Build the subtree for one snapshot (root transcript or a nested agent's).
|
|
278
|
-
* Returns the top-level nodes, each already carrying its own children.
|
|
279
|
-
*/
|
|
280
|
-
function buildScopeNodes(
|
|
281
|
-
messages: readonly Message[],
|
|
282
|
-
indexOffset: number,
|
|
283
|
-
scope: string,
|
|
284
|
-
agentId: string | undefined,
|
|
285
|
-
depth: number,
|
|
286
|
-
opts: RenderPlanOptions,
|
|
287
|
-
ancestorAgentIds: readonly string[],
|
|
288
|
-
): TreeNode[] {
|
|
289
|
-
const maxDepth = opts.maxDepth ?? MAX_NEST_DEPTH;
|
|
290
|
-
|
|
291
|
-
const resultsByCallId = new Map<string, number[]>();
|
|
292
|
-
for (let i = 0; i < messages.length; i++) {
|
|
293
|
-
const message = messages[i]!;
|
|
294
|
-
if (message.role !== 'tool' || !message.callId) continue;
|
|
295
|
-
const bucket = resultsByCallId.get(message.callId);
|
|
296
|
-
if (bucket) bucket.push(indexOffset + i);
|
|
297
|
-
else resultsByCallId.set(message.callId, [indexOffset + i]);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const nodes: TreeNode[] = [];
|
|
301
|
-
const placed = new Set<number>();
|
|
302
|
-
const messageAt = (absIdx: number): Message => messages[absIdx - indexOffset]!;
|
|
303
|
-
// Calls issued by later messages of the SAME run attach to the run's head
|
|
304
|
-
// node, because the head owns the only visible header, that is what makes
|
|
305
|
-
// them siblings of each other rather than only children of their own
|
|
306
|
-
// (headerless) message, and therefore what makes ├/└ resolve correctly
|
|
307
|
-
// across a merged turn.
|
|
308
|
-
const turns = computeAssistantTurns(messages, indexOffset);
|
|
309
|
-
const headNodes = new Map<string, TreeNode>();
|
|
310
|
-
|
|
311
|
-
for (let i = 0; i < messages.length; i++) {
|
|
312
|
-
const message = messages[i]!;
|
|
313
|
-
const absIdx = indexOffset + i;
|
|
314
|
-
|
|
315
|
-
if (message.role === 'tool') {
|
|
316
|
-
if (placed.has(absIdx)) continue;
|
|
317
|
-
nodes.push({ id: `${scope}m:${absIdx}`, kind: 'message', absIdx, message, scope, agentId, children: [] });
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (message.role !== 'assistant') {
|
|
322
|
-
nodes.push({ id: `${scope}m:${absIdx}`, kind: 'message', absIdx, message, scope, agentId, children: [] });
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const head: TreeNode = { id: `${scope}m:${absIdx}`, kind: 'message', absIdx, message, scope, agentId, children: [] };
|
|
327
|
-
nodes.push(head);
|
|
328
|
-
|
|
329
|
-
const turn = turns.get(absIdx);
|
|
330
|
-
if (turn?.isHead) headNodes.set(turn.turnKey, head);
|
|
331
|
-
// Non-head members hang their calls off the run's head so the whole turn's
|
|
332
|
-
// activity is one sibling list under one header.
|
|
333
|
-
const callParent = turn ? (headNodes.get(turn.turnKey) ?? head) : head;
|
|
334
|
-
|
|
335
|
-
const calls = message.toolCalls ?? [];
|
|
336
|
-
for (let k = 0; k < calls.length; k++) {
|
|
337
|
-
const call = calls[k]!;
|
|
338
|
-
const callNode: TreeNode = {
|
|
339
|
-
id: `${scope}c:${absIdx}:${k}`,
|
|
340
|
-
kind: 'toolcall',
|
|
341
|
-
absIdx,
|
|
342
|
-
callIndex: k,
|
|
343
|
-
message,
|
|
344
|
-
scope,
|
|
345
|
-
agentId,
|
|
346
|
-
children: [],
|
|
347
|
-
};
|
|
348
|
-
callParent.children.push(callNode);
|
|
349
|
-
|
|
350
|
-
const resultIdxs = call.id === undefined ? [] : (resultsByCallId.get(call.id) ?? []);
|
|
351
|
-
const resultMessages: Message[] = [];
|
|
352
|
-
for (const resultIdx of resultIdxs) {
|
|
353
|
-
if (placed.has(resultIdx)) continue;
|
|
354
|
-
placed.add(resultIdx);
|
|
355
|
-
const resultMessage = messageAt(resultIdx);
|
|
356
|
-
resultMessages.push(resultMessage);
|
|
357
|
-
callNode.children.push({
|
|
358
|
-
id: `${scope}m:${resultIdx}`,
|
|
359
|
-
kind: 'message',
|
|
360
|
-
absIdx: resultIdx,
|
|
361
|
-
message: resultMessage,
|
|
362
|
-
scope,
|
|
363
|
-
agentId,
|
|
364
|
-
children: [],
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Splice a spawned agent's own plan in beneath the call that spawned it.
|
|
369
|
-
const childAgentId = opts.resolveAgentSnapshot
|
|
370
|
-
? spawnedAgentIdOf(call, resultMessages)
|
|
371
|
-
: undefined;
|
|
372
|
-
if (!childAgentId) continue;
|
|
373
|
-
if (ancestorAgentIds.includes(childAgentId)) {
|
|
374
|
-
callNode.truncated = 'cycle';
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
// A spliced child scope's own rows start two levels below this call
|
|
378
|
-
// (head, then its calls, then their results), so the deepest row it can
|
|
379
|
-
// contribute sits at depth+4. Guard on that, not on the base, or the
|
|
380
|
-
// ceiling is overshot by exactly those two levels.
|
|
381
|
-
if (depth + 4 > maxDepth) {
|
|
382
|
-
callNode.truncated = 'depth';
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
const childMessages = opts.resolveAgentSnapshot!(childAgentId);
|
|
386
|
-
if (!childMessages || childMessages.length === 0) continue;
|
|
387
|
-
const childScope = `${scope}a:${childAgentId}/`;
|
|
388
|
-
callNode.children.push(
|
|
389
|
-
...buildScopeNodes(childMessages, 0, childScope, childAgentId, depth + 2, opts, [...ancestorAgentIds, childAgentId]),
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return nodes;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Flatten the tree depth-first, resolving each row's connector and the `│`
|
|
399
|
-
* gutters of ancestors whose subtrees are still open.
|
|
400
|
-
*
|
|
401
|
-
* `openAncestorDepths` is computed from live structure on every call, so a row
|
|
402
|
-
* that stops being last flips `└`→`├` on the next rebuild with no cached state
|
|
403
|
-
* to go stale.
|
|
404
|
-
*/
|
|
405
|
-
function flatten(
|
|
406
|
-
nodes: readonly TreeNode[],
|
|
407
|
-
depth: number,
|
|
408
|
-
openAncestorDepths: readonly number[],
|
|
409
|
-
out: RenderNode[],
|
|
410
|
-
): void {
|
|
411
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
412
|
-
const node = nodes[i]!;
|
|
413
|
-
const isLast = i === nodes.length - 1;
|
|
414
|
-
const connector: TreeConnector | undefined = depth === 0 ? undefined : (isLast ? '└' : '├');
|
|
415
|
-
|
|
416
|
-
out.push({
|
|
417
|
-
id: node.id,
|
|
418
|
-
kind: node.kind,
|
|
419
|
-
absIdx: node.absIdx,
|
|
420
|
-
callIndex: node.callIndex,
|
|
421
|
-
depth,
|
|
422
|
-
message: node.message,
|
|
423
|
-
scope: node.scope,
|
|
424
|
-
agentId: node.agentId,
|
|
425
|
-
connector,
|
|
426
|
-
openAncestorDepths,
|
|
427
|
-
truncated: node.truncated,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
if (node.children.length === 0) continue;
|
|
431
|
-
// This row's subtree continues below it only when it is NOT the last
|
|
432
|
-
// sibling; that is exactly when descendants must draw `│` in this depth's
|
|
433
|
-
// gutter to show the parent chain is still open.
|
|
434
|
-
const childOpen = depth === 0 || isLast ? openAncestorDepths : [...openAncestorDepths, depth];
|
|
435
|
-
flatten(node.children, depth + 1, childOpen, out);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Build the ordered render plan for a message slice.
|
|
441
|
-
*
|
|
442
|
-
* Every message appears exactly once, plus one node per tool call, plus the
|
|
443
|
-
* spliced-in nodes of any agent a call spawned.
|
|
444
|
-
*/
|
|
445
|
-
export function buildRenderPlan(
|
|
446
|
-
messages: readonly Message[],
|
|
447
|
-
indexOffset = 0,
|
|
448
|
-
opts: RenderPlanOptions = {},
|
|
449
|
-
): RenderNode[] {
|
|
450
|
-
const tree = buildScopeNodes(messages, indexOffset, '', undefined, 0, opts, []);
|
|
451
|
-
const plan: RenderNode[] = [];
|
|
452
|
-
flatten(tree, 0, [], plan);
|
|
453
|
-
return plan;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/** Collapse key for a planned row, scope-aware. Root scope keys are unchanged. */
|
|
457
|
-
export function collapseKeyForNode(node: RenderNode): string {
|
|
458
|
-
return `msg_${node.scope}${node.absIdx}`;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/** Collapse key for an entire spawned-agent subtree. */
|
|
462
|
-
export function agentSubtreeCollapseKey(agentId: string): string {
|
|
463
|
-
return `agent_${agentId}`;
|
|
464
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* conversation-user-receipts.ts, tracks which system-role message indices
|
|
3
|
-
* are a direct receipt for an explicit user action (e.g. answering the
|
|
4
|
-
* startup recovery modal's Resume/Keep/Remove choice), as opposed to ambient
|
|
5
|
-
* boot chatter (provider registration, the resume notice, etc.). Kept in its
|
|
6
|
-
* own file so ConversationManager (core/conversation.ts), already near its
|
|
7
|
-
* line-count gate, doesn't have to carry the bookkeeping inline.
|
|
8
|
-
*
|
|
9
|
-
* ConversationManager.rebuildHistory()'s splash-vs-transcript check consults
|
|
10
|
-
* this via ConversationManager.addUserActionReceipt(): a receipt the user is
|
|
11
|
-
* actively waiting on must displace the splash the way a user message does,
|
|
12
|
-
* while quiet startup plumbing keeps rendering underneath it. Indices mirror
|
|
13
|
-
* messageKindRegistry's own lifecycle, cleared on resetAll(), purged for
|
|
14
|
-
* indices an undo() frees for reuse, deleted when a later add overwrites the
|
|
15
|
-
* same recycled index with a non-receipt message.
|
|
16
|
-
*/
|
|
17
|
-
export class UserReceiptIndices {
|
|
18
|
-
private readonly indices = new Set<number>();
|
|
19
|
-
|
|
20
|
-
add(index: number): void {
|
|
21
|
-
this.indices.add(index);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
delete(index: number): void {
|
|
25
|
-
this.indices.delete(index);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
has(index: number): boolean {
|
|
29
|
-
return this.indices.has(index);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
clear(): void {
|
|
33
|
-
this.indices.clear();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Drop every tracked index >= minIndex, mirrors undo()'s tail-splice. */
|
|
37
|
-
purgeFrom(minIndex: number): void {
|
|
38
|
-
for (const key of this.indices) {
|
|
39
|
-
if (key >= minIndex) this.indices.delete(key);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import type { CommandRegistry } from '../command-registry.ts';
|
|
2
|
-
import type { PanelDeepLinkTarget } from '../../panels/panel-manager.ts';
|
|
3
|
-
import { requirePanelManager } from './runtime-services.ts';
|
|
4
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Parse `--target <id>[:<kind>]` out of the args following `<panel-id>
|
|
8
|
-
* [top|bottom]` (item 4). Splits the args array in place (removing the
|
|
9
|
-
* flag + its value) so positional `pane` parsing downstream is unaffected by
|
|
10
|
-
* where the flag appears. Only `open fleet` currently has a deep-link
|
|
11
|
-
* consumer (FleetPanel.receiveDeepLink), other panel ids just ignore an
|
|
12
|
-
* unused target via PanelManager's optional-chaining delivery.
|
|
13
|
-
*/
|
|
14
|
-
function extractTargetFlag(rest: string[]): PanelDeepLinkTarget | undefined {
|
|
15
|
-
const flagIdx = rest.indexOf('--target');
|
|
16
|
-
if (flagIdx < 0) return undefined;
|
|
17
|
-
const raw = rest[flagIdx + 1];
|
|
18
|
-
rest.splice(flagIdx, 2);
|
|
19
|
-
if (!raw) return undefined;
|
|
20
|
-
const sep = raw.indexOf(':');
|
|
21
|
-
return sep >= 0 ? { id: raw.slice(0, sep), kind: raw.slice(sep + 1) } : { id: raw };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function registerOperatorPanelCommand(registry: CommandRegistry): void {
|
|
25
|
-
registry.register({
|
|
26
|
-
name: 'panel',
|
|
27
|
-
aliases: ['panels'],
|
|
28
|
-
description: 'Open, place, resize, or list panels. Usage: /panel [open <id> [top|bottom]|close <id>|list|toggle|move|focus|split|width|height]',
|
|
29
|
-
usage: '[open <id> [top|bottom] [--target <id>[:<kind>]]|close <id>|list|toggle|move <top|bottom|other> [id]|focus <top|bottom|toggle>|split [show|hide|toggle]|width <left|right|reset>|height <up|down|reset>]',
|
|
30
|
-
argsHint: '<open|close|list|toggle|move|focus|split|width|height> [id]',
|
|
31
|
-
handler(args, ctx) {
|
|
32
|
-
const pm = requirePanelManager(ctx);
|
|
33
|
-
const sub = args[0]?.toLowerCase() ?? '';
|
|
34
|
-
if (!sub || sub === 'toggle') {
|
|
35
|
-
// (the purge): 'panel-list' (the browse-all-panels picker) was
|
|
36
|
-
// DELETE-disposition, a picker over a handful of panels is dead
|
|
37
|
-
// weight now (see the Ctrl+P selectionModal repoint in
|
|
38
|
-
// shell/ui-openers.ts for its interactive replacement). Bare
|
|
39
|
-
// `/panel` and `/panel toggle` now do what "toggle" actually means:
|
|
40
|
-
// toggle the workspace, which opens 'fleet' by default when nothing
|
|
41
|
-
// is open (PanelManager.toggle()) rather than force-opening a
|
|
42
|
-
// specific browse panel.
|
|
43
|
-
pm.toggle();
|
|
44
|
-
ctx.renderRequest();
|
|
45
|
-
} else if (sub === 'list') {
|
|
46
|
-
// Print a text listing instead of opening a picker panel, grouped
|
|
47
|
-
// by category from the live registry, so this can never drift from
|
|
48
|
-
// what's actually registered.
|
|
49
|
-
const byCategory = pm.getTypesByCategory();
|
|
50
|
-
const lines: string[] = [];
|
|
51
|
-
for (const [category, entries] of byCategory) {
|
|
52
|
-
lines.push(`${category}:`);
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
lines.push(` ${entry.icon} ${entry.id}: ${entry.name}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
ctx.print(lines.length > 0 ? lines.join('\n') : 'No panels registered.');
|
|
58
|
-
} else if (sub === 'open') {
|
|
59
|
-
const id = args[1];
|
|
60
|
-
const rest = args.slice(2);
|
|
61
|
-
const target = extractTargetFlag(rest);
|
|
62
|
-
const pane = rest[0]?.toLowerCase();
|
|
63
|
-
if (!id) { ctx.print('Usage: /panel open <panel-id>'); return; }
|
|
64
|
-
if (pane && pane !== 'top' && pane !== 'bottom') {
|
|
65
|
-
ctx.print('Usage: /panel open <panel-id> [top|bottom] [--target <id>[:<kind>]]');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
// A MIGRATE-TO-MODAL id (e.g. 'sessions' -> the session-picker modal)
|
|
70
|
-
// resolves to a modal, not a panel: opening it fires the redirect and
|
|
71
|
-
// no panel lands in the workspace. Report that honestly rather than
|
|
72
|
-
// claiming "Panel opened: <id>".
|
|
73
|
-
const redirectTarget = pm.getModalRedirect(id);
|
|
74
|
-
// focus rule 1a: the command path leaves focus in the composer
|
|
75
|
-
// ("mid-command-flow"), showPanel does not grab panel focus here.
|
|
76
|
-
// forward the deep-link target so the panel lands on the row.
|
|
77
|
-
if (ctx.showPanel) ctx.showPanel(id, pane as 'top' | 'bottom' | undefined, target);
|
|
78
|
-
else {
|
|
79
|
-
pm.open(id, pane as 'top' | 'bottom' | undefined, target);
|
|
80
|
-
pm.show();
|
|
81
|
-
ctx.renderRequest();
|
|
82
|
-
}
|
|
83
|
-
if (redirectTarget) {
|
|
84
|
-
ctx.print(`"${id}" moved to the ${redirectTarget} modal; opening it.`);
|
|
85
|
-
} else {
|
|
86
|
-
ctx.print(`Panel opened: ${id}${pane ? ` (${pane} pane)` : ''}`);
|
|
87
|
-
}
|
|
88
|
-
} catch (e) {
|
|
89
|
-
// A deleted/unknown panel id throws "No panel type registered with
|
|
90
|
-
// id: <id>" from PanelManager.open, surface the same friendly line
|
|
91
|
-
// the bare-/panel path uses instead of leaking the raw error. Any
|
|
92
|
-
// genuinely unexpected failure still shows its real message.
|
|
93
|
-
const message = summarizeError(e);
|
|
94
|
-
if (message.includes('No panel type registered with id')) {
|
|
95
|
-
ctx.print(`Unknown panel "${id}". Use /panel list to see available panels.`);
|
|
96
|
-
} else {
|
|
97
|
-
ctx.print(`Error: ${message}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} else if (sub === 'close') {
|
|
101
|
-
const id = args[1];
|
|
102
|
-
if (!id) { ctx.print('Usage: /panel close <panel-id>'); return; }
|
|
103
|
-
try {
|
|
104
|
-
pm.close(id);
|
|
105
|
-
ctx.focusPrompt?.();
|
|
106
|
-
ctx.renderRequest();
|
|
107
|
-
ctx.print(`Panel closed: ${id}`);
|
|
108
|
-
} catch (e) {
|
|
109
|
-
ctx.print(`Error: ${summarizeError(e)}`);
|
|
110
|
-
}
|
|
111
|
-
} else if (sub === 'move') {
|
|
112
|
-
const dest = args[1]?.toLowerCase();
|
|
113
|
-
if (dest !== 'top' && dest !== 'bottom' && dest !== 'other') {
|
|
114
|
-
ctx.print('Usage: /panel move <top|bottom|other> [panel-id]');
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const panelId = args[2];
|
|
118
|
-
try {
|
|
119
|
-
if (dest === 'other') pm.moveToOtherPane(panelId);
|
|
120
|
-
else pm.moveToPane(dest, panelId);
|
|
121
|
-
ctx.renderRequest();
|
|
122
|
-
ctx.print(`Panel moved to ${dest} pane`);
|
|
123
|
-
} catch (e) {
|
|
124
|
-
ctx.print(`Error: ${summarizeError(e)}`);
|
|
125
|
-
}
|
|
126
|
-
} else if (sub === 'focus') {
|
|
127
|
-
const pane = args[1]?.toLowerCase();
|
|
128
|
-
if (pane !== 'top' && pane !== 'bottom' && pane !== 'toggle') {
|
|
129
|
-
ctx.print('Usage: /panel focus <top|bottom|toggle>');
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (pane === 'toggle') pm.togglePaneFocus();
|
|
133
|
-
else pm.focusPane(pane);
|
|
134
|
-
ctx.renderRequest();
|
|
135
|
-
ctx.print(`Focused ${pm.getFocusedPane()} pane`);
|
|
136
|
-
} else if (sub === 'split') {
|
|
137
|
-
const mode = args[1]?.toLowerCase() ?? 'toggle';
|
|
138
|
-
if (mode !== 'toggle' && mode !== 'show' && mode !== 'hide') {
|
|
139
|
-
ctx.print('Usage: /panel split [show|hide|toggle]');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (mode === 'show' && !pm.isBottomPaneVisible()) pm.toggleBottomPane();
|
|
143
|
-
if (mode === 'hide' && pm.isBottomPaneVisible()) pm.toggleBottomPane();
|
|
144
|
-
if (mode === 'toggle') pm.toggleBottomPane();
|
|
145
|
-
ctx.renderRequest();
|
|
146
|
-
ctx.print(pm.isBottomPaneVisible() ? 'Bottom pane visible' : 'Bottom pane hidden');
|
|
147
|
-
} else if (sub === 'width') {
|
|
148
|
-
const dir = args[1]?.toLowerCase();
|
|
149
|
-
if (dir !== 'left' && dir !== 'right' && dir !== 'reset') {
|
|
150
|
-
ctx.print('Usage: /panel width <left|right|reset>');
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (dir === 'left') pm.widenLeft();
|
|
154
|
-
else if (dir === 'right') pm.widenRight();
|
|
155
|
-
else pm.setSplitRatio(0.6);
|
|
156
|
-
ctx.renderRequest();
|
|
157
|
-
ctx.print(`Panel width ratio: ${pm.getSplitRatio().toFixed(2)}`);
|
|
158
|
-
} else if (sub === 'height') {
|
|
159
|
-
const dir = args[1]?.toLowerCase();
|
|
160
|
-
if (dir !== 'up' && dir !== 'down' && dir !== 'reset') {
|
|
161
|
-
ctx.print('Usage: /panel height <up|down|reset>');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (dir === 'up') pm.setVerticalSplitRatio(pm.getVerticalSplitRatio() - 0.05);
|
|
165
|
-
else if (dir === 'down') pm.setVerticalSplitRatio(pm.getVerticalSplitRatio() + 0.05);
|
|
166
|
-
else pm.setVerticalSplitRatio(0.5);
|
|
167
|
-
ctx.renderRequest();
|
|
168
|
-
ctx.print(`Panel height ratio: ${pm.getVerticalSplitRatio().toFixed(2)}`);
|
|
169
|
-
} else {
|
|
170
|
-
const id = args[0]!;
|
|
171
|
-
try {
|
|
172
|
-
// bare `/panel <id>` is the same command path as `/panel open
|
|
173
|
-
// <id>`, composer stays focused.
|
|
174
|
-
if (ctx.showPanel) ctx.showPanel(id);
|
|
175
|
-
else {
|
|
176
|
-
pm.open(id);
|
|
177
|
-
pm.show();
|
|
178
|
-
ctx.renderRequest();
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
ctx.print(`Unknown panel "${id}". Use /panel list to see available panels.`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
}
|