@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,139 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.0.0] — 2026-07-03
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
- d26fc23f ci: drop temporary renderer-excellence branch trigger after merge
|
|
11
|
+
- 532c900b integrate renderer-excellence: renderer performance and correctness effort (v1.0.0 scope)
|
|
12
|
+
- cdf5931d fix(input): normalize CR line endings from bracketed pastes
|
|
13
|
+
- 1318959f chore(perf): re-capture baseline reference medians on a quiet box
|
|
14
|
+
- 946a2c83 integrate WO-210 into renderer-excellence
|
|
15
|
+
- 209ade5f perf(budgets): ratchet transcript.append_one 20ms->6ms; streaming tail proven non-material (WO-210)
|
|
16
|
+
- c7e445c2 integrate WO-209 into renderer-excellence
|
|
17
|
+
- df4d8d03 integrate WO-208 into renderer-excellence
|
|
18
|
+
- f5476715 perf(conversation): per-message Line[] cache — append-one 45ms→0.9ms (WO-209)
|
|
19
|
+
- 76539cd8 perf(renderer): WO-208 coalesce main.ts render() fan-out into one same-tick composite
|
|
20
|
+
- 204342c4 refactor(renderer): delete the six dead exports parked during the R2 literal-ownership split
|
|
21
|
+
- 9c1dbdae integrate WO-207 into renderer-excellence
|
|
22
|
+
- ae660b2a integrate WO-206 into renderer-excellence
|
|
23
|
+
- 6acd74f6 integrate WO-205 into renderer-excellence
|
|
24
|
+
- c6eb004c feat(architecture): WO-206 no-unused-exports rule for src/renderer
|
|
25
|
+
- 8997719b feat(renderer): WO-207 — palette unification across the renderer layer
|
|
26
|
+
- 3ec18bb1 refactor(renderer): WO-205 dedup + dead code + honest picker
|
|
27
|
+
- 43eb8165 ci: run full CI on renderer-excellence pushes for the v1.0.0 effort
|
|
28
|
+
- 6e483f55 fix(renderer): diff panel keeps its shipped colors — the panels are the reference, not diff-view
|
|
29
|
+
- 50755108 integrate WO-204 into renderer-excellence
|
|
30
|
+
- 17425f6a integrate WO-203 into renderer-excellence
|
|
31
|
+
- 68bc13c3 fix(renderer): WO-203 verified correctness fixes
|
|
32
|
+
- 721f4d91 feat(renderer): WO-204 — restore conversation diff rendering, align diff surface colors
|
|
33
|
+
- c5aff410 integrate WO-202 into renderer-excellence
|
|
34
|
+
- 0f03a124 integrate WO-201 into renderer-excellence
|
|
35
|
+
- e5ba2978 test(renderer): WO-201 golden contract expansion — splash, transcript scenes, all overlays
|
|
36
|
+
- 210d5814 perf(scripts): WO-202 line-production benchmarks above the compositor
|
|
37
|
+
- bf6459af fix(tests): skills delete test polls the rescanned state, not just file removal
|
|
38
|
+
|
|
39
|
+
## [0.29.0] — 2026-07-03
|
|
40
|
+
|
|
41
|
+
### Changes
|
|
42
|
+
- 2842a791 perf(ci): parallelize the per-file test runner; raise the test job timeout
|
|
43
|
+
- 218bb217 merge: panel-system remediation — 38 work orders, consoles not signposts (0.29.0)
|
|
44
|
+
- a37c028e fix(input): WO-160 smoke findings — overlay write-back clobber, /ops view parity, panel-list honesty, modal viewport
|
|
45
|
+
- 5741f633 fix(tests): isolate GitPanel non-repo test from the runner's in-tree TMPDIR
|
|
46
|
+
- 6eaea7c4 fix(panels): WO-160 consistency sweep — alias tests, action-substitute signposts
|
|
47
|
+
- 42164e34 fix(panels): cockpit c/i leave the key unconsumed on terminal or absent roster entries
|
|
48
|
+
- 10247db1 feat(input): make Ctrl+O a real keybind for the Ops Control panel
|
|
49
|
+
- ac598536 refactor(panels): route selected-row reads through getSelectedItem and ban raw [this.selectedIndex]
|
|
50
|
+
- 34bac3f0 chore(panels): tighten hex-literal baseline to exact actual counts
|
|
51
|
+
- 3d2caf90 integrate WO-153 into panel-ux-remediation
|
|
52
|
+
- 1f0ed5ca integrate WO-152 into panel-ux-remediation
|
|
53
|
+
- 8b6fbce6 integrate WO-151 into panel-ux-remediation
|
|
54
|
+
- ce0b7cb8 fix(panels): closure-audit findings — incident/settings-sync filtered desyncs, subscription start signposts
|
|
55
|
+
- ba7db35e feat(panels): WO-152 registry taxonomy, icons, always-register, lifecycle flags
|
|
56
|
+
- 7cbdd4ec feat(panels): WO-153 filter-model convergence onto ScrollableListPanel
|
|
57
|
+
- 40cb9153 feat(panels): WO-151 keybindings, help discoverability, one hint grammar
|
|
58
|
+
- 782e7752 fix(panels): services test/detail act on the filtered selection
|
|
59
|
+
- e8783a89 integrate WO-154 into panel-ux-remediation
|
|
60
|
+
- cdb399b9 integrate WO-140 into panel-ux-remediation
|
|
61
|
+
- 943089ea integrate WO-139 into panel-ux-remediation
|
|
62
|
+
- d527c223 integrate WO-141 into panel-ux-remediation
|
|
63
|
+
- a65ee307 integrate WO-150 into panel-ux-remediation
|
|
64
|
+
- a73e874b feat(panels): WO-140 planning cluster polish (wrfc/project-planning/work-plan)
|
|
65
|
+
- e1921ab8 feat(panels): WO-141 keep-panel hardening + residual signpost sweep
|
|
66
|
+
- 8b018746 feat(panels): WO-139 provider-cost leftovers — subscription/local-auth/cost-tracker
|
|
67
|
+
- da7b9550 feat(panels): WO-150 unify focus ownership in PanelManager
|
|
68
|
+
- 4c569ae8 feat(shell): WO-154 main-UX shell splash, footer cost/compact, overlay posture
|
|
69
|
+
- 7218349c fix(panels): wave 3a review findings — filtered-selection desyncs, residual signposts, dead constants
|
|
70
|
+
- 1cc9f195 integrate WO-138 into panel-ux-remediation
|
|
71
|
+
- 179a9a91 integrate WO-137 into panel-ux-remediation
|
|
72
|
+
- 4df3036e integrate WO-136 into panel-ux-remediation
|
|
73
|
+
- 20533671 integrate WO-135 into panel-ux-remediation
|
|
74
|
+
- 2f116f82 integrate WO-134 into panel-ux-remediation
|
|
75
|
+
- 595df267 integrate WO-133 into panel-ux-remediation
|
|
76
|
+
- 490dc881 feat(panels): WO-137 diagnostics trio — security, debug, system-messages
|
|
77
|
+
- 066fd91a feat(panels): WO-138 session-comm quad real actions (remote/communication/routes/qr)
|
|
78
|
+
- a9a1ebe0 feat(panels): WO-134 Plugins + Hooks control rooms
|
|
79
|
+
- e4380896 feat(panels): WO-136 Docs + Intelligence live actions
|
|
80
|
+
- 8f6a3f45 feat(panels): WO-133 explorer + preview lazy loading, git decorations, diff/reload keys
|
|
81
|
+
- 744dc460 feat(panels): WO-135 marketplace + skills real actions
|
|
82
|
+
- a04c86e9 fix(panels): reserve summary row inside stacked scrollable section budgets
|
|
83
|
+
- b2a43d9b fix(panels): reserve a row for the window-summary inside the scrollable budget
|
|
84
|
+
- acb3e3b5 fix(panels): bind w to dispatch teamwork review from Tasks; stop git panel swallowing i on loaded repos
|
|
85
|
+
- 4bc04330 integrate WO-130 into panel-ux-remediation
|
|
86
|
+
- e15c010b integrate WO-128 into panel-ux-remediation
|
|
87
|
+
- 3020d2e1 integrate WO-127 into panel-ux-remediation
|
|
88
|
+
- 1a0c0d76 integrate WO-132 into panel-ux-remediation
|
|
89
|
+
- 79ac40bd integrate WO-131 into panel-ux-remediation
|
|
90
|
+
- 61092aa0 integrate WO-126 into panel-ux-remediation
|
|
91
|
+
- f83d6820 feat(panels): WO-131 agent-runtime trio — tools + tasks + orchestration
|
|
92
|
+
- 8dafc5c0 feat(panels): WO-132 Git + Diff consoles — stage/unstage/commit, explicit init confirm, self-loading diff panel
|
|
93
|
+
- 24c5127e feat(panels): WO-128 plan dashboard revival
|
|
94
|
+
- 2c8e8d1f feat(panels): WO-126 symbol outline on tree-sitter
|
|
95
|
+
- 2d424e1c feat(panels): WO-130 Cockpit operator polish
|
|
96
|
+
- 66c0deeb feat(panels): WO-127 eval console — baseline seed, live run, regressions
|
|
97
|
+
- 1a53ef2c fix(panels): drop redundant slash-command guidance from knowledge review mode
|
|
98
|
+
- 16ba104d fix(tests): operator-surfaces gate verifies forensics alias resolves to incident
|
|
99
|
+
- a5b8f4e9 integrate FOLLOWUPS into panel-ux-remediation
|
|
100
|
+
- f60988eb integrate WO-125 into panel-ux-remediation
|
|
101
|
+
- 39996bfd integrate WO-124 into panel-ux-remediation
|
|
102
|
+
- 78fb4916 integrate WO-123 into panel-ux-remediation
|
|
103
|
+
- e5294103 integrate WO-122 into panel-ux-remediation
|
|
104
|
+
- 371f4d60 integrate WO-121 into panel-ux-remediation
|
|
105
|
+
- 1ff1e0ae integrate WO-120 into panel-ux-remediation
|
|
106
|
+
- 933b09a3 feat(panels): WO-120 register ops-control panel and wire operator actions
|
|
107
|
+
- 39bfc1a1 feat(panels): WO-125 wire worktree and sandbox lifecycle verbs to their registries
|
|
108
|
+
- 4992fd03 feat(panels): WO-121 control-plane approvals console
|
|
109
|
+
- ea1c47b0 feat(panels): WO-123 knowledge console replaces static command catalogue
|
|
110
|
+
- 91273a60 feat(panels): WO-124 settings-sync console — resolve conflicts in-panel, Tab browse modes
|
|
111
|
+
- 74f5d39d feat(panels): WO-122 policy governance console actions and depth
|
|
112
|
+
- f4679a7e fix(panels): broaden hex-literal ratchet to 3/6/8-digit forms, clear remaining raw hex in fullscreen-primitives and diff-panel
|
|
113
|
+
- 6e1b814c fix(panels): forensics resolves as alias to incident, not duplicate registration
|
|
114
|
+
- 6a0f4405 fix(tests): operator-surfaces gate asserts merged provider-health id instead of retired providers id
|
|
115
|
+
- 09d81371 fix(tests): retarget legacy-turn-bus ban from deleted context-visualizer-panel to its WO-113 successor token-budget-panel
|
|
116
|
+
- e835a821 integrate WO-114 into panel-ux-remediation
|
|
117
|
+
- 433b4c25 integrate WO-113 into panel-ux-remediation
|
|
118
|
+
- 70466337 integrate WO-112 into panel-ux-remediation
|
|
119
|
+
- a1ebf5da fix(tests): contract module count 30 after WO-110+WO-111 both deleted a module
|
|
120
|
+
- fd25fb19 integrate WO-111 into panel-ux-remediation
|
|
121
|
+
- 0d72a2ef integrate WO-110 into panel-ux-remediation
|
|
122
|
+
- 0a344b60 WO-112: Provider console merge: providers + accounts into provider-health
|
|
123
|
+
- d4c7c8b4 WO-111: Automation console merge: schedule + watchers into automation
|
|
124
|
+
- 1fc22d27 WO-110: Agent console merge: agent-logs into inspector
|
|
125
|
+
- 0d4858ff WO-114: Incident console merge: forensics into incident
|
|
126
|
+
- a4cdfb3c WO-113: Context/token merge: context into tokens
|
|
127
|
+
- fd8e84ef integrate WO-002 into panel-ux-remediation
|
|
128
|
+
- 1a85ece3 WO-002: Panel palette and chrome sweep (extendPalette everywhere)
|
|
129
|
+
- 1cd556da integrate WO-006 into panel-ux-remediation
|
|
130
|
+
- d6effd51 integrate WO-005 into panel-ux-remediation
|
|
131
|
+
- d7735d20 integrate WO-004 into panel-ux-remediation
|
|
132
|
+
- 9b1de69a integrate WO-003 into panel-ux-remediation
|
|
133
|
+
- e149323f integrate WO-001 into panel-ux-remediation
|
|
134
|
+
- 45e90fa7 WO-006: Shared-test decongestion: per-panel spec modules
|
|
135
|
+
- 2d7d5777 WO-001: Theme-token backbone + architecture-gate hex enforcement
|
|
136
|
+
- 207cd46b WO-003: Action-callback plumbing pattern + ConfirmState generalization
|
|
137
|
+
- abf6771b WO-005: session-maintenance evaluator dedup
|
|
138
|
+
- 4fe16129 WO-004: Dead code and dead keybinding removal
|
|
139
|
+
|
|
7
140
|
## [0.28.0] — 2026-06-30
|
|
8
141
|
|
|
9
142
|
Panel navigation & UI/UX overhaul, plus the accumulated post-0.27.0 UX fixes.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversation-line-cache.ts — per-message Line[] production cache (WO-209).
|
|
3
|
+
*
|
|
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
|
+
* (45.5 ms p50 / 71.4 MB / 695 k objects per rebuild), because
|
|
7
|
+
* ConversationManager.rebuildHistory() clears the buffer and calls
|
|
8
|
+
* appendConversationMessages() over the entire snapshot on every dirty flag —
|
|
9
|
+
* and markDirty() fires on every mutation. The marginal work for one appended
|
|
10
|
+
* message is ~1/1000th of that.
|
|
11
|
+
*
|
|
12
|
+
* This module memoises the per-message render. Each message's rendered output is
|
|
13
|
+
* a PURE function of its complete inputs:
|
|
14
|
+
* - message identity + content (via a content signature; snapshots are fresh
|
|
15
|
+
* structuredClone copies each call, so string content compares by value and
|
|
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
|
+
* - the live values of every collapseState key the render READS (recorded via a
|
|
29
|
+
* proxy during a miss; a collapse toggle flips a recorded value and
|
|
30
|
+
* invalidates exactly the owning message)
|
|
31
|
+
*
|
|
32
|
+
* Invalidation is by comparison of those complete inputs — the file-preview
|
|
33
|
+
* contentVersion precedent generalised: instead of a single version counter we
|
|
34
|
+
* compare the full input tuple, which is provably complete (a from-scratch
|
|
35
|
+
* rebuild reads nothing else).
|
|
36
|
+
*
|
|
37
|
+
* Correctness contract: a cache-served rebuild is BYTE-IDENTICAL to a cold
|
|
38
|
+
* appendConversationMessages() rebuild. This is guaranteed by construction — a
|
|
39
|
+
* miss renders through the exact same per-message render functions into an
|
|
40
|
+
* isolated scratch context, and the captured lines / block metas / error lines
|
|
41
|
+
* are replayed at the same buffer offsets a cold render would have produced.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { createEmptyLine, type Line } from '../types/grid.ts';
|
|
45
|
+
import type { BlockMeta } from './conversation-types.ts';
|
|
46
|
+
import type { ConversationRenderContext } from './conversation-rendering.ts';
|
|
47
|
+
import {
|
|
48
|
+
renderConversationAssistantMessage,
|
|
49
|
+
renderConversationSystemMessage,
|
|
50
|
+
renderConversationToolMessage,
|
|
51
|
+
renderConversationUserMessage,
|
|
52
|
+
} from './conversation-rendering.ts';
|
|
53
|
+
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
54
|
+
// SystemMessageKind imported from runtime directly to avoid a cycle, mirroring
|
|
55
|
+
// conversation-rendering.ts's own import.
|
|
56
|
+
import type { SystemMessageKind } from '@/runtime/index.ts';
|
|
57
|
+
|
|
58
|
+
type Message = ConversationMessageSnapshot;
|
|
59
|
+
|
|
60
|
+
/** The display-config values every message render depends on. */
|
|
61
|
+
interface RenderConfig {
|
|
62
|
+
readonly lineNumberMode: 'all' | 'code' | 'off';
|
|
63
|
+
readonly collapseThreshold: number;
|
|
64
|
+
readonly showThinking: boolean;
|
|
65
|
+
readonly showReasoningSummary: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Content signature: the render-relevant fields of a message, captured so an
|
|
70
|
+
* unchanged message is recognised without re-rendering. Array-valued fields are
|
|
71
|
+
* pre-serialised to strings so equality is a value comparison.
|
|
72
|
+
*/
|
|
73
|
+
interface ContentSig {
|
|
74
|
+
readonly role: Message['role'];
|
|
75
|
+
/** String content, or JSON of ContentPart[] for array-valued user content. */
|
|
76
|
+
readonly content: string;
|
|
77
|
+
readonly cancelled?: boolean;
|
|
78
|
+
readonly model?: string;
|
|
79
|
+
readonly provider?: string;
|
|
80
|
+
readonly reasoningContent?: string;
|
|
81
|
+
readonly reasoningSummary?: string;
|
|
82
|
+
readonly toolCallsJson?: string;
|
|
83
|
+
readonly callId?: string;
|
|
84
|
+
readonly toolName?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Non-content inputs that key the cache entry. */
|
|
88
|
+
interface KeyMeta {
|
|
89
|
+
readonly role: Message['role'];
|
|
90
|
+
readonly width: number;
|
|
91
|
+
readonly lineNumberMode: 'all' | 'code' | 'off';
|
|
92
|
+
readonly collapseThreshold: number;
|
|
93
|
+
readonly showThinking: boolean;
|
|
94
|
+
readonly showReasoningSummary: boolean;
|
|
95
|
+
readonly blockBase: number;
|
|
96
|
+
readonly kind: SystemMessageKind | undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface CacheEntry {
|
|
100
|
+
readonly keyMeta: KeyMeta;
|
|
101
|
+
readonly contentSig: ContentSig;
|
|
102
|
+
/** Rendered lines for this message, INCLUDING the trailing blank line. */
|
|
103
|
+
readonly lines: Line[];
|
|
104
|
+
/** Block metas with startLine RELATIVE to the message's first line. */
|
|
105
|
+
readonly blocks: BlockMeta[];
|
|
106
|
+
/** Error-navigation line offsets, RELATIVE to the message's first line. */
|
|
107
|
+
readonly errorRelLines: number[];
|
|
108
|
+
/** [collapseKey, value] pairs the render read; a change invalidates the entry. */
|
|
109
|
+
readonly collapseDeps: Array<[string, boolean | undefined]>;
|
|
110
|
+
/** Memoised rebase of blocks/errors at appliedBase (avoids realloc when the
|
|
111
|
+
* message's buffer offset is unchanged across rebuilds — the common case). */
|
|
112
|
+
appliedBase: number;
|
|
113
|
+
appliedBlocks: BlockMeta[] | null;
|
|
114
|
+
appliedErrors: number[] | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Build the content signature for a message. */
|
|
118
|
+
function contentSigOf(m: Message): ContentSig {
|
|
119
|
+
if (m.role === 'user') {
|
|
120
|
+
return {
|
|
121
|
+
role: 'user',
|
|
122
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
123
|
+
cancelled: m.cancelled,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (m.role === 'assistant') {
|
|
127
|
+
return {
|
|
128
|
+
role: 'assistant',
|
|
129
|
+
content: m.content,
|
|
130
|
+
model: m.model,
|
|
131
|
+
provider: m.provider,
|
|
132
|
+
reasoningContent: m.reasoningContent,
|
|
133
|
+
reasoningSummary: m.reasoningSummary,
|
|
134
|
+
toolCallsJson: m.toolCalls ? JSON.stringify(m.toolCalls) : undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (m.role === 'system') {
|
|
138
|
+
return { role: 'system', content: m.content };
|
|
139
|
+
}
|
|
140
|
+
return { role: 'tool', content: m.content, callId: m.callId, toolName: m.toolName };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compare a stored signature against a message WITHOUT allocating a new
|
|
145
|
+
* signature object on the hot (unchanged string-content) path. Array-valued
|
|
146
|
+
* fields fall back to JSON serialisation (rare).
|
|
147
|
+
*/
|
|
148
|
+
function contentUnchanged(sig: ContentSig, m: Message): boolean {
|
|
149
|
+
if (sig.role !== m.role) return false;
|
|
150
|
+
switch (m.role) {
|
|
151
|
+
case 'user':
|
|
152
|
+
return (
|
|
153
|
+
sig.content === (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)) &&
|
|
154
|
+
sig.cancelled === m.cancelled
|
|
155
|
+
);
|
|
156
|
+
case 'assistant':
|
|
157
|
+
return (
|
|
158
|
+
sig.content === m.content &&
|
|
159
|
+
sig.model === m.model &&
|
|
160
|
+
sig.provider === m.provider &&
|
|
161
|
+
sig.reasoningContent === m.reasoningContent &&
|
|
162
|
+
sig.reasoningSummary === m.reasoningSummary &&
|
|
163
|
+
sig.toolCallsJson === (m.toolCalls ? JSON.stringify(m.toolCalls) : undefined)
|
|
164
|
+
);
|
|
165
|
+
case 'system':
|
|
166
|
+
return sig.content === m.content;
|
|
167
|
+
case 'tool':
|
|
168
|
+
return sig.content === m.content && sig.callId === m.callId && sig.toolName === m.toolName;
|
|
169
|
+
default:
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Wrap the real collapseState so reads (.get/.has) are recorded while writes
|
|
176
|
+
* (.set — the auto-collapse default) pass straight through to the real map, so
|
|
177
|
+
* the persistent collapse defaults are established exactly as a cold render
|
|
178
|
+
* would establish them.
|
|
179
|
+
*/
|
|
180
|
+
function makeRecordingCollapseState(
|
|
181
|
+
real: Map<string, boolean>,
|
|
182
|
+
readKeys: Set<string>,
|
|
183
|
+
): Map<string, boolean> {
|
|
184
|
+
return new Proxy(real, {
|
|
185
|
+
get(target, prop, receiver) {
|
|
186
|
+
if (prop === 'get') {
|
|
187
|
+
return (key: string): boolean | undefined => {
|
|
188
|
+
readKeys.add(key);
|
|
189
|
+
return target.get(key);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (prop === 'has') {
|
|
193
|
+
return (key: string): boolean => {
|
|
194
|
+
readKeys.add(key);
|
|
195
|
+
return target.has(key);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
void receiver;
|
|
199
|
+
const value = Reflect.get(target, prop, target);
|
|
200
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Dispatch a single message to its render function. */
|
|
206
|
+
function renderOne(
|
|
207
|
+
ctx: ConversationRenderContext,
|
|
208
|
+
message: Message,
|
|
209
|
+
width: number,
|
|
210
|
+
absoluteIdx: number,
|
|
211
|
+
cfg: RenderConfig,
|
|
212
|
+
): void {
|
|
213
|
+
if (message.role === 'user') {
|
|
214
|
+
renderConversationUserMessage(ctx, message, width);
|
|
215
|
+
} else if (message.role === 'assistant') {
|
|
216
|
+
renderConversationAssistantMessage(ctx, message, width, cfg.lineNumberMode, cfg.collapseThreshold, absoluteIdx);
|
|
217
|
+
} else if (message.role === 'system') {
|
|
218
|
+
renderConversationSystemMessage(ctx, message, width, absoluteIdx);
|
|
219
|
+
} else if (message.role === 'tool') {
|
|
220
|
+
renderConversationToolMessage(ctx, message, width, absoluteIdx);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* MessageLineCache — per-message Line[] memoisation for ConversationManager.
|
|
226
|
+
*
|
|
227
|
+
* Keyed by absolute message index; each entry validates its COMPLETE input tuple
|
|
228
|
+
* before serving. A rebuild that reuses entries is byte-identical to a cold
|
|
229
|
+
* rebuild; the cache is a pure memoisation with no observable behaviour of its
|
|
230
|
+
* own beyond speed and reduced allocation churn.
|
|
231
|
+
*/
|
|
232
|
+
export class MessageLineCache {
|
|
233
|
+
private entries: Map<number, CacheEntry> = new Map();
|
|
234
|
+
|
|
235
|
+
/** Drop all cached entries (wholesale message replacement / reset). */
|
|
236
|
+
public clear(): void {
|
|
237
|
+
this.entries.clear();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Number of retained entries (for tests / diagnostics). */
|
|
241
|
+
public get size(): number {
|
|
242
|
+
return this.entries.size;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Render `messages` into `context`, reusing cached lines for unchanged
|
|
247
|
+
* messages. Mirrors appendConversationMessages exactly on a cold cache; a warm
|
|
248
|
+
* cache replays identical bytes at identical offsets.
|
|
249
|
+
*
|
|
250
|
+
* @param context the live render context (real history buffer,
|
|
251
|
+
* block/error registries, collapse state, config).
|
|
252
|
+
* @param messages the visible message slice to render.
|
|
253
|
+
* @param width render width.
|
|
254
|
+
* @param messageLineRegistry absolute-index → first-line map, written here.
|
|
255
|
+
* @param msgIndexOffset absolute index of messages[0] (post-clearDisplay slice).
|
|
256
|
+
* @param streamingPlaceholderAbsIdx absolute index of the in-progress streaming
|
|
257
|
+
* placeholder to leave uncached (-1 when not streaming);
|
|
258
|
+
* the incremental streaming path owns its content.
|
|
259
|
+
*/
|
|
260
|
+
public renderInto(
|
|
261
|
+
context: ConversationRenderContext,
|
|
262
|
+
messages: Message[],
|
|
263
|
+
width: number,
|
|
264
|
+
messageLineRegistry: number[],
|
|
265
|
+
msgIndexOffset: number,
|
|
266
|
+
streamingPlaceholderAbsIdx: number,
|
|
267
|
+
): void {
|
|
268
|
+
const cfg: RenderConfig = {
|
|
269
|
+
lineNumberMode: context.configManager?.get('display.lineNumbers') ?? 'off',
|
|
270
|
+
collapseThreshold: context.configManager?.get('display.collapseThreshold') ?? 30,
|
|
271
|
+
showThinking: context.configManager?.get('display.showThinking') ?? false,
|
|
272
|
+
showReasoningSummary: context.configManager?.get('display.showReasoningSummary') ?? false,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const touched = new Set<number>();
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < messages.length; i++) {
|
|
278
|
+
const message = messages[i]!;
|
|
279
|
+
const absoluteIdx = msgIndexOffset + i;
|
|
280
|
+
const base = context.history.getLineCount();
|
|
281
|
+
const blockBase = context.blockRegistry.length;
|
|
282
|
+
messageLineRegistry[absoluteIdx] = base;
|
|
283
|
+
|
|
284
|
+
const uncacheable = absoluteIdx === streamingPlaceholderAbsIdx;
|
|
285
|
+
const kind = context.messageKindRegistry.get(absoluteIdx);
|
|
286
|
+
|
|
287
|
+
if (!uncacheable) {
|
|
288
|
+
const existing = this.entries.get(absoluteIdx);
|
|
289
|
+
if (existing && this.isValid(existing, message, width, cfg, blockBase, kind, context.collapseState)) {
|
|
290
|
+
this.applyEntry(context, existing, base);
|
|
291
|
+
touched.add(absoluteIdx);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const entry = this.renderScratch(context, message, width, absoluteIdx, cfg, blockBase, kind);
|
|
297
|
+
this.applyEntry(context, entry, base);
|
|
298
|
+
if (!uncacheable) {
|
|
299
|
+
this.entries.set(absoluteIdx, entry);
|
|
300
|
+
touched.add(absoluteIdx);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Mark-and-sweep: a full rebuild renders every currently-visible message, so
|
|
305
|
+
// any entry not touched this pass is off-screen (or evicted by a shrink) and
|
|
306
|
+
// is dropped to bound memory to the visible set.
|
|
307
|
+
if (this.entries.size > touched.size) {
|
|
308
|
+
for (const key of this.entries.keys()) {
|
|
309
|
+
if (!touched.has(key)) this.entries.delete(key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Validate a cached entry against the message's current complete inputs. */
|
|
315
|
+
private isValid(
|
|
316
|
+
entry: CacheEntry,
|
|
317
|
+
message: Message,
|
|
318
|
+
width: number,
|
|
319
|
+
cfg: RenderConfig,
|
|
320
|
+
blockBase: number,
|
|
321
|
+
kind: SystemMessageKind | undefined,
|
|
322
|
+
collapseState: Map<string, boolean>,
|
|
323
|
+
): boolean {
|
|
324
|
+
const k = entry.keyMeta;
|
|
325
|
+
if (
|
|
326
|
+
k.role !== message.role ||
|
|
327
|
+
k.width !== width ||
|
|
328
|
+
k.lineNumberMode !== cfg.lineNumberMode ||
|
|
329
|
+
k.collapseThreshold !== cfg.collapseThreshold ||
|
|
330
|
+
k.showThinking !== cfg.showThinking ||
|
|
331
|
+
k.showReasoningSummary !== cfg.showReasoningSummary ||
|
|
332
|
+
k.blockBase !== blockBase ||
|
|
333
|
+
k.kind !== kind
|
|
334
|
+
) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (!contentUnchanged(entry.contentSig, message)) return false;
|
|
338
|
+
for (const [key, value] of entry.collapseDeps) {
|
|
339
|
+
if (collapseState.get(key) !== value) return false;
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Render a single message into an isolated scratch context, capturing its
|
|
346
|
+
* lines, block metas (message-relative), error-line offsets (message-relative),
|
|
347
|
+
* and the collapse-state reads it depends on. Collapse-default WRITES pass
|
|
348
|
+
* through to the real collapseState so persistent defaults match a cold render.
|
|
349
|
+
*/
|
|
350
|
+
private renderScratch(
|
|
351
|
+
context: ConversationRenderContext,
|
|
352
|
+
message: Message,
|
|
353
|
+
width: number,
|
|
354
|
+
absoluteIdx: number,
|
|
355
|
+
cfg: RenderConfig,
|
|
356
|
+
blockBase: number,
|
|
357
|
+
kind: SystemMessageKind | undefined,
|
|
358
|
+
): CacheEntry {
|
|
359
|
+
const scratchLines: Line[] = [];
|
|
360
|
+
const scratchHistory = {
|
|
361
|
+
addLine: (line: Line): void => { scratchLines.push(line); },
|
|
362
|
+
addLines: (lines: Line[]): void => { for (const line of lines) scratchLines.push(line); },
|
|
363
|
+
getLineCount: (): number => scratchLines.length,
|
|
364
|
+
};
|
|
365
|
+
// Pre-size to blockBase so blockRegistry.length (the global block index the
|
|
366
|
+
// collapseKey embeds) matches the live registry; slice(blockBase) recovers
|
|
367
|
+
// only this message's blocks afterwards.
|
|
368
|
+
const scratchBlocks: BlockMeta[] = new Array(blockBase);
|
|
369
|
+
const scratchErrors: number[] = [];
|
|
370
|
+
const readKeys = new Set<string>();
|
|
371
|
+
const recordingCollapse = makeRecordingCollapseState(context.collapseState, readKeys);
|
|
372
|
+
|
|
373
|
+
const scratchCtx: ConversationRenderContext = {
|
|
374
|
+
history: scratchHistory,
|
|
375
|
+
blockRegistry: scratchBlocks,
|
|
376
|
+
collapseState: recordingCollapse,
|
|
377
|
+
errorLineRegistry: scratchErrors,
|
|
378
|
+
messageKindRegistry: context.messageKindRegistry,
|
|
379
|
+
configManager: context.configManager,
|
|
380
|
+
splashOptions: context.splashOptions,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
renderOne(scratchCtx, message, width, absoluteIdx, cfg);
|
|
384
|
+
// Trailing blank line, exactly as appendConversationMessages appends per message.
|
|
385
|
+
scratchLines.push(createEmptyLine(width));
|
|
386
|
+
|
|
387
|
+
const collapseDeps: Array<[string, boolean | undefined]> = [];
|
|
388
|
+
for (const key of readKeys) collapseDeps.push([key, context.collapseState.get(key)]);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
keyMeta: {
|
|
392
|
+
role: message.role,
|
|
393
|
+
width,
|
|
394
|
+
lineNumberMode: cfg.lineNumberMode,
|
|
395
|
+
collapseThreshold: cfg.collapseThreshold,
|
|
396
|
+
showThinking: cfg.showThinking,
|
|
397
|
+
showReasoningSummary: cfg.showReasoningSummary,
|
|
398
|
+
blockBase,
|
|
399
|
+
kind,
|
|
400
|
+
},
|
|
401
|
+
contentSig: contentSigOf(message),
|
|
402
|
+
lines: scratchLines,
|
|
403
|
+
blocks: scratchBlocks.slice(blockBase),
|
|
404
|
+
errorRelLines: scratchErrors,
|
|
405
|
+
collapseDeps,
|
|
406
|
+
appliedBase: -1,
|
|
407
|
+
appliedBlocks: null,
|
|
408
|
+
appliedErrors: null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Replay an entry into the live context at buffer offset `base`. Line objects
|
|
414
|
+
* are shared (never mutated post-production — the compositor reads them into a
|
|
415
|
+
* separate back-buffer). Block/error rebasing is memoised per base so an
|
|
416
|
+
* unchanged message pays zero allocation across rebuilds.
|
|
417
|
+
*/
|
|
418
|
+
private applyEntry(context: ConversationRenderContext, entry: CacheEntry, base: number): void {
|
|
419
|
+
context.history.addLines(entry.lines);
|
|
420
|
+
|
|
421
|
+
if (entry.appliedBase !== base || entry.appliedBlocks === null || entry.appliedErrors === null) {
|
|
422
|
+
entry.appliedBlocks = entry.blocks.map((b) => ({ ...b, startLine: b.startLine + base }));
|
|
423
|
+
entry.appliedErrors = entry.errorRelLines.map((e) => e + base);
|
|
424
|
+
entry.appliedBase = base;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const registry = context.blockRegistry;
|
|
428
|
+
for (const block of entry.appliedBlocks) registry.push(block);
|
|
429
|
+
const errors = context.errorLineRegistry;
|
|
430
|
+
for (const line of entry.appliedErrors) errors.push(line);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
2
2
|
import { renderMarkdownTracked } from '../renderer/markdown.ts';
|
|
3
|
+
import { renderDiffView } from '../renderer/diff-view.ts';
|
|
3
4
|
import { DARK_THEME } from '../renderer/theme.ts';
|
|
4
5
|
import { renderToolCallBlock } from '../renderer/tool-call.ts';
|
|
5
6
|
import { renderThinkingBlock } from '../renderer/thinking.ts';
|
|
@@ -41,7 +42,7 @@ function summarizeCallId(callId: string, maxLength = 24): string {
|
|
|
41
42
|
return callId.length <= maxLength ? callId : `${callId.slice(0, maxLength - 1)}…`;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
interface ConversationRenderContext {
|
|
45
|
+
export interface ConversationRenderContext {
|
|
45
46
|
readonly history: {
|
|
46
47
|
addLine: (line: Line) => void;
|
|
47
48
|
addLines: (lines: Line[]) => void;
|
|
@@ -231,6 +232,9 @@ export function renderConversationToolMessage(
|
|
|
231
232
|
const hasHunk = contentLines.some((l) => l.startsWith('@@ '));
|
|
232
233
|
const isDiff = hasDiffHeader && hasHunk;
|
|
233
234
|
const blockType: 'diff' | 'tool' = isDiff ? 'diff' : 'tool';
|
|
235
|
+
// Parsed once, ahead of the collapse check, so it's available for the
|
|
236
|
+
// block-registry meta merge below regardless of collapsed/expanded state.
|
|
237
|
+
const diffParse = isDiff ? parseDiffForApply(message.content) : undefined;
|
|
234
238
|
|
|
235
239
|
const isShort = message.content.length <= 200;
|
|
236
240
|
const isCollapsed = isShort
|
|
@@ -271,6 +275,10 @@ export function renderConversationToolMessage(
|
|
|
271
275
|
dim: true,
|
|
272
276
|
});
|
|
273
277
|
context.history.addLines(rendered);
|
|
278
|
+
} else if (isDiff) {
|
|
279
|
+
// No filename banner: the diff text's own --- / +++ headers already
|
|
280
|
+
// identify the file (matches the pre-v0.9.6 call site in tool-call.ts).
|
|
281
|
+
context.history.addLines(renderDiffView(message.content, width));
|
|
274
282
|
} else {
|
|
275
283
|
let contentToRender = message.content;
|
|
276
284
|
const trimmed = contentToRender.trimStart();
|
|
@@ -295,8 +303,8 @@ export function renderConversationToolMessage(
|
|
|
295
303
|
rawContent: message.content,
|
|
296
304
|
};
|
|
297
305
|
|
|
298
|
-
if (isDiff) {
|
|
299
|
-
meta = { ...meta, ...
|
|
306
|
+
if (isDiff && diffParse) {
|
|
307
|
+
meta = { ...meta, ...diffParse };
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
context.blockRegistry.push(meta);
|