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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvc-tui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "CVC — Cognitive Version Control terminal UI (Ink + React 19). Sidecar binary embedded in the cvc Python wheel.",
@@ -38,7 +38,7 @@
38
38
  "scripts": {
39
39
  "dev": "bun run src/entry.tsx 2>/dev/null || tsx --watch src/entry.tsx",
40
40
  "start": "bun run src/entry.tsx 2>/dev/null || tsx src/entry.tsx",
41
- "build": "bun build src/entry.tsx --target=node --outfile=dist/entry.js 2>/dev/null || (tsc -p tsconfig.build.json && chmod +x dist/entry.js)",
41
+ "build": "rm -rf dist && (bun build src/entry.tsx --target=node --outfile=dist/entry.js 2>/dev/null || esbuild src/entry.tsx --bundle --platform=node --format=esm --target=node20 --packages=bundle --alias:react-devtools-core=./src/lib/react-devtools-stub.ts --outfile=dist/entry.js) && chmod +x dist/entry.js",
42
42
  "build:compile": "bun build src/entry.tsx --compile --outfile=dist/cvc-tui",
43
43
  "type-check": "tsc --noEmit -p tsconfig.json",
44
44
  "test": "vitest run",
@@ -1,102 +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
- // Tab-completion logic. Pure functions where possible, with one
6
- // fs-touching helper for path expansion.
7
- import * as fs from 'node:fs';
8
- import * as os from 'node:os';
9
- import * as path from 'node:path';
10
- import { listCommandNames } from './slash/registry.js';
11
- /**
12
- * Find the token under the cursor: a maximal run of non-whitespace ending at
13
- * `cursor`. Returns null when no token (cursor on whitespace at boundary).
14
- */
15
- export function tokenAtCursor(text, cursor) {
16
- if (cursor < 0)
17
- cursor = 0;
18
- if (cursor > text.length)
19
- cursor = text.length;
20
- let s = cursor;
21
- while (s > 0 && !/\s/.test(text[s - 1]))
22
- s--;
23
- const tok = text.slice(s, cursor);
24
- if (!tok)
25
- return null;
26
- const isSlash = tok.startsWith('/') && s === firstNonSpaceOnLine(text, s);
27
- const isPath = !isSlash && (tok.startsWith('~/') || tok.startsWith('./') || tok.startsWith('../') || tok.startsWith('/'));
28
- return { start: s, token: tok, isSlash, isPath };
29
- }
30
- function firstNonSpaceOnLine(text, off) {
31
- // Walk back to start of logical line, then forward over spaces/tabs.
32
- const lineStart = text.lastIndexOf('\n', off - 1) + 1;
33
- let i = lineStart;
34
- while (i < text.length && (text[i] === ' ' || text[i] === '\t'))
35
- i++;
36
- return i;
37
- }
38
- /** Slash-command candidates whose name starts with `prefix`. */
39
- export function slashCandidates(prefix) {
40
- return listCommandNames()
41
- .filter((n) => n.startsWith(prefix))
42
- .sort();
43
- }
44
- /**
45
- * Path candidates for `prefix`. Reads the parent directory and returns
46
- * matching entries (files + dirs, dirs suffixed with `/`).
47
- *
48
- * Side-effect: touches the filesystem (fs.readdirSync).
49
- */
50
- export function pathCandidates(prefix) {
51
- let abs = prefix;
52
- let displayPrefix = prefix;
53
- if (prefix.startsWith('~/')) {
54
- abs = path.join(os.homedir(), prefix.slice(2));
55
- }
56
- // Decide directory + leaf to filter by.
57
- const lastSlash = abs.lastIndexOf('/');
58
- let dirAbs;
59
- let dirDisplay;
60
- let leaf;
61
- if (lastSlash < 0) {
62
- dirAbs = '.';
63
- dirDisplay = '';
64
- leaf = abs;
65
- }
66
- else {
67
- dirAbs = abs.slice(0, lastSlash + 1) || '/';
68
- dirDisplay = displayPrefix.slice(0, lastSlash + 1);
69
- leaf = abs.slice(lastSlash + 1);
70
- }
71
- let entries;
72
- try {
73
- entries = fs.readdirSync(dirAbs, { withFileTypes: true });
74
- }
75
- catch {
76
- return [];
77
- }
78
- const out = [];
79
- for (const e of entries) {
80
- if (!e.name.startsWith(leaf))
81
- continue;
82
- if (leaf === '' && e.name.startsWith('.'))
83
- continue; // skip hidden when no leaf
84
- out.push(dirDisplay + e.name + (e.isDirectory() ? '/' : ''));
85
- }
86
- return out.sort();
87
- }
88
- /** Compute candidates for the given context. */
89
- export function candidatesFor(ctx) {
90
- if (ctx.isSlash)
91
- return slashCandidates(ctx.token);
92
- if (ctx.isPath)
93
- return pathCandidates(ctx.token);
94
- return [];
95
- }
96
- /** Apply a candidate to the buffer at the given start offset (replacing token). */
97
- export function applyCandidate(text, start, tokenLen, candidate) {
98
- const before = text.slice(0, start);
99
- const after = text.slice(start + tokenLen);
100
- const next = before + candidate + after;
101
- return { text: next, cursor: start + candidate.length };
102
- }
@@ -1,508 +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 { STARTUP_IMAGE, STARTUP_QUERY } from '../config/env.js';
6
- import { STREAM_BATCH_MS } from '../config/timing.js';
7
- import { SETUP_REQUIRED_TITLE, buildSetupRequiredSections } from '../content/setup.js';
8
- import { rpcErrorMessage } from '../lib/rpc.js';
9
- import { topLevelSubagents } from '../lib/subagentTree.js';
10
- import { formatToolCall, stripAnsi } from '../lib/text.js';
11
- import { fromSkin } from '../theme.js';
12
- import { applyDelegationStatus, getDelegationState } from './delegationStore.js';
13
- import { patchOverlayState } from './overlayStore.js';
14
- import { turnController } from './turnController.js';
15
- import { getUiState, patchUiState } from './uiStore.js';
16
- const NO_PROVIDER_RE = /\bNo (?:LLM|inference) provider configured\b/i;
17
- const statusFromBusy = () => (getUiState().busy ? 'running…' : 'ready');
18
- const applySkin = (s) => patchUiState({
19
- theme: fromSkin(s.colors ?? {}, s.branding ?? {}, s.banner_logo ?? '', s.banner_hero ?? '', s.tool_prefix ?? '', s.help_header ?? '')
20
- });
21
- const dropBgTask = (taskId) => patchUiState(state => {
22
- const next = new Set(state.bgTasks);
23
- next.delete(taskId);
24
- return { ...state, bgTasks: next };
25
- });
26
- const pushUnique = (max) => (xs, x) => xs.at(-1) === x ? xs : [...xs, x].slice(-max);
27
- const pushThinking = pushUnique(6);
28
- const pushNote = pushUnique(6);
29
- const pushTool = pushUnique(8);
30
- export function createGatewayEventHandler(ctx) {
31
- const { rpc } = ctx.gateway;
32
- const { STARTUP_RESUME_ID, newSession, resumeById, setCatalog } = ctx.session;
33
- const { bellOnComplete, stdout, sys } = ctx.system;
34
- const { appendMessage, panel, setHistoryItems } = ctx.transcript;
35
- const { setInput } = ctx.composer;
36
- const { submitRef } = ctx.submission;
37
- const { setProcessing: setVoiceProcessing, setRecording: setVoiceRecording, setVoiceEnabled } = ctx.voice;
38
- let pendingThinkingStatus = '';
39
- let thinkingStatusTimer = null;
40
- let startupPromptSubmitted = false;
41
- // Inject the disk-save callback into turnController so recordMessageComplete
42
- // can fire-and-forget a persist without having to plumb a gateway ref around.
43
- turnController.persistSpawnTree = async (subagents, sessionId) => {
44
- try {
45
- const startedAt = subagents.reduce((min, s) => {
46
- if (!s.startedAt) {
47
- return min;
48
- }
49
- return min === 0 ? s.startedAt : Math.min(min, s.startedAt);
50
- }, 0);
51
- const top = topLevelSubagents(subagents)
52
- .map(s => s.goal)
53
- .filter(Boolean)
54
- .slice(0, 2);
55
- const label = top.length ? top.join(' · ') : `${subagents.length} subagents`;
56
- await rpc('spawn_tree.save', {
57
- finished_at: Date.now() / 1000,
58
- label: label.slice(0, 120),
59
- session_id: sessionId ?? 'default',
60
- started_at: startedAt ? startedAt / 1000 : null,
61
- subagents
62
- });
63
- }
64
- catch {
65
- // Persistence is best-effort; in-memory history is the authoritative
66
- // same-session source. A write failure doesn't block the turn.
67
- }
68
- };
69
- // Refresh delegation caps at most every 5s so the status bar HUD can
70
- // render a /warning close to the configured cap without spamming the RPC.
71
- let lastDelegationFetchAt = 0;
72
- const refreshDelegationStatus = (force = false) => {
73
- const now = Date.now();
74
- if (!force && now - lastDelegationFetchAt < 5000) {
75
- return;
76
- }
77
- lastDelegationFetchAt = now;
78
- rpc('delegation.status', {})
79
- .then(r => applyDelegationStatus(r))
80
- .catch(() => { });
81
- };
82
- const setStatus = (status) => {
83
- pendingThinkingStatus = '';
84
- if (thinkingStatusTimer) {
85
- clearTimeout(thinkingStatusTimer);
86
- thinkingStatusTimer = null;
87
- }
88
- patchUiState({ status });
89
- };
90
- const scheduleThinkingStatus = (status) => {
91
- pendingThinkingStatus = status;
92
- if (thinkingStatusTimer) {
93
- return;
94
- }
95
- thinkingStatusTimer = setTimeout(() => {
96
- thinkingStatusTimer = null;
97
- patchUiState({ status: pendingThinkingStatus || statusFromBusy() });
98
- }, STREAM_BATCH_MS);
99
- };
100
- const restoreStatusAfter = (ms) => {
101
- turnController.clearStatusTimer();
102
- turnController.statusTimer = setTimeout(() => {
103
- turnController.statusTimer = null;
104
- patchUiState({ status: statusFromBusy() });
105
- }, ms);
106
- };
107
- const scheduleStartupPrompt = () => {
108
- if (startupPromptSubmitted || (!STARTUP_QUERY && !STARTUP_IMAGE)) {
109
- return;
110
- }
111
- startupPromptSubmitted = true;
112
- setTimeout(async () => {
113
- let sid = getUiState().sid;
114
- for (let i = 0; !sid && i < 40; i += 1) {
115
- await new Promise(resolve => setTimeout(resolve, 100));
116
- sid = getUiState().sid;
117
- }
118
- if (!sid) {
119
- return sys('startup query skipped: no active session');
120
- }
121
- if (STARTUP_IMAGE) {
122
- try {
123
- await rpc('image.attach', { path: STARTUP_IMAGE, session_id: sid });
124
- }
125
- catch (e) {
126
- sys(`startup image attach failed: ${rpcErrorMessage(e)}`);
127
- }
128
- }
129
- submitRef.current(STARTUP_QUERY || 'What do you see in this image?');
130
- }, 0);
131
- };
132
- // Terminal statuses are never overwritten by late-arriving live events —
133
- // otherwise a stale `subagent.start` / `spawn_requested` can clobber a
134
- // `failed` or `interrupted` terminal state (Copilot review #14045).
135
- const isTerminalStatus = (s) => s === 'completed' || s === 'failed' || s === 'interrupted';
136
- const keepTerminalElseRunning = (s) => (isTerminalStatus(s) ? s : 'running');
137
- const handleReady = (skin) => {
138
- if (skin) {
139
- applySkin(skin);
140
- }
141
- rpc('commands.catalog', {})
142
- .then(r => {
143
- if (!r?.pairs) {
144
- return;
145
- }
146
- setCatalog({
147
- canon: (r.canon ?? {}),
148
- categories: r.categories ?? [],
149
- pairs: r.pairs,
150
- skillCount: (r.skill_count ?? 0),
151
- sub: (r.sub ?? {})
152
- });
153
- if (r.warning) {
154
- turnController.pushActivity(String(r.warning), 'warn');
155
- }
156
- })
157
- .catch((e) => turnController.pushActivity(`command catalog unavailable: ${rpcErrorMessage(e)}`, 'info'));
158
- if (STARTUP_RESUME_ID) {
159
- patchUiState({ status: 'resuming…' });
160
- resumeById(STARTUP_RESUME_ID);
161
- scheduleStartupPrompt();
162
- return;
163
- }
164
- // Opt-in: when `display.tui_auto_resume_recent` is true, look up
165
- // the most recent human-facing session and resume it instead of
166
- // forging a brand-new one. Mirrors classic CLI's `hermes -c` /
167
- // `hermes --tui` muscle memory and addresses the audit's "session
168
- // unrecoverable after disconnection" gap. Default off so existing
169
- // users aren't surprised.
170
- rpc('config.get', { key: 'full' })
171
- .then(cfg => {
172
- if (!cfg?.config?.display?.tui_auto_resume_recent) {
173
- patchUiState({ status: 'forging session…' });
174
- newSession();
175
- scheduleStartupPrompt();
176
- return;
177
- }
178
- return rpc('session.most_recent', {}).then(r => {
179
- const target = r?.session_id;
180
- if (target) {
181
- patchUiState({ status: 'resuming most recent…' });
182
- resumeById(target);
183
- scheduleStartupPrompt();
184
- return;
185
- }
186
- patchUiState({ status: 'forging session…' });
187
- newSession();
188
- scheduleStartupPrompt();
189
- });
190
- })
191
- .catch(() => {
192
- patchUiState({ status: 'forging session…' });
193
- newSession();
194
- scheduleStartupPrompt();
195
- });
196
- };
197
- return (ev) => {
198
- const sid = getUiState().sid;
199
- if (ev.session_id && sid && ev.session_id !== sid && !ev.type.startsWith('gateway.')) {
200
- return;
201
- }
202
- switch (ev.type) {
203
- case 'gateway.ready':
204
- handleReady(ev.payload?.skin);
205
- return;
206
- case 'skin.changed':
207
- if (ev.payload) {
208
- applySkin(ev.payload);
209
- }
210
- return;
211
- case 'session.info': {
212
- const info = ev.payload;
213
- patchUiState(state => ({
214
- ...state,
215
- info,
216
- status: state.status === 'starting agent…' ? 'ready' : state.status,
217
- usage: info.usage ? { ...state.usage, ...info.usage } : state.usage
218
- }));
219
- setHistoryItems(prev => prev.map(m => (m.kind === 'intro' ? { ...m, info } : m)));
220
- return;
221
- }
222
- case 'thinking.delta': {
223
- const text = ev.payload?.text;
224
- if (text !== undefined) {
225
- const value = String(text);
226
- scheduleThinkingStatus(value || statusFromBusy());
227
- if (value) {
228
- turnController.recordReasoningDelta(value);
229
- }
230
- }
231
- return;
232
- }
233
- case 'message.start':
234
- turnController.startMessage();
235
- return;
236
- case 'status.update': {
237
- const p = ev.payload;
238
- if (!p?.text) {
239
- return;
240
- }
241
- setStatus(p.text);
242
- if (p.kind === 'compressing') {
243
- sys(p.text);
244
- return;
245
- }
246
- if (p.kind === 'goal') {
247
- sys(p.text);
248
- return;
249
- }
250
- if (!p.kind || p.kind === 'status') {
251
- return;
252
- }
253
- if (turnController.lastStatusNote !== p.text) {
254
- turnController.lastStatusNote = p.text;
255
- turnController.pushActivity(p.text, p.kind === 'error' ? 'error' : p.kind === 'warn' || p.kind === 'approval' ? 'warn' : 'info');
256
- }
257
- restoreStatusAfter(4000);
258
- return;
259
- }
260
- case 'gateway.stderr': {
261
- const line = String(ev.payload.line).slice(0, 120);
262
- turnController.pushActivity(line, 'info');
263
- return;
264
- }
265
- case 'browser.progress': {
266
- const message = String(ev.payload?.message ?? '').trim();
267
- if (message) {
268
- sys(message);
269
- }
270
- return;
271
- }
272
- case 'voice.status': {
273
- // Continuous VAD loop reports its internal state so the status bar
274
- // can show listening / transcribing / idle without polling.
275
- const state = String(ev.payload?.state ?? '');
276
- if (state === 'listening') {
277
- setVoiceRecording(true);
278
- setVoiceProcessing(false);
279
- }
280
- else if (state === 'transcribing') {
281
- setVoiceRecording(false);
282
- setVoiceProcessing(true);
283
- }
284
- else {
285
- setVoiceRecording(false);
286
- setVoiceProcessing(false);
287
- }
288
- return;
289
- }
290
- case 'voice.transcript': {
291
- // CLI parity: the 3-strikes silence detector flipped off automatically.
292
- // Mirror that on the UI side and tell the user why the mode is off.
293
- if (ev.payload?.no_speech_limit) {
294
- setVoiceEnabled(false);
295
- setVoiceRecording(false);
296
- setVoiceProcessing(false);
297
- sys('voice: no speech detected 3 times, continuous mode stopped');
298
- return;
299
- }
300
- const text = String(ev.payload?.text ?? '').trim();
301
- if (!text) {
302
- return;
303
- }
304
- // CLI parity: _pending_input.put(transcript) unconditionally feeds
305
- // the transcript to the agent as its next turn — draft handling
306
- // doesn't apply because voice-mode users are speaking, not typing.
307
- //
308
- // We can't branch on composer input from inside a setInput updater
309
- // (React strict mode double-invokes it, duplicating the submit).
310
- // Just clear + defer submit so the cleared input is committed before
311
- // submit reads it.
312
- setInput('');
313
- setTimeout(() => submitRef.current(text), 0);
314
- return;
315
- }
316
- case 'gateway.start_timeout': {
317
- const { cwd, python, stderr_tail: stderrTail } = ev.payload ?? {};
318
- const trace = python || cwd ? ` · ${String(python || '')} ${String(cwd || '')}`.trim() : '';
319
- setStatus('gateway startup timeout');
320
- turnController.pushActivity(`gateway startup timed out${trace} · /logs to inspect`, 'error');
321
- // Surface the most useful stderr lines inline so users can tell
322
- // "wrong python", "missing dep", and "config parse failure"
323
- // apart without leaving the TUI. Filter blank rows BEFORE
324
- // taking the last N so trailing empty lines in the buffer
325
- // don't crowd out actual content; truncate to match the
326
- // 120-char clip used for `gateway.stderr` activity entries.
327
- const STDERR_LINE_CAP = 120;
328
- const STDERR_LINES_MAX = 8;
329
- const tailLines = (stderrTail ?? '')
330
- .split('\n')
331
- .map(l => l.trim())
332
- .filter(Boolean)
333
- .slice(-STDERR_LINES_MAX);
334
- for (const line of tailLines) {
335
- turnController.pushActivity(line.slice(0, STDERR_LINE_CAP), 'error');
336
- }
337
- return;
338
- }
339
- case 'gateway.protocol_error':
340
- setStatus('protocol warning');
341
- restoreStatusAfter(4000);
342
- if (!turnController.protocolWarned) {
343
- turnController.protocolWarned = true;
344
- turnController.pushActivity('protocol noise detected · /logs to inspect', 'info');
345
- }
346
- if (ev.payload?.preview) {
347
- turnController.pushActivity(`protocol noise: ${String(ev.payload.preview).slice(0, 120)}`, 'info');
348
- }
349
- return;
350
- case 'reasoning.delta':
351
- if (ev.payload?.text) {
352
- turnController.recordReasoningDelta(ev.payload.text);
353
- }
354
- return;
355
- case 'reasoning.available':
356
- turnController.recordReasoningAvailable(String(ev.payload?.text ?? ''));
357
- return;
358
- case 'tool.progress':
359
- if (ev.payload?.preview && ev.payload.name) {
360
- turnController.recordToolProgress(ev.payload.name, ev.payload.preview);
361
- }
362
- return;
363
- case 'tool.generating':
364
- if (ev.payload?.name) {
365
- turnController.pushTrail(`drafting ${ev.payload.name}…`);
366
- }
367
- return;
368
- case 'tool.start':
369
- turnController.recordTodos(ev.payload.todos);
370
- turnController.recordToolStart(ev.payload.tool_id, ev.payload.name ?? 'tool', ev.payload.context ?? '');
371
- return;
372
- case 'tool.complete': {
373
- const inlineDiffText = ev.payload.inline_diff && getUiState().inlineDiffs ? stripAnsi(String(ev.payload.inline_diff)).trim() : '';
374
- if (inlineDiffText) {
375
- turnController.recordInlineDiffToolComplete(inlineDiffText, ev.payload.tool_id, ev.payload.name, ev.payload.error, ev.payload.duration_s);
376
- }
377
- else {
378
- turnController.recordToolComplete(ev.payload.tool_id, ev.payload.name, ev.payload.error, ev.payload.summary, ev.payload.duration_s, ev.payload.todos);
379
- }
380
- return;
381
- }
382
- case 'clarify.request':
383
- patchOverlayState({
384
- clarify: { choices: ev.payload.choices, question: ev.payload.question, requestId: ev.payload.request_id }
385
- });
386
- setStatus('waiting for input…');
387
- return;
388
- case 'approval.request': {
389
- const description = String(ev.payload.description ?? 'dangerous command');
390
- patchOverlayState({ approval: { command: String(ev.payload.command ?? ''), description } });
391
- setStatus('approval needed');
392
- return;
393
- }
394
- case 'sudo.request':
395
- patchOverlayState({ sudo: { requestId: ev.payload.request_id } });
396
- setStatus('sudo password needed');
397
- return;
398
- case 'secret.request':
399
- patchOverlayState({
400
- secret: { envVar: ev.payload.env_var, prompt: ev.payload.prompt, requestId: ev.payload.request_id }
401
- });
402
- setStatus('secret input needed');
403
- return;
404
- case 'background.complete':
405
- dropBgTask(ev.payload.task_id);
406
- sys(`[bg ${ev.payload.task_id}] ${ev.payload.text}`);
407
- return;
408
- case 'review.summary': {
409
- // Self-improvement background review emitted a persistent summary
410
- // of what it saved to memory/skills. Surface it as a system line
411
- // in the transcript so it never gets lost to a transient status
412
- // flash. Python-side already formats it as "💾 Self-improvement
413
- // review: …".
414
- const text = String(ev.payload?.text ?? '').trim();
415
- if (text) {
416
- sys(text);
417
- }
418
- return;
419
- }
420
- case 'subagent.spawn_requested':
421
- // Child built but not yet running (waiting on ThreadPoolExecutor slot).
422
- // Preserve completed state if a later event races in before this one.
423
- turnController.upsertSubagent(ev.payload, c => (isTerminalStatus(c.status) ? {} : { status: 'queued' }));
424
- // Prime the status-bar HUD: fetch caps (once every 5s) so we can
425
- // warn as depth/concurrency approaches the configured ceiling.
426
- if (getDelegationState().maxSpawnDepth === null) {
427
- refreshDelegationStatus(true);
428
- }
429
- else {
430
- refreshDelegationStatus();
431
- }
432
- return;
433
- case 'subagent.start':
434
- turnController.upsertSubagent(ev.payload, c => (isTerminalStatus(c.status) ? {} : { status: 'running' }));
435
- return;
436
- case 'subagent.thinking': {
437
- const text = String(ev.payload.text ?? '').trim();
438
- if (!text) {
439
- return;
440
- }
441
- // Update-only: never resurrect subagents whose spawn_requested/start
442
- // we missed or that already flushed via message.complete.
443
- turnController.upsertSubagent(ev.payload, c => ({
444
- status: keepTerminalElseRunning(c.status),
445
- thinking: pushThinking(c.thinking, text)
446
- }), { createIfMissing: false });
447
- return;
448
- }
449
- case 'subagent.tool': {
450
- const line = formatToolCall(ev.payload.tool_name ?? 'delegate_task', ev.payload.tool_preview ?? ev.payload.text ?? '');
451
- turnController.upsertSubagent(ev.payload, c => ({
452
- status: keepTerminalElseRunning(c.status),
453
- tools: pushTool(c.tools, line)
454
- }), { createIfMissing: false });
455
- return;
456
- }
457
- case 'subagent.progress': {
458
- const text = String(ev.payload.text ?? '').trim();
459
- if (!text) {
460
- return;
461
- }
462
- turnController.upsertSubagent(ev.payload, c => ({
463
- notes: pushNote(c.notes, text),
464
- status: keepTerminalElseRunning(c.status)
465
- }), { createIfMissing: false });
466
- return;
467
- }
468
- case 'subagent.complete':
469
- turnController.upsertSubagent(ev.payload, c => ({
470
- durationSeconds: ev.payload.duration_seconds ?? c.durationSeconds,
471
- status: ev.payload.status ?? 'completed',
472
- summary: ev.payload.summary || ev.payload.text || c.summary
473
- }), { createIfMissing: false });
474
- return;
475
- case 'message.delta':
476
- turnController.recordMessageDelta(ev.payload ?? {});
477
- return;
478
- case 'message.complete': {
479
- const { finalMessages, finalText, wasInterrupted } = turnController.recordMessageComplete(ev.payload ?? {});
480
- if (!wasInterrupted) {
481
- const msgs = finalMessages.length ? finalMessages : [{ role: 'assistant', text: finalText }];
482
- msgs.forEach(appendMessage);
483
- if (bellOnComplete && stdout?.isTTY) {
484
- stdout.write('\x07');
485
- }
486
- }
487
- setStatus('ready');
488
- if (ev.payload?.usage) {
489
- patchUiState(state => ({ ...state, usage: { ...state.usage, ...ev.payload.usage } }));
490
- }
491
- return;
492
- }
493
- case 'error':
494
- turnController.recordError();
495
- {
496
- const message = String(ev.payload?.message || 'unknown error');
497
- turnController.pushActivity(message, 'error');
498
- if (NO_PROVIDER_RE.test(message)) {
499
- panel(SETUP_REQUIRED_TITLE, buildSetupRequiredSections());
500
- setStatus('setup required');
501
- return;
502
- }
503
- sys(`error: ${message}`);
504
- setStatus('ready');
505
- }
506
- }
507
- };
508
- }