@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
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lane-graph/paint.ts, drawing the work tree's rows into transcript lines.
|
|
3
|
+
*
|
|
4
|
+
* Columns (screen width W, gutter starting at GUTTER_X):
|
|
5
|
+
* - the gutter from GUTTER_X, 2 columns per lane (layout.ts)
|
|
6
|
+
* - ▸ / ▾ two columns left of the text column when a bead has a body
|
|
7
|
+
* - name and key argument from the text column
|
|
8
|
+
* - the result summary right-aligned so it ends 9 columns left of the time
|
|
9
|
+
* - the time right-aligned so it ends at column W-4
|
|
10
|
+
*
|
|
11
|
+
* An opened body is a fill from the text column to W-4 with 2 columns of side
|
|
12
|
+
* padding and a padding row above and below; the lanes keep running down the
|
|
13
|
+
* gutter beside it. The focused row gets a fill from column 1 to W-2 and the
|
|
14
|
+
* brand ┃ in column 0.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createEmptyLine, createStyledCell, type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
18
|
+
import { activeTokens } from '../theme.ts';
|
|
19
|
+
import { expandTabs, getDisplayWidth, truncateDisplay, wrapPreservingIndent } from '../../utils/terminal-width.ts';
|
|
20
|
+
import { SurfaceCanvas } from '../surface-kit.ts';
|
|
21
|
+
import { inset, type KitInset } from '../surface-kit-parts.ts';
|
|
22
|
+
import { buildDiffRows, drawDiffRow, semanticChips, type DiffRow } from '../changes-modal.ts';
|
|
23
|
+
import { highlightCodeLines } from '../code-block.ts';
|
|
24
|
+
import { languageForPath, parseChanges } from '../../input/changes-model.ts';
|
|
25
|
+
import type { SemanticDiff } from '../semantic-diff.ts';
|
|
26
|
+
import { glyphText, type LaneGlyphs } from './glyphs.ts';
|
|
27
|
+
import type { GutterRow } from './layout.ts';
|
|
28
|
+
import type { BeadBody, BeadStatus, BeadSummary, BodyTone, ReadBodyFile, SummaryTone } from './bead.ts';
|
|
29
|
+
|
|
30
|
+
/** Column where every turn's gutter starts (one right of the ┃ bar column). */
|
|
31
|
+
const GUTTER_X = 3;
|
|
32
|
+
/** Columns between the end of the summary and the end of the time. */
|
|
33
|
+
const SUMMARY_GAP = 9;
|
|
34
|
+
/** Rows an output, list or text body shows before "… N more". */
|
|
35
|
+
const BODY_CAP = 12;
|
|
36
|
+
/** Rows a diff body shows before "… N more lines". */
|
|
37
|
+
const DIFF_CAP = 40;
|
|
38
|
+
|
|
39
|
+
export interface GraphPaint {
|
|
40
|
+
readonly width: number;
|
|
41
|
+
/** Columns from GUTTER_X to the turn's text column (layout.gutterWidth). */
|
|
42
|
+
readonly gutterWidth: number;
|
|
43
|
+
readonly glyphs: LaneGlyphs;
|
|
44
|
+
/** Spinner frame for running beads. */
|
|
45
|
+
readonly frame: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The turn's text column. */
|
|
49
|
+
export function textColumn(p: Pick<GraphPaint, 'gutterWidth'>): number {
|
|
50
|
+
return GUTTER_X + p.gutterWidth;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Column the time's last character sits on. */
|
|
54
|
+
function timeEnd(width: number): number {
|
|
55
|
+
return width - 4;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Added to every lane index while an agent view draws (its spine takes the agent's color). */
|
|
59
|
+
let laneColorBase = 0;
|
|
60
|
+
|
|
61
|
+
/** A lane's color: the theme's `lanes` tokens in rotation. */
|
|
62
|
+
export function laneColor(index: number): string {
|
|
63
|
+
const lanes = activeTokens().lanes;
|
|
64
|
+
if (lanes.length === 0) return activeTokens().brand;
|
|
65
|
+
const at = index + laneColorBase;
|
|
66
|
+
return lanes[((at % lanes.length) + lanes.length) % lanes.length]!;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Draw with every lane shifted by `base`: an agent opened full screen draws
|
|
71
|
+
* its own spine in the color its lane has in main, and its children in the
|
|
72
|
+
* colors after it. Synchronous; the base is restored afterwards.
|
|
73
|
+
*/
|
|
74
|
+
export function withLaneColorBase<T>(base: number, draw: () => T): T {
|
|
75
|
+
const previous = laneColorBase;
|
|
76
|
+
laneColorBase = base;
|
|
77
|
+
try {
|
|
78
|
+
return draw();
|
|
79
|
+
} finally {
|
|
80
|
+
laneColorBase = previous;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function summaryColor(tone: SummaryTone): string {
|
|
85
|
+
const t = activeTokens();
|
|
86
|
+
return tone === 'good' ? t.success : tone === 'warn' ? t.warning : tone === 'bad' ? t.error : t.textFaint;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function bodyColor(tone: BodyTone): string {
|
|
90
|
+
const t = activeTokens();
|
|
91
|
+
switch (tone) {
|
|
92
|
+
case 'good': return t.success;
|
|
93
|
+
case 'bad': return t.error;
|
|
94
|
+
case 'warn': return t.warning;
|
|
95
|
+
case 'muted': return t.textMuted;
|
|
96
|
+
case 'faint': return t.textFaint;
|
|
97
|
+
default: return t.text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface TextStyle {
|
|
102
|
+
readonly fg: string;
|
|
103
|
+
readonly bold?: boolean;
|
|
104
|
+
readonly strikethrough?: boolean;
|
|
105
|
+
readonly bg?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Row text in the active glyph set (plain ASCII for the ascii set). */
|
|
109
|
+
function tx(p: Pick<GraphPaint, 'glyphs'>, text: string): string {
|
|
110
|
+
return glyphText(text, p.glyphs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Write text from `x`, never past `endExclusive`; returns the column after it. */
|
|
114
|
+
function putText(line: Line, x: number, endExclusive: number, text: string, style: TextStyle): number {
|
|
115
|
+
let col = x;
|
|
116
|
+
for (const ch of text) {
|
|
117
|
+
const w = getDisplayWidth(ch);
|
|
118
|
+
if (w <= 0) continue;
|
|
119
|
+
if (col + w > endExclusive || col + w > line.length) break;
|
|
120
|
+
const prev = line[col]!;
|
|
121
|
+
line[col] = createStyledCell(ch, { fg: style.fg, bg: style.bg ?? prev.bg, bold: style.bold ?? false, strikethrough: style.strikethrough ?? false });
|
|
122
|
+
if (w > 1) line[col + 1] = createStyledCell('', { fg: style.fg, bg: style.bg ?? prev.bg });
|
|
123
|
+
col += w;
|
|
124
|
+
}
|
|
125
|
+
return col;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Write text so its last column is `xr`; returns its first column. */
|
|
129
|
+
function putRight(line: Line, xr: number, text: string, style: TextStyle): number {
|
|
130
|
+
const start = xr - getDisplayWidth(text) + 1;
|
|
131
|
+
putText(line, start, xr + 1, text, style);
|
|
132
|
+
return start;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The character a bead's status draws as, and its color. */
|
|
136
|
+
function beadMark(status: BeadStatus, glyphs: LaneGlyphs, lane: string, frame: number): { char: string; fg: string } {
|
|
137
|
+
const t = activeTokens();
|
|
138
|
+
switch (status) {
|
|
139
|
+
case 'ok': return { char: glyphs.ok, fg: t.success };
|
|
140
|
+
case 'warn': return { char: glyphs.warn, fg: t.warning };
|
|
141
|
+
case 'err': return { char: glyphs.err, fg: t.error };
|
|
142
|
+
case 'run': return { char: glyphs.run[frame % glyphs.run.length]!, fg: lane };
|
|
143
|
+
case 'wait': return { char: glyphs.wait, fg: t.warning };
|
|
144
|
+
case 'cancel': return { char: glyphs.cancel, fg: t.textFaint };
|
|
145
|
+
case 'bg': return { char: glyphs.background, fg: t.success };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Draw a gutter row onto `line` from GUTTER_X. The bead cell (if any) draws
|
|
151
|
+
* as `beadStatus`'s mark; lane lines keep their lane's color.
|
|
152
|
+
*/
|
|
153
|
+
export function paintGutter(line: Line, row: GutterRow, p: GraphPaint, beadStatus?: BeadStatus): void {
|
|
154
|
+
for (let col = 0; col < row.length; col++) {
|
|
155
|
+
const cell = row[col];
|
|
156
|
+
const x = GUTTER_X + col;
|
|
157
|
+
if (!cell || x >= line.length) continue;
|
|
158
|
+
const lane = laneColor(cell.color);
|
|
159
|
+
const bg = line[x]!.bg;
|
|
160
|
+
if (cell.role === 'bead') {
|
|
161
|
+
const mark = beadMark(beadStatus ?? 'ok', p.glyphs, lane, p.frame);
|
|
162
|
+
line[x] = createStyledCell(mark.char, { fg: mark.fg, bg, bold: true });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const isMark = cell.role === 'folded' || cell.role === 'turn';
|
|
166
|
+
line[x] = createStyledCell(p.glyphs.role[cell.role], { fg: lane, bg, bold: isMark });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The fill and ┃ of the focused row (and of every row of its opened body's frame). */
|
|
171
|
+
export function applyFocus(line: Line, width: number, glyphs: LaneGlyphs): void {
|
|
172
|
+
const t = activeTokens();
|
|
173
|
+
for (let x = 1; x <= width - 2 && x < line.length; x++) {
|
|
174
|
+
const cell = line[x]!;
|
|
175
|
+
if (cell.bg === '') line[x] = { ...cell, bg: t.backgroundPanel };
|
|
176
|
+
}
|
|
177
|
+
if (line.length > 0) line[0] = createStyledCell(glyphs.focus, { fg: t.brand, bold: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Right side of a row: the summary ending SUMMARY_GAP columns left of the time, and the time. Returns the column the left text must stop before. */
|
|
181
|
+
function paintRight(line: Line, p: GraphPaint, summary: BeadSummary | null, summaryMark: string | undefined, time: string | undefined, timeFg: string): number {
|
|
182
|
+
const end = timeEnd(p.width);
|
|
183
|
+
let limit = end + 1;
|
|
184
|
+
if (time) limit = putRight(line, end, tx(p, time), { fg: timeFg }) - 2;
|
|
185
|
+
if (summary) {
|
|
186
|
+
const sumEnd = end - SUMMARY_GAP;
|
|
187
|
+
const room = Math.max(0, sumEnd - textColumn(p) - 12);
|
|
188
|
+
const text = tx(p, truncateDisplay(summaryMark ? `${summaryMark} ${summary.text}` : summary.text, Math.max(8, room), p.glyphs.ellipsis));
|
|
189
|
+
limit = Math.min(limit, putRight(line, sumEnd, text, { fg: summaryColor(summary.tone) }) - 2);
|
|
190
|
+
}
|
|
191
|
+
return limit;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface BeadRowSpec {
|
|
195
|
+
readonly status: BeadStatus;
|
|
196
|
+
readonly name: string;
|
|
197
|
+
readonly arg: string;
|
|
198
|
+
readonly summary: BeadSummary | null;
|
|
199
|
+
readonly time: string | undefined;
|
|
200
|
+
/** The lane's color (running beads spin and count up in it). */
|
|
201
|
+
readonly laneColor: string;
|
|
202
|
+
readonly hasBody: boolean;
|
|
203
|
+
readonly open: boolean;
|
|
204
|
+
/** WRFC: the finding this row answers ("↳ jitter can overshoot"). */
|
|
205
|
+
readonly answers?: string | undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function paintBeadRow(gutter: GutterRow, spec: BeadRowSpec, p: GraphPaint): Line {
|
|
209
|
+
const t = activeTokens();
|
|
210
|
+
const line = createEmptyLine(p.width);
|
|
211
|
+
paintGutter(line, gutter, p, spec.status);
|
|
212
|
+
const x = textColumn(p);
|
|
213
|
+
if (spec.hasBody) putText(line, x - 2, x - 1, spec.open ? p.glyphs.opened : p.glyphs.closed, { fg: spec.open ? t.textMuted : t.textFaint });
|
|
214
|
+
const limit = paintRight(line, p, spec.summary, undefined, spec.time, spec.status === 'run' ? spec.laneColor : t.textFaint);
|
|
215
|
+
const cancelled = spec.status === 'cancel';
|
|
216
|
+
const nameFg = spec.status === 'run' || spec.status === 'wait' ? t.text : spec.status === 'err' ? t.error : t.textMuted;
|
|
217
|
+
let cx = putText(line, x, limit, tx(p, spec.name), { fg: nameFg, bold: spec.status === 'run', strikethrough: cancelled });
|
|
218
|
+
if (spec.arg) {
|
|
219
|
+
const room = limit - (cx + 1);
|
|
220
|
+
const answers = spec.answers ? ` ${p.glyphs.answers} ${spec.answers}` : '';
|
|
221
|
+
const argRoom = Math.max(0, room - Math.min(getDisplayWidth(answers), Math.floor(room / 2)));
|
|
222
|
+
cx = putText(line, cx + 1, limit, tx(p, truncateDisplay(spec.arg, argRoom, p.glyphs.ellipsis)), { fg: cancelled ? t.textFaint : t.text, strikethrough: cancelled });
|
|
223
|
+
if (answers) putText(line, cx, limit, tx(p, truncateDisplay(answers, Math.max(0, limit - cx), p.glyphs.ellipsis)), { fg: t.warning });
|
|
224
|
+
}
|
|
225
|
+
return line;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface LaneRowSpec {
|
|
229
|
+
readonly name: string;
|
|
230
|
+
readonly arg: string;
|
|
231
|
+
readonly laneColor: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A lane opening (├─╮): the agent's name in its lane color, its task muted. */
|
|
235
|
+
export function paintSpawnRow(gutter: GutterRow, spec: LaneRowSpec, p: GraphPaint): Line {
|
|
236
|
+
const line = createEmptyLine(p.width);
|
|
237
|
+
paintGutter(line, gutter, p);
|
|
238
|
+
const x = textColumn(p);
|
|
239
|
+
const limit = timeEnd(p.width) + 1;
|
|
240
|
+
const cx = putText(line, x, limit, tx(p, spec.name), { fg: spec.laneColor, bold: true });
|
|
241
|
+
putText(line, cx + 1, limit, tx(p, truncateDisplay(spec.arg, Math.max(0, limit - cx - 1), p.glyphs.ellipsis)), { fg: activeTokens().textMuted });
|
|
242
|
+
return line;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** A lane merging back (├─╯) with its result line; amber with ! when it carries a failure. */
|
|
246
|
+
export function paintMergeRow(gutter: GutterRow, text: string, warn: boolean, p: GraphPaint): Line {
|
|
247
|
+
const t = activeTokens();
|
|
248
|
+
const line = createEmptyLine(p.width);
|
|
249
|
+
paintGutter(line, gutter, p);
|
|
250
|
+
const x = textColumn(p);
|
|
251
|
+
const limit = timeEnd(p.width) + 1;
|
|
252
|
+
if (warn) {
|
|
253
|
+
putText(line, x, limit, p.glyphs.warn, { fg: t.warning, bold: true });
|
|
254
|
+
putText(line, x + 2, limit, tx(p, truncateDisplay(text, Math.max(0, limit - x - 2), p.glyphs.ellipsis)), { fg: t.warning });
|
|
255
|
+
} else {
|
|
256
|
+
putText(line, x, limit, tx(p, truncateDisplay(text, Math.max(0, limit - x), p.glyphs.ellipsis)), { fg: t.textFaint });
|
|
257
|
+
}
|
|
258
|
+
return line;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface FoldedRowSpec extends LaneRowSpec {
|
|
262
|
+
/** ✓ ! ✕ before the summary. */
|
|
263
|
+
readonly outcome: 'ok' | 'warn' | 'err' | 'run';
|
|
264
|
+
readonly summary: string;
|
|
265
|
+
readonly time: string | undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A finished lane folded to one ◉ bead on its parent's lane. */
|
|
269
|
+
export function paintFoldedRow(gutter: GutterRow, spec: FoldedRowSpec, p: GraphPaint): Line {
|
|
270
|
+
const t = activeTokens();
|
|
271
|
+
const line = createEmptyLine(p.width);
|
|
272
|
+
paintGutter(line, gutter, p);
|
|
273
|
+
const x = textColumn(p);
|
|
274
|
+
const tone: SummaryTone = spec.outcome === 'ok' ? 'good' : spec.outcome === 'warn' ? 'warn' : spec.outcome === 'err' ? 'bad' : 'faint';
|
|
275
|
+
const mark = spec.outcome === 'ok' ? p.glyphs.ok : spec.outcome === 'warn' ? p.glyphs.warn : spec.outcome === 'err' ? p.glyphs.err : p.glyphs.run[p.frame % p.glyphs.run.length]!;
|
|
276
|
+
const limit = paintRight(line, p, { text: spec.summary, tone }, mark, spec.time, spec.outcome === 'run' ? spec.laneColor : t.textFaint);
|
|
277
|
+
let cx = putText(line, x, limit, tx(p, spec.name), { fg: spec.laneColor, bold: true });
|
|
278
|
+
cx = putText(line, cx + 1, Math.max(cx + 1, limit - 2), tx(p, truncateDisplay(spec.arg, Math.max(0, limit - cx - 3), p.glyphs.ellipsis)), { fg: t.textMuted });
|
|
279
|
+
putText(line, cx + 1, limit, p.glyphs.closed, { fg: t.textMuted });
|
|
280
|
+
return line;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The turn header: "◆ model · 4 tools · 1 agent · 1.9s". Folded, the ◆ sits in
|
|
285
|
+
* the gutter and a ▸ closes the row.
|
|
286
|
+
*/
|
|
287
|
+
export function paintHeadRow(gutter: GutterRow, text: string, folded: boolean, p: GraphPaint): Line {
|
|
288
|
+
const t = activeTokens();
|
|
289
|
+
const line = createEmptyLine(p.width);
|
|
290
|
+
paintGutter(line, gutter, p);
|
|
291
|
+
const x = textColumn(p);
|
|
292
|
+
const limit = timeEnd(p.width) + 1;
|
|
293
|
+
let cx = x;
|
|
294
|
+
if (!folded) cx = putText(line, cx, limit, `${p.glyphs.role.turn} `, { fg: t.brand, bold: true });
|
|
295
|
+
cx = putText(line, cx, limit - (folded ? 2 : 0), tx(p, truncateDisplay(text, Math.max(0, limit - cx - (folded ? 2 : 0)), p.glyphs.ellipsis)), { fg: t.textFaint });
|
|
296
|
+
if (folded) putText(line, cx + 1, limit, p.glyphs.closed, { fg: t.textMuted });
|
|
297
|
+
return line;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Move an already-rendered transcript line `shift` columns right (prose rendered
|
|
302
|
+
* at a narrower width so it starts at the text column) and draw `gutter` over
|
|
303
|
+
* its left edge.
|
|
304
|
+
*/
|
|
305
|
+
export function shiftUnderGutter(source: Line, shift: number, gutter: GutterRow | null, p: GraphPaint): Line {
|
|
306
|
+
const line = createEmptyLine(p.width);
|
|
307
|
+
for (let i = 0; i < source.length; i++) {
|
|
308
|
+
const target = i + shift;
|
|
309
|
+
if (target >= p.width) break;
|
|
310
|
+
line[target] = source[i]!;
|
|
311
|
+
}
|
|
312
|
+
if (gutter) paintGutter(line, gutter, p);
|
|
313
|
+
return line;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Bodies
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
export interface BodyPaintOptions {
|
|
321
|
+
/** Show every row instead of the first BODY_CAP / DIFF_CAP. */
|
|
322
|
+
readonly expanded: boolean;
|
|
323
|
+
/** The semantic summary for a diff body: undefined while it is being read, null when there is none. */
|
|
324
|
+
readonly semantic?: SemanticDiff | null | undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The body fill's inset: from the text column to W-4, `h` rows. */
|
|
328
|
+
function bodyInset(canvas: SurfaceCanvas, p: GraphPaint, h: number, bg: string): KitInset {
|
|
329
|
+
const x = textColumn(p);
|
|
330
|
+
return inset(canvas, x, 0, timeEnd(p.width) - x + 1, h, bg);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function moreText(n: number, what: string, p: GraphPaint): string {
|
|
334
|
+
return `${p.glyphs.ellipsis} ${n} more${what ? ` ${what}` : ''}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Text lines wrapped in full to the inset's text width. A body is file text or
|
|
339
|
+
* command output, so its layout is kept: indentation and inner spacing stay,
|
|
340
|
+
* tabs become spaces, and wrapped rows keep the line's indent.
|
|
341
|
+
*/
|
|
342
|
+
function wrapAll(lines: ReadonlyArray<{ text: string; fg: string }>, width: number): Array<{ text: string; fg: string }> {
|
|
343
|
+
const out: Array<{ text: string; fg: string }> = [];
|
|
344
|
+
for (const line of lines) {
|
|
345
|
+
const parts = wrapPreservingIndent(line.text, Math.max(8, width));
|
|
346
|
+
for (const part of parts.length > 0 ? parts : ['']) out.push({ text: part, fg: line.fg });
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** One drawn row of a read body: colored runs from the inset's left edge. */
|
|
352
|
+
type ReadRow = ReadonlyArray<{ readonly text: string; readonly fg: string; readonly bold?: boolean; readonly italic?: boolean }>;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* A file's lines as drawn rows: syntax colored for the file's language, tabs
|
|
356
|
+
* as two spaces, and each line wrapped the way a command's output wraps
|
|
357
|
+
* (indentation kept, continuation rows indented like the first).
|
|
358
|
+
*/
|
|
359
|
+
function readRows(file: ReadBodyFile, width: number): ReadRow[] {
|
|
360
|
+
const t = activeTokens();
|
|
361
|
+
const lines = file.lines.map(expandTabs);
|
|
362
|
+
const tokens = highlightCodeLines(lines, file.path ? languageForPath(file.path) : '');
|
|
363
|
+
const rows: ReadRow[] = [];
|
|
364
|
+
lines.forEach((line, i) => {
|
|
365
|
+
const chars = Array.from(line);
|
|
366
|
+
// Each character's style, from the line's tokens (plain when they do not spell the line).
|
|
367
|
+
const lineTokens = tokens[i] ?? [];
|
|
368
|
+
const styles: Array<{ fg: string; bold?: boolean; italic?: boolean }> = [];
|
|
369
|
+
if (lineTokens.map((tk) => tk.text).join('') === line) {
|
|
370
|
+
for (const tk of lineTokens) for (const _ of Array.from(tk.text)) styles.push({ fg: tk.fg, bold: tk.bold, italic: tk.italic });
|
|
371
|
+
} else {
|
|
372
|
+
for (let k = 0; k < chars.length; k++) styles.push({ fg: t.text });
|
|
373
|
+
}
|
|
374
|
+
let cursor = 0;
|
|
375
|
+
for (const part of wrapPreservingIndent(line, Math.max(8, width))) {
|
|
376
|
+
const pc = Array.from(part);
|
|
377
|
+
let lead = 0;
|
|
378
|
+
while (pc[lead] === ' ') lead++;
|
|
379
|
+
while (chars[cursor] === ' ') cursor++;
|
|
380
|
+
const runs: Array<{ text: string; fg: string; bold?: boolean; italic?: boolean }> = [];
|
|
381
|
+
if (lead > 0) runs.push({ text: ' '.repeat(lead), fg: t.text });
|
|
382
|
+
for (let k = lead; k < pc.length; k++) {
|
|
383
|
+
const style = styles[cursor] ?? { fg: t.text };
|
|
384
|
+
cursor++;
|
|
385
|
+
const last = runs[runs.length - 1];
|
|
386
|
+
if (last && last.fg === style.fg && last.bold === style.bold && last.italic === style.italic) last.text += pc[k];
|
|
387
|
+
else runs.push({ text: pc[k]!, ...style });
|
|
388
|
+
}
|
|
389
|
+
rows.push(runs);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
return rows;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* An opened read: the text the model read, drawn like a command's output
|
|
397
|
+
* (indented exactly, syntax colored, "… N more lines" past the same BODY_CAP).
|
|
398
|
+
* One file shows its text alone, since the row already names it; several
|
|
399
|
+
* files show one short section each, headed by the path and line count, and
|
|
400
|
+
* share BODY_CAP between them (at least 3 rows each).
|
|
401
|
+
*/
|
|
402
|
+
function paintReadBody(files: readonly ReadBodyFile[], p: GraphPaint, options: BodyPaintOptions, innerWidth: number): { lines: Line[]; capped: boolean } {
|
|
403
|
+
const t = activeTokens();
|
|
404
|
+
const headed = files.length > 1;
|
|
405
|
+
const perFile = headed ? Math.max(3, Math.floor(BODY_CAP / files.length)) : BODY_CAP;
|
|
406
|
+
type Drawn = { kind: 'row'; row: ReadRow } | { kind: 'header'; file: ReadBodyFile } | { kind: 'more'; hidden: number } | { kind: 'error'; text: string } | { kind: 'gap' };
|
|
407
|
+
const drawn: Drawn[] = [];
|
|
408
|
+
let capped = false;
|
|
409
|
+
files.forEach((file, index) => {
|
|
410
|
+
if (headed) {
|
|
411
|
+
if (index > 0) drawn.push({ kind: 'gap' });
|
|
412
|
+
drawn.push({ kind: 'header', file });
|
|
413
|
+
}
|
|
414
|
+
if (file.error !== undefined) {
|
|
415
|
+
for (const part of wrapPreservingIndent(file.error, Math.max(8, innerWidth))) drawn.push({ kind: 'error', text: part });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const rows = readRows(file, innerWidth);
|
|
419
|
+
// Trailing blank rows say nothing.
|
|
420
|
+
while (rows.length > 0 && rows[rows.length - 1]!.length === 0) rows.pop();
|
|
421
|
+
const cap = options.expanded ? rows.length : Math.min(rows.length, perFile);
|
|
422
|
+
for (let k = 0; k < cap; k++) drawn.push({ kind: 'row', row: rows[k]! });
|
|
423
|
+
if (rows.length > cap) {
|
|
424
|
+
capped = true;
|
|
425
|
+
drawn.push({ kind: 'more', hidden: rows.length - cap });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
const height = drawn.length + 2;
|
|
429
|
+
const canvas = new SurfaceCanvas(p.width, height);
|
|
430
|
+
const pn = bodyInset(canvas, p, height, t.backgroundPanel);
|
|
431
|
+
const right = pn.l + innerWidth;
|
|
432
|
+
drawn.forEach((item, k) => {
|
|
433
|
+
const y = 1 + k;
|
|
434
|
+
if (item.kind === 'header') {
|
|
435
|
+
// A file that could not be read has no line count to state; its error follows.
|
|
436
|
+
const count = item.file.error !== undefined ? '' : `${item.file.lineCount} line${item.file.lineCount === 1 ? '' : 's'}`;
|
|
437
|
+
const path = truncateDisplay(item.file.path ?? 'file', Math.max(1, innerWidth - (count ? getDisplayWidth(count) + 2 : 0)), p.glyphs.ellipsis);
|
|
438
|
+
const cx = canvas.put(pn.l, y, path, { fg: t.text, bold: true });
|
|
439
|
+
if (count) canvas.put(cx + 2, y, count, { fg: t.textFaint });
|
|
440
|
+
} else if (item.kind === 'row') {
|
|
441
|
+
let cx = pn.l;
|
|
442
|
+
for (const run of item.row) {
|
|
443
|
+
const room = right - cx;
|
|
444
|
+
if (room <= 0) break;
|
|
445
|
+
cx = canvas.put(cx, y, truncateDisplay(run.text, room, p.glyphs.ellipsis), { fg: run.fg, bold: run.bold, italic: run.italic });
|
|
446
|
+
}
|
|
447
|
+
} else if (item.kind === 'more') {
|
|
448
|
+
canvas.put(pn.l, y, moreText(item.hidden, 'lines', p), { fg: t.textFaint });
|
|
449
|
+
} else if (item.kind === 'error') {
|
|
450
|
+
canvas.put(pn.l, y, truncateDisplay(item.text, innerWidth, p.glyphs.ellipsis), { fg: t.error });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return { lines: canvas.lines, capped };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Draw an opened bead's body. Returns full-width lines (the caller draws the
|
|
458
|
+
* lane gutter over their left edge) and whether rows were left out.
|
|
459
|
+
*/
|
|
460
|
+
export function paintBody(body: BeadBody, p: GraphPaint, options: BodyPaintOptions): { lines: Line[]; capped: boolean } {
|
|
461
|
+
const t = activeTokens();
|
|
462
|
+
const x = textColumn(p);
|
|
463
|
+
const innerWidth = timeEnd(p.width) - x + 1 - 4;
|
|
464
|
+
|
|
465
|
+
if (body.kind === 'diff') {
|
|
466
|
+
const files = parseChanges(body.diff);
|
|
467
|
+
const codeWidth = Math.max(8, innerWidth - 8);
|
|
468
|
+
let { rows } = buildDiffRows(files, codeWidth, files.length > 1);
|
|
469
|
+
if (!body.numbered) {
|
|
470
|
+
// Line numbers are unknown: the "⋯ line N" separators would state one.
|
|
471
|
+
rows = rows.filter((row, k) => row.kind !== 'sep' || k > 0).map((row): DiffRow => (row.kind === 'sep' ? { ...row, text: p.glyphs.ellipsis } : row));
|
|
472
|
+
}
|
|
473
|
+
const semanticRows = options.semantic && options.semantic.totalChanges > 0 ? 2 : 0;
|
|
474
|
+
const cap = options.expanded ? rows.length : Math.min(rows.length, DIFF_CAP);
|
|
475
|
+
const hidden = rows.length - cap;
|
|
476
|
+
const height = 1 + semanticRows + cap + (hidden > 0 ? 1 : 0) + 1;
|
|
477
|
+
const canvas = new SurfaceCanvas(p.width, height);
|
|
478
|
+
const pn = bodyInset(canvas, p, height, t.backgroundPanel);
|
|
479
|
+
let y = 1;
|
|
480
|
+
if (semanticRows > 0 && options.semantic) {
|
|
481
|
+
canvas.put(pn.l, y, '◈', { fg: t.brandEnd });
|
|
482
|
+
let cx = pn.l + 2;
|
|
483
|
+
for (const chip of semanticChips(options.semantic)) {
|
|
484
|
+
if (cx + getDisplayWidth(chip.text) > pn.r) break;
|
|
485
|
+
cx = canvas.put(cx, y, chip.text, { fg: chip.fg }) + 2;
|
|
486
|
+
}
|
|
487
|
+
y += 2;
|
|
488
|
+
}
|
|
489
|
+
for (let k = 0; k < cap; k++) drawDiffRow(canvas, pn, y + k, rows[k]!, -1, { lineNumbers: body.numbered });
|
|
490
|
+
if (hidden > 0) canvas.put(pn.l, y + cap, moreText(hidden, 'lines', p), { fg: t.textFaint });
|
|
491
|
+
return { lines: canvas.lines, capped: hidden > 0 };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (body.kind === 'error') {
|
|
495
|
+
const wrapped = wrapAll([{ text: body.text, fg: t.error }], innerWidth);
|
|
496
|
+
const height = wrapped.length + 2;
|
|
497
|
+
const canvas = new SurfaceCanvas(p.width, height);
|
|
498
|
+
const pn = bodyInset(canvas, p, height, t.backgroundError);
|
|
499
|
+
wrapped.forEach((row, k) => canvas.put(pn.l, 1 + k, row.text, { fg: row.fg }));
|
|
500
|
+
return { lines: canvas.lines, capped: false };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (body.kind === 'read') return paintReadBody(body.files, p, options, innerWidth);
|
|
504
|
+
|
|
505
|
+
let rows: Array<{ text: string; fg: string }>;
|
|
506
|
+
let footer: string | undefined;
|
|
507
|
+
if (body.kind === 'output') {
|
|
508
|
+
rows = wrapAll(body.lines.map((l) => ({ text: l.text, fg: bodyColor(l.tone) })), innerWidth);
|
|
509
|
+
footer = body.footer;
|
|
510
|
+
} else if (body.kind === 'list') {
|
|
511
|
+
rows = body.items.map((item) => {
|
|
512
|
+
const detail = item.detail ? ` ${item.detail}` : '';
|
|
513
|
+
return { text: truncateDisplay(expandTabs(`${item.text}${detail}`), innerWidth, p.glyphs.ellipsis), fg: t.text };
|
|
514
|
+
});
|
|
515
|
+
} else {
|
|
516
|
+
rows = wrapAll(body.lines.map((text) => ({ text, fg: t.text })), innerWidth);
|
|
517
|
+
}
|
|
518
|
+
const cap = options.expanded ? rows.length : Math.min(rows.length, BODY_CAP);
|
|
519
|
+
const hidden = rows.length - cap;
|
|
520
|
+
const tail = (hidden > 0 ? 1 : 0) + (footer ? (hidden > 0 ? 1 : 2) : 0);
|
|
521
|
+
const height = cap + tail + 2;
|
|
522
|
+
const canvas = new SurfaceCanvas(p.width, height);
|
|
523
|
+
const pn = bodyInset(canvas, p, height, t.backgroundPanel);
|
|
524
|
+
for (let k = 0; k < cap; k++) canvas.put(pn.l, 1 + k, truncateDisplay(rows[k]!.text, innerWidth, p.glyphs.ellipsis), { fg: rows[k]!.fg });
|
|
525
|
+
let y = 1 + cap;
|
|
526
|
+
if (hidden > 0) canvas.put(pn.l, y++, moreText(hidden, 'lines', p), { fg: t.textFaint });
|
|
527
|
+
if (footer) {
|
|
528
|
+
if (hidden === 0) y++;
|
|
529
|
+
canvas.put(pn.l, y, truncateDisplay(footer, innerWidth, p.glyphs.ellipsis), { fg: t.textMuted });
|
|
530
|
+
}
|
|
531
|
+
// A list is short by design: name the items that did not fit rather than "lines".
|
|
532
|
+
if (body.kind === 'list' && hidden > 0) {
|
|
533
|
+
canvas.fill(pn.l, 1 + cap, innerWidth, 1, t.backgroundPanel);
|
|
534
|
+
canvas.put(pn.l, 1 + cap, moreText(hidden, '', p), { fg: t.textFaint });
|
|
535
|
+
}
|
|
536
|
+
return { lines: canvas.lines, capped: hidden > 0 };
|
|
537
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lane-graph/semantic-memo.ts, the ◈ semantic summary of an opened edit's
|
|
3
|
+
* diff, computed once per diff and remembered.
|
|
4
|
+
*
|
|
5
|
+
* computeSemanticDiff parses with tree-sitter and is asynchronous, while the
|
|
6
|
+
* transcript renders synchronously. The first render of an opened edit starts
|
|
7
|
+
* the computation and draws without the summary; when it lands, every
|
|
8
|
+
* registered listener is told so the transcript repaints with it. The summary
|
|
9
|
+
* compares the diff's own before and after text (context plus removed lines
|
|
10
|
+
* against context plus added lines), so it names what the edit touched.
|
|
11
|
+
*
|
|
12
|
+
* Bounded: the most recent MAX_ENTRIES diffs are kept.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SemanticDiff } from '../semantic-diff.ts';
|
|
16
|
+
|
|
17
|
+
const MAX_ENTRIES = 200;
|
|
18
|
+
|
|
19
|
+
const memo = new Map<string, SemanticDiff | null | 'pending'>();
|
|
20
|
+
const listeners = new Set<() => void>();
|
|
21
|
+
let generation = 0;
|
|
22
|
+
|
|
23
|
+
/** Bumps every time a summary lands: a render that drew an opened diff keys its cache on it. */
|
|
24
|
+
export function semanticSummaryGeneration(): number {
|
|
25
|
+
return generation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Be told when a summary that was being computed is ready. Returns the unsubscribe. */
|
|
29
|
+
export function onSemanticSummaryReady(listener: () => void): () => void {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => { listeners.delete(listener); };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function beforeAfter(diff: string): { before: string; after: string } {
|
|
35
|
+
const before: string[] = [];
|
|
36
|
+
const after: string[] = [];
|
|
37
|
+
for (const line of diff.split('\n')) {
|
|
38
|
+
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@') || line.startsWith('diff ') || line.startsWith('index ')) continue;
|
|
39
|
+
if (line.startsWith('+')) after.push(line.slice(1));
|
|
40
|
+
else if (line.startsWith('-')) before.push(line.slice(1));
|
|
41
|
+
else if (line.startsWith(' ')) { before.push(line.slice(1)); after.push(line.slice(1)); }
|
|
42
|
+
}
|
|
43
|
+
return { before: before.join('\n'), after: after.join('\n') };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The summary for `diff`: a SemanticDiff, null when there is none to show,
|
|
48
|
+
* or undefined while it is being computed (the first call starts it).
|
|
49
|
+
*/
|
|
50
|
+
export function semanticSummaryFor(diff: string, path: string | undefined): SemanticDiff | null | undefined {
|
|
51
|
+
if (!path) return null;
|
|
52
|
+
const key = `${path}\u0000${diff}`;
|
|
53
|
+
const known = memo.get(key);
|
|
54
|
+
if (known === 'pending') return undefined;
|
|
55
|
+
if (known !== undefined) return known;
|
|
56
|
+
memo.set(key, 'pending');
|
|
57
|
+
while (memo.size > MAX_ENTRIES) {
|
|
58
|
+
const oldest = memo.keys().next();
|
|
59
|
+
if (oldest.done) break;
|
|
60
|
+
memo.delete(oldest.value);
|
|
61
|
+
}
|
|
62
|
+
const { before, after } = beforeAfter(diff);
|
|
63
|
+
void import('../semantic-diff.ts')
|
|
64
|
+
.then(({ computeSemanticDiff }) => computeSemanticDiff(path, before, after))
|
|
65
|
+
.catch(() => null)
|
|
66
|
+
.then((result) => {
|
|
67
|
+
memo.set(key, result);
|
|
68
|
+
generation++;
|
|
69
|
+
for (const listener of listeners) listener();
|
|
70
|
+
});
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Seed a summary directly (tests and golden frames, which must not wait on tree-sitter). */
|
|
75
|
+
export function primeSemanticSummary(diff: string, path: string, summary: SemanticDiff | null): void {
|
|
76
|
+
memo.set(`${path}\u0000${diff}`, summary);
|
|
77
|
+
}
|