cvc-tui 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/entry.js +71147 -61
  2. package/package.json +2 -2
  3. package/dist/app/completion.js +0 -102
  4. package/dist/app/createGatewayEventHandler.js +0 -508
  5. package/dist/app/createSlashHandler.js +0 -101
  6. package/dist/app/delegationStore.js +0 -51
  7. package/dist/app/gatewayContext.js +0 -17
  8. package/dist/app/historyStore.js +0 -123
  9. package/dist/app/inputBuffer.js +0 -120
  10. package/dist/app/inputSelectionStore.js +0 -8
  11. package/dist/app/inputStore.js +0 -28
  12. package/dist/app/interfaces.js +0 -6
  13. package/dist/app/overlayStore.js +0 -40
  14. package/dist/app/promptStore.js +0 -44
  15. package/dist/app/queueStore.js +0 -25
  16. package/dist/app/scroll.js +0 -44
  17. package/dist/app/setupHandoff.js +0 -28
  18. package/dist/app/slash/commands/core.js +0 -479
  19. package/dist/app/slash/commands/debug.js +0 -44
  20. package/dist/app/slash/commands/ops.js +0 -498
  21. package/dist/app/slash/commands/session.js +0 -431
  22. package/dist/app/slash/commands/setup.js +0 -20
  23. package/dist/app/slash/commands/toggles.js +0 -40
  24. package/dist/app/slash/registry.js +0 -18
  25. package/dist/app/slash/types.js +0 -1
  26. package/dist/app/spawnHistoryStore.js +0 -105
  27. package/dist/app/turnController.js +0 -650
  28. package/dist/app/turnStore.js +0 -48
  29. package/dist/app/uiStore.js +0 -36
  30. package/dist/app/useComposerState.js +0 -265
  31. package/dist/app/useConfigSync.js +0 -144
  32. package/dist/app/useInputHandlers.js +0 -403
  33. package/dist/app/useLongRunToolCharms.js +0 -50
  34. package/dist/app/useMainApp.js +0 -629
  35. package/dist/app/useSessionLifecycle.js +0 -175
  36. package/dist/app/useSubmission.js +0 -287
  37. package/dist/app.js +0 -15
  38. package/dist/banner.js +0 -57
  39. package/dist/components/agentsOverlay.js +0 -474
  40. package/dist/components/appChrome.js +0 -252
  41. package/dist/components/appLayout.js +0 -121
  42. package/dist/components/appOverlays.js +0 -65
  43. package/dist/components/branding.js +0 -97
  44. package/dist/components/fpsOverlay.js +0 -22
  45. package/dist/components/helpHint.js +0 -21
  46. package/dist/components/markdown.js +0 -501
  47. package/dist/components/maskedPrompt.js +0 -12
  48. package/dist/components/messageLine.js +0 -82
  49. package/dist/components/modelPicker.js +0 -254
  50. package/dist/components/overlayControls.js +0 -30
  51. package/dist/components/overlays/confirmPrompt.js +0 -25
  52. package/dist/components/overlays/helpOverlay.js +0 -76
  53. package/dist/components/overlays/historySearch.js +0 -49
  54. package/dist/components/overlays/modelPicker.js +0 -60
  55. package/dist/components/overlays/overlayUtils.js +0 -19
  56. package/dist/components/overlays/secretPrompt.js +0 -36
  57. package/dist/components/overlays/sessionPicker.js +0 -93
  58. package/dist/components/overlays/skillsHub.js +0 -71
  59. package/dist/components/prompts.js +0 -95
  60. package/dist/components/queuedMessages.js +0 -24
  61. package/dist/components/sessionPicker.js +0 -130
  62. package/dist/components/skillsHub.js +0 -165
  63. package/dist/components/streamingAssistant.js +0 -35
  64. package/dist/components/streamingMarkdown.js +0 -144
  65. package/dist/components/textInput.js +0 -794
  66. package/dist/components/themed.js +0 -12
  67. package/dist/components/thinking.js +0 -496
  68. package/dist/components/todoPanel.js +0 -40
  69. package/dist/components/transcript.js +0 -22
  70. package/dist/config/env.js +0 -18
  71. package/dist/config/limits.js +0 -22
  72. package/dist/config/timing.js +0 -18
  73. package/dist/content/charms.js +0 -5
  74. package/dist/content/faces.js +0 -21
  75. package/dist/content/fortunes.js +0 -29
  76. package/dist/content/hotkeys.js +0 -38
  77. package/dist/content/placeholders.js +0 -15
  78. package/dist/content/setup.js +0 -14
  79. package/dist/content/verbs.js +0 -41
  80. package/dist/domain/details.js +0 -53
  81. package/dist/domain/messages.js +0 -63
  82. package/dist/domain/paths.js +0 -16
  83. package/dist/domain/providers.js +0 -11
  84. package/dist/domain/roles.js +0 -6
  85. package/dist/domain/slash.js +0 -11
  86. package/dist/domain/usage.js +0 -1
  87. package/dist/domain/viewport.js +0 -33
  88. package/dist/gateway/client.js +0 -312
  89. package/dist/gatewayClient.js +0 -574
  90. package/dist/gatewayTypes.js +0 -1
  91. package/dist/hooks/useCompletion.js +0 -86
  92. package/dist/hooks/useGitBranch.js +0 -58
  93. package/dist/hooks/useInputHistory.js +0 -12
  94. package/dist/hooks/useQueue.js +0 -57
  95. package/dist/hooks/useVirtualHistory.js +0 -401
  96. package/dist/lib/circularBuffer.js +0 -43
  97. package/dist/lib/clipboard.js +0 -126
  98. package/dist/lib/editor.js +0 -41
  99. package/dist/lib/editor.test.js +0 -58
  100. package/dist/lib/emoji.js +0 -49
  101. package/dist/lib/externalCli.js +0 -11
  102. package/dist/lib/forceTruecolor.js +0 -26
  103. package/dist/lib/fpsStore.js +0 -36
  104. package/dist/lib/gracefulExit.js +0 -29
  105. package/dist/lib/history.js +0 -69
  106. package/dist/lib/inputMetrics.js +0 -143
  107. package/dist/lib/liveProgress.js +0 -51
  108. package/dist/lib/liveProgress.test.js +0 -89
  109. package/dist/lib/mathUnicode.js +0 -685
  110. package/dist/lib/memory.js +0 -123
  111. package/dist/lib/memoryMonitor.js +0 -76
  112. package/dist/lib/messages.js +0 -3
  113. package/dist/lib/messages.test.js +0 -25
  114. package/dist/lib/osc52.js +0 -53
  115. package/dist/lib/perfPane.js +0 -94
  116. package/dist/lib/platform.js +0 -312
  117. package/dist/lib/precisionWheel.js +0 -25
  118. package/dist/lib/reasoning.js +0 -39
  119. package/dist/lib/rpc.js +0 -26
  120. package/dist/lib/subagentTree.js +0 -287
  121. package/dist/lib/syntax.js +0 -89
  122. package/dist/lib/terminalModes.js +0 -46
  123. package/dist/lib/terminalParity.js +0 -48
  124. package/dist/lib/terminalSetup.js +0 -321
  125. package/dist/lib/text.js +0 -203
  126. package/dist/lib/text.test.js +0 -18
  127. package/dist/lib/todo.js +0 -2
  128. package/dist/lib/todo.test.js +0 -22
  129. package/dist/lib/viewportStore.js +0 -82
  130. package/dist/lib/virtualHeights.js +0 -61
  131. package/dist/lib/wheelAccel.js +0 -143
  132. package/dist/theme.js +0 -398
  133. package/dist/types.js +0 -1
@@ -1,650 +0,0 @@
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();
@@ -1,48 +0,0 @@
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());