cvc-tui 0.4.4 → 0.4.7
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/NOTICES.md +13 -0
- package/dist/app/completion.js +102 -0
- package/dist/app/createGatewayEventHandler.js +508 -0
- package/dist/app/createSlashHandler.js +101 -0
- package/dist/app/delegationStore.js +51 -0
- package/dist/app/gatewayContext.js +17 -0
- package/dist/app/historyStore.js +123 -0
- package/dist/app/inputBuffer.js +120 -0
- package/dist/app/inputSelectionStore.js +8 -0
- package/dist/app/inputStore.js +28 -0
- package/dist/app/interfaces.js +6 -0
- package/dist/app/overlayStore.js +40 -0
- package/dist/app/promptStore.js +44 -0
- package/dist/app/queueStore.js +25 -0
- package/dist/app/scroll.js +44 -0
- package/dist/app/setupHandoff.js +28 -0
- package/dist/app/slash/commands/core.js +479 -0
- package/dist/app/slash/commands/debug.js +44 -0
- package/dist/app/slash/commands/ops.js +512 -0
- package/dist/app/slash/commands/session.js +431 -0
- package/dist/app/slash/commands/setup.js +20 -0
- package/dist/app/slash/commands/toggles.js +40 -0
- package/dist/app/slash/registry.js +18 -0
- package/dist/app/slash/types.js +1 -0
- package/dist/app/spawnHistoryStore.js +105 -0
- package/dist/app/turnController.js +650 -0
- package/dist/app/turnStore.js +48 -0
- package/dist/app/uiStore.js +36 -0
- package/dist/app/useComposerState.js +265 -0
- package/dist/app/useConfigSync.js +144 -0
- package/dist/app/useInputHandlers.js +403 -0
- package/dist/app/useLongRunToolCharms.js +50 -0
- package/dist/app/useMainApp.js +638 -0
- package/dist/app/useSessionLifecycle.js +175 -0
- package/dist/app/useSubmission.js +287 -0
- package/dist/app.js +15 -0
- package/dist/banner.js +63 -0
- package/dist/components/agentsOverlay.js +474 -0
- package/dist/components/appChrome.js +252 -0
- package/dist/components/appLayout.js +122 -0
- package/dist/components/appOverlays.js +65 -0
- package/dist/components/branding.js +97 -0
- package/dist/components/fpsOverlay.js +22 -0
- package/dist/components/helpHint.js +21 -0
- package/dist/components/markdown.js +501 -0
- package/dist/components/maskedPrompt.js +12 -0
- package/dist/components/messageLine.js +82 -0
- package/dist/components/modelPicker.js +254 -0
- package/dist/components/overlayControls.js +30 -0
- package/dist/components/overlays/confirmPrompt.js +25 -0
- package/dist/components/overlays/helpOverlay.js +76 -0
- package/dist/components/overlays/historySearch.js +49 -0
- package/dist/components/overlays/modelPicker.js +60 -0
- package/dist/components/overlays/overlayUtils.js +19 -0
- package/dist/components/overlays/secretPrompt.js +36 -0
- package/dist/components/overlays/sessionPicker.js +93 -0
- package/dist/components/overlays/skillsHub.js +71 -0
- package/dist/components/prompts.js +95 -0
- package/dist/components/queuedMessages.js +24 -0
- package/dist/components/sessionPicker.js +130 -0
- package/dist/components/skillsHub.js +165 -0
- package/dist/components/streamingAssistant.js +35 -0
- package/dist/components/streamingMarkdown.js +144 -0
- package/dist/components/textInput.js +794 -0
- package/dist/components/themed.js +12 -0
- package/dist/components/thinking.js +496 -0
- package/dist/components/todoPanel.js +40 -0
- package/dist/components/transcript.js +22 -0
- package/dist/config/env.js +18 -0
- package/dist/config/limits.js +22 -0
- package/dist/config/timing.js +25 -0
- package/dist/content/charms.js +5 -0
- package/dist/content/faces.js +21 -0
- package/dist/content/fortunes.js +29 -0
- package/dist/content/hotkeys.js +38 -0
- package/dist/content/placeholders.js +15 -0
- package/dist/content/setup.js +14 -0
- package/dist/content/verbs.js +41 -0
- package/dist/domain/details.js +53 -0
- package/dist/domain/messages.js +63 -0
- package/dist/domain/paths.js +16 -0
- package/dist/domain/providers.js +11 -0
- package/dist/domain/roles.js +6 -0
- package/dist/domain/slash.js +11 -0
- package/dist/domain/usage.js +1 -0
- package/dist/domain/viewport.js +33 -0
- package/dist/entry.js +64 -70236
- package/dist/gateway/client.js +312 -0
- package/dist/gatewayClient.js +574 -0
- package/dist/gatewayTypes.js +1 -0
- package/dist/hooks/useCompletion.js +86 -0
- package/dist/hooks/useGitBranch.js +58 -0
- package/dist/hooks/useInputHistory.js +12 -0
- package/dist/hooks/useQueue.js +57 -0
- package/dist/hooks/useVirtualHistory.js +401 -0
- package/dist/lib/circularBuffer.js +43 -0
- package/dist/lib/clipboard.js +126 -0
- package/dist/lib/editor.js +41 -0
- package/dist/lib/editor.test.js +58 -0
- package/dist/lib/emoji.js +49 -0
- package/dist/lib/externalCli.js +11 -0
- package/dist/lib/forceTruecolor.js +26 -0
- package/dist/lib/fpsStore.js +36 -0
- package/dist/lib/gracefulExit.js +29 -0
- package/dist/lib/history.js +69 -0
- package/dist/lib/inputMetrics.js +143 -0
- package/dist/lib/liveProgress.js +51 -0
- package/dist/lib/liveProgress.test.js +89 -0
- package/dist/lib/localSessionInfo.js +116 -0
- package/dist/lib/mathUnicode.js +685 -0
- package/dist/lib/memory.js +123 -0
- package/dist/lib/memoryMonitor.js +76 -0
- package/dist/lib/messages.js +3 -0
- package/dist/lib/messages.test.js +25 -0
- package/dist/lib/osc52.js +53 -0
- package/dist/lib/perfPane.js +94 -0
- package/dist/lib/platform.js +312 -0
- package/dist/lib/precisionWheel.js +25 -0
- package/dist/lib/react-devtools-stub.js +12 -0
- package/dist/lib/reasoning.js +39 -0
- package/dist/lib/rpc.js +26 -0
- package/dist/lib/subagentTree.js +287 -0
- package/dist/lib/syntax.js +89 -0
- package/dist/lib/terminalModes.js +46 -0
- package/dist/lib/terminalParity.js +48 -0
- package/dist/lib/terminalSetup.js +321 -0
- package/dist/lib/text.js +203 -0
- package/dist/lib/text.test.js +18 -0
- package/dist/lib/todo.js +2 -0
- package/dist/lib/todo.test.js +22 -0
- package/dist/lib/viewportStore.js +82 -0
- package/dist/lib/virtualHeights.js +61 -0
- package/dist/lib/wheelAccel.js +143 -0
- package/dist/protocol/interpolation.js +4 -0
- package/dist/protocol/paste.js +3 -0
- package/dist/theme.js +398 -0
- package/dist/types.js +1 -0
- package/dist/vendor/cvc-ink/dist/entry-exports.js +52737 -0
- package/dist/vendor/cvc-ink/index.js +1 -0
- package/dist/vendor/cvc-ink/package.json +9 -0
- package/dist/vendor/cvc-ink/text-input.js +1 -0
- package/package.json +9 -9
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
+
import { REASONING_PULSE_MS, STREAM_BATCH_MS, STREAM_IDLE_BATCH_MS, STREAM_SCROLL_BATCH_MS, STREAM_TYPING_BATCH_MS } from '../config/timing.js';
|
|
6
|
+
import { appendToolShelfMessage, isToolShelfMessage } from '../lib/liveProgress.js';
|
|
7
|
+
import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js';
|
|
8
|
+
import { boundedLiveRenderText, buildToolTrailLine, estimateTokensRough, isTransientTrailLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js';
|
|
9
|
+
import { resetFlowOverlays } from './overlayStore.js';
|
|
10
|
+
import { pushSnapshot } from './spawnHistoryStore.js';
|
|
11
|
+
import { archiveDoneTodos, getTurnState, patchTurnState, resetTurnState } from './turnStore.js';
|
|
12
|
+
import { getUiState, patchUiState } from './uiStore.js';
|
|
13
|
+
const INTERRUPT_COOLDOWN_MS = 1500;
|
|
14
|
+
const ACTIVITY_LIMIT = 8;
|
|
15
|
+
const TRAIL_LIMIT = 8;
|
|
16
|
+
// Extracts the raw patch from a diff-only segment produced by
|
|
17
|
+
// pushInlineDiffSegment. Used at message.complete to dedupe against final
|
|
18
|
+
// assistant text that narrates the same patch. Returns null for anything
|
|
19
|
+
// else so real assistant narration never gets touched.
|
|
20
|
+
const diffSegmentBody = (msg) => {
|
|
21
|
+
if (msg.kind !== 'diff') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const m = msg.text.match(/^```diff\n([\s\S]*?)\n```$/);
|
|
25
|
+
return m ? m[1] : null;
|
|
26
|
+
};
|
|
27
|
+
const hasDetails = (msg) => Boolean(msg.thinking || msg.tools?.length || msg.toolTokens);
|
|
28
|
+
const isTodoStatus = (status) => status === 'pending' || status === 'in_progress' || status === 'completed' || status === 'cancelled';
|
|
29
|
+
const parseTodos = (value) => {
|
|
30
|
+
if (!Array.isArray(value)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return value
|
|
34
|
+
.map(item => {
|
|
35
|
+
if (!item || typeof item !== 'object') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const row = item;
|
|
39
|
+
const status = row.status;
|
|
40
|
+
if (!isTodoStatus(status)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: String(row.content ?? '').trim(),
|
|
45
|
+
id: String(row.id ?? '').trim(),
|
|
46
|
+
status
|
|
47
|
+
};
|
|
48
|
+
})
|
|
49
|
+
.filter((item) => Boolean(item?.id && item.content));
|
|
50
|
+
};
|
|
51
|
+
const textSegments = (segments) => segments.filter(msg => msg.role === 'assistant' && msg.kind !== 'diff').map(msg => msg.text);
|
|
52
|
+
const finalTail = (finalText, segments) => {
|
|
53
|
+
let tail = finalText;
|
|
54
|
+
for (const text of textSegments(segments)) {
|
|
55
|
+
const trimmed = text.trim();
|
|
56
|
+
if (trimmed && tail.startsWith(trimmed)) {
|
|
57
|
+
tail = tail.slice(trimmed.length).trimStart();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return tail;
|
|
61
|
+
};
|
|
62
|
+
const clear = (t) => {
|
|
63
|
+
if (t) {
|
|
64
|
+
clearTimeout(t);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
class TurnController {
|
|
69
|
+
bufRef = '';
|
|
70
|
+
interrupted = false;
|
|
71
|
+
lastStatusNote = '';
|
|
72
|
+
persistedToolLabels = new Set();
|
|
73
|
+
persistSpawnTree;
|
|
74
|
+
protocolWarned = false;
|
|
75
|
+
reasoningText = '';
|
|
76
|
+
segmentMessages = [];
|
|
77
|
+
pendingSegmentTools = [];
|
|
78
|
+
statusTimer = null;
|
|
79
|
+
toolTokenAcc = 0;
|
|
80
|
+
turnTools = [];
|
|
81
|
+
activeTools = [];
|
|
82
|
+
activeReasoningText = '';
|
|
83
|
+
reasoningSegmentIndex = null;
|
|
84
|
+
activityId = 0;
|
|
85
|
+
reasoningStreamingTimer = null;
|
|
86
|
+
reasoningTimer = null;
|
|
87
|
+
streamTimer = null;
|
|
88
|
+
streamDelay = STREAM_IDLE_BATCH_MS;
|
|
89
|
+
toolProgressTimer = null;
|
|
90
|
+
boostStreamingForTyping() {
|
|
91
|
+
this.streamDelay = STREAM_TYPING_BATCH_MS;
|
|
92
|
+
}
|
|
93
|
+
boostStreamingForScroll() {
|
|
94
|
+
this.streamDelay = Math.max(this.streamDelay, STREAM_SCROLL_BATCH_MS);
|
|
95
|
+
}
|
|
96
|
+
relaxStreaming() {
|
|
97
|
+
this.streamDelay = STREAM_IDLE_BATCH_MS;
|
|
98
|
+
}
|
|
99
|
+
clearReasoning() {
|
|
100
|
+
this.reasoningTimer = clear(this.reasoningTimer);
|
|
101
|
+
this.activeReasoningText = '';
|
|
102
|
+
this.reasoningSegmentIndex = null;
|
|
103
|
+
this.reasoningText = '';
|
|
104
|
+
this.toolTokenAcc = 0;
|
|
105
|
+
patchTurnState({ reasoning: '', reasoningTokens: 0, toolTokens: 0 });
|
|
106
|
+
}
|
|
107
|
+
clearStatusTimer() {
|
|
108
|
+
this.statusTimer = clear(this.statusTimer);
|
|
109
|
+
}
|
|
110
|
+
endReasoningPhase() {
|
|
111
|
+
this.reasoningStreamingTimer = clear(this.reasoningStreamingTimer);
|
|
112
|
+
patchTurnState({ reasoningActive: false, reasoningStreaming: false });
|
|
113
|
+
}
|
|
114
|
+
idle() {
|
|
115
|
+
this.endReasoningPhase();
|
|
116
|
+
this.activeTools = [];
|
|
117
|
+
this.streamTimer = clear(this.streamTimer);
|
|
118
|
+
this.bufRef = '';
|
|
119
|
+
this.pendingSegmentTools = [];
|
|
120
|
+
this.segmentMessages = [];
|
|
121
|
+
patchTurnState({
|
|
122
|
+
streamPendingTools: [],
|
|
123
|
+
streamSegments: [],
|
|
124
|
+
streaming: '',
|
|
125
|
+
subagents: [],
|
|
126
|
+
tools: [],
|
|
127
|
+
turnTrail: []
|
|
128
|
+
});
|
|
129
|
+
patchUiState({ busy: false });
|
|
130
|
+
resetFlowOverlays();
|
|
131
|
+
}
|
|
132
|
+
interruptTurn({ appendMessage, gw, sid, sys }) {
|
|
133
|
+
this.interrupted = true;
|
|
134
|
+
gw.request('session.interrupt', { session_id: sid }).catch(() => { });
|
|
135
|
+
this.closeReasoningSegment();
|
|
136
|
+
const segments = this.segmentMessages;
|
|
137
|
+
const partial = this.bufRef.trimStart();
|
|
138
|
+
const tools = this.pendingSegmentTools;
|
|
139
|
+
// Drain streaming/segment state off the nanostore before writing the
|
|
140
|
+
// preserved snapshot to the transcript — otherwise each flushed segment
|
|
141
|
+
// appears in both `turn.streamSegments` and the transcript for one frame.
|
|
142
|
+
this.idle();
|
|
143
|
+
this.clearReasoning();
|
|
144
|
+
this.turnTools = [];
|
|
145
|
+
patchTurnState({ activity: [], outcome: '' });
|
|
146
|
+
for (const msg of segments) {
|
|
147
|
+
appendMessage(msg);
|
|
148
|
+
}
|
|
149
|
+
// Always surface an interruption indicator — if there's an in-flight
|
|
150
|
+
// `partial` or pending tools, fold them into a single assistant message;
|
|
151
|
+
// otherwise emit a sys note so the transcript always records that the
|
|
152
|
+
// turn was cancelled, even when only prior `segments` were preserved.
|
|
153
|
+
if (partial || tools.length) {
|
|
154
|
+
appendMessage({
|
|
155
|
+
role: 'assistant',
|
|
156
|
+
text: partial ? `${partial}\n\n*[interrupted]*` : '*[interrupted]*',
|
|
157
|
+
...(tools.length && { tools })
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
sys('interrupted');
|
|
162
|
+
}
|
|
163
|
+
patchUiState({ status: 'interrupted' });
|
|
164
|
+
this.clearStatusTimer();
|
|
165
|
+
this.statusTimer = setTimeout(() => {
|
|
166
|
+
this.statusTimer = null;
|
|
167
|
+
patchUiState({ status: 'ready' });
|
|
168
|
+
}, INTERRUPT_COOLDOWN_MS);
|
|
169
|
+
}
|
|
170
|
+
pruneTransient() {
|
|
171
|
+
this.turnTools = this.turnTools.filter(line => !isTransientTrailLine(line));
|
|
172
|
+
patchTurnState(state => {
|
|
173
|
+
const next = state.turnTrail.filter(line => !isTransientTrailLine(line));
|
|
174
|
+
return next.length === state.turnTrail.length ? state : { ...state, turnTrail: next };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
syncReasoningSegment() {
|
|
178
|
+
const thinking = this.activeReasoningText.trim();
|
|
179
|
+
if (!thinking) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const msg = {
|
|
183
|
+
kind: 'trail',
|
|
184
|
+
role: 'system',
|
|
185
|
+
text: '',
|
|
186
|
+
thinking,
|
|
187
|
+
thinkingTokens: estimateTokensRough(thinking),
|
|
188
|
+
toolTokens: this.toolTokenAcc || undefined
|
|
189
|
+
};
|
|
190
|
+
if (this.reasoningSegmentIndex === null) {
|
|
191
|
+
this.reasoningSegmentIndex = this.segmentMessages.length;
|
|
192
|
+
this.segmentMessages = [...this.segmentMessages, msg];
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this.segmentMessages = this.segmentMessages.map((item, i) => (i === this.reasoningSegmentIndex ? msg : item));
|
|
196
|
+
}
|
|
197
|
+
patchTurnState({ streamSegments: this.segmentMessages });
|
|
198
|
+
}
|
|
199
|
+
closeReasoningSegment() {
|
|
200
|
+
this.syncReasoningSegment();
|
|
201
|
+
this.activeReasoningText = '';
|
|
202
|
+
this.reasoningSegmentIndex = null;
|
|
203
|
+
}
|
|
204
|
+
pushSegment(msg) {
|
|
205
|
+
this.segmentMessages = appendToolShelfMessage(this.segmentMessages, msg);
|
|
206
|
+
}
|
|
207
|
+
flushStreamingSegment() {
|
|
208
|
+
const raw = this.bufRef.trimStart();
|
|
209
|
+
const split = raw
|
|
210
|
+
? hasReasoningTag(raw)
|
|
211
|
+
? splitReasoning(raw)
|
|
212
|
+
: { reasoning: '', text: raw }
|
|
213
|
+
: { reasoning: '', text: '' };
|
|
214
|
+
if (split.reasoning && !this.reasoningText.trim()) {
|
|
215
|
+
this.reasoningText = split.reasoning;
|
|
216
|
+
this.activeReasoningText = split.reasoning;
|
|
217
|
+
patchTurnState({ reasoning: this.reasoningText, reasoningTokens: estimateTokensRough(this.reasoningText) });
|
|
218
|
+
this.syncReasoningSegment();
|
|
219
|
+
}
|
|
220
|
+
const msg = {
|
|
221
|
+
role: split.text ? 'assistant' : 'system',
|
|
222
|
+
text: split.text,
|
|
223
|
+
...(!split.text && { kind: 'trail' }),
|
|
224
|
+
...(this.pendingSegmentTools.length && { tools: this.pendingSegmentTools })
|
|
225
|
+
};
|
|
226
|
+
this.streamTimer = clear(this.streamTimer);
|
|
227
|
+
if (split.text || hasDetails(msg)) {
|
|
228
|
+
this.pushSegment(msg);
|
|
229
|
+
}
|
|
230
|
+
this.pendingSegmentTools = [];
|
|
231
|
+
this.bufRef = '';
|
|
232
|
+
patchTurnState({ streamPendingTools: [], streamSegments: this.segmentMessages, streaming: '' });
|
|
233
|
+
}
|
|
234
|
+
pulseReasoningStreaming() {
|
|
235
|
+
this.reasoningStreamingTimer = clear(this.reasoningStreamingTimer);
|
|
236
|
+
patchTurnState({ reasoningActive: true, reasoningStreaming: true });
|
|
237
|
+
this.reasoningStreamingTimer = setTimeout(() => {
|
|
238
|
+
this.reasoningStreamingTimer = null;
|
|
239
|
+
patchTurnState({ reasoningStreaming: false });
|
|
240
|
+
}, REASONING_PULSE_MS);
|
|
241
|
+
}
|
|
242
|
+
recordTodos(value) {
|
|
243
|
+
if (this.interrupted) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const todos = parseTodos(value);
|
|
247
|
+
if (todos !== null) {
|
|
248
|
+
patchTurnState({ todos });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
flushPendingToolsIntoLastSegment() {
|
|
252
|
+
if (!this.pendingSegmentTools.length) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const next = appendToolShelfMessage(this.segmentMessages, {
|
|
256
|
+
kind: 'trail',
|
|
257
|
+
role: 'system',
|
|
258
|
+
text: '',
|
|
259
|
+
tools: this.pendingSegmentTools
|
|
260
|
+
});
|
|
261
|
+
if (next.length === this.segmentMessages.length + 1) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
this.segmentMessages = next;
|
|
265
|
+
this.pendingSegmentTools = [];
|
|
266
|
+
patchTurnState({ streamPendingTools: [], streamSegments: this.segmentMessages });
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
pushInlineDiffSegment(diffText, tools = []) {
|
|
270
|
+
// Strip CLI chrome the gateway emits before the unified diff (e.g. a
|
|
271
|
+
// leading "┊ review diff" header written by `_emit_inline_diff` for the
|
|
272
|
+
// terminal printer). That header only makes sense as stdout dressing,
|
|
273
|
+
// not inside a markdown ```diff block.
|
|
274
|
+
const stripped = diffText.replace(/^\s*┊[^\n]*\n?/, '').trim();
|
|
275
|
+
if (!stripped) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// Flush any in-progress streaming text as its own segment first, so the
|
|
279
|
+
// diff lands BETWEEN the assistant narration that preceded the edit and
|
|
280
|
+
// whatever the agent streams afterwards — not glued onto the final
|
|
281
|
+
// message. This is the whole point of segment-anchored diffs: the diff
|
|
282
|
+
// renders where the edit actually happened.
|
|
283
|
+
this.flushStreamingSegment();
|
|
284
|
+
const block = `\`\`\`diff\n${stripped}\n\`\`\``;
|
|
285
|
+
// Skip consecutive duplicates (same tool firing tool.complete twice, or
|
|
286
|
+
// two edits producing the same patch). Keeping this cheap — deeper
|
|
287
|
+
// dedupe against the final assistant text happens at message.complete.
|
|
288
|
+
if (this.segmentMessages.at(-1)?.text === block) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.segmentMessages = [
|
|
292
|
+
...this.segmentMessages,
|
|
293
|
+
{ kind: 'diff', role: 'assistant', text: block, ...(tools.length && { tools }) }
|
|
294
|
+
];
|
|
295
|
+
patchTurnState({ streamSegments: this.segmentMessages });
|
|
296
|
+
}
|
|
297
|
+
pushActivity(text, tone = 'info', replaceLabel) {
|
|
298
|
+
patchTurnState(state => {
|
|
299
|
+
const base = replaceLabel
|
|
300
|
+
? state.activity.filter(item => !sameToolTrailGroup(replaceLabel, item.text))
|
|
301
|
+
: state.activity;
|
|
302
|
+
const tail = base.at(-1);
|
|
303
|
+
if (tail?.text === text && tail.tone === tone) {
|
|
304
|
+
return state;
|
|
305
|
+
}
|
|
306
|
+
return { ...state, activity: [...base, { id: ++this.activityId, text, tone }].slice(-ACTIVITY_LIMIT) };
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
pushTrail(line) {
|
|
310
|
+
if (this.interrupted) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
patchTurnState(state => {
|
|
314
|
+
if (state.turnTrail.at(-1) === line) {
|
|
315
|
+
return state;
|
|
316
|
+
}
|
|
317
|
+
const next = [...state.turnTrail.filter(item => !isTransientTrailLine(item)), line].slice(-TRAIL_LIMIT);
|
|
318
|
+
this.turnTools = next;
|
|
319
|
+
return { ...state, turnTrail: next };
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
recordError() {
|
|
323
|
+
this.idle();
|
|
324
|
+
this.clearReasoning();
|
|
325
|
+
this.clearStatusTimer();
|
|
326
|
+
this.pendingSegmentTools = [];
|
|
327
|
+
this.segmentMessages = [];
|
|
328
|
+
this.turnTools = [];
|
|
329
|
+
this.persistedToolLabels.clear();
|
|
330
|
+
}
|
|
331
|
+
recordMessageComplete(payload) {
|
|
332
|
+
this.closeReasoningSegment();
|
|
333
|
+
// Ink renders markdown via <Md>; the gateway's Rich-rendered ANSI
|
|
334
|
+
// (`payload.rendered`) is for terminals that can't. Prioritising
|
|
335
|
+
// `rendered` here garbles output whenever a user opts into
|
|
336
|
+
// `display.final_response_markdown: render` because raw ANSI escapes
|
|
337
|
+
// pass through into the React tree. Prefer raw text and fall back
|
|
338
|
+
// only when the gateway elected not to send any (#16391).
|
|
339
|
+
const rawText = (payload.text ?? payload.rendered ?? this.bufRef).trimStart();
|
|
340
|
+
const split = splitReasoning(rawText);
|
|
341
|
+
const finalText = finalTail(split.text, this.segmentMessages);
|
|
342
|
+
const existingReasoning = this.reasoningText.trim() || String(payload.reasoning ?? '').trim();
|
|
343
|
+
const savedReasoning = [existingReasoning, existingReasoning ? '' : split.reasoning].filter(Boolean).join('\n\n');
|
|
344
|
+
const savedToolTokens = this.toolTokenAcc;
|
|
345
|
+
let tools = this.pendingSegmentTools;
|
|
346
|
+
const last = this.segmentMessages[this.segmentMessages.length - 1];
|
|
347
|
+
if (tools.length && isToolShelfMessage(last)) {
|
|
348
|
+
this.segmentMessages = [
|
|
349
|
+
...this.segmentMessages.slice(0, -1),
|
|
350
|
+
{ ...last, tools: [...(last.tools ?? []), ...tools] }
|
|
351
|
+
];
|
|
352
|
+
this.pendingSegmentTools = [];
|
|
353
|
+
tools = [];
|
|
354
|
+
}
|
|
355
|
+
// Drop diff-only segments the agent is about to narrate in the final
|
|
356
|
+
// reply. Without this, a closing "here's the diff …" message would
|
|
357
|
+
// render two stacked copies of the same patch. Only touches segments
|
|
358
|
+
// with `kind: 'diff'` emitted by pushInlineDiffSegment — real
|
|
359
|
+
// assistant narration stays put.
|
|
360
|
+
const finalHasOwnDiffFence = /```(?:diff|patch)\b/i.test(finalText);
|
|
361
|
+
const segments = this.segmentMessages.filter(msg => {
|
|
362
|
+
const body = diffSegmentBody(msg);
|
|
363
|
+
return body === null || (!finalHasOwnDiffFence && !finalText.includes(body));
|
|
364
|
+
});
|
|
365
|
+
const hasReasoningSegment = this.reasoningSegmentIndex !== null || segments.some(msg => Boolean(msg.thinking?.trim()));
|
|
366
|
+
const finalThinking = hasReasoningSegment ? '' : savedReasoning.trim();
|
|
367
|
+
const finalDetails = {
|
|
368
|
+
kind: 'trail',
|
|
369
|
+
role: 'system',
|
|
370
|
+
text: '',
|
|
371
|
+
thinking: finalThinking || undefined,
|
|
372
|
+
thinkingTokens: finalThinking ? estimateTokensRough(finalThinking) : undefined,
|
|
373
|
+
toolTokens: savedToolTokens || undefined,
|
|
374
|
+
...(tools.length && { tools })
|
|
375
|
+
};
|
|
376
|
+
// Archive prepended so the trail msg anchors under the user prompt,
|
|
377
|
+
// not between thinking/tools and final assistant text.
|
|
378
|
+
const finalMessages = [
|
|
379
|
+
...archiveDoneTodos(),
|
|
380
|
+
...segments,
|
|
381
|
+
...(hasDetails(finalDetails) ? [finalDetails] : [])
|
|
382
|
+
];
|
|
383
|
+
if (finalText) {
|
|
384
|
+
finalMessages.push({ role: 'assistant', text: finalText });
|
|
385
|
+
}
|
|
386
|
+
const wasInterrupted = this.interrupted;
|
|
387
|
+
// Archive the turn's spawn tree to history BEFORE idle() drops subagents
|
|
388
|
+
// from turnState. Lets /replay and the overlay's history nav pull up
|
|
389
|
+
// finished fan-outs without a round-trip to disk.
|
|
390
|
+
const finishedSubagents = getTurnState().subagents;
|
|
391
|
+
const sessionId = getUiState().sid;
|
|
392
|
+
if (finishedSubagents.length > 0) {
|
|
393
|
+
pushSnapshot(finishedSubagents, { sessionId, startedAt: null });
|
|
394
|
+
// Fire-and-forget disk persistence so /replay survives process restarts.
|
|
395
|
+
// The same snapshot lives in memory via spawnHistoryStore for immediate
|
|
396
|
+
// recall — disk is the long-term archive.
|
|
397
|
+
void this.persistSpawnTree?.(finishedSubagents, sessionId);
|
|
398
|
+
}
|
|
399
|
+
this.idle();
|
|
400
|
+
this.clearReasoning();
|
|
401
|
+
this.turnTools = [];
|
|
402
|
+
this.persistedToolLabels.clear();
|
|
403
|
+
this.bufRef = '';
|
|
404
|
+
this.interrupted = false;
|
|
405
|
+
patchTurnState({ activity: [], outcome: '' });
|
|
406
|
+
return { finalMessages, finalText, wasInterrupted };
|
|
407
|
+
}
|
|
408
|
+
recordMessageDelta({ text }) {
|
|
409
|
+
if (this.interrupted || !text) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
this.pruneTransient();
|
|
413
|
+
this.endReasoningPhase();
|
|
414
|
+
// Always accumulate the raw text delta. The pre-#16391 path replaced
|
|
415
|
+
// the entire buffer with `rendered` (an *incremental* Rich ANSI
|
|
416
|
+
// fragment), which on every tick discarded everything streamed so far
|
|
417
|
+
// — visible as overlapping coloured text and lost prose under
|
|
418
|
+
// `display.final_response_markdown: render`.
|
|
419
|
+
this.bufRef += text;
|
|
420
|
+
if (getUiState().streaming) {
|
|
421
|
+
this.scheduleStreaming();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
recordReasoningAvailable(text) {
|
|
425
|
+
if (this.interrupted || !getUiState().showReasoning) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const incoming = text.trim();
|
|
429
|
+
if (!incoming || this.reasoningText.trim()) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.reasoningText = incoming;
|
|
433
|
+
this.activeReasoningText = incoming;
|
|
434
|
+
this.scheduleReasoning();
|
|
435
|
+
this.syncReasoningSegment();
|
|
436
|
+
this.pulseReasoningStreaming();
|
|
437
|
+
}
|
|
438
|
+
recordReasoningDelta(text) {
|
|
439
|
+
if (this.interrupted || !getUiState().showReasoning) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!this.activeReasoningText.trim() && this.pendingSegmentTools.length) {
|
|
443
|
+
this.flushStreamingSegment();
|
|
444
|
+
}
|
|
445
|
+
this.reasoningText += text;
|
|
446
|
+
this.activeReasoningText += text;
|
|
447
|
+
if (this.reasoningText.length > 80_000) {
|
|
448
|
+
this.reasoningText = this.reasoningText.slice(-60_000);
|
|
449
|
+
}
|
|
450
|
+
this.scheduleReasoning();
|
|
451
|
+
this.syncReasoningSegment();
|
|
452
|
+
this.pulseReasoningStreaming();
|
|
453
|
+
}
|
|
454
|
+
recordToolComplete(toolId, fallbackName, error, summary, duration, todos) {
|
|
455
|
+
if (this.interrupted) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.recordTodos(todos);
|
|
459
|
+
const line = this.completeTool(toolId, fallbackName, error, summary, duration);
|
|
460
|
+
this.pendingSegmentTools = [...this.pendingSegmentTools, line];
|
|
461
|
+
this.flushPendingToolsIntoLastSegment();
|
|
462
|
+
this.publishToolState();
|
|
463
|
+
}
|
|
464
|
+
recordInlineDiffToolComplete(diffText, toolId, fallbackName, error, duration) {
|
|
465
|
+
if (this.interrupted) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.flushStreamingSegment();
|
|
469
|
+
this.pushInlineDiffSegment(diffText, [this.completeTool(toolId, fallbackName, error, '', duration)]);
|
|
470
|
+
this.publishToolState();
|
|
471
|
+
}
|
|
472
|
+
completeTool(toolId, fallbackName, error, summary, duration) {
|
|
473
|
+
const done = this.activeTools.find(tool => tool.id === toolId);
|
|
474
|
+
const name = done?.name ?? fallbackName ?? 'tool';
|
|
475
|
+
const label = toolTrailLabel(name);
|
|
476
|
+
const fallbackDuration = done?.startedAt ? (Date.now() - done.startedAt) / 1000 : undefined;
|
|
477
|
+
const line = buildToolTrailLine(name, done?.context || '', Boolean(error), error || summary || '', duration ?? fallbackDuration);
|
|
478
|
+
this.activeTools = this.activeTools.filter(tool => tool.id !== toolId);
|
|
479
|
+
const next = this.turnTools.filter(item => !sameToolTrailGroup(label, item));
|
|
480
|
+
if (!this.activeTools.length) {
|
|
481
|
+
next.push('analyzing tool output…');
|
|
482
|
+
}
|
|
483
|
+
this.turnTools = next.slice(-TRAIL_LIMIT);
|
|
484
|
+
return line;
|
|
485
|
+
}
|
|
486
|
+
publishToolState() {
|
|
487
|
+
patchTurnState({
|
|
488
|
+
streamPendingTools: this.pendingSegmentTools,
|
|
489
|
+
tools: this.activeTools,
|
|
490
|
+
turnTrail: this.turnTools
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
recordToolProgress(toolName, preview) {
|
|
494
|
+
if (this.interrupted) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const index = this.activeTools.findIndex(tool => tool.name === toolName);
|
|
498
|
+
if (index < 0) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.activeTools = this.activeTools.map((tool, i) => (i === index ? { ...tool, context: preview } : tool));
|
|
502
|
+
if (this.toolProgressTimer) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.toolProgressTimer = setTimeout(() => {
|
|
506
|
+
this.toolProgressTimer = null;
|
|
507
|
+
patchTurnState({ tools: [...this.activeTools] });
|
|
508
|
+
}, STREAM_BATCH_MS);
|
|
509
|
+
}
|
|
510
|
+
recordToolStart(toolId, name, context) {
|
|
511
|
+
if (this.interrupted) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.flushStreamingSegment();
|
|
515
|
+
this.closeReasoningSegment();
|
|
516
|
+
this.pruneTransient();
|
|
517
|
+
this.endReasoningPhase();
|
|
518
|
+
const sample = `${name} ${context}`.trim();
|
|
519
|
+
this.toolTokenAcc += sample ? estimateTokensRough(sample) : 0;
|
|
520
|
+
this.activeTools = [...this.activeTools, { context, id: toolId, name, startedAt: Date.now() }];
|
|
521
|
+
patchTurnState({ toolTokens: this.toolTokenAcc, tools: this.activeTools });
|
|
522
|
+
}
|
|
523
|
+
reset() {
|
|
524
|
+
this.clearReasoning();
|
|
525
|
+
this.clearStatusTimer();
|
|
526
|
+
this.idle();
|
|
527
|
+
this.bufRef = '';
|
|
528
|
+
this.interrupted = false;
|
|
529
|
+
this.lastStatusNote = '';
|
|
530
|
+
this.activeReasoningText = '';
|
|
531
|
+
this.pendingSegmentTools = [];
|
|
532
|
+
this.protocolWarned = false;
|
|
533
|
+
this.reasoningSegmentIndex = null;
|
|
534
|
+
this.segmentMessages = [];
|
|
535
|
+
this.turnTools = [];
|
|
536
|
+
this.toolTokenAcc = 0;
|
|
537
|
+
this.persistedToolLabels.clear();
|
|
538
|
+
patchTurnState({ activity: [], outcome: '' });
|
|
539
|
+
}
|
|
540
|
+
fullReset() {
|
|
541
|
+
this.reset();
|
|
542
|
+
resetTurnState();
|
|
543
|
+
}
|
|
544
|
+
scheduleReasoning() {
|
|
545
|
+
if (this.reasoningTimer) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
this.reasoningTimer = setTimeout(() => {
|
|
549
|
+
this.reasoningTimer = null;
|
|
550
|
+
patchTurnState({
|
|
551
|
+
reasoning: this.reasoningText,
|
|
552
|
+
reasoningTokens: estimateTokensRough(this.reasoningText)
|
|
553
|
+
});
|
|
554
|
+
}, STREAM_BATCH_MS);
|
|
555
|
+
}
|
|
556
|
+
scheduleStreaming() {
|
|
557
|
+
if (this.streamTimer) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.streamTimer = setTimeout(() => {
|
|
561
|
+
this.streamTimer = null;
|
|
562
|
+
const raw = this.bufRef.trimStart();
|
|
563
|
+
const visible = hasReasoningTag(raw) ? splitReasoning(raw).text : raw;
|
|
564
|
+
patchTurnState({ streaming: boundedLiveRenderText(visible) });
|
|
565
|
+
}, this.streamDelay);
|
|
566
|
+
}
|
|
567
|
+
startMessage() {
|
|
568
|
+
this.endReasoningPhase();
|
|
569
|
+
this.clearReasoning();
|
|
570
|
+
this.activeTools = [];
|
|
571
|
+
this.activeReasoningText = '';
|
|
572
|
+
this.reasoningSegmentIndex = null;
|
|
573
|
+
this.turnTools = [];
|
|
574
|
+
this.toolTokenAcc = 0;
|
|
575
|
+
this.interrupted = false;
|
|
576
|
+
this.persistedToolLabels.clear();
|
|
577
|
+
patchUiState({ busy: true });
|
|
578
|
+
patchTurnState({ activity: [], outcome: '', subagents: [], toolTokens: 0, tools: [], turnTrail: [] });
|
|
579
|
+
}
|
|
580
|
+
upsertSubagent(p, patch, opts = { createIfMissing: true }) {
|
|
581
|
+
// Stable id: prefer the server-issued subagent_id (survives nested
|
|
582
|
+
// grandchildren + cross-tree joins). Fall back to the composite key
|
|
583
|
+
// for older gateways that omit the field — those produce a flat list.
|
|
584
|
+
const id = p.subagent_id || `sa:${p.task_index}:${p.goal || 'subagent'}`;
|
|
585
|
+
patchTurnState(state => {
|
|
586
|
+
const existing = state.subagents.find(item => item.id === id);
|
|
587
|
+
// Late events (subagent.complete/tool/progress arriving after message.complete
|
|
588
|
+
// has already fired idle()) would otherwise resurrect a finished
|
|
589
|
+
// subagent into turn.subagents and block the "finished" title on the
|
|
590
|
+
// /agents overlay. When `createIfMissing` is false we drop silently.
|
|
591
|
+
if (!existing && !opts.createIfMissing) {
|
|
592
|
+
return state;
|
|
593
|
+
}
|
|
594
|
+
const base = existing ?? {
|
|
595
|
+
depth: p.depth ?? 0,
|
|
596
|
+
goal: p.goal,
|
|
597
|
+
id,
|
|
598
|
+
index: p.task_index,
|
|
599
|
+
model: p.model,
|
|
600
|
+
notes: [],
|
|
601
|
+
parentId: p.parent_id ?? null,
|
|
602
|
+
startedAt: Date.now(),
|
|
603
|
+
status: 'running',
|
|
604
|
+
taskCount: p.task_count ?? 1,
|
|
605
|
+
thinking: [],
|
|
606
|
+
toolCount: p.tool_count ?? 0,
|
|
607
|
+
tools: [],
|
|
608
|
+
toolsets: p.toolsets
|
|
609
|
+
};
|
|
610
|
+
// Map snake_case payload keys onto camelCase state. Only overwrite
|
|
611
|
+
// when the event actually carries the field; `??` preserves prior
|
|
612
|
+
// values across streaming events that emit partial payloads.
|
|
613
|
+
const outputTail = p.output_tail
|
|
614
|
+
? p.output_tail.map(e => ({
|
|
615
|
+
isError: Boolean(e.is_error),
|
|
616
|
+
preview: String(e.preview ?? ''),
|
|
617
|
+
tool: String(e.tool ?? 'tool')
|
|
618
|
+
}))
|
|
619
|
+
: base.outputTail;
|
|
620
|
+
const next = {
|
|
621
|
+
...base,
|
|
622
|
+
apiCalls: p.api_calls ?? base.apiCalls,
|
|
623
|
+
costUsd: p.cost_usd ?? base.costUsd,
|
|
624
|
+
depth: p.depth ?? base.depth,
|
|
625
|
+
filesRead: p.files_read ?? base.filesRead,
|
|
626
|
+
filesWritten: p.files_written ?? base.filesWritten,
|
|
627
|
+
goal: p.goal || base.goal,
|
|
628
|
+
inputTokens: p.input_tokens ?? base.inputTokens,
|
|
629
|
+
iteration: p.iteration ?? base.iteration,
|
|
630
|
+
model: p.model ?? base.model,
|
|
631
|
+
outputTail,
|
|
632
|
+
outputTokens: p.output_tokens ?? base.outputTokens,
|
|
633
|
+
parentId: p.parent_id ?? base.parentId,
|
|
634
|
+
reasoningTokens: p.reasoning_tokens ?? base.reasoningTokens,
|
|
635
|
+
taskCount: p.task_count ?? base.taskCount,
|
|
636
|
+
toolCount: p.tool_count ?? base.toolCount,
|
|
637
|
+
toolsets: p.toolsets ?? base.toolsets,
|
|
638
|
+
...patch(base)
|
|
639
|
+
};
|
|
640
|
+
// Stable order: by spawn (depth, parent, index) rather than insert time.
|
|
641
|
+
// Without it, grandchildren can shuffle relative to siblings when
|
|
642
|
+
// events arrive out of order under high concurrency.
|
|
643
|
+
const subagents = existing
|
|
644
|
+
? state.subagents.map(item => (item.id === id ? next : item))
|
|
645
|
+
: [...state.subagents, next].sort((a, b) => a.depth - b.depth || a.index - b.index);
|
|
646
|
+
return { ...state, subagents };
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
export const turnController = new TurnController();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
+
import { atom } from 'nanostores';
|
|
6
|
+
import { useSyncExternalStore } from 'react';
|
|
7
|
+
import { isTodoDone } from '../lib/liveProgress.js';
|
|
8
|
+
const buildTurnState = () => ({
|
|
9
|
+
activity: [],
|
|
10
|
+
outcome: '',
|
|
11
|
+
reasoning: '',
|
|
12
|
+
reasoningActive: false,
|
|
13
|
+
reasoningStreaming: false,
|
|
14
|
+
reasoningTokens: 0,
|
|
15
|
+
streamPendingTools: [],
|
|
16
|
+
streamSegments: [],
|
|
17
|
+
streaming: '',
|
|
18
|
+
subagents: [],
|
|
19
|
+
todoCollapsed: false,
|
|
20
|
+
todos: [],
|
|
21
|
+
toolTokens: 0,
|
|
22
|
+
tools: [],
|
|
23
|
+
turnTrail: []
|
|
24
|
+
});
|
|
25
|
+
export const $turnState = atom(buildTurnState());
|
|
26
|
+
export const getTurnState = () => $turnState.get();
|
|
27
|
+
const subscribeTurn = (cb) => $turnState.listen(() => cb());
|
|
28
|
+
export const useTurnSelector = (selector) => useSyncExternalStore(subscribeTurn, () => selector($turnState.get()), () => selector($turnState.get()));
|
|
29
|
+
export const patchTurnState = (next) => $turnState.set(typeof next === 'function' ? next($turnState.get()) : { ...$turnState.get(), ...next });
|
|
30
|
+
export const toggleTodoCollapsed = () => patchTurnState(state => ({ ...state, todoCollapsed: !state.todoCollapsed }));
|
|
31
|
+
export const archiveDoneTodos = () => archiveTodosAtTurnEnd();
|
|
32
|
+
export const archiveTodosAtTurnEnd = () => {
|
|
33
|
+
const state = $turnState.get();
|
|
34
|
+
if (!state.todos.length) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const done = isTodoDone(state.todos);
|
|
38
|
+
const msg = {
|
|
39
|
+
kind: 'trail',
|
|
40
|
+
role: 'system',
|
|
41
|
+
text: '',
|
|
42
|
+
todos: state.todos,
|
|
43
|
+
...(done ? { todoCollapsedByDefault: true } : { todoIncomplete: true })
|
|
44
|
+
};
|
|
45
|
+
patchTurnState({ todoCollapsed: false, todos: [] });
|
|
46
|
+
return [msg];
|
|
47
|
+
};
|
|
48
|
+
export const resetTurnState = () => $turnState.set(buildTurnState());
|