@pellux/goodvibes-tui 2.0.21 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-reference.ts +2 -2
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- package/src/verification/verification-ledger.ts +0 -707
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* work-tree-render.ts, drawing one TurnModel into transcript lines.
|
|
3
|
+
*
|
|
4
|
+
* Lays the turn's lanes out (lane-graph/layout.ts), paints every row
|
|
5
|
+
* (lane-graph/paint.ts), opens bodies under their beads, and reports what the
|
|
6
|
+
* rest of the transcript needs: the block registry entries (so copy, save,
|
|
7
|
+
* bookmark, Tab and /expand work on beads), the line each message landed on,
|
|
8
|
+
* the focusable rows, and the gutter a still-streaming answer continues with.
|
|
9
|
+
*
|
|
10
|
+
* Line numbers in the result are relative to the turn's first line.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createEmptyLine, type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
14
|
+
import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
|
|
15
|
+
import { MARKDOWN_TEXT_COL, renderMarkdown, renderMarkdownTracked } from '../renderer/markdown.ts';
|
|
16
|
+
import { renderSystemMessage } from '../renderer/system-message.ts';
|
|
17
|
+
import { renderThinkingBlock } from '../renderer/thinking.ts';
|
|
18
|
+
import { renderConversationFoldedRow } from '../renderer/conversation-surface.ts';
|
|
19
|
+
import { BORDERS } from '../renderer/layout.ts';
|
|
20
|
+
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
21
|
+
import { activeTokens } from '../renderer/theme.ts';
|
|
22
|
+
import { laneGlyphs, type TreeGlyphSetName } from '../renderer/lane-graph/glyphs.ts';
|
|
23
|
+
import { layoutLaneGraph, SPINE, type GutterRow, type LaneEvent } from '../renderer/lane-graph/layout.ts';
|
|
24
|
+
import {
|
|
25
|
+
applyFocus,
|
|
26
|
+
laneColor,
|
|
27
|
+
paintBeadRow,
|
|
28
|
+
paintBody,
|
|
29
|
+
paintFoldedRow,
|
|
30
|
+
paintGutter,
|
|
31
|
+
paintHeadRow,
|
|
32
|
+
paintMergeRow,
|
|
33
|
+
paintSpawnRow,
|
|
34
|
+
shiftUnderGutter,
|
|
35
|
+
textColumn,
|
|
36
|
+
type GraphPaint,
|
|
37
|
+
} from '../renderer/lane-graph/paint.ts';
|
|
38
|
+
import { semanticSummaryFor } from '../renderer/lane-graph/semantic-memo.ts';
|
|
39
|
+
import type { BlockMeta } from './conversation-types.ts';
|
|
40
|
+
import { beadKeyOf, beadMoreKeyOf, type BeadModel, type LaneModel, type TurnModel, type TurnRow } from './work-tree-model.ts';
|
|
41
|
+
|
|
42
|
+
export interface TurnRenderOptions {
|
|
43
|
+
readonly width: number;
|
|
44
|
+
readonly glyphSet: TreeGlyphSetName;
|
|
45
|
+
/** Focused row id, when the keyboard is in the work tree. */
|
|
46
|
+
readonly focusId: string | null;
|
|
47
|
+
readonly lineNumberMode: 'all' | 'code' | 'off';
|
|
48
|
+
readonly collapseThreshold: number;
|
|
49
|
+
/** Code blocks longer than the threshold start collapsed: their default is written here. */
|
|
50
|
+
readonly collapseState: Map<string, boolean>;
|
|
51
|
+
/** Whether a system message at this index is navigable (error navigation). */
|
|
52
|
+
readonly isNavigableSystem: (messageIndex: number) => boolean;
|
|
53
|
+
/** Spinner frame for running beads. */
|
|
54
|
+
readonly frame: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TurnRender {
|
|
58
|
+
readonly lines: Line[];
|
|
59
|
+
/** Block metas with startLine relative to the turn and blockIndex relative to the turn's first block. */
|
|
60
|
+
readonly blocks: BlockMeta[];
|
|
61
|
+
readonly errorLines: number[];
|
|
62
|
+
/** absolute message index → first relative line. */
|
|
63
|
+
readonly messageLines: ReadonlyMap<number, number>;
|
|
64
|
+
readonly focusIds: ReadonlySet<string>;
|
|
65
|
+
/** The gutter under the turn's last row and the turn's text column, for a streaming continuation. */
|
|
66
|
+
readonly tail: { readonly gutter: GutterRow; readonly gutterWidth: number };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Focus ids: beads by bead id, lanes and turns by their collapse key. */
|
|
70
|
+
export function laneFocusId(lane: LaneModel): string {
|
|
71
|
+
return lane.key;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function eventFor(row: TurnRow, folded: boolean): LaneEvent {
|
|
75
|
+
switch (row.kind) {
|
|
76
|
+
case 'head': return folded ? { kind: 'turn' } : { kind: 'head' };
|
|
77
|
+
case 'prose': return { kind: 'row' };
|
|
78
|
+
case 'bead': return { kind: 'bead', lane: row.bead.lane };
|
|
79
|
+
case 'spawn': return { kind: 'spawn', parent: row.lane.parent, lane: row.lane.id };
|
|
80
|
+
case 'merge': return { kind: 'merge', lane: row.lane.id };
|
|
81
|
+
case 'folded': return { kind: 'folded', lane: row.lane.parent, child: row.lane.id };
|
|
82
|
+
case 'answer': return { kind: 'answer' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Prose at the text column: markdown rendered narrower and shifted, code blocks registered. */
|
|
87
|
+
function proseLines(
|
|
88
|
+
content: string,
|
|
89
|
+
messageIndex: number,
|
|
90
|
+
p: GraphPaint,
|
|
91
|
+
options: TurnRenderOptions,
|
|
92
|
+
blocks: BlockMeta[],
|
|
93
|
+
baseLine: number,
|
|
94
|
+
): Line[] {
|
|
95
|
+
const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
|
|
96
|
+
const width = Math.max(20, p.width - shift);
|
|
97
|
+
const numbered = options.lineNumberMode === 'all';
|
|
98
|
+
const numWidth = numbered ? Math.max(3, String(content.split('\n').length).length) : 0;
|
|
99
|
+
const gutter = numbered ? numWidth + 3 : 0;
|
|
100
|
+
const { lines, codeBlocks } = renderMarkdownTracked(content, width - gutter, { codeBlockLineNumbers: options.lineNumberMode === 'code' });
|
|
101
|
+
for (const cb of codeBlocks) {
|
|
102
|
+
const collapseKey = `code_${messageIndex}_${cb.startOffset}`;
|
|
103
|
+
if (cb.rawContent.split('\n').length > options.collapseThreshold && !options.collapseState.has(collapseKey)) {
|
|
104
|
+
options.collapseState.set(collapseKey, true);
|
|
105
|
+
}
|
|
106
|
+
blocks.push({ blockIndex: blocks.length, collapseKey, type: 'code', startLine: baseLine + cb.startOffset, lineCount: cb.lineCount, rawContent: cb.rawContent });
|
|
107
|
+
}
|
|
108
|
+
if (!numbered) return lines;
|
|
109
|
+
return lines.map((line, i) => {
|
|
110
|
+
const out = createEmptyLine(width);
|
|
111
|
+
const label = UIFactory.stringToLine(`${String(i + 1).padStart(numWidth)} │ `, gutter, { fg: activeTokens().textFaint });
|
|
112
|
+
for (let c = 0; c < gutter; c++) out[MARKDOWN_TEXT_COL + c] = label[c] ?? out[MARKDOWN_TEXT_COL + c]!;
|
|
113
|
+
for (let c = MARKDOWN_TEXT_COL; c < line.length && c + gutter < width; c++) out[c + gutter] = line[c]!;
|
|
114
|
+
return out;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function systemLines(content: string, p: GraphPaint): Line[] {
|
|
119
|
+
const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
|
|
120
|
+
return renderSystemMessage(content, Math.max(20, p.width - shift));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The process id a backgrounded call reported (`process_id` or `processId`, top level or on its first command). */
|
|
124
|
+
export function backgroundProcessId(result: string | undefined): string | undefined {
|
|
125
|
+
if (!result || !/process_?[iI]d/.test(result)) return undefined;
|
|
126
|
+
try {
|
|
127
|
+
const parsed: unknown = JSON.parse(result);
|
|
128
|
+
const find = (value: unknown, depth: number): string | undefined => {
|
|
129
|
+
if (depth > 3 || value === null || typeof value !== 'object') return undefined;
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
for (const item of value) { const id = find(item, depth + 1); if (id) return id; }
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const rec = value as Record<string, unknown>;
|
|
135
|
+
for (const key of ['process_id', 'processId'] as const) if (typeof rec[key] === 'string' && (rec[key] as string).length > 0) return rec[key] as string;
|
|
136
|
+
for (const child of Object.values(rec)) { const id = find(child, depth + 1); if (id) return id; }
|
|
137
|
+
return undefined;
|
|
138
|
+
};
|
|
139
|
+
return find(parsed, 0);
|
|
140
|
+
} catch {
|
|
141
|
+
return /"process_?[iI]d"\s*:\s*"([^"]+)"/.exec(result)?.[1];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function beadMeta(bead: BeadModel, startLine: number, lineCount: number, capped: boolean, laneKey: string | undefined): BlockMeta {
|
|
146
|
+
const isDiff = bead.body?.kind === 'diff';
|
|
147
|
+
const raw = bead.result ?? JSON.stringify(bead.call.arguments, null, 2);
|
|
148
|
+
let meta: BlockMeta = {
|
|
149
|
+
blockIndex: 0,
|
|
150
|
+
collapseKey: beadKeyOf(bead.id),
|
|
151
|
+
type: isDiff ? 'diff' : 'tool',
|
|
152
|
+
startLine,
|
|
153
|
+
lineCount,
|
|
154
|
+
rawContent: isDiff && bead.body?.kind === 'diff' ? bead.body.diff : raw,
|
|
155
|
+
toolName: bead.call.name,
|
|
156
|
+
workTree: {
|
|
157
|
+
kind: 'bead', id: bead.id, hasBody: bead.body !== null, open: bead.open, capped, moreKey: beadMoreKeyOf(bead.id), laneKey,
|
|
158
|
+
processId: bead.status === 'bg' ? backgroundProcessId(bead.result) : undefined,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
if (bead.body?.kind === 'diff' && bead.body.numbered) {
|
|
162
|
+
const parsed = parseDiffForApply(bead.body.diff);
|
|
163
|
+
if (parsed.filePath) meta = { ...meta, ...parsed };
|
|
164
|
+
}
|
|
165
|
+
return meta;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function renderTurn(model: TurnModel, options: TurnRenderOptions): TurnRender {
|
|
169
|
+
const glyphs = laneGlyphs(options.glyphSet);
|
|
170
|
+
const folded = model.folded;
|
|
171
|
+
// A turn with nothing for its header to say and no calls or agents is plain
|
|
172
|
+
// prose: no spine, no header row, the text at the text column.
|
|
173
|
+
const bare = model.headerText === '' && model.toolCount === 0 && model.agentCount === 0;
|
|
174
|
+
// A folded turn is its header plus its answer; nothing else draws.
|
|
175
|
+
const rows = bare ? model.rows.filter((r) => r.kind !== 'head') : folded ? model.rows.filter((r) => r.kind === 'head' || r.kind === 'answer') : model.rows;
|
|
176
|
+
const layout = layoutLaneGraph(rows.map((r) => ((folded || bare) && (r.kind === 'answer' || r.kind === 'prose') ? { kind: 'row' } as const : eventFor(r, folded))));
|
|
177
|
+
const p: GraphPaint = { width: options.width, gutterWidth: layout.gutterWidth, glyphs, frame: options.frame };
|
|
178
|
+
|
|
179
|
+
const lines: Line[] = [];
|
|
180
|
+
const blocks: BlockMeta[] = [];
|
|
181
|
+
const errorLines: number[] = [];
|
|
182
|
+
const messageLines = new Map<number, number>();
|
|
183
|
+
const focusIds = new Set<string>();
|
|
184
|
+
const laneOfBead = new Map<string, string>();
|
|
185
|
+
const noteMessage = (index: number | undefined, line: number): void => {
|
|
186
|
+
if (index !== undefined && index >= 0 && !messageLines.has(index)) messageLines.set(index, line);
|
|
187
|
+
};
|
|
188
|
+
const focusIf = (line: Line, id: string): void => {
|
|
189
|
+
focusIds.add(id);
|
|
190
|
+
if (options.focusId === id) applyFocus(line, options.width, glyphs);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const resultMembers: Array<{ index: number; key: string }> = [];
|
|
194
|
+
for (const row of model.rows) {
|
|
195
|
+
const bead = row.kind === 'bead' || row.kind === 'spawn' || row.kind === 'folded' ? row.bead : undefined;
|
|
196
|
+
if (bead?.resultIndex !== undefined) resultMembers.push({ index: bead.resultIndex, key: beadKeyOf(bead.id) });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
rows.forEach((row, i) => {
|
|
200
|
+
const gutter = layout.rows[i]!;
|
|
201
|
+
const cont = layout.cont[i]!;
|
|
202
|
+
// Two prose blocks in a row keep a blank row between them, lanes running past it.
|
|
203
|
+
if ((row.kind === 'prose' || row.kind === 'answer') && rows[i - 1]?.kind === 'prose') {
|
|
204
|
+
const blank = createEmptyLine(options.width);
|
|
205
|
+
if (!folded && !bare) paintGutter(blank, layout.cont[i - 1]!, p);
|
|
206
|
+
lines.push(blank);
|
|
207
|
+
}
|
|
208
|
+
const start = lines.length;
|
|
209
|
+
switch (row.kind) {
|
|
210
|
+
case 'head': {
|
|
211
|
+
const line = paintHeadRow(gutter, model.headerText, folded, p);
|
|
212
|
+
focusIf(line, model.turnKey);
|
|
213
|
+
lines.push(line);
|
|
214
|
+
noteMessage(model.headIndex, start);
|
|
215
|
+
blocks.push({
|
|
216
|
+
blockIndex: 0,
|
|
217
|
+
collapseKey: model.turnKey,
|
|
218
|
+
type: 'assistant_turn',
|
|
219
|
+
startLine: start,
|
|
220
|
+
lineCount: 1,
|
|
221
|
+
rawContent: `assistant turn: ${model.headerText}`,
|
|
222
|
+
// Result messages behind closed beads, paired with the bead keys that reveal them.
|
|
223
|
+
groupMemberIndexes: resultMembers.map((m) => m.index),
|
|
224
|
+
groupMemberKeys: resultMembers.map((m) => m.key),
|
|
225
|
+
workTree: { kind: 'turn', id: model.turnKey, hasBody: true, open: !folded, capped: false },
|
|
226
|
+
});
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'prose': {
|
|
230
|
+
if (row.role === 'thinking' || row.role === 'summary') {
|
|
231
|
+
const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
|
|
232
|
+
const width = Math.max(20, p.width - shift);
|
|
233
|
+
let body: Line[];
|
|
234
|
+
if (row.role === 'thinking') {
|
|
235
|
+
// Collapsed by default: one row naming the size; the text stays behind the toggle.
|
|
236
|
+
const collapseKey = `msg_${row.messageIndex}_thinking`;
|
|
237
|
+
if (!options.collapseState.has(collapseKey)) options.collapseState.set(collapseKey, true);
|
|
238
|
+
const collapsed = options.collapseState.get(collapseKey) === true;
|
|
239
|
+
const count = row.content.split('\n').length;
|
|
240
|
+
body = collapsed
|
|
241
|
+
? [renderConversationFoldedRow(width, {
|
|
242
|
+
marker: BORDERS.THINKING.char,
|
|
243
|
+
markerFg: activeTokens().reasoning,
|
|
244
|
+
label: 'thinking',
|
|
245
|
+
labelFg: activeTokens().reasoning,
|
|
246
|
+
detailFg: activeTokens().textMuted,
|
|
247
|
+
}, [{ text: ` ${glyphs.closed} ${count} line${count === 1 ? '' : 's'} `, fg: activeTokens().textFaint }], '')]
|
|
248
|
+
: renderThinkingBlock(row.content, width);
|
|
249
|
+
blocks.push({ blockIndex: 0, collapseKey, type: 'thinking', startLine: start, lineCount: body.length, rawContent: row.content });
|
|
250
|
+
} else {
|
|
251
|
+
body = renderThinkingBlock(row.content, width);
|
|
252
|
+
}
|
|
253
|
+
for (const source of body) lines.push(shiftUnderGutter(source, shift, cont, p));
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
const body = row.role === 'assistant' ? proseLines(row.content, row.messageIndex, p, options, blocks, start) : systemLines(row.content, p);
|
|
257
|
+
const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
|
|
258
|
+
for (const source of body) lines.push(shiftUnderGutter(source, shift, cont, p));
|
|
259
|
+
if (row.role === 'system' && options.isNavigableSystem(row.messageIndex)) errorLines.push(start);
|
|
260
|
+
noteMessage(row.messageIndex, start);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'answer': {
|
|
264
|
+
const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
|
|
265
|
+
const body = proseLines(row.content, row.messageIndex, p, options, blocks, start);
|
|
266
|
+
const first = Math.max(0, body.findIndex((l) => l.some((c) => c.char !== ' ' && c.char !== '')));
|
|
267
|
+
body.forEach((source, k) => {
|
|
268
|
+
const before = i > 0 ? layout.cont[i - 1]! : cont;
|
|
269
|
+
const g = folded || bare ? null : k === first ? gutter : k < first ? before : cont;
|
|
270
|
+
lines.push(shiftUnderGutter(source, shift, g, p));
|
|
271
|
+
});
|
|
272
|
+
noteMessage(row.messageIndex, start);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case 'bead': {
|
|
276
|
+
const bead = row.bead;
|
|
277
|
+
const color = laneColor(layout.laneColor.get(bead.lane) ?? 0);
|
|
278
|
+
const line = paintBeadRow(gutter, {
|
|
279
|
+
status: bead.status,
|
|
280
|
+
name: bead.name,
|
|
281
|
+
arg: bead.arg,
|
|
282
|
+
summary: bead.summary,
|
|
283
|
+
time: bead.time,
|
|
284
|
+
laneColor: color,
|
|
285
|
+
hasBody: bead.body !== null,
|
|
286
|
+
open: bead.open,
|
|
287
|
+
answers: bead.answers,
|
|
288
|
+
}, p);
|
|
289
|
+
focusIf(line, bead.id);
|
|
290
|
+
lines.push(line);
|
|
291
|
+
let capped = false;
|
|
292
|
+
if (bead.open && bead.body) {
|
|
293
|
+
const semantic = bead.body.kind === 'diff' ? semanticSummaryFor(bead.body.diff, bead.body.path) : undefined;
|
|
294
|
+
const drawn = paintBody(bead.body, p, { expanded: bead.expanded, semantic });
|
|
295
|
+
capped = drawn.capped;
|
|
296
|
+
for (const bodyLine of drawn.lines) {
|
|
297
|
+
paintGutter(bodyLine, cont, p);
|
|
298
|
+
lines.push(bodyLine);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const laneKey = bead.lane === SPINE ? undefined : laneOfBead.get(bead.lane);
|
|
302
|
+
blocks.push(beadMeta(bead, start, lines.length - start, capped || (bead.open && bead.expanded), laneKey));
|
|
303
|
+
noteMessage(bead.scope === '' ? bead.messageIndex : undefined, start);
|
|
304
|
+
noteMessage(bead.resultIndex, start);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'spawn':
|
|
308
|
+
case 'folded': {
|
|
309
|
+
const lane = row.lane;
|
|
310
|
+
laneOfBead.set(lane.id, lane.key);
|
|
311
|
+
const color = laneColor(layout.laneColor.get(lane.id) ?? 0);
|
|
312
|
+
const line = row.kind === 'spawn'
|
|
313
|
+
? paintSpawnRow(gutter, { name: lane.name, arg: lane.arg, laneColor: color }, p)
|
|
314
|
+
: paintFoldedRow(gutter, { name: lane.name, arg: lane.arg, laneColor: color, outcome: lane.outcome, summary: lane.foldSummary, time: lane.time }, p);
|
|
315
|
+
focusIf(line, laneFocusId(lane));
|
|
316
|
+
lines.push(line);
|
|
317
|
+
blocks.push({
|
|
318
|
+
blockIndex: 0,
|
|
319
|
+
collapseKey: lane.key,
|
|
320
|
+
type: 'tool',
|
|
321
|
+
startLine: start,
|
|
322
|
+
lineCount: 1,
|
|
323
|
+
rawContent: `${lane.name}: ${lane.arg}\n${lane.mergeText}`,
|
|
324
|
+
toolName: row.bead.call.name,
|
|
325
|
+
workTree: {
|
|
326
|
+
kind: 'lane', id: laneFocusId(lane), hasBody: true, open: row.kind === 'spawn', capped: false, finished: lane.outcome !== 'run', laneKey: lane.key,
|
|
327
|
+
agentId: lane.id, colorIndex: layout.laneColor.get(lane.id) ?? 0,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
noteMessage(row.bead.scope === '' ? row.bead.messageIndex : undefined, start);
|
|
331
|
+
noteMessage(row.bead.resultIndex, start);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'merge': {
|
|
335
|
+
lines.push(paintMergeRow(gutter, row.lane.mergeText, row.lane.outcome === 'warn' || row.lane.outcome === 'err', p));
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
blocks.forEach((b, k) => { b.blockIndex = k; });
|
|
342
|
+
return {
|
|
343
|
+
lines,
|
|
344
|
+
blocks,
|
|
345
|
+
errorLines,
|
|
346
|
+
messageLines,
|
|
347
|
+
focusIds,
|
|
348
|
+
tail: { gutter: layout.cont[layout.cont.length - 1] ?? [], gutterWidth: layout.gutterWidth },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Text still streaming into the latest turn: at that turn's text column, with
|
|
354
|
+
* the lanes live under its last row running down the gutter beside it.
|
|
355
|
+
*/
|
|
356
|
+
export function renderStreamingContinuation(
|
|
357
|
+
content: string,
|
|
358
|
+
width: number,
|
|
359
|
+
tail: { readonly gutter: GutterRow; readonly gutterWidth: number },
|
|
360
|
+
glyphSet: TreeGlyphSetName,
|
|
361
|
+
): Line[] {
|
|
362
|
+
const p: GraphPaint = { width, gutterWidth: tail.gutterWidth, glyphs: laneGlyphs(glyphSet), frame: 0 };
|
|
363
|
+
const shift = Math.max(0, textColumn(p) - MARKDOWN_TEXT_COL);
|
|
364
|
+
return renderMarkdown(content, Math.max(20, width - shift), { isStreaming: true }).map((line) => shiftUnderGutter(line, shift, tail.gutter, p));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** One bead with no turn around it (a result whose call is outside the slice). */
|
|
368
|
+
export function renderSingleBead(bead: BeadModel, options: TurnRenderOptions): TurnRender {
|
|
369
|
+
const glyphs = laneGlyphs(options.glyphSet);
|
|
370
|
+
const p: GraphPaint = { width: options.width, gutterWidth: 6, glyphs, frame: options.frame };
|
|
371
|
+
const lines: Line[] = [paintBeadRow([{ role: 'bead', color: 0 }], {
|
|
372
|
+
status: bead.status, name: bead.name, arg: bead.arg, summary: bead.summary, time: bead.time,
|
|
373
|
+
laneColor: laneColor(0), hasBody: bead.body !== null, open: bead.open,
|
|
374
|
+
}, p)];
|
|
375
|
+
const focusIds = new Set([bead.id]);
|
|
376
|
+
if (options.focusId === bead.id) applyFocus(lines[0]!, options.width, glyphs);
|
|
377
|
+
let capped = false;
|
|
378
|
+
if (bead.open && bead.body) {
|
|
379
|
+
const drawn = paintBody(bead.body, p, { expanded: bead.expanded, semantic: bead.body.kind === 'diff' ? semanticSummaryFor(bead.body.diff, bead.body.path) : undefined });
|
|
380
|
+
capped = drawn.capped;
|
|
381
|
+
lines.push(...drawn.lines);
|
|
382
|
+
}
|
|
383
|
+
return { lines, blocks: [beadMeta(bead, 0, lines.length, capped, undefined)], errorLines: [], messageLines: new Map(), focusIds, tail: { gutter: [], gutterWidth: 6 } };
|
|
384
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* work-tree-sources.ts, the live facts the conversation work tree reads that
|
|
3
|
+
* the message snapshot does not carry.
|
|
4
|
+
*
|
|
5
|
+
* A message snapshot says which tools were called and what they returned. It
|
|
6
|
+
* does not say when they ran, which agent a call started or how that agent is
|
|
7
|
+
* doing, or which call a permission prompt is holding. Those come from here,
|
|
8
|
+
* read at render time, so the work tree states them without the transcript
|
|
9
|
+
* format changing. Every field is optional: with no sources the work tree
|
|
10
|
+
* still draws, just without times, agent lanes or waiting beads.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
14
|
+
|
|
15
|
+
type Message = ConversationMessageSnapshot;
|
|
16
|
+
|
|
17
|
+
export interface CallTiming {
|
|
18
|
+
/** Epoch ms the call started executing. */
|
|
19
|
+
readonly startedAt?: number | undefined;
|
|
20
|
+
/** Set once it settled. */
|
|
21
|
+
readonly durationMs?: number | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** How a finished turn ended. A completed turn's header says nothing extra; the others say so. */
|
|
25
|
+
export type TurnOutcome = 'completed' | 'failed' | 'cancelled';
|
|
26
|
+
|
|
27
|
+
export interface TurnTiming {
|
|
28
|
+
readonly startedAt: number;
|
|
29
|
+
readonly endedAt?: number | undefined;
|
|
30
|
+
/** Set once the turn ended. */
|
|
31
|
+
readonly outcome?: TurnOutcome | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* userMessageFingerprint() of the user message the turn answered, when
|
|
34
|
+
* known. A record whose fingerprint does not match the message now at its
|
|
35
|
+
* index (the transcript was compacted, reset or replaced) is not this turn's.
|
|
36
|
+
*/
|
|
37
|
+
readonly fingerprint?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A short identity for a user message: its length and opening text. Enough to
|
|
42
|
+
* tell whether the message at an index is still the one a record was made
|
|
43
|
+
* for, without hashing whole messages on every build.
|
|
44
|
+
*/
|
|
45
|
+
export function userMessageFingerprint(message: Message | undefined): string | undefined {
|
|
46
|
+
if (!message || message.role !== 'user') return undefined;
|
|
47
|
+
const text = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
|
|
48
|
+
return `${text.length}:${text.slice(0, 80)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One phase of a WRFC chain: the agent that ran it and what it found. */
|
|
52
|
+
export interface WrfcPhaseInfo {
|
|
53
|
+
readonly agentId: string;
|
|
54
|
+
readonly role: 'engineer' | 'reviewer' | 'fixer' | 'integrator' | 'verifier' | 'orchestrator' | 'owner';
|
|
55
|
+
readonly task: string;
|
|
56
|
+
readonly status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
57
|
+
readonly startedAt?: number | undefined;
|
|
58
|
+
readonly completedAt?: number | undefined;
|
|
59
|
+
/** A review's findings (the latest review report's issues), when this phase is that review. */
|
|
60
|
+
readonly findings?: readonly string[] | undefined;
|
|
61
|
+
/** Whether this review passed, when known. */
|
|
62
|
+
readonly passed?: boolean | undefined;
|
|
63
|
+
/** First lines of the phase agent's report. */
|
|
64
|
+
readonly output?: string | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What the work tree needs about a spawned agent to draw its lane. */
|
|
68
|
+
export interface AgentLaneInfo {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
/** The template or role name ("engineer", "tester"). */
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly task: string;
|
|
73
|
+
readonly status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
74
|
+
readonly startedAt?: number | undefined;
|
|
75
|
+
readonly completedAt?: number | undefined;
|
|
76
|
+
readonly toolCallCount: number;
|
|
77
|
+
/** Real cost in dollars when priced; undefined when not. */
|
|
78
|
+
readonly costUsd?: number | undefined;
|
|
79
|
+
readonly error?: string | undefined;
|
|
80
|
+
/** The agent's own transcript (its tool calls become beads in its lane). */
|
|
81
|
+
readonly messages: readonly Message[];
|
|
82
|
+
/** Set when this agent owns a WRFC chain: its lane shows the chain's phases. */
|
|
83
|
+
readonly wrfcPhases?: readonly WrfcPhaseInfo[] | undefined;
|
|
84
|
+
/** Whether the chain passed, for a WRFC owner. */
|
|
85
|
+
readonly wrfcPassed?: boolean | undefined;
|
|
86
|
+
/** The harness name for an agent run by a hosted harness ("Claude Code"): it names the lane. */
|
|
87
|
+
readonly hostedLabel?: string | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface WorkTreeSources {
|
|
91
|
+
readonly callTiming?: ((callId: string) => CallTiming | undefined) | undefined;
|
|
92
|
+
/** Wall time of the turn answering the user message at this absolute index. */
|
|
93
|
+
readonly turnTiming?: ((userMessageIndex: number) => TurnTiming | undefined) | undefined;
|
|
94
|
+
readonly agent?: ((agentId: string) => AgentLaneInfo | null) | undefined;
|
|
95
|
+
/** Call ids a permission prompt is holding right now. */
|
|
96
|
+
readonly waitingCallIds?: (() => ReadonlySet<string>) | undefined;
|
|
97
|
+
/** Whether the main conversation is working on a turn right now. */
|
|
98
|
+
readonly turnActive?: (() => boolean) | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* How the turn answering the user message at this index ended, when it is
|
|
101
|
+
* known from a live turn-end event or the session's saved record. Checked
|
|
102
|
+
* against the message's fingerprint by the model.
|
|
103
|
+
*/
|
|
104
|
+
readonly turnOutcome?: ((userMessageIndex: number) => { readonly outcome: TurnOutcome; readonly fingerprint: string } | undefined) | undefined;
|
|
105
|
+
readonly now?: (() => number) | undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Most calls whose timing is kept; the oldest go first. */
|
|
109
|
+
const MAX_CALLS = 5000;
|
|
110
|
+
/** Most turns whose timing is kept. */
|
|
111
|
+
const MAX_TURNS = 1000;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tool and turn timings, fed by the runtime's tool and turn events. Bounded:
|
|
115
|
+
* a long session keeps the most recent MAX_CALLS calls and MAX_TURNS turns,
|
|
116
|
+
* and a call that falls out simply renders without a time.
|
|
117
|
+
*/
|
|
118
|
+
export class WorkTreeTimingStore {
|
|
119
|
+
private readonly calls = new Map<string, { startedAt?: number; durationMs?: number }>();
|
|
120
|
+
private readonly turns = new Map<number, { startedAt: number; endedAt?: number; outcome?: TurnOutcome; fingerprint?: string }>();
|
|
121
|
+
private openTurn: number | undefined;
|
|
122
|
+
|
|
123
|
+
callStarted(callId: string, startedAt: number): void {
|
|
124
|
+
const known = this.calls.get(callId);
|
|
125
|
+
this.calls.delete(callId);
|
|
126
|
+
this.calls.set(callId, { ...known, startedAt });
|
|
127
|
+
this.trim(this.calls, MAX_CALLS);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
callSettled(callId: string, durationMs: number | undefined, endedAt: number): void {
|
|
131
|
+
const known = this.calls.get(callId);
|
|
132
|
+
const started = known?.startedAt ?? (durationMs !== undefined ? endedAt - durationMs : undefined);
|
|
133
|
+
const duration = durationMs ?? (started !== undefined ? endedAt - started : undefined);
|
|
134
|
+
this.calls.delete(callId);
|
|
135
|
+
this.calls.set(callId, { startedAt: started, durationMs: duration });
|
|
136
|
+
this.trim(this.calls, MAX_CALLS);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
turnStarted(userMessageIndex: number, startedAt: number, fingerprint?: string): void {
|
|
140
|
+
this.turns.delete(userMessageIndex);
|
|
141
|
+
this.turns.set(userMessageIndex, fingerprint !== undefined ? { startedAt, fingerprint } : { startedAt });
|
|
142
|
+
this.openTurn = userMessageIndex;
|
|
143
|
+
this.trim(this.turns, MAX_TURNS);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Close the open turn with how it ended. Returns the closed turn's index and
|
|
148
|
+
* record, or undefined when no turn was open.
|
|
149
|
+
*/
|
|
150
|
+
turnEnded(endedAt: number, outcome: TurnOutcome = 'completed'): { readonly index: number; readonly timing: TurnTiming } | undefined {
|
|
151
|
+
if (this.openTurn === undefined) return undefined;
|
|
152
|
+
const index = this.openTurn;
|
|
153
|
+
const turn = this.turns.get(index);
|
|
154
|
+
this.openTurn = undefined;
|
|
155
|
+
if (!turn) return undefined;
|
|
156
|
+
if (turn.endedAt === undefined) {
|
|
157
|
+
turn.endedAt = endedAt;
|
|
158
|
+
turn.outcome = outcome;
|
|
159
|
+
}
|
|
160
|
+
return { index, timing: turn };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
callTiming(callId: string): CallTiming | undefined {
|
|
164
|
+
return this.calls.get(callId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
turnTiming(userMessageIndex: number): TurnTiming | undefined {
|
|
168
|
+
return this.turns.get(userMessageIndex);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
clear(): void {
|
|
172
|
+
this.calls.clear();
|
|
173
|
+
this.turns.clear();
|
|
174
|
+
this.openTurn = undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private trim<K, V>(map: Map<K, V>, max: number): void {
|
|
178
|
+
while (map.size > max) {
|
|
179
|
+
const oldest = map.keys().next();
|
|
180
|
+
if (oldest.done) return;
|
|
181
|
+
map.delete(oldest.value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|