cvc-tui 0.4.0 → 0.4.2
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/dist/entry.js +71148 -61
- package/package.json +2 -2
- package/dist/app/completion.js +0 -102
- package/dist/app/createGatewayEventHandler.js +0 -508
- package/dist/app/createSlashHandler.js +0 -101
- package/dist/app/delegationStore.js +0 -51
- package/dist/app/gatewayContext.js +0 -17
- package/dist/app/historyStore.js +0 -123
- package/dist/app/inputBuffer.js +0 -120
- package/dist/app/inputSelectionStore.js +0 -8
- package/dist/app/inputStore.js +0 -28
- package/dist/app/interfaces.js +0 -6
- package/dist/app/overlayStore.js +0 -40
- package/dist/app/promptStore.js +0 -44
- package/dist/app/queueStore.js +0 -25
- package/dist/app/scroll.js +0 -44
- package/dist/app/setupHandoff.js +0 -28
- package/dist/app/slash/commands/core.js +0 -479
- package/dist/app/slash/commands/debug.js +0 -44
- package/dist/app/slash/commands/ops.js +0 -498
- package/dist/app/slash/commands/session.js +0 -431
- package/dist/app/slash/commands/setup.js +0 -20
- package/dist/app/slash/commands/toggles.js +0 -40
- package/dist/app/slash/registry.js +0 -18
- package/dist/app/slash/types.js +0 -1
- package/dist/app/spawnHistoryStore.js +0 -105
- package/dist/app/turnController.js +0 -650
- package/dist/app/turnStore.js +0 -48
- package/dist/app/uiStore.js +0 -36
- package/dist/app/useComposerState.js +0 -265
- package/dist/app/useConfigSync.js +0 -144
- package/dist/app/useInputHandlers.js +0 -403
- package/dist/app/useLongRunToolCharms.js +0 -50
- package/dist/app/useMainApp.js +0 -629
- package/dist/app/useSessionLifecycle.js +0 -175
- package/dist/app/useSubmission.js +0 -287
- package/dist/app.js +0 -15
- package/dist/banner.js +0 -57
- package/dist/components/agentsOverlay.js +0 -474
- package/dist/components/appChrome.js +0 -252
- package/dist/components/appLayout.js +0 -121
- package/dist/components/appOverlays.js +0 -65
- package/dist/components/branding.js +0 -97
- package/dist/components/fpsOverlay.js +0 -22
- package/dist/components/helpHint.js +0 -21
- package/dist/components/markdown.js +0 -501
- package/dist/components/maskedPrompt.js +0 -12
- package/dist/components/messageLine.js +0 -82
- package/dist/components/modelPicker.js +0 -254
- package/dist/components/overlayControls.js +0 -30
- package/dist/components/overlays/confirmPrompt.js +0 -25
- package/dist/components/overlays/helpOverlay.js +0 -76
- package/dist/components/overlays/historySearch.js +0 -49
- package/dist/components/overlays/modelPicker.js +0 -60
- package/dist/components/overlays/overlayUtils.js +0 -19
- package/dist/components/overlays/secretPrompt.js +0 -36
- package/dist/components/overlays/sessionPicker.js +0 -93
- package/dist/components/overlays/skillsHub.js +0 -71
- package/dist/components/prompts.js +0 -95
- package/dist/components/queuedMessages.js +0 -24
- package/dist/components/sessionPicker.js +0 -130
- package/dist/components/skillsHub.js +0 -165
- package/dist/components/streamingAssistant.js +0 -35
- package/dist/components/streamingMarkdown.js +0 -144
- package/dist/components/textInput.js +0 -794
- package/dist/components/themed.js +0 -12
- package/dist/components/thinking.js +0 -496
- package/dist/components/todoPanel.js +0 -40
- package/dist/components/transcript.js +0 -22
- package/dist/config/env.js +0 -18
- package/dist/config/limits.js +0 -22
- package/dist/config/timing.js +0 -18
- package/dist/content/charms.js +0 -5
- package/dist/content/faces.js +0 -21
- package/dist/content/fortunes.js +0 -29
- package/dist/content/hotkeys.js +0 -38
- package/dist/content/placeholders.js +0 -15
- package/dist/content/setup.js +0 -14
- package/dist/content/verbs.js +0 -41
- package/dist/domain/details.js +0 -53
- package/dist/domain/messages.js +0 -63
- package/dist/domain/paths.js +0 -16
- package/dist/domain/providers.js +0 -11
- package/dist/domain/roles.js +0 -6
- package/dist/domain/slash.js +0 -11
- package/dist/domain/usage.js +0 -1
- package/dist/domain/viewport.js +0 -33
- package/dist/gateway/client.js +0 -312
- package/dist/gatewayClient.js +0 -574
- package/dist/gatewayTypes.js +0 -1
- package/dist/hooks/useCompletion.js +0 -86
- package/dist/hooks/useGitBranch.js +0 -58
- package/dist/hooks/useInputHistory.js +0 -12
- package/dist/hooks/useQueue.js +0 -57
- package/dist/hooks/useVirtualHistory.js +0 -401
- package/dist/lib/circularBuffer.js +0 -43
- package/dist/lib/clipboard.js +0 -126
- package/dist/lib/editor.js +0 -41
- package/dist/lib/editor.test.js +0 -58
- package/dist/lib/emoji.js +0 -49
- package/dist/lib/externalCli.js +0 -11
- package/dist/lib/forceTruecolor.js +0 -26
- package/dist/lib/fpsStore.js +0 -36
- package/dist/lib/gracefulExit.js +0 -29
- package/dist/lib/history.js +0 -69
- package/dist/lib/inputMetrics.js +0 -143
- package/dist/lib/liveProgress.js +0 -51
- package/dist/lib/liveProgress.test.js +0 -89
- package/dist/lib/mathUnicode.js +0 -685
- package/dist/lib/memory.js +0 -123
- package/dist/lib/memoryMonitor.js +0 -76
- package/dist/lib/messages.js +0 -3
- package/dist/lib/messages.test.js +0 -25
- package/dist/lib/osc52.js +0 -53
- package/dist/lib/perfPane.js +0 -94
- package/dist/lib/platform.js +0 -312
- package/dist/lib/precisionWheel.js +0 -25
- package/dist/lib/reasoning.js +0 -39
- package/dist/lib/rpc.js +0 -26
- package/dist/lib/subagentTree.js +0 -287
- package/dist/lib/syntax.js +0 -89
- package/dist/lib/terminalModes.js +0 -46
- package/dist/lib/terminalParity.js +0 -48
- package/dist/lib/terminalSetup.js +0 -321
- package/dist/lib/text.js +0 -203
- package/dist/lib/text.test.js +0 -18
- package/dist/lib/todo.js +0 -2
- package/dist/lib/todo.test.js +0 -22
- package/dist/lib/viewportStore.js +0 -82
- package/dist/lib/virtualHeights.js +0 -61
- package/dist/lib/wheelAccel.js +0 -143
- package/dist/theme.js +0 -398
- package/dist/types.js +0 -1
package/dist/app/useMainApp.js
DELETED
|
@@ -1,629 +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 { useApp, useHasSelection, useSelection, useStdout, useTerminalTitle } from '@cvc/ink';
|
|
6
|
-
import { useStore } from '@nanostores/react';
|
|
7
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
-
import { STARTUP_RESUME_ID } from '../config/env.js';
|
|
9
|
-
import { FULL_RENDER_TAIL_ITEMS, MAX_HISTORY, WHEEL_SCROLL_STEP } from '../config/limits.js';
|
|
10
|
-
import { SECTION_NAMES, sectionMode } from '../domain/details.js';
|
|
11
|
-
import { attachedImageNotice, imageTokenMeta } from '../domain/messages.js';
|
|
12
|
-
import { fmtCwdBranch, shortCwd } from '../domain/paths.js';
|
|
13
|
-
import { useGitBranch } from '../hooks/useGitBranch.js';
|
|
14
|
-
import { useVirtualHistory } from '../hooks/useVirtualHistory.js';
|
|
15
|
-
import { composerPromptWidth } from '../lib/inputMetrics.js';
|
|
16
|
-
import { appendTranscriptMessage } from '../lib/messages.js';
|
|
17
|
-
import { DEFAULT_VOICE_RECORD_KEY, isMac } from '../lib/platform.js';
|
|
18
|
-
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js';
|
|
19
|
-
import { terminalParityHints } from '../lib/terminalParity.js';
|
|
20
|
-
import { buildToolTrailLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js';
|
|
21
|
-
import { estimatedMsgHeight, messageHeightKey } from '../lib/virtualHeights.js';
|
|
22
|
-
import { createGatewayEventHandler } from './createGatewayEventHandler.js';
|
|
23
|
-
import { createSlashHandler } from './createSlashHandler.js';
|
|
24
|
-
import { getInputSelection } from './inputSelectionStore.js';
|
|
25
|
-
import { $overlayState, patchOverlayState } from './overlayStore.js';
|
|
26
|
-
import { scrollWithSelectionBy } from './scroll.js';
|
|
27
|
-
import { turnController } from './turnController.js';
|
|
28
|
-
import { patchTurnState, useTurnSelector } from './turnStore.js';
|
|
29
|
-
import { $uiState, getUiState, patchUiState } from './uiStore.js';
|
|
30
|
-
import { useComposerState } from './useComposerState.js';
|
|
31
|
-
import { useConfigSync } from './useConfigSync.js';
|
|
32
|
-
import { useInputHandlers } from './useInputHandlers.js';
|
|
33
|
-
import { useLongRunToolCharms } from './useLongRunToolCharms.js';
|
|
34
|
-
import { useSessionLifecycle } from './useSessionLifecycle.js';
|
|
35
|
-
import { useSubmission } from './useSubmission.js';
|
|
36
|
-
const GOOD_VIBES_RE = /\b(good bot|thanks|thank you|thx|ty|ily|love you)\b/i;
|
|
37
|
-
const BRACKET_PASTE_ON = '\x1b[?2004h';
|
|
38
|
-
const BRACKET_PASTE_OFF = '\x1b[?2004l';
|
|
39
|
-
const MAX_HEIGHT_CACHE_BUCKETS = 12;
|
|
40
|
-
const capHistory = (items) => {
|
|
41
|
-
if (items.length <= MAX_HISTORY) {
|
|
42
|
-
return items;
|
|
43
|
-
}
|
|
44
|
-
return items[0]?.kind === 'intro' ? [items[0], ...items.slice(-(MAX_HISTORY - 1))] : items.slice(-MAX_HISTORY);
|
|
45
|
-
};
|
|
46
|
-
const statusColorOf = (status, t) => {
|
|
47
|
-
if (status === 'ready') {
|
|
48
|
-
return t.ok;
|
|
49
|
-
}
|
|
50
|
-
if (status.startsWith('error')) {
|
|
51
|
-
return t.error;
|
|
52
|
-
}
|
|
53
|
-
if (status === 'interrupted') {
|
|
54
|
-
return t.warn;
|
|
55
|
-
}
|
|
56
|
-
return t.muted;
|
|
57
|
-
};
|
|
58
|
-
export function useMainApp(gw) {
|
|
59
|
-
const { exit } = useApp();
|
|
60
|
-
const { stdout } = useStdout();
|
|
61
|
-
const [cols, setCols] = useState(stdout?.columns ?? 80);
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (!stdout) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const sync = () => setCols(stdout.columns ?? 80);
|
|
67
|
-
stdout.on('resize', sync);
|
|
68
|
-
if (stdout.isTTY) {
|
|
69
|
-
stdout.write(BRACKET_PASTE_ON);
|
|
70
|
-
}
|
|
71
|
-
return () => {
|
|
72
|
-
stdout.off('resize', sync);
|
|
73
|
-
if (stdout.isTTY) {
|
|
74
|
-
stdout.write(BRACKET_PASTE_OFF);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}, [stdout]);
|
|
78
|
-
const [historyItems, setHistoryItems] = useState(() => [{ kind: 'intro', role: 'system', text: '' }]);
|
|
79
|
-
const [lastUserMsg, setLastUserMsg] = useState('');
|
|
80
|
-
const [stickyPrompt, setStickyPrompt] = useState('');
|
|
81
|
-
const [catalog, setCatalog] = useState(null);
|
|
82
|
-
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
83
|
-
const [voiceRecording, setVoiceRecording] = useState(false);
|
|
84
|
-
const [voiceProcessing, setVoiceProcessing] = useState(false);
|
|
85
|
-
const [voiceRecordKey, setVoiceRecordKey] = useState(DEFAULT_VOICE_RECORD_KEY);
|
|
86
|
-
const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now());
|
|
87
|
-
const [turnStartedAt, setTurnStartedAt] = useState(null);
|
|
88
|
-
const [goodVibesTick, setGoodVibesTick] = useState(0);
|
|
89
|
-
const [bellOnComplete, setBellOnComplete] = useState(false);
|
|
90
|
-
const ui = useStore($uiState);
|
|
91
|
-
const overlay = useStore($overlayState);
|
|
92
|
-
const turnLiveTailActive = useTurnSelector(state => Boolean(state.streaming ||
|
|
93
|
-
state.streamPendingTools.length ||
|
|
94
|
-
state.streamSegments.length ||
|
|
95
|
-
state.reasoning.trim() ||
|
|
96
|
-
state.reasoningActive ||
|
|
97
|
-
state.tools.length ||
|
|
98
|
-
state.subagents.length ||
|
|
99
|
-
state.todos.length));
|
|
100
|
-
const slashFlightRef = useRef(0);
|
|
101
|
-
const slashRef = useRef(() => false);
|
|
102
|
-
const colsRef = useRef(cols);
|
|
103
|
-
const scrollRef = useRef(null);
|
|
104
|
-
const onEventRef = useRef(() => { });
|
|
105
|
-
const clipboardPasteRef = useRef(() => { });
|
|
106
|
-
const submitRef = useRef(() => { });
|
|
107
|
-
const terminalHintsShownRef = useRef(new Set());
|
|
108
|
-
const historyItemsRef = useRef(historyItems);
|
|
109
|
-
const lastUserMsgRef = useRef(lastUserMsg);
|
|
110
|
-
const msgIdsRef = useRef(new WeakMap());
|
|
111
|
-
const msgIdSeqRef = useRef(0);
|
|
112
|
-
const heightCachesRef = useRef(new Map());
|
|
113
|
-
colsRef.current = cols;
|
|
114
|
-
historyItemsRef.current = historyItems;
|
|
115
|
-
lastUserMsgRef.current = lastUserMsg;
|
|
116
|
-
const hasSelection = useHasSelection();
|
|
117
|
-
const selection = useSelection();
|
|
118
|
-
const lastCopiedVersionRef = useRef(-1);
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
selection.setSelectionBgColor(ui.theme.color.selectionBg);
|
|
121
|
-
}, [selection, ui.theme.color.selectionBg]);
|
|
122
|
-
// macOS Terminal.app does not forward Cmd+C to fullscreen TUIs that enable
|
|
123
|
-
// mouse tracking, so the only reliable native-feeling path is iTerm-style
|
|
124
|
-
// copy-on-select: once a drag creates a stable TUI selection, write it to
|
|
125
|
-
// the system clipboard while keeping the highlight visible.
|
|
126
|
-
//
|
|
127
|
-
// Subscribe directly via the ink selection bus (not useSyncExternalStore)
|
|
128
|
-
// so React doesn't re-render MainApp on every drag-move tick. The version
|
|
129
|
-
// ref de-dupes against re-entrant notifications.
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (!isMac) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
return selection.subscribe(() => {
|
|
135
|
-
if (!selection.hasSelection()) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const state = selection.getState();
|
|
139
|
-
if (state?.isDragging) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const version = selection.version();
|
|
143
|
-
if (version === lastCopiedVersionRef.current) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
lastCopiedVersionRef.current = version;
|
|
147
|
-
void selection.copySelectionNoClear();
|
|
148
|
-
});
|
|
149
|
-
}, [selection]);
|
|
150
|
-
const clearSelection = useCallback(() => {
|
|
151
|
-
selection.clearSelection();
|
|
152
|
-
getInputSelection()?.collapseToEnd();
|
|
153
|
-
}, [selection]);
|
|
154
|
-
const composer = useComposerState({
|
|
155
|
-
gw,
|
|
156
|
-
onClipboardPaste: quiet => clipboardPasteRef.current(quiet),
|
|
157
|
-
onImageAttached: info => {
|
|
158
|
-
sys(attachedImageNotice(info));
|
|
159
|
-
},
|
|
160
|
-
submitRef
|
|
161
|
-
});
|
|
162
|
-
const { actions: composerActions, refs: composerRefs, state: composerState } = composer;
|
|
163
|
-
const empty = !historyItems.some(msg => msg.kind !== 'intro');
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
void terminalParityHints()
|
|
166
|
-
.then(hints => {
|
|
167
|
-
for (const hint of hints) {
|
|
168
|
-
if (terminalHintsShownRef.current.has(hint.key)) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
terminalHintsShownRef.current.add(hint.key);
|
|
172
|
-
turnController.pushActivity(hint.message, hint.tone);
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
.catch(() => { });
|
|
176
|
-
}, []);
|
|
177
|
-
const messageId = useCallback((msg) => {
|
|
178
|
-
const hit = msgIdsRef.current.get(msg);
|
|
179
|
-
if (hit) {
|
|
180
|
-
return hit;
|
|
181
|
-
}
|
|
182
|
-
const next = `${messageHeightKey(msg)}:${++msgIdSeqRef.current}`;
|
|
183
|
-
msgIdsRef.current.set(msg, next);
|
|
184
|
-
return next;
|
|
185
|
-
}, []);
|
|
186
|
-
const virtualRows = useMemo(() => historyItems.map((msg, index) => ({ index, key: messageId(msg), msg })), [historyItems, messageId]);
|
|
187
|
-
const detailsLayoutKey = useMemo(() => {
|
|
188
|
-
const thinking = sectionMode('thinking', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride);
|
|
189
|
-
const tools = sectionMode('tools', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride);
|
|
190
|
-
return `${thinking}:${tools}`;
|
|
191
|
-
}, [ui.detailsMode, ui.detailsModeCommandOverride, ui.sections]);
|
|
192
|
-
const detailsVisible = detailsLayoutKey !== 'hidden:hidden';
|
|
193
|
-
const userPromptWidth = composerPromptWidth(ui.theme.brand.prompt);
|
|
194
|
-
const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${userPromptWidth}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`;
|
|
195
|
-
const heightCache = useMemo(() => {
|
|
196
|
-
let cache = heightCachesRef.current.get(heightCacheKey);
|
|
197
|
-
if (!cache) {
|
|
198
|
-
cache = new Map();
|
|
199
|
-
heightCachesRef.current.set(heightCacheKey, cache);
|
|
200
|
-
if (heightCachesRef.current.size > MAX_HEIGHT_CACHE_BUCKETS) {
|
|
201
|
-
heightCachesRef.current.delete(heightCachesRef.current.keys().next().value);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return cache;
|
|
205
|
-
}, [heightCacheKey]);
|
|
206
|
-
// Index of the first user-role message — separator-rendering in
|
|
207
|
-
// appLayout.tsx skips this row, so the height estimator must skip it
|
|
208
|
-
// too. -1 when no user message exists yet (no row will gate true).
|
|
209
|
-
const firstUserIdx = useMemo(() => virtualRows.findIndex(r => r.msg.role === 'user'), [virtualRows]);
|
|
210
|
-
const estimateRowHeight = useCallback((index) => estimatedMsgHeight(virtualRows[index].msg, cols, {
|
|
211
|
-
compact: ui.compact,
|
|
212
|
-
details: detailsVisible,
|
|
213
|
-
limitHistory: index < virtualRows.length - FULL_RENDER_TAIL_ITEMS,
|
|
214
|
-
userPrompt: ui.theme.brand.prompt,
|
|
215
|
-
withSeparator: virtualRows[index].msg.role === 'user' && firstUserIdx >= 0 && index > firstUserIdx
|
|
216
|
-
}), [cols, detailsVisible, firstUserIdx, ui.compact, ui.theme.brand.prompt, virtualRows]);
|
|
217
|
-
const syncHeightCache = useCallback((heights) => {
|
|
218
|
-
for (const row of virtualRows) {
|
|
219
|
-
const h = heights.get(row.key);
|
|
220
|
-
if (h) {
|
|
221
|
-
heightCache.set(row.key, h);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}, [heightCache, virtualRows]);
|
|
225
|
-
const virtualHistory = useVirtualHistory(scrollRef, virtualRows, cols, {
|
|
226
|
-
estimateHeight: estimateRowHeight,
|
|
227
|
-
initialHeights: heightCache,
|
|
228
|
-
liveTailActive: turnLiveTailActive,
|
|
229
|
-
onHeightsChange: syncHeightCache
|
|
230
|
-
});
|
|
231
|
-
const scrollWithSelection = useCallback((delta) => scrollWithSelectionBy(delta, { scrollRef, selection }), [selection]);
|
|
232
|
-
const appendMessage = useCallback((msg) => setHistoryItems(prev => capHistory(appendTranscriptMessage(prev, msg))), []);
|
|
233
|
-
const sys = useCallback((text) => appendMessage({ role: 'system', text }), [appendMessage]);
|
|
234
|
-
const page = useCallback((text, title) => patchOverlayState({ pager: { lines: text.split('\n'), offset: 0, title } }), []);
|
|
235
|
-
const panel = useCallback((title, sections) => appendMessage({ kind: 'panel', panelData: { sections, title }, role: 'system', text: '' }), [appendMessage]);
|
|
236
|
-
const maybeWarn = useCallback((value) => {
|
|
237
|
-
const warning = value?.warning;
|
|
238
|
-
if (typeof warning === 'string' && warning) {
|
|
239
|
-
sys(`warning: ${warning}`);
|
|
240
|
-
}
|
|
241
|
-
}, [sys]);
|
|
242
|
-
const maybeGoodVibes = useCallback((text) => {
|
|
243
|
-
if (GOOD_VIBES_RE.test(text)) {
|
|
244
|
-
setGoodVibesTick(v => v + 1);
|
|
245
|
-
}
|
|
246
|
-
}, []);
|
|
247
|
-
const rpc = useCallback(async (method, params = {}) => {
|
|
248
|
-
try {
|
|
249
|
-
const result = asRpcResult(await gw.request(method, params));
|
|
250
|
-
if (result) {
|
|
251
|
-
return result;
|
|
252
|
-
}
|
|
253
|
-
sys(`error: invalid response: ${method}`);
|
|
254
|
-
}
|
|
255
|
-
catch (e) {
|
|
256
|
-
sys(`error: ${rpcErrorMessage(e)}`);
|
|
257
|
-
}
|
|
258
|
-
return null;
|
|
259
|
-
}, [gw, sys]);
|
|
260
|
-
const gateway = useMemo(() => ({ gw, rpc }), [gw, rpc]);
|
|
261
|
-
const die = useCallback(() => {
|
|
262
|
-
gw.kill();
|
|
263
|
-
exit();
|
|
264
|
-
// Ink's exit() calls unmount() which resets terminal modes but does NOT
|
|
265
|
-
// call process.exit(). Without an explicit exit the Node process stays
|
|
266
|
-
// alive (stdin listener keeps the event loop open), so the process.on('exit')
|
|
267
|
-
// handler in entry.tsx — which sends the final resetTerminalModes() — never
|
|
268
|
-
// fires. This leaves kitty keyboard protocol, mouse modes, etc. enabled
|
|
269
|
-
// in the parent shell. See issue #19194.
|
|
270
|
-
process.exit(0);
|
|
271
|
-
}, [exit, gw]);
|
|
272
|
-
const session = useSessionLifecycle({
|
|
273
|
-
colsRef,
|
|
274
|
-
composerActions,
|
|
275
|
-
gw,
|
|
276
|
-
panel,
|
|
277
|
-
rpc,
|
|
278
|
-
scrollRef,
|
|
279
|
-
setHistoryItems,
|
|
280
|
-
setLastUserMsg,
|
|
281
|
-
setSessionStartedAt,
|
|
282
|
-
setStickyPrompt,
|
|
283
|
-
setVoiceProcessing,
|
|
284
|
-
setVoiceRecording,
|
|
285
|
-
sys
|
|
286
|
-
});
|
|
287
|
-
useEffect(() => {
|
|
288
|
-
if (ui.busy) {
|
|
289
|
-
setTurnStartedAt(prev => prev ?? Date.now());
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
setTurnStartedAt(null);
|
|
293
|
-
}
|
|
294
|
-
}, [ui.busy]);
|
|
295
|
-
useConfigSync({ gw, setBellOnComplete, setVoiceEnabled, setVoiceRecordKey, sid: ui.sid });
|
|
296
|
-
// Tab title: `⚠` waiting on approval/sudo/secret/clarify, `⏳` busy, `✓` idle.
|
|
297
|
-
const model = ui.info?.model?.replace(/^.*\//, '') ?? '';
|
|
298
|
-
const marker = overlay.approval || overlay.sudo || overlay.secret || overlay.clarify ? '⚠' : ui.busy ? '⏳' : '✓';
|
|
299
|
-
const tabCwd = ui.info?.cwd;
|
|
300
|
-
useTerminalTitle(model ? `${marker} ${model}${tabCwd ? ` · ${shortCwd(tabCwd, 24)}` : ''}` : 'CVC');
|
|
301
|
-
useEffect(() => {
|
|
302
|
-
if (!ui.sid || !stdout) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
let timer;
|
|
306
|
-
const onResize = () => {
|
|
307
|
-
clearTimeout(timer);
|
|
308
|
-
timer = setTimeout(() => {
|
|
309
|
-
timer = undefined;
|
|
310
|
-
void rpc('terminal.resize', { cols: stdout.columns ?? 80, session_id: ui.sid });
|
|
311
|
-
}, 100);
|
|
312
|
-
};
|
|
313
|
-
stdout.on('resize', onResize);
|
|
314
|
-
return () => {
|
|
315
|
-
clearTimeout(timer);
|
|
316
|
-
stdout.off('resize', onResize);
|
|
317
|
-
};
|
|
318
|
-
}, [rpc, stdout, ui.sid]);
|
|
319
|
-
const answerClarify = useCallback((answer) => {
|
|
320
|
-
const clarify = overlay.clarify;
|
|
321
|
-
if (!clarify) {
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const label = toolTrailLabel('clarify');
|
|
325
|
-
turnController.turnTools = turnController.turnTools.filter(line => !sameToolTrailGroup(label, line));
|
|
326
|
-
patchTurnState({ turnTrail: turnController.turnTools });
|
|
327
|
-
rpc('clarify.respond', { answer, request_id: clarify.requestId }).then(r => {
|
|
328
|
-
if (!r) {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (answer) {
|
|
332
|
-
turnController.persistedToolLabels.add(label);
|
|
333
|
-
appendMessage({
|
|
334
|
-
kind: 'trail',
|
|
335
|
-
role: 'system',
|
|
336
|
-
text: '',
|
|
337
|
-
tools: [buildToolTrailLine('clarify', clarify.question)]
|
|
338
|
-
});
|
|
339
|
-
appendMessage({ role: 'user', text: answer });
|
|
340
|
-
patchUiState({ status: 'running…' });
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
sys('prompt cancelled');
|
|
344
|
-
}
|
|
345
|
-
patchOverlayState({ clarify: null });
|
|
346
|
-
});
|
|
347
|
-
}, [appendMessage, overlay.clarify, rpc, sys]);
|
|
348
|
-
const paste = useCallback((quiet = false) => rpc('clipboard.paste', { session_id: getUiState().sid }).then(r => {
|
|
349
|
-
if (!r) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (r.attached) {
|
|
353
|
-
const meta = imageTokenMeta(r);
|
|
354
|
-
return sys(`📎 Image #${r.count} attached from clipboard${meta ? ` · ${meta}` : ''}`);
|
|
355
|
-
}
|
|
356
|
-
if (!quiet) {
|
|
357
|
-
sys(r.message || 'No image found in clipboard');
|
|
358
|
-
}
|
|
359
|
-
}), [rpc, sys]);
|
|
360
|
-
clipboardPasteRef.current = paste;
|
|
361
|
-
const { dispatchSubmission, send, sendQueued, submit } = useSubmission({
|
|
362
|
-
appendMessage,
|
|
363
|
-
composerActions,
|
|
364
|
-
composerRefs,
|
|
365
|
-
composerState,
|
|
366
|
-
gw,
|
|
367
|
-
maybeGoodVibes,
|
|
368
|
-
setLastUserMsg,
|
|
369
|
-
slashRef,
|
|
370
|
-
submitRef,
|
|
371
|
-
sys
|
|
372
|
-
});
|
|
373
|
-
// Drain one queued message whenever the session settles (busy → false):
|
|
374
|
-
// agent turn ends, interrupt, shell.exec finishes, error recovered, or the
|
|
375
|
-
// session first comes up with pre-queued messages. Without this, shell.exec
|
|
376
|
-
// and error paths never emit message.complete, so anything enqueued while
|
|
377
|
-
// `!sleep` / a failed turn was running would stay stuck forever.
|
|
378
|
-
useEffect(() => {
|
|
379
|
-
if (!ui.sid ||
|
|
380
|
-
ui.busy ||
|
|
381
|
-
composerRefs.queueEditRef.current !== null ||
|
|
382
|
-
composerRefs.queueRef.current.length === 0) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const next = composerActions.dequeue();
|
|
386
|
-
if (next) {
|
|
387
|
-
patchUiState({ busy: true, status: 'running…' });
|
|
388
|
-
sendQueued(next);
|
|
389
|
-
}
|
|
390
|
-
}, [ui.sid, ui.busy, composerActions, composerRefs, sendQueued]);
|
|
391
|
-
const { pagerPageSize } = useInputHandlers({
|
|
392
|
-
actions: {
|
|
393
|
-
answerClarify,
|
|
394
|
-
appendMessage,
|
|
395
|
-
die,
|
|
396
|
-
dispatchSubmission,
|
|
397
|
-
guardBusySessionSwitch: session.guardBusySessionSwitch,
|
|
398
|
-
newSession: session.newSession,
|
|
399
|
-
sys
|
|
400
|
-
},
|
|
401
|
-
composer: { actions: composerActions, refs: composerRefs, state: composerState },
|
|
402
|
-
gateway,
|
|
403
|
-
terminal: { hasSelection, scrollRef, scrollWithSelection, selection, stdout },
|
|
404
|
-
voice: {
|
|
405
|
-
enabled: voiceEnabled,
|
|
406
|
-
recordKey: voiceRecordKey,
|
|
407
|
-
recording: voiceRecording,
|
|
408
|
-
setProcessing: setVoiceProcessing,
|
|
409
|
-
setRecording: setVoiceRecording,
|
|
410
|
-
setVoiceEnabled
|
|
411
|
-
},
|
|
412
|
-
wheelStep: WHEEL_SCROLL_STEP
|
|
413
|
-
});
|
|
414
|
-
const onEvent = useMemo(() => createGatewayEventHandler({
|
|
415
|
-
composer: { setInput: composerActions.setInput },
|
|
416
|
-
gateway,
|
|
417
|
-
session: {
|
|
418
|
-
STARTUP_RESUME_ID,
|
|
419
|
-
colsRef,
|
|
420
|
-
newSession: session.newSession,
|
|
421
|
-
resetSession: session.resetSession,
|
|
422
|
-
resumeById: session.resumeById,
|
|
423
|
-
setCatalog
|
|
424
|
-
},
|
|
425
|
-
submission: { submitRef },
|
|
426
|
-
system: { bellOnComplete, stdout, sys },
|
|
427
|
-
transcript: { appendMessage, panel, setHistoryItems },
|
|
428
|
-
voice: {
|
|
429
|
-
setProcessing: setVoiceProcessing,
|
|
430
|
-
setRecording: setVoiceRecording,
|
|
431
|
-
setVoiceEnabled
|
|
432
|
-
}
|
|
433
|
-
}), [
|
|
434
|
-
appendMessage,
|
|
435
|
-
bellOnComplete,
|
|
436
|
-
clearSelection,
|
|
437
|
-
composerActions.setInput,
|
|
438
|
-
gateway,
|
|
439
|
-
panel,
|
|
440
|
-
session.newSession,
|
|
441
|
-
session.resetSession,
|
|
442
|
-
session.resumeById,
|
|
443
|
-
setVoiceEnabled,
|
|
444
|
-
setVoiceProcessing,
|
|
445
|
-
setVoiceRecording,
|
|
446
|
-
stdout,
|
|
447
|
-
submitRef,
|
|
448
|
-
sys
|
|
449
|
-
]);
|
|
450
|
-
onEventRef.current = onEvent;
|
|
451
|
-
useEffect(() => {
|
|
452
|
-
const handler = (ev) => onEventRef.current(ev);
|
|
453
|
-
const exitHandler = () => {
|
|
454
|
-
turnController.reset();
|
|
455
|
-
patchUiState({ busy: false, sid: null, status: 'gateway exited' });
|
|
456
|
-
turnController.pushActivity('gateway exited · /logs to inspect', 'error');
|
|
457
|
-
sys('error: gateway exited');
|
|
458
|
-
};
|
|
459
|
-
gw.on('event', handler);
|
|
460
|
-
gw.on('exit', exitHandler);
|
|
461
|
-
gw.drain();
|
|
462
|
-
// entry.tsx's setupGracefulExit handles process cleanup on real exit.
|
|
463
|
-
return () => {
|
|
464
|
-
gw.off('event', handler);
|
|
465
|
-
gw.off('exit', exitHandler);
|
|
466
|
-
};
|
|
467
|
-
}, [gw, sys]);
|
|
468
|
-
useLongRunToolCharms();
|
|
469
|
-
const slash = useMemo(() => createSlashHandler({
|
|
470
|
-
composer: {
|
|
471
|
-
enqueue: composerActions.enqueue,
|
|
472
|
-
hasSelection,
|
|
473
|
-
paste,
|
|
474
|
-
queueRef: composerRefs.queueRef,
|
|
475
|
-
selection,
|
|
476
|
-
setInput: composerActions.setInput
|
|
477
|
-
},
|
|
478
|
-
gateway,
|
|
479
|
-
local: {
|
|
480
|
-
catalog,
|
|
481
|
-
getHistoryItems: () => historyItemsRef.current,
|
|
482
|
-
getLastUserMsg: () => lastUserMsgRef.current,
|
|
483
|
-
maybeWarn,
|
|
484
|
-
setCatalog
|
|
485
|
-
},
|
|
486
|
-
session: {
|
|
487
|
-
closeSession: session.closeSession,
|
|
488
|
-
die,
|
|
489
|
-
guardBusySessionSwitch: session.guardBusySessionSwitch,
|
|
490
|
-
newSession: session.newSession,
|
|
491
|
-
resetVisibleHistory: session.resetVisibleHistory,
|
|
492
|
-
resumeById: session.resumeById,
|
|
493
|
-
setSessionStartedAt
|
|
494
|
-
},
|
|
495
|
-
slashFlightRef,
|
|
496
|
-
transcript: { page, panel, send, setHistoryItems, sys, trimLastExchange: session.trimLastExchange },
|
|
497
|
-
voice: { setVoiceEnabled, setVoiceRecordKey }
|
|
498
|
-
}), [
|
|
499
|
-
catalog,
|
|
500
|
-
composerActions,
|
|
501
|
-
composerRefs,
|
|
502
|
-
die,
|
|
503
|
-
gateway,
|
|
504
|
-
hasSelection,
|
|
505
|
-
maybeWarn,
|
|
506
|
-
page,
|
|
507
|
-
panel,
|
|
508
|
-
paste,
|
|
509
|
-
selection,
|
|
510
|
-
send,
|
|
511
|
-
session,
|
|
512
|
-
sys
|
|
513
|
-
]);
|
|
514
|
-
slashRef.current = slash;
|
|
515
|
-
const respondWith = useCallback((method, params, done) => rpc(method, params).then(r => r && done()), [rpc]);
|
|
516
|
-
const answerApproval = useCallback((choice) => respondWith('approval.respond', { choice, session_id: ui.sid }, () => {
|
|
517
|
-
patchOverlayState({ approval: null });
|
|
518
|
-
patchTurnState({ outcome: choice === 'deny' ? 'denied' : `approved (${choice})` });
|
|
519
|
-
patchUiState({ status: 'running…' });
|
|
520
|
-
}), [respondWith, ui.sid]);
|
|
521
|
-
const answerSudo = useCallback((pw) => {
|
|
522
|
-
if (!overlay.sudo) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
return respondWith('sudo.respond', { password: pw, request_id: overlay.sudo.requestId }, () => {
|
|
526
|
-
patchOverlayState({ sudo: null });
|
|
527
|
-
patchUiState({ status: 'running…' });
|
|
528
|
-
});
|
|
529
|
-
}, [overlay.sudo, respondWith]);
|
|
530
|
-
const answerSecret = useCallback((value) => {
|
|
531
|
-
if (!overlay.secret) {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
return respondWith('secret.respond', { request_id: overlay.secret.requestId, value }, () => {
|
|
535
|
-
patchOverlayState({ secret: null });
|
|
536
|
-
patchUiState({ status: 'running…' });
|
|
537
|
-
});
|
|
538
|
-
}, [overlay.secret, respondWith]);
|
|
539
|
-
const onModelSelect = useCallback((value) => {
|
|
540
|
-
patchOverlayState({ modelPicker: false });
|
|
541
|
-
slashRef.current(`/model ${value}`);
|
|
542
|
-
}, []);
|
|
543
|
-
const hasReasoning = useTurnSelector(state => Boolean(state.reasoning.trim()));
|
|
544
|
-
// Per-section overrides win over the global mode — when every section is
|
|
545
|
-
// resolved to hidden, the only thing ToolTrail will surface is the
|
|
546
|
-
// floating-alert backstop (errors/warnings). Mirror that so we don't
|
|
547
|
-
// render an empty wrapper Box above the streaming area in quiet mode.
|
|
548
|
-
const anyPanelVisible = SECTION_NAMES.some(s => sectionMode(s, ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden');
|
|
549
|
-
const thinkingPanelVisible = sectionMode('thinking', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden';
|
|
550
|
-
const toolsPanelVisible = sectionMode('tools', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden';
|
|
551
|
-
const activityPanelVisible = sectionMode('activity', ui.detailsMode, ui.sections, ui.detailsModeCommandOverride) !== 'hidden';
|
|
552
|
-
const showProgressArea = useTurnSelector(state => anyPanelVisible
|
|
553
|
-
? Boolean(ui.busy ||
|
|
554
|
-
state.outcome ||
|
|
555
|
-
state.streamPendingTools.length ||
|
|
556
|
-
state.streamSegments.some(segment => {
|
|
557
|
-
const hasThinking = Boolean(segment.thinking?.trim());
|
|
558
|
-
const hasTrailTools = Boolean(segment.tools?.length);
|
|
559
|
-
if (segment.kind === 'trail' && !segment.text) {
|
|
560
|
-
return ((thinkingPanelVisible && hasThinking) || ((toolsPanelVisible || activityPanelVisible) && hasTrailTools));
|
|
561
|
-
}
|
|
562
|
-
return (Boolean(segment.text?.trim()) ||
|
|
563
|
-
(thinkingPanelVisible && hasThinking) ||
|
|
564
|
-
((toolsPanelVisible || activityPanelVisible) && hasTrailTools));
|
|
565
|
-
}) ||
|
|
566
|
-
state.subagents.length ||
|
|
567
|
-
state.tools.length ||
|
|
568
|
-
state.todos.length ||
|
|
569
|
-
state.turnTrail.length ||
|
|
570
|
-
(thinkingPanelVisible && hasReasoning) ||
|
|
571
|
-
state.activity.length)
|
|
572
|
-
: state.activity.some(item => item.tone !== 'info'));
|
|
573
|
-
const appActions = useMemo(() => ({
|
|
574
|
-
answerApproval,
|
|
575
|
-
answerClarify,
|
|
576
|
-
answerSecret,
|
|
577
|
-
answerSudo,
|
|
578
|
-
clearSelection,
|
|
579
|
-
onModelSelect,
|
|
580
|
-
resumeById: session.resumeById,
|
|
581
|
-
setStickyPrompt
|
|
582
|
-
}), [answerApproval, answerClarify, answerSecret, answerSudo, clearSelection, onModelSelect, session.resumeById]);
|
|
583
|
-
const appComposer = useMemo(() => ({
|
|
584
|
-
cols,
|
|
585
|
-
compIdx: composerState.compIdx,
|
|
586
|
-
completions: composerState.completions,
|
|
587
|
-
empty,
|
|
588
|
-
handleTextPaste: composerActions.handleTextPaste,
|
|
589
|
-
input: composerState.input,
|
|
590
|
-
inputBuf: composerState.inputBuf,
|
|
591
|
-
pagerPageSize,
|
|
592
|
-
queueEditIdx: composerState.queueEditIdx,
|
|
593
|
-
queuedDisplay: composerState.queuedDisplay,
|
|
594
|
-
submit,
|
|
595
|
-
updateInput: composerActions.setInput,
|
|
596
|
-
voiceRecordKey
|
|
597
|
-
}), [cols, composerActions, composerState, empty, pagerPageSize, submit, voiceRecordKey]);
|
|
598
|
-
// Pass current progress through unfrozen — streaming update throttling
|
|
599
|
-
// handles interaction load; progress must stay truthful so panels don't
|
|
600
|
-
// randomly disappear when the live tail scrolls offscreen.
|
|
601
|
-
const appProgress = useMemo(() => ({ showProgressArea }), [showProgressArea]);
|
|
602
|
-
const cwd = ui.info?.cwd || process.env.HERMES_CWD || process.cwd();
|
|
603
|
-
const gitBranch = useGitBranch(cwd);
|
|
604
|
-
const appStatus = useMemo(() => ({
|
|
605
|
-
cwdLabel: fmtCwdBranch(cwd, gitBranch),
|
|
606
|
-
goodVibesTick,
|
|
607
|
-
sessionStartedAt: ui.sid ? sessionStartedAt : null,
|
|
608
|
-
showStickyPrompt: !!stickyPrompt,
|
|
609
|
-
statusColor: statusColorOf(ui.status, ui.theme.color),
|
|
610
|
-
stickyPrompt,
|
|
611
|
-
turnStartedAt: ui.sid ? turnStartedAt : null,
|
|
612
|
-
// CLI parity: the classic prompt_toolkit status bar shows a red dot
|
|
613
|
-
// on REC (cli.py:_get_voice_status_fragments line 2344).
|
|
614
|
-
voiceLabel: voiceRecording ? '● REC' : voiceProcessing ? '◉ STT' : `voice ${voiceEnabled ? 'on' : 'off'}`
|
|
615
|
-
}), [
|
|
616
|
-
cwd,
|
|
617
|
-
gitBranch,
|
|
618
|
-
goodVibesTick,
|
|
619
|
-
sessionStartedAt,
|
|
620
|
-
stickyPrompt,
|
|
621
|
-
turnStartedAt,
|
|
622
|
-
ui,
|
|
623
|
-
voiceEnabled,
|
|
624
|
-
voiceProcessing,
|
|
625
|
-
voiceRecording
|
|
626
|
-
]);
|
|
627
|
-
const appTranscript = useMemo(() => ({ historyItems, scrollRef, virtualHistory, virtualRows }), [historyItems, virtualHistory, virtualRows]);
|
|
628
|
-
return { appActions, appComposer, appProgress, appStatus, appTranscript, gateway };
|
|
629
|
-
}
|