@pellux/goodvibes-tui 2.0.20 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +59 -4
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -1,304 +1,140 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* conversation-line-cache.ts, per-
|
|
2
|
+
* conversation-line-cache.ts, per-unit Line[] production cache.
|
|
3
3
|
*
|
|
4
4
|
* The measured defect (perf baseline 2026-07-03, transcript.build_1k): appending
|
|
5
|
-
* ONE message to an N-message conversation re-rendered all N messages
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and markDirty() fires on every mutation. The marginal work for one appended
|
|
10
|
-
* message is ~1/1000th of that.
|
|
5
|
+
* ONE message to an N-message conversation re-rendered all N messages, because
|
|
6
|
+
* ConversationManager.rebuildHistory() clears the buffer and renders the whole
|
|
7
|
+
* snapshot on every dirty flag. The marginal work for one appended message is a
|
|
8
|
+
* tiny fraction of that.
|
|
11
9
|
*
|
|
12
|
-
* This module memoises the
|
|
13
|
-
* a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* array-valued fields, user ContentPart[] and assistant toolCalls, are
|
|
17
|
-
* serialised; a streaming assistant message mutates its content string in
|
|
18
|
-
* place, which changes the signature and invalidates the entry)
|
|
19
|
-
* - render width
|
|
20
|
-
* - the four display-config values the render reads (line-number mode,
|
|
21
|
-
* collapse threshold, showThinking, showReasoningSummary)
|
|
22
|
-
* - the block-registry base at message start (the code-block collapseKey embeds
|
|
23
|
-
* the GLOBAL block index, `code_${msgIdx}_${blockIdx}`, so a shift in an
|
|
24
|
-
* earlier message's block count changes this message's keys)
|
|
25
|
-
* - the absolute message index (embedded in every collapseKey and used for the
|
|
26
|
-
* system-message kind lookup)
|
|
27
|
-
* - the system-message kind (drives the error-navigation registry side effect)
|
|
28
|
-
* - this message's tool-group membership (see conversation-tool-groups.ts):
|
|
29
|
-
* whether it's folded under a group header, which member owns that header,
|
|
30
|
-
* and the group's honest tool/line counts, a group growing as new tool
|
|
31
|
-
* results stream in changes these for every existing member
|
|
32
|
-
* - the live values of every collapseState key the render READS (recorded via a
|
|
33
|
-
* proxy during a miss; a collapse toggle flips a recorded value and
|
|
34
|
-
* invalidates exactly the owning message)
|
|
10
|
+
* This module memoises the render of each transcript UNIT (see
|
|
11
|
+
* work-tree-model.ts transcriptUnits): a user message, a standalone system
|
|
12
|
+
* message, or one assistant turn with its whole work tree. A unit's lines are a
|
|
13
|
+
* pure function of its complete inputs:
|
|
35
14
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
15
|
+
* message unit: the message's render-relevant fields, the width, the display
|
|
16
|
+
* config the render reads, the theme's token table (identity), the
|
|
17
|
+
* system-message kind, and the live value of every collapse key it read.
|
|
18
|
+
*
|
|
19
|
+
* turn unit: the turn's model signature (work-tree-model.ts turnSignature:
|
|
20
|
+
* every row's status, text, time, fold and open state, and the content of
|
|
21
|
+
* its prose and opened bodies), the width, the display config, the theme,
|
|
22
|
+
* the glyph set, the collapse keys the draw read (code blocks, thinking),
|
|
23
|
+
* the focused row when it is one of this turn's rows, the spinner frame
|
|
24
|
+
* while the turn has something live, and the semantic-summary generation
|
|
25
|
+
* while it draws an opened diff.
|
|
26
|
+
*
|
|
27
|
+
* The turn MODEL is rebuilt every pass (it is cheap: it reads messages and the
|
|
28
|
+
* live sources); the DRAW (markdown, lane layout, painting) is what the cache
|
|
29
|
+
* saves.
|
|
40
30
|
*
|
|
41
31
|
* Correctness contract: a cache-served rebuild is BYTE-IDENTICAL to a cold
|
|
42
|
-
* appendConversationMessages() rebuild.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* are replayed at the same buffer offsets a cold render would have produced.
|
|
32
|
+
* appendConversationMessages() rebuild. A miss renders through the same
|
|
33
|
+
* functions into an isolated scratch context, and the captured lines, block
|
|
34
|
+
* metas and error lines are replayed at the same buffer offsets.
|
|
46
35
|
*/
|
|
47
36
|
|
|
48
37
|
import { createEmptyLine, type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
49
38
|
import type { BlockMeta } from './conversation-types.ts';
|
|
50
|
-
import type { ConversationRenderContext } from './conversation-rendering.ts';
|
|
51
39
|
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
renderConversationUserMessage,
|
|
57
|
-
collectToolCallOutcomes,
|
|
58
|
-
isTurnCollapsed,
|
|
59
|
-
type ToolCallOutcome,
|
|
40
|
+
buildConversationTurnModel,
|
|
41
|
+
drawConversationTurn,
|
|
42
|
+
renderConversationMessageUnit,
|
|
43
|
+
type ConversationRenderContext,
|
|
60
44
|
} from './conversation-rendering.ts';
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
type RenderNode,
|
|
67
|
-
} from './conversation-turn-structure.ts';
|
|
45
|
+
import { activeTokens } from '../renderer/theme.ts';
|
|
46
|
+
import { semanticSummaryGeneration } from '../renderer/lane-graph/semantic-memo.ts';
|
|
47
|
+
import { syntaxHighlightGeneration, syntaxHighlightMisses } from '../renderer/code-block.ts';
|
|
48
|
+
import type { GutterRow } from '../renderer/lane-graph/layout.ts';
|
|
49
|
+
import { transcriptUnits, turnHasOpenDiff, turnSignature, type TranscriptUnit } from './work-tree-model.ts';
|
|
68
50
|
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
69
51
|
// SystemMessageKind imported from runtime directly to avoid a cycle, mirroring
|
|
70
52
|
// conversation-rendering.ts's own import.
|
|
71
53
|
import type { SystemMessageKind } from '@/runtime/index.ts';
|
|
72
54
|
|
|
73
55
|
type Message = ConversationMessageSnapshot;
|
|
56
|
+
type Part = string | number | boolean | undefined | null | object;
|
|
74
57
|
|
|
75
|
-
/** The
|
|
76
|
-
interface
|
|
77
|
-
readonly
|
|
78
|
-
readonly
|
|
79
|
-
readonly showThinking: boolean;
|
|
80
|
-
readonly showReasoningSummary: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Content signature: the render-relevant fields of a message, captured so an
|
|
85
|
-
* unchanged message is recognised without re-rendering. Array-valued fields are
|
|
86
|
-
* pre-serialised to strings so equality is a value comparison.
|
|
87
|
-
*/
|
|
88
|
-
interface ContentSig {
|
|
89
|
-
readonly role: Message['role'];
|
|
90
|
-
/** String content, or JSON of ContentPart[] for array-valued user content. */
|
|
91
|
-
readonly content: string;
|
|
92
|
-
readonly cancelled?: boolean;
|
|
93
|
-
readonly model?: string;
|
|
94
|
-
readonly provider?: string;
|
|
95
|
-
readonly reasoningContent?: string;
|
|
96
|
-
readonly reasoningSummary?: string;
|
|
97
|
-
readonly toolCallsJson?: string;
|
|
98
|
-
readonly callId?: string;
|
|
99
|
-
readonly toolName?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Non-content inputs that key the cache entry. */
|
|
103
|
-
interface KeyMeta {
|
|
104
|
-
readonly role: Message['role'];
|
|
105
|
-
readonly width: number;
|
|
106
|
-
readonly lineNumberMode: 'all' | 'code' | 'off';
|
|
107
|
-
readonly collapseThreshold: number;
|
|
108
|
-
readonly showThinking: boolean;
|
|
109
|
-
readonly showReasoningSummary: boolean;
|
|
110
|
-
readonly blockBase: number;
|
|
111
|
-
readonly kind: SystemMessageKind | undefined;
|
|
112
|
-
/**
|
|
113
|
-
* Per-call completion signature for an assistant message's tool calls (see
|
|
114
|
-
* pendingToolKeyOf), undefined when the message has none. Each call's id
|
|
115
|
-
* is paired with whether it has a matching result yet, in call order, so a
|
|
116
|
-
* SINGLE call finishing invalidates and re-renders that call's glyph from
|
|
117
|
-
* ◌ to ✓ immediately. A prior aggregate boolean only flipped once EVERY
|
|
118
|
-
* call in the message had completed, so in a multi-call turn the earlier
|
|
119
|
-
* calls kept showing ◌ until the last result arrived.
|
|
120
|
-
*/
|
|
121
|
-
readonly pendingToolKey: string | undefined;
|
|
122
|
-
/**
|
|
123
|
-
* Turn membership and tree-structure fields, flattened so a change
|
|
124
|
-
* invalidates the entry like any other structural input.
|
|
125
|
-
*
|
|
126
|
-
* This is what makes connector glyphs safe to recompute rather than cache:
|
|
127
|
-
* when a sibling arrives, the previously-last row's `connector` flips
|
|
128
|
-
* `└`→`├`, that value differs from the cached one, and exactly that row
|
|
129
|
-
* re-renders. Nothing else in the row changes, so the compositor repaints a
|
|
130
|
-
* single connector cell.
|
|
131
|
-
*/
|
|
132
|
-
readonly turnKey: string | undefined;
|
|
133
|
-
readonly turnIsHead: boolean;
|
|
134
|
-
readonly turnToolCount: number;
|
|
135
|
-
readonly turnSharedLabel: string | undefined;
|
|
136
|
-
readonly turnHasReasoning: boolean;
|
|
137
|
-
/** Row depth and connector, both change when structure around a row changes. */
|
|
138
|
-
readonly depth: number;
|
|
139
|
-
readonly connector: string | undefined;
|
|
140
|
-
/** Ancestor gutter pattern, flattened; a change repaints the row's gutters. */
|
|
141
|
-
readonly openAncestors: string;
|
|
142
|
-
/** Whether this row is followed by the blank separator. */
|
|
143
|
-
readonly trailingBlank: boolean;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Per-call completion signature for an assistant message's tool calls, in
|
|
148
|
-
* call order, `id:0` or `id:1` per call, joined. Undefined for a message
|
|
149
|
-
* with no tool calls at all (nothing to key). Comparing this instead of an
|
|
150
|
-
* aggregate boolean lets exactly the calls whose completion status changed
|
|
151
|
-
* invalidate the entry, rather than waiting for every call in the message to
|
|
152
|
-
* complete before any of them re-renders as done.
|
|
153
|
-
*/
|
|
154
|
-
function pendingToolKeyOf(m: Message, outcomes: ReadonlyMap<string, ToolCallOutcome>): string | undefined {
|
|
155
|
-
if (m.role !== 'assistant' || !m.toolCalls || m.toolCalls.length === 0) return undefined;
|
|
156
|
-
// The OUTCOME, not merely ran/not-ran: a call whose result turns out to be a
|
|
157
|
-
// failure must repaint its glyph from ◌ to ✗, which a boolean would miss.
|
|
158
|
-
return m.toolCalls
|
|
159
|
-
.map((tc) => `${tc.id ?? ''}:${(tc.id !== undefined && outcomes.get(tc.id)) || 'pending'}`)
|
|
160
|
-
.join('|');
|
|
58
|
+
/** The gutter a streaming continuation of the last turn draws with. */
|
|
59
|
+
export interface TurnTail {
|
|
60
|
+
readonly gutter: GutterRow;
|
|
61
|
+
readonly gutterWidth: number;
|
|
161
62
|
}
|
|
162
63
|
|
|
163
64
|
interface CacheEntry {
|
|
164
|
-
|
|
165
|
-
readonly
|
|
166
|
-
/**
|
|
65
|
+
/** Every input the unit's lines depend on, compared element by element. */
|
|
66
|
+
readonly key: readonly Part[];
|
|
67
|
+
/** [collapseKey, value] pairs the render read; a change invalidates the entry. */
|
|
68
|
+
readonly collapseDeps: ReadonlyArray<readonly [string, boolean | undefined]>;
|
|
69
|
+
/** Focusable row ids this unit drew (a focus change re-renders only units that hold it). */
|
|
70
|
+
readonly focusIds: ReadonlySet<string>;
|
|
71
|
+
/** The focus id this entry was drawn with, when it is one of its own rows. */
|
|
72
|
+
readonly focusDrawn: string | null;
|
|
73
|
+
/** Rendered lines, INCLUDING the trailing blank line. */
|
|
167
74
|
readonly lines: Line[];
|
|
168
|
-
/** Block metas with startLine
|
|
75
|
+
/** Block metas with startLine relative to the unit. */
|
|
169
76
|
readonly blocks: BlockMeta[];
|
|
170
|
-
/** Error-navigation line offsets
|
|
77
|
+
/** Error-navigation line offsets relative to the unit. */
|
|
171
78
|
readonly errorRelLines: number[];
|
|
172
|
-
/**
|
|
173
|
-
readonly
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
79
|
+
/** absolute message index → relative line (every message the unit covers). */
|
|
80
|
+
readonly messageLines: ReadonlyMap<number, number>;
|
|
81
|
+
readonly tail: TurnTail | null;
|
|
82
|
+
/**
|
|
83
|
+
* Set when the unit drew code with the regex placeholder while its
|
|
84
|
+
* tree-sitter parse was on its way: the highlighter generation it saw. The
|
|
85
|
+
* entry is stale once a parse lands (the generation moves on), so the next
|
|
86
|
+
* build draws the real highlighting instead of keeping the placeholder.
|
|
87
|
+
*/
|
|
88
|
+
readonly highlightGeneration?: number | undefined;
|
|
179
89
|
}
|
|
180
90
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
role: 'user',
|
|
186
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
187
|
-
cancelled: m.cancelled,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
if (m.role === 'assistant') {
|
|
191
|
-
return {
|
|
192
|
-
role: 'assistant',
|
|
193
|
-
content: m.content,
|
|
194
|
-
model: m.model,
|
|
195
|
-
provider: m.provider,
|
|
196
|
-
reasoningContent: m.reasoningContent,
|
|
197
|
-
reasoningSummary: m.reasoningSummary,
|
|
198
|
-
toolCallsJson: m.toolCalls ? JSON.stringify(m.toolCalls) : undefined,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
if (m.role === 'system') {
|
|
202
|
-
return { role: 'system', content: m.content };
|
|
203
|
-
}
|
|
204
|
-
return { role: 'tool', content: m.content, callId: m.callId, toolName: m.toolName };
|
|
91
|
+
function sameParts(a: readonly Part[], b: readonly Part[]): boolean {
|
|
92
|
+
if (a.length !== b.length) return false;
|
|
93
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
94
|
+
return true;
|
|
205
95
|
}
|
|
206
96
|
|
|
207
|
-
|
|
208
|
-
* Compare a stored signature against a message WITHOUT allocating a new
|
|
209
|
-
* signature object on the hot (unchanged string-content) path. Array-valued
|
|
210
|
-
* fields fall back to JSON serialisation (rare).
|
|
211
|
-
*/
|
|
212
|
-
function contentUnchanged(sig: ContentSig, m: Message): boolean {
|
|
213
|
-
if (sig.role !== m.role) return false;
|
|
97
|
+
function messageParts(m: Message): Part[] {
|
|
214
98
|
switch (m.role) {
|
|
215
|
-
case 'user':
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
220
|
-
case 'assistant':
|
|
221
|
-
return (
|
|
222
|
-
sig.content === m.content &&
|
|
223
|
-
sig.model === m.model &&
|
|
224
|
-
sig.provider === m.provider &&
|
|
225
|
-
sig.reasoningContent === m.reasoningContent &&
|
|
226
|
-
sig.reasoningSummary === m.reasoningSummary &&
|
|
227
|
-
sig.toolCallsJson === (m.toolCalls ? JSON.stringify(m.toolCalls) : undefined)
|
|
228
|
-
);
|
|
229
|
-
case 'system':
|
|
230
|
-
return sig.content === m.content;
|
|
231
|
-
case 'tool':
|
|
232
|
-
return sig.content === m.content && sig.callId === m.callId && sig.toolName === m.toolName;
|
|
233
|
-
default:
|
|
234
|
-
return false;
|
|
99
|
+
case 'user': return ['user', typeof m.content === 'string' ? m.content : JSON.stringify(m.content), m.cancelled];
|
|
100
|
+
case 'system': return ['system', m.content];
|
|
101
|
+
case 'tool': return ['tool', m.content, m.callId, m.toolName];
|
|
102
|
+
case 'assistant': return ['assistant', m.content];
|
|
235
103
|
}
|
|
236
104
|
}
|
|
237
105
|
|
|
238
106
|
/**
|
|
239
107
|
* Wrap the real collapseState so reads (.get/.has) are recorded while writes
|
|
240
|
-
* (
|
|
241
|
-
* the
|
|
242
|
-
* would establish them.
|
|
108
|
+
* (defaults such as a long code block starting collapsed) pass straight through
|
|
109
|
+
* to the real map, exactly as a cold render would establish them.
|
|
243
110
|
*/
|
|
244
|
-
function makeRecordingCollapseState(
|
|
245
|
-
real: Map<string, boolean>,
|
|
246
|
-
readKeys: Set<string>,
|
|
247
|
-
): Map<string, boolean> {
|
|
111
|
+
function makeRecordingCollapseState(real: Map<string, boolean>, readKeys: Set<string>): Map<string, boolean> {
|
|
248
112
|
return new Proxy(real, {
|
|
249
|
-
get(target, prop
|
|
113
|
+
get(target, prop) {
|
|
250
114
|
if (prop === 'get') {
|
|
251
|
-
return (key: string): boolean | undefined => {
|
|
252
|
-
readKeys.add(key);
|
|
253
|
-
return target.get(key);
|
|
254
|
-
};
|
|
115
|
+
return (key: string): boolean | undefined => { readKeys.add(key); return target.get(key); };
|
|
255
116
|
}
|
|
256
117
|
if (prop === 'has') {
|
|
257
|
-
return (key: string): boolean => {
|
|
258
|
-
readKeys.add(key);
|
|
259
|
-
return target.has(key);
|
|
260
|
-
};
|
|
118
|
+
return (key: string): boolean => { readKeys.add(key); return target.has(key); };
|
|
261
119
|
}
|
|
262
|
-
void receiver;
|
|
263
120
|
const value = Reflect.get(target, prop, target);
|
|
264
121
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
265
122
|
},
|
|
266
123
|
});
|
|
267
124
|
}
|
|
268
125
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
ctx: ConversationRenderContext,
|
|
272
|
-
node: RenderNode,
|
|
273
|
-
width: number,
|
|
274
|
-
cfg: RenderConfig,
|
|
275
|
-
): void {
|
|
276
|
-
if (node.kind === 'toolcall') {
|
|
277
|
-
renderConversationToolCallNode(ctx, node, width);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const message = node.message;
|
|
281
|
-
if (message.role === 'user') {
|
|
282
|
-
renderConversationUserMessage(ctx, message, width, node.absIdx);
|
|
283
|
-
} else if (message.role === 'assistant') {
|
|
284
|
-
renderConversationAssistantMessage(ctx, message, width, cfg.lineNumberMode, cfg.collapseThreshold, node.absIdx);
|
|
285
|
-
} else if (message.role === 'system') {
|
|
286
|
-
renderConversationSystemMessage(ctx, message, width, node.absIdx);
|
|
287
|
-
} else if (message.role === 'tool') {
|
|
288
|
-
renderConversationToolMessage(ctx, message, width, node.absIdx, node);
|
|
289
|
-
}
|
|
126
|
+
function unitId(unit: TranscriptUnit): string {
|
|
127
|
+
return unit.kind === 'turn' ? `t:${unit.headIndex}` : `m:${unit.index}`;
|
|
290
128
|
}
|
|
291
129
|
|
|
292
130
|
/**
|
|
293
|
-
* MessageLineCache, per-
|
|
294
|
-
*
|
|
295
|
-
* Keyed by absolute message index; each entry validates its COMPLETE input tuple
|
|
296
|
-
* before serving. A rebuild that reuses entries is byte-identical to a cold
|
|
297
|
-
* rebuild; the cache is a pure memoisation with no observable behaviour of its
|
|
298
|
-
* own beyond speed and reduced allocation churn.
|
|
131
|
+
* MessageLineCache, per-unit Line[] memoisation for ConversationManager.
|
|
132
|
+
* A rebuild that reuses entries is byte-identical to a cold rebuild.
|
|
299
133
|
*/
|
|
300
134
|
export class MessageLineCache {
|
|
301
135
|
private entries: Map<string, CacheEntry> = new Map();
|
|
136
|
+
private _lastTail: TurnTail | null = null;
|
|
137
|
+
private _live = false;
|
|
302
138
|
|
|
303
139
|
/** Drop all cached entries (wholesale message replacement / reset). */
|
|
304
140
|
public clear(): void {
|
|
@@ -310,20 +146,23 @@ export class MessageLineCache {
|
|
|
310
146
|
return this.entries.size;
|
|
311
147
|
}
|
|
312
148
|
|
|
149
|
+
/** The gutter under the last rendered unit, when that unit is a turn. */
|
|
150
|
+
public get lastTail(): TurnTail | null {
|
|
151
|
+
return this._lastTail;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Whether the last pass drew anything live (a running bead or lane, a working turn). */
|
|
155
|
+
public get live(): boolean {
|
|
156
|
+
return this._live;
|
|
157
|
+
}
|
|
158
|
+
|
|
313
159
|
/**
|
|
314
|
-
* Render `messages` into `context`, reusing cached lines for unchanged
|
|
315
|
-
* messages. Mirrors appendConversationMessages exactly on a cold cache; a warm
|
|
316
|
-
* cache replays identical bytes at identical offsets.
|
|
160
|
+
* Render `messages` into `context`, reusing cached lines for unchanged units.
|
|
317
161
|
*
|
|
318
|
-
* @param context the live render context (real history buffer,
|
|
319
|
-
* block/error registries, collapse state, config).
|
|
320
|
-
* @param messages the visible message slice to render.
|
|
321
|
-
* @param width render width.
|
|
322
|
-
* @param messageLineRegistry absolute-index → first-line map, written here.
|
|
323
162
|
* @param msgIndexOffset absolute index of messages[0] (post-clearDisplay slice).
|
|
324
163
|
* @param streamingPlaceholderAbsIdx absolute index of the in-progress streaming
|
|
325
|
-
* placeholder
|
|
326
|
-
* the incremental streaming path
|
|
164
|
+
* placeholder (-1 when not streaming); its prose is drawn
|
|
165
|
+
* by the incremental streaming path.
|
|
327
166
|
*/
|
|
328
167
|
public renderInto(
|
|
329
168
|
context: ConversationRenderContext,
|
|
@@ -332,228 +171,158 @@ export class MessageLineCache {
|
|
|
332
171
|
messageLineRegistry: number[],
|
|
333
172
|
msgIndexOffset: number,
|
|
334
173
|
streamingPlaceholderAbsIdx: number,
|
|
174
|
+
/**
|
|
175
|
+
* Called after each unit is in the buffer, with the absolute index of the
|
|
176
|
+
* unit's last message and whether it is the turn still streaming. The
|
|
177
|
+
* conversation draws display-only output (a command's printed text) there,
|
|
178
|
+
* after the messages it followed.
|
|
179
|
+
*/
|
|
180
|
+
afterUnit?: (lastIndex: number, streaming: boolean) => void,
|
|
335
181
|
): void {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const assistantTurns = computeAssistantTurns(messages, msgIndexOffset);
|
|
348
|
-
// Structural plan: a permutation of the slice (results lifted under their
|
|
349
|
-
// calls, spawned agents spliced under the call that spawned them), rebuilt
|
|
350
|
-
// every pass so connectors and nesting always reflect live structure.
|
|
351
|
-
const plan = buildRenderPlan(messages, msgIndexOffset, {
|
|
352
|
-
resolveAgentSnapshot: context.resolveAgentSnapshot,
|
|
353
|
-
});
|
|
354
|
-
const renderContext: ConversationRenderContext = { ...context, completedToolCallIds, toolCallOutcomes, assistantTurns };
|
|
355
|
-
|
|
182
|
+
const config = context.configManager;
|
|
183
|
+
const common: Part[] = [
|
|
184
|
+
width,
|
|
185
|
+
config?.get('display.lineNumbers') ?? 'off',
|
|
186
|
+
config?.get('display.collapseThreshold') ?? 30,
|
|
187
|
+
config?.get('display.showThinking') ?? false,
|
|
188
|
+
config?.get('display.showReasoningSummary') ?? false,
|
|
189
|
+
activeTokens(),
|
|
190
|
+
context.treeGlyphSet ?? 'rounded',
|
|
191
|
+
];
|
|
192
|
+
const focus = context.focusId ?? null;
|
|
356
193
|
const touched = new Set<string>();
|
|
357
|
-
|
|
194
|
+
this._lastTail = null;
|
|
195
|
+
this._live = false;
|
|
358
196
|
|
|
359
|
-
for (
|
|
360
|
-
const
|
|
197
|
+
for (const unit of transcriptUnits(messages, msgIndexOffset)) {
|
|
198
|
+
const id = unitId(unit);
|
|
361
199
|
const base = context.history.getLineCount();
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
200
|
+
const readKeys = new Set<string>();
|
|
201
|
+
const scratchCollapse = makeRecordingCollapseState(context.collapseState, readKeys);
|
|
202
|
+
const scratchContext: ConversationRenderContext = { ...context, collapseState: scratchCollapse };
|
|
203
|
+
|
|
204
|
+
// The turn still streaming ends without its blank row: the streamed text continues under its spine.
|
|
205
|
+
const streaming = unit.kind === 'turn' && streamingPlaceholderAbsIdx >= unit.start && streamingPlaceholderAbsIdx <= unit.end;
|
|
206
|
+
let key: Part[];
|
|
207
|
+
let turnModel: ReturnType<typeof buildConversationTurnModel> | undefined;
|
|
208
|
+
if (unit.kind === 'turn') {
|
|
209
|
+
turnModel = buildConversationTurnModel(scratchContext, messages, msgIndexOffset, unit, streamingPlaceholderAbsIdx);
|
|
210
|
+
if (turnModel.live) this._live = true;
|
|
211
|
+
key = [
|
|
212
|
+
'turn', ...common, streaming,
|
|
213
|
+
turnModel.live ? context.frame ?? 0 : null,
|
|
214
|
+
turnHasOpenDiff(turnModel) ? semanticSummaryGeneration() : null,
|
|
215
|
+
...turnSignature(turnModel),
|
|
216
|
+
];
|
|
217
|
+
} else {
|
|
218
|
+
const message = messages[unit.index - msgIndexOffset]!;
|
|
219
|
+
const kind: SystemMessageKind | undefined = context.messageKindRegistry.get(unit.index);
|
|
220
|
+
key = ['message', ...common, unit.index, kind, ...messageParts(message)];
|
|
375
221
|
}
|
|
376
222
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (!uncacheable) {
|
|
385
|
-
const existing = this.entries.get(node.id);
|
|
386
|
-
if (existing && this.isValid(existing, node, width, cfg, blockBase, kind, pendingToolKey, turn, trailingBlank, context.collapseState)) {
|
|
387
|
-
this.applyEntry(context, existing, base);
|
|
388
|
-
touched.add(node.id);
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
223
|
+
const existing = this.entries.get(id);
|
|
224
|
+
const lastIndex = unit.kind === 'turn' ? unit.end : unit.index;
|
|
225
|
+
if (existing && this.isValid(existing, key, focus, context.collapseState)) {
|
|
226
|
+
this.apply(context, existing, base, messageLineRegistry);
|
|
227
|
+
touched.add(id);
|
|
228
|
+
afterUnit?.(lastIndex, streaming);
|
|
229
|
+
continue;
|
|
391
230
|
}
|
|
392
231
|
|
|
393
|
-
const
|
|
394
|
-
this.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
232
|
+
const missesBefore = syntaxHighlightMisses();
|
|
233
|
+
const drawn = this.renderScratch(scratchContext, unit, turnModel, messages, msgIndexOffset, width, key, readKeys, focus, streaming);
|
|
234
|
+
const entry: CacheEntry = syntaxHighlightMisses() !== missesBefore ? { ...drawn, highlightGeneration: syntaxHighlightGeneration() } : drawn;
|
|
235
|
+
this.apply(context, entry, base, messageLineRegistry);
|
|
236
|
+
this.entries.set(id, entry);
|
|
237
|
+
touched.add(id);
|
|
238
|
+
afterUnit?.(lastIndex, streaming);
|
|
399
239
|
}
|
|
400
240
|
|
|
401
|
-
// Mark-and-sweep: a full rebuild plans every
|
|
402
|
-
//
|
|
403
|
-
// no longer exists) and is dropped to bound memory to the visible set.
|
|
241
|
+
// Mark-and-sweep: a full rebuild plans every visible unit, so an entry not
|
|
242
|
+
// touched this pass is gone and is dropped to bound memory.
|
|
404
243
|
if (this.entries.size > touched.size) {
|
|
405
|
-
for (const
|
|
406
|
-
if (!touched.has(key)) this.entries.delete(key);
|
|
407
|
-
}
|
|
244
|
+
for (const id of this.entries.keys()) if (!touched.has(id)) this.entries.delete(id);
|
|
408
245
|
}
|
|
409
246
|
}
|
|
410
247
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
entry
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
blockBase: number,
|
|
418
|
-
kind: SystemMessageKind | undefined,
|
|
419
|
-
pendingToolKey: string | undefined,
|
|
420
|
-
turn: AssistantTurnMembership | undefined,
|
|
421
|
-
trailingBlank: boolean,
|
|
422
|
-
collapseState: Map<string, boolean>,
|
|
423
|
-
): boolean {
|
|
424
|
-
const k = entry.keyMeta;
|
|
425
|
-
if (
|
|
426
|
-
k.role !== node.message.role ||
|
|
427
|
-
k.width !== width ||
|
|
428
|
-
k.lineNumberMode !== cfg.lineNumberMode ||
|
|
429
|
-
k.collapseThreshold !== cfg.collapseThreshold ||
|
|
430
|
-
k.showThinking !== cfg.showThinking ||
|
|
431
|
-
k.showReasoningSummary !== cfg.showReasoningSummary ||
|
|
432
|
-
k.blockBase !== blockBase ||
|
|
433
|
-
k.kind !== kind ||
|
|
434
|
-
k.pendingToolKey !== pendingToolKey ||
|
|
435
|
-
k.turnKey !== turn?.turnKey ||
|
|
436
|
-
k.turnIsHead !== (turn?.isHead ?? false) ||
|
|
437
|
-
k.turnToolCount !== (turn?.toolCallCount ?? 0) ||
|
|
438
|
-
k.turnSharedLabel !== turn?.sharedToolLabel ||
|
|
439
|
-
k.turnHasReasoning !== (turn?.hasReasoning ?? false) ||
|
|
440
|
-
k.depth !== node.depth ||
|
|
441
|
-
k.connector !== node.connector ||
|
|
442
|
-
k.openAncestors !== node.openAncestorDepths.join(',') ||
|
|
443
|
-
k.trailingBlank !== trailingBlank
|
|
444
|
-
) {
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
if (!contentUnchanged(entry.contentSig, node.message)) return false;
|
|
448
|
-
for (const [key, value] of entry.collapseDeps) {
|
|
449
|
-
if (collapseState.get(key) !== value) return false;
|
|
450
|
-
}
|
|
248
|
+
private isValid(entry: CacheEntry, key: readonly Part[], focus: string | null, collapseState: Map<string, boolean>): boolean {
|
|
249
|
+
if (!sameParts(entry.key, key)) return false;
|
|
250
|
+
if (entry.highlightGeneration !== undefined && entry.highlightGeneration !== syntaxHighlightGeneration()) return false;
|
|
251
|
+
const focusHere = focus !== null && entry.focusIds.has(focus) ? focus : null;
|
|
252
|
+
if (focusHere !== entry.focusDrawn) return false;
|
|
253
|
+
for (const [k, value] of entry.collapseDeps) if (collapseState.get(k) !== value) return false;
|
|
451
254
|
return true;
|
|
452
255
|
}
|
|
453
256
|
|
|
454
|
-
/**
|
|
455
|
-
* Render a single planned row into an isolated scratch context, capturing its
|
|
456
|
-
* lines, block metas (row-relative), error-line offsets (row-relative), and
|
|
457
|
-
* the collapse-state reads it depends on. Collapse-default WRITES pass
|
|
458
|
-
* through to the real collapseState so persistent defaults match a cold render.
|
|
459
|
-
*/
|
|
460
257
|
private renderScratch(
|
|
461
258
|
context: ConversationRenderContext,
|
|
462
|
-
|
|
259
|
+
unit: TranscriptUnit,
|
|
260
|
+
turnModel: ReturnType<typeof buildConversationTurnModel> | undefined,
|
|
261
|
+
messages: readonly Message[],
|
|
262
|
+
offset: number,
|
|
463
263
|
width: number,
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
turn: AssistantTurnMembership | undefined,
|
|
469
|
-
trailingBlank: boolean,
|
|
264
|
+
key: Part[],
|
|
265
|
+
readKeys: Set<string>,
|
|
266
|
+
focus: string | null,
|
|
267
|
+
streaming: boolean,
|
|
470
268
|
): CacheEntry {
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (scratchLines.length > 0 && trailingBlank) {
|
|
501
|
-
scratchLines.push(createEmptyLine(width));
|
|
269
|
+
const lines: Line[] = [];
|
|
270
|
+
const blocks: BlockMeta[] = [];
|
|
271
|
+
const errors: number[] = [];
|
|
272
|
+
const messageLines = new Map<number, number>();
|
|
273
|
+
let focusIds: ReadonlySet<string> = new Set();
|
|
274
|
+
let tail: TurnTail | null = null;
|
|
275
|
+
|
|
276
|
+
if (unit.kind === 'turn' && turnModel) {
|
|
277
|
+
const render = drawConversationTurn(context, turnModel, width);
|
|
278
|
+
lines.push(...render.lines);
|
|
279
|
+
blocks.push(...render.blocks);
|
|
280
|
+
errors.push(...render.errorLines);
|
|
281
|
+
for (const [index, line] of render.messageLines) messageLines.set(index, line);
|
|
282
|
+
for (let index = unit.start; index <= unit.end; index++) if (!messageLines.has(index)) messageLines.set(index, 0);
|
|
283
|
+
focusIds = render.focusIds;
|
|
284
|
+
tail = render.tail;
|
|
285
|
+
} else if (unit.kind === 'message') {
|
|
286
|
+
const scratch: ConversationRenderContext = {
|
|
287
|
+
...context,
|
|
288
|
+
history: {
|
|
289
|
+
addLine: (line: Line): void => { lines.push(line); },
|
|
290
|
+
addLines: (more: Line[]): void => { for (const line of more) lines.push(line); },
|
|
291
|
+
getLineCount: (): number => lines.length,
|
|
292
|
+
},
|
|
293
|
+
blockRegistry: blocks,
|
|
294
|
+
errorLineRegistry: errors,
|
|
295
|
+
};
|
|
296
|
+
renderConversationMessageUnit(scratch, messages[unit.index - offset]!, width, unit.index);
|
|
297
|
+
messageLines.set(unit.index, 0);
|
|
502
298
|
}
|
|
299
|
+
if (lines.length > 0 && !streaming) lines.push(createEmptyLine(width));
|
|
503
300
|
|
|
504
|
-
const collapseDeps: Array<[string, boolean | undefined]> = [];
|
|
505
|
-
for (const
|
|
506
|
-
|
|
301
|
+
const collapseDeps: Array<readonly [string, boolean | undefined]> = [];
|
|
302
|
+
for (const k of readKeys) collapseDeps.push([k, context.collapseState.get(k)]);
|
|
507
303
|
return {
|
|
508
|
-
|
|
509
|
-
role: node.message.role,
|
|
510
|
-
width,
|
|
511
|
-
lineNumberMode: cfg.lineNumberMode,
|
|
512
|
-
collapseThreshold: cfg.collapseThreshold,
|
|
513
|
-
showThinking: cfg.showThinking,
|
|
514
|
-
showReasoningSummary: cfg.showReasoningSummary,
|
|
515
|
-
blockBase,
|
|
516
|
-
kind,
|
|
517
|
-
pendingToolKey,
|
|
518
|
-
turnKey: turn?.turnKey,
|
|
519
|
-
turnIsHead: turn?.isHead ?? false,
|
|
520
|
-
turnToolCount: turn?.toolCallCount ?? 0,
|
|
521
|
-
turnSharedLabel: turn?.sharedToolLabel,
|
|
522
|
-
turnHasReasoning: turn?.hasReasoning ?? false,
|
|
523
|
-
depth: node.depth,
|
|
524
|
-
connector: node.connector,
|
|
525
|
-
openAncestors: node.openAncestorDepths.join(','),
|
|
526
|
-
trailingBlank,
|
|
527
|
-
},
|
|
528
|
-
contentSig: contentSigOf(node.message),
|
|
529
|
-
lines: scratchLines,
|
|
530
|
-
blocks: scratchBlocks.slice(blockBase),
|
|
531
|
-
errorRelLines: scratchErrors,
|
|
304
|
+
key,
|
|
532
305
|
collapseDeps,
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
306
|
+
focusIds,
|
|
307
|
+
focusDrawn: focus !== null && focusIds.has(focus) ? focus : null,
|
|
308
|
+
lines,
|
|
309
|
+
blocks,
|
|
310
|
+
errorRelLines: errors,
|
|
311
|
+
messageLines,
|
|
312
|
+
tail,
|
|
536
313
|
};
|
|
537
314
|
}
|
|
538
315
|
|
|
539
316
|
/**
|
|
540
317
|
* Replay an entry into the live context at buffer offset `base`. Line objects
|
|
541
|
-
* are shared (never mutated
|
|
542
|
-
* separate back-buffer). Block/error rebasing is memoised per base so an
|
|
543
|
-
* unchanged message pays zero allocation across rebuilds.
|
|
318
|
+
* are shared (never mutated after production).
|
|
544
319
|
*/
|
|
545
|
-
private
|
|
320
|
+
private apply(context: ConversationRenderContext, entry: CacheEntry, base: number, messageLineRegistry: number[]): void {
|
|
546
321
|
context.history.addLines(entry.lines);
|
|
547
|
-
|
|
548
|
-
if (entry.appliedBase !== base || entry.appliedBlocks === null || entry.appliedErrors === null) {
|
|
549
|
-
entry.appliedBlocks = entry.blocks.map((b) => ({ ...b, startLine: b.startLine + base }));
|
|
550
|
-
entry.appliedErrors = entry.errorRelLines.map((e) => e + base);
|
|
551
|
-
entry.appliedBase = base;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
322
|
const registry = context.blockRegistry;
|
|
555
|
-
for (const block of entry.
|
|
556
|
-
const
|
|
557
|
-
for (const line of entry.
|
|
323
|
+
for (const block of entry.blocks) registry.push({ ...block, blockIndex: registry.length, startLine: block.startLine + base });
|
|
324
|
+
for (const line of entry.errorRelLines) context.errorLineRegistry.push(line + base);
|
|
325
|
+
for (const [index, line] of entry.messageLines) messageLineRegistry[index] = base + line;
|
|
326
|
+
this._lastTail = entry.tail;
|
|
558
327
|
}
|
|
559
328
|
}
|