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/uiStore.js
DELETED
|
@@ -1,36 +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, computed } from 'nanostores';
|
|
6
|
-
import { MOUSE_TRACKING } from '../config/env.js';
|
|
7
|
-
import { ZERO } from '../domain/usage.js';
|
|
8
|
-
import { DEFAULT_THEME } from '../theme.js';
|
|
9
|
-
import { DEFAULT_INDICATOR_STYLE } from './interfaces.js';
|
|
10
|
-
const buildUiState = () => ({
|
|
11
|
-
bgTasks: new Set(),
|
|
12
|
-
busy: false,
|
|
13
|
-
busyInputMode: 'queue',
|
|
14
|
-
compact: false,
|
|
15
|
-
detailsMode: 'collapsed',
|
|
16
|
-
detailsModeCommandOverride: false,
|
|
17
|
-
indicatorStyle: DEFAULT_INDICATOR_STYLE,
|
|
18
|
-
info: null,
|
|
19
|
-
inlineDiffs: true,
|
|
20
|
-
mouseTracking: MOUSE_TRACKING,
|
|
21
|
-
sections: {},
|
|
22
|
-
showCost: false,
|
|
23
|
-
showReasoning: false,
|
|
24
|
-
sid: null,
|
|
25
|
-
status: 'summoning hermes…',
|
|
26
|
-
statusBar: 'top',
|
|
27
|
-
streaming: true,
|
|
28
|
-
theme: DEFAULT_THEME,
|
|
29
|
-
usage: ZERO
|
|
30
|
-
});
|
|
31
|
-
export const $uiState = atom(buildUiState());
|
|
32
|
-
export const $uiTheme = computed($uiState, state => state.theme);
|
|
33
|
-
export const $uiSessionId = computed($uiState, state => state.sid);
|
|
34
|
-
export const getUiState = () => $uiState.get();
|
|
35
|
-
export const patchUiState = (next) => $uiState.set(typeof next === 'function' ? next($uiState.get()) : { ...$uiState.get(), ...next });
|
|
36
|
-
export const resetUiState = () => $uiState.set(buildUiState());
|
|
@@ -1,265 +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 { spawnSync } from 'node:child_process';
|
|
6
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { useStdin, withInkSuspended } from '@cvc/ink';
|
|
10
|
-
import { useStore } from '@nanostores/react';
|
|
11
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
12
|
-
import { LARGE_PASTE } from '../config/limits.js';
|
|
13
|
-
import { useCompletion } from '../hooks/useCompletion.js';
|
|
14
|
-
import { useInputHistory } from '../hooks/useInputHistory.js';
|
|
15
|
-
import { useQueue } from '../hooks/useQueue.js';
|
|
16
|
-
import { isUsableClipboardText, readClipboardText } from '../lib/clipboard.js';
|
|
17
|
-
import { resolveEditor } from '../lib/editor.js';
|
|
18
|
-
import { readOsc52Clipboard } from '../lib/osc52.js';
|
|
19
|
-
import { isRemoteShellSession } from '../lib/terminalSetup.js';
|
|
20
|
-
import { pasteTokenLabel, stripTrailingPasteNewlines } from '../lib/text.js';
|
|
21
|
-
import { $isBlocked } from './overlayStore.js';
|
|
22
|
-
import { getUiState } from './uiStore.js';
|
|
23
|
-
const PASTE_SNIP_MAX_COUNT = 32;
|
|
24
|
-
const PASTE_SNIP_MAX_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
25
|
-
const trimSnips = (snips) => {
|
|
26
|
-
let total = 0;
|
|
27
|
-
const out = [];
|
|
28
|
-
for (let i = snips.length - 1; i >= 0; i--) {
|
|
29
|
-
const snip = snips[i];
|
|
30
|
-
const size = snip.text.length;
|
|
31
|
-
if (out.length >= PASTE_SNIP_MAX_COUNT || total + size > PASTE_SNIP_MAX_TOTAL_BYTES) {
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
total += size;
|
|
35
|
-
out.unshift(snip);
|
|
36
|
-
}
|
|
37
|
-
return out.length === snips.length ? snips : out;
|
|
38
|
-
};
|
|
39
|
-
/** Insert text at the cursor position, adding spacing to separate from adjacent non-whitespace. */
|
|
40
|
-
function insertAtCursor(value, cursor, text) {
|
|
41
|
-
const lead = cursor > 0 && !/\s/.test(value[cursor - 1] ?? '') ? ' ' : '';
|
|
42
|
-
const tail = cursor < value.length && !/\s/.test(value[cursor] ?? '') ? ' ' : '';
|
|
43
|
-
const insert = `${lead}${text}${tail}`;
|
|
44
|
-
return {
|
|
45
|
-
cursor: cursor + insert.length,
|
|
46
|
-
value: value.slice(0, cursor) + insert + value.slice(cursor)
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Quick client-side heuristic to detect text that looks like a dropped file path.
|
|
51
|
-
* When this returns true the composer sends RPC calls to the server for actual
|
|
52
|
-
* validation. Keep in sync with _detect_file_drop() in cli.py — see that
|
|
53
|
-
* function for the canonical prefix list.
|
|
54
|
-
*/
|
|
55
|
-
export function looksLikeDroppedPath(text) {
|
|
56
|
-
const trimmed = text.trim();
|
|
57
|
-
if (!trimmed || trimmed.includes('\n')) {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
// file:// URIs, relative, home-relative, quoted, and Windows drive paths
|
|
61
|
-
if (trimmed.startsWith('file://') ||
|
|
62
|
-
trimmed.startsWith('~/') ||
|
|
63
|
-
trimmed.startsWith('./') ||
|
|
64
|
-
trimmed.startsWith('../') ||
|
|
65
|
-
trimmed.startsWith('"/') ||
|
|
66
|
-
trimmed.startsWith("'/") ||
|
|
67
|
-
trimmed.startsWith('"~') ||
|
|
68
|
-
trimmed.startsWith("'~") ||
|
|
69
|
-
/^[A-Za-z]:[/\\]/.test(trimmed) ||
|
|
70
|
-
/^["'][A-Za-z]:[/\\]/.test(trimmed)) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
// Bare absolute paths (start with /) — require a second '/' or a '.' to avoid
|
|
74
|
-
// false positives on short strings like "/api" or "/help" which would trigger
|
|
75
|
-
// unnecessary RPC round-trips.
|
|
76
|
-
if (trimmed.startsWith('/')) {
|
|
77
|
-
const rest = trimmed.slice(1);
|
|
78
|
-
return rest.includes('/') || rest.includes('.');
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
export function useComposerState({ gw, onClipboardPaste, onImageAttached, submitRef }) {
|
|
83
|
-
const [input, setInput] = useState('');
|
|
84
|
-
const [inputBuf, setInputBuf] = useState([]);
|
|
85
|
-
const [pasteSnips, setPasteSnips] = useState([]);
|
|
86
|
-
const isBlocked = useStore($isBlocked);
|
|
87
|
-
const { querier } = useStdin();
|
|
88
|
-
const { queueRef, queueEditRef, queuedDisplay, queueEditIdx, enqueue, dequeue, removeQ, replaceQ, setQueueEdit, syncQueue } = useQueue();
|
|
89
|
-
const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory();
|
|
90
|
-
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, isBlocked, gw);
|
|
91
|
-
const clearIn = useCallback(() => {
|
|
92
|
-
setInput('');
|
|
93
|
-
setInputBuf([]);
|
|
94
|
-
setPasteSnips([]);
|
|
95
|
-
setQueueEdit(null);
|
|
96
|
-
setHistoryIdx(null);
|
|
97
|
-
historyDraftRef.current = '';
|
|
98
|
-
}, [historyDraftRef, setQueueEdit, setHistoryIdx]);
|
|
99
|
-
const handleResolvedPaste = useCallback(async ({ bracketed, cursor, text, value }) => {
|
|
100
|
-
const cleanedText = stripTrailingPasteNewlines(text);
|
|
101
|
-
if (!cleanedText || !/[^\n]/.test(cleanedText)) {
|
|
102
|
-
if (bracketed) {
|
|
103
|
-
void onClipboardPaste(true);
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
const sid = getUiState().sid;
|
|
108
|
-
if (sid && looksLikeDroppedPath(cleanedText)) {
|
|
109
|
-
try {
|
|
110
|
-
const attached = await gw.request('image.attach', {
|
|
111
|
-
path: cleanedText,
|
|
112
|
-
session_id: sid
|
|
113
|
-
});
|
|
114
|
-
if (attached?.name) {
|
|
115
|
-
onImageAttached?.(attached);
|
|
116
|
-
const remainder = attached.remainder?.trim() ?? '';
|
|
117
|
-
if (!remainder) {
|
|
118
|
-
return { cursor, value };
|
|
119
|
-
}
|
|
120
|
-
return insertAtCursor(value, cursor, remainder);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// Fall back to generic file-drop detection below.
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const dropped = await gw.request('input.detect_drop', {
|
|
128
|
-
session_id: sid,
|
|
129
|
-
text: cleanedText
|
|
130
|
-
});
|
|
131
|
-
if (dropped?.matched && dropped.text) {
|
|
132
|
-
return insertAtCursor(value, cursor, dropped.text);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// Fall through to normal text paste behavior.
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const lineCount = cleanedText.split('\n').length;
|
|
140
|
-
if (cleanedText.length < LARGE_PASTE.chars && lineCount < LARGE_PASTE.lines) {
|
|
141
|
-
return {
|
|
142
|
-
cursor: cursor + cleanedText.length,
|
|
143
|
-
value: value.slice(0, cursor) + cleanedText + value.slice(cursor)
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
const label = pasteTokenLabel(cleanedText, lineCount);
|
|
147
|
-
const inserted = insertAtCursor(value, cursor, label);
|
|
148
|
-
setPasteSnips(prev => trimSnips([...prev, { label, text: cleanedText }]));
|
|
149
|
-
void gw
|
|
150
|
-
.request('paste.collapse', { text: cleanedText })
|
|
151
|
-
.then(r => {
|
|
152
|
-
const path = r?.path;
|
|
153
|
-
if (!path) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
setPasteSnips(prev => prev.map(s => (s.label === label ? { ...s, path } : s)));
|
|
157
|
-
})
|
|
158
|
-
.catch(() => { });
|
|
159
|
-
return inserted;
|
|
160
|
-
}, [gw, onClipboardPaste, onImageAttached]);
|
|
161
|
-
const handleTextPaste = useCallback(({ bracketed, cursor, hotkey, text, value }) => {
|
|
162
|
-
if (hotkey) {
|
|
163
|
-
const preferOsc52 = isRemoteShellSession(process.env);
|
|
164
|
-
const readPreferredText = preferOsc52
|
|
165
|
-
? readOsc52Clipboard(querier).then(async (osc52Text) => {
|
|
166
|
-
if (isUsableClipboardText(osc52Text)) {
|
|
167
|
-
return osc52Text;
|
|
168
|
-
}
|
|
169
|
-
return readClipboardText();
|
|
170
|
-
})
|
|
171
|
-
: readClipboardText().then(async (clipText) => {
|
|
172
|
-
if (isUsableClipboardText(clipText)) {
|
|
173
|
-
return clipText;
|
|
174
|
-
}
|
|
175
|
-
return readOsc52Clipboard(querier);
|
|
176
|
-
});
|
|
177
|
-
return readPreferredText.then(async (preferredText) => {
|
|
178
|
-
if (isUsableClipboardText(preferredText)) {
|
|
179
|
-
return handleResolvedPaste({ bracketed: false, cursor, text: preferredText, value });
|
|
180
|
-
}
|
|
181
|
-
void onClipboardPaste(false);
|
|
182
|
-
return null;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
return handleResolvedPaste({ bracketed: !!bracketed, cursor, text, value });
|
|
186
|
-
}, [handleResolvedPaste, onClipboardPaste, querier]);
|
|
187
|
-
const openEditor = useCallback(async () => {
|
|
188
|
-
const dir = mkdtempSync(join(tmpdir(), 'hermes-'));
|
|
189
|
-
const file = join(dir, 'prompt.md');
|
|
190
|
-
const [cmd, ...args] = resolveEditor();
|
|
191
|
-
writeFileSync(file, [...inputBuf, input].join('\n'));
|
|
192
|
-
let exitCode = null;
|
|
193
|
-
await withInkSuspended(async () => {
|
|
194
|
-
exitCode = spawnSync(cmd, [...args, file], { stdio: 'inherit' }).status;
|
|
195
|
-
});
|
|
196
|
-
try {
|
|
197
|
-
if (exitCode !== 0) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const text = readFileSync(file, 'utf8').trimEnd();
|
|
201
|
-
if (!text) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
setInput('');
|
|
205
|
-
setInputBuf([]);
|
|
206
|
-
submitRef.current(text);
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
rmSync(dir, { force: true, recursive: true });
|
|
210
|
-
}
|
|
211
|
-
}, [input, inputBuf, submitRef]);
|
|
212
|
-
const actions = useMemo(() => ({
|
|
213
|
-
clearIn,
|
|
214
|
-
dequeue,
|
|
215
|
-
enqueue,
|
|
216
|
-
handleTextPaste,
|
|
217
|
-
openEditor,
|
|
218
|
-
pushHistory,
|
|
219
|
-
removeQueue: removeQ,
|
|
220
|
-
replaceQueue: replaceQ,
|
|
221
|
-
setCompIdx,
|
|
222
|
-
setHistoryIdx,
|
|
223
|
-
setInput,
|
|
224
|
-
setInputBuf,
|
|
225
|
-
setPasteSnips,
|
|
226
|
-
setQueueEdit,
|
|
227
|
-
syncQueue
|
|
228
|
-
}), [
|
|
229
|
-
clearIn,
|
|
230
|
-
dequeue,
|
|
231
|
-
enqueue,
|
|
232
|
-
handleTextPaste,
|
|
233
|
-
openEditor,
|
|
234
|
-
pushHistory,
|
|
235
|
-
removeQ,
|
|
236
|
-
replaceQ,
|
|
237
|
-
setCompIdx,
|
|
238
|
-
setHistoryIdx,
|
|
239
|
-
setQueueEdit,
|
|
240
|
-
syncQueue
|
|
241
|
-
]);
|
|
242
|
-
const refs = useMemo(() => ({
|
|
243
|
-
historyDraftRef,
|
|
244
|
-
historyRef,
|
|
245
|
-
queueEditRef,
|
|
246
|
-
queueRef,
|
|
247
|
-
submitRef
|
|
248
|
-
}), [historyDraftRef, historyRef, queueEditRef, queueRef, submitRef]);
|
|
249
|
-
const state = useMemo(() => ({
|
|
250
|
-
compIdx,
|
|
251
|
-
compReplace,
|
|
252
|
-
completions,
|
|
253
|
-
historyIdx,
|
|
254
|
-
input,
|
|
255
|
-
inputBuf,
|
|
256
|
-
pasteSnips,
|
|
257
|
-
queueEditIdx,
|
|
258
|
-
queuedDisplay
|
|
259
|
-
}), [compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]);
|
|
260
|
-
return {
|
|
261
|
-
actions,
|
|
262
|
-
refs,
|
|
263
|
-
state
|
|
264
|
-
};
|
|
265
|
-
}
|
|
@@ -1,144 +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 { useEffect, useRef } from 'react';
|
|
6
|
-
import { resolveDetailsMode, resolveSections } from '../domain/details.js';
|
|
7
|
-
import { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } from '../lib/platform.js';
|
|
8
|
-
import { asRpcResult } from '../lib/rpc.js';
|
|
9
|
-
import { DEFAULT_INDICATOR_STYLE, INDICATOR_STYLES } from './interfaces.js';
|
|
10
|
-
import { turnController } from './turnController.js';
|
|
11
|
-
import { patchUiState } from './uiStore.js';
|
|
12
|
-
const STATUSBAR_ALIAS = {
|
|
13
|
-
bottom: 'bottom',
|
|
14
|
-
off: 'off',
|
|
15
|
-
on: 'top',
|
|
16
|
-
top: 'top'
|
|
17
|
-
};
|
|
18
|
-
export const normalizeStatusBar = (raw) => raw === false ? 'off' : typeof raw === 'string' ? (STATUSBAR_ALIAS[raw.trim().toLowerCase()] ?? 'top') : 'top';
|
|
19
|
-
const BUSY_MODES = new Set(['interrupt', 'queue', 'steer']);
|
|
20
|
-
// TUI defaults to `queue` even though the framework default
|
|
21
|
-
// (`hermes_cli/config.py`) is `interrupt`. Rationale: in a full-screen
|
|
22
|
-
// TUI you're typically authoring the next prompt while the agent is
|
|
23
|
-
// still streaming, and an unintended interrupt loses work. Set
|
|
24
|
-
// `display.busy_input_mode: interrupt` (or `steer`) explicitly to
|
|
25
|
-
// opt out per-config; CLI / messaging adapters keep their `interrupt`
|
|
26
|
-
// default unchanged.
|
|
27
|
-
const TUI_BUSY_DEFAULT = 'queue';
|
|
28
|
-
export const normalizeBusyInputMode = (raw) => {
|
|
29
|
-
if (typeof raw !== 'string') {
|
|
30
|
-
return TUI_BUSY_DEFAULT;
|
|
31
|
-
}
|
|
32
|
-
const v = raw.trim().toLowerCase();
|
|
33
|
-
return BUSY_MODES.has(v) ? v : TUI_BUSY_DEFAULT;
|
|
34
|
-
};
|
|
35
|
-
const INDICATOR_STYLE_SET = new Set(INDICATOR_STYLES);
|
|
36
|
-
export const normalizeIndicatorStyle = (raw) => {
|
|
37
|
-
if (typeof raw !== 'string') {
|
|
38
|
-
return DEFAULT_INDICATOR_STYLE;
|
|
39
|
-
}
|
|
40
|
-
const v = raw.trim().toLowerCase();
|
|
41
|
-
return INDICATOR_STYLE_SET.has(v) ? v : DEFAULT_INDICATOR_STYLE;
|
|
42
|
-
};
|
|
43
|
-
const FALSEY_MOUSE = new Set(['0', 'false', 'no', 'off']);
|
|
44
|
-
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
45
|
-
export const normalizeMouseTracking = (display) => {
|
|
46
|
-
const raw = hasOwn(display, 'mouse_tracking') ? display.mouse_tracking : display.tui_mouse;
|
|
47
|
-
if (raw === false || raw === 0) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
return typeof raw === 'string' ? !FALSEY_MOUSE.has(raw.trim().toLowerCase()) : true;
|
|
51
|
-
};
|
|
52
|
-
const MTIME_POLL_MS = 5000;
|
|
53
|
-
const quietRpc = async (gw, method, params = {}) => {
|
|
54
|
-
try {
|
|
55
|
-
return asRpcResult(await gw.request(method, params));
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
const _voiceRecordKeyFromConfig = (cfg) => {
|
|
62
|
-
const raw = cfg?.config?.voice?.record_key;
|
|
63
|
-
return raw ? parseVoiceRecordKey(raw) : DEFAULT_VOICE_RECORD_KEY;
|
|
64
|
-
};
|
|
65
|
-
/** Fetch ``config.get full`` and fan the result through ``applyDisplay``.
|
|
66
|
-
*
|
|
67
|
-
* Extracted so the mtime-reload path can be exercised by the test
|
|
68
|
-
* suite without a React runtime (Copilot round-12 review on #19835).
|
|
69
|
-
* Both the initial hydration and the mtime poller use this shared
|
|
70
|
-
* helper, so a regression in the fetch/apply plumbing now fails the
|
|
71
|
-
* useConfigSync tests instead of only being visible at runtime. */
|
|
72
|
-
export async function hydrateFullConfig(gw, setBell, setVoiceRecordKey) {
|
|
73
|
-
const cfg = await quietRpc(gw, 'config.get', { key: 'full' });
|
|
74
|
-
applyDisplay(cfg, setBell, setVoiceRecordKey);
|
|
75
|
-
return cfg;
|
|
76
|
-
}
|
|
77
|
-
export const applyDisplay = (cfg, setBell, setVoiceRecordKey) => {
|
|
78
|
-
const d = cfg?.config?.display ?? {};
|
|
79
|
-
setBell(!!d.bell_on_complete);
|
|
80
|
-
// Only push the voice record key when the RPC actually returned a
|
|
81
|
-
// config payload. ``quietRpc()`` collapses failures to ``null``; if we
|
|
82
|
-
// reset the cached shortcut on every null we would clobber a custom
|
|
83
|
-
// binding after one transient RPC error until the next config edit
|
|
84
|
-
// (Copilot round-8 review on #19835). The mtime-poll loop advances
|
|
85
|
-
// ``mtimeRef`` before this call, so staying silent on null preserves
|
|
86
|
-
// the last-good state and lets the next successful poll refresh it.
|
|
87
|
-
if (setVoiceRecordKey && cfg) {
|
|
88
|
-
setVoiceRecordKey(_voiceRecordKeyFromConfig(cfg));
|
|
89
|
-
}
|
|
90
|
-
patchUiState({
|
|
91
|
-
busyInputMode: normalizeBusyInputMode(d.busy_input_mode),
|
|
92
|
-
compact: !!d.tui_compact,
|
|
93
|
-
detailsMode: resolveDetailsMode(d),
|
|
94
|
-
detailsModeCommandOverride: false,
|
|
95
|
-
indicatorStyle: normalizeIndicatorStyle(d.tui_status_indicator),
|
|
96
|
-
inlineDiffs: d.inline_diffs !== false,
|
|
97
|
-
mouseTracking: normalizeMouseTracking(d),
|
|
98
|
-
sections: resolveSections(d.sections),
|
|
99
|
-
showCost: !!d.show_cost,
|
|
100
|
-
showReasoning: !!d.show_reasoning,
|
|
101
|
-
statusBar: normalizeStatusBar(d.tui_statusbar),
|
|
102
|
-
streaming: d.streaming !== false
|
|
103
|
-
});
|
|
104
|
-
};
|
|
105
|
-
export function useConfigSync({ gw, setBellOnComplete, setVoiceEnabled, setVoiceRecordKey, sid }) {
|
|
106
|
-
const mtimeRef = useRef(0);
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (!sid) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
// Keep startup cheap: voice.toggle status probes optional audio/STT deps and
|
|
112
|
-
// can run long enough to delay prompt.submit on the single stdio RPC pipe.
|
|
113
|
-
// Environment flags are enough to initialize the UI bit; the heavier status
|
|
114
|
-
// check still runs when the user opens /voice.
|
|
115
|
-
setVoiceEnabled(process.env.HERMES_VOICE === '1');
|
|
116
|
-
quietRpc(gw, 'config.get', { key: 'mtime' }).then(r => {
|
|
117
|
-
mtimeRef.current = Number(r?.mtime ?? 0);
|
|
118
|
-
});
|
|
119
|
-
void hydrateFullConfig(gw, setBellOnComplete, setVoiceRecordKey);
|
|
120
|
-
}, [gw, setBellOnComplete, setVoiceEnabled, setVoiceRecordKey, sid]);
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (!sid) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const id = setInterval(() => {
|
|
126
|
-
quietRpc(gw, 'config.get', { key: 'mtime' }).then(r => {
|
|
127
|
-
const next = Number(r?.mtime ?? 0);
|
|
128
|
-
if (!mtimeRef.current) {
|
|
129
|
-
if (next) {
|
|
130
|
-
mtimeRef.current = next;
|
|
131
|
-
}
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (!next || next === mtimeRef.current) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
mtimeRef.current = next;
|
|
138
|
-
quietRpc(gw, 'reload.mcp', { session_id: sid, confirm: true }).then(r => r && turnController.pushActivity('MCP reloaded after config change'));
|
|
139
|
-
void hydrateFullConfig(gw, setBellOnComplete, setVoiceRecordKey);
|
|
140
|
-
});
|
|
141
|
-
}, MTIME_POLL_MS);
|
|
142
|
-
return () => clearInterval(id);
|
|
143
|
-
}, [gw, setBellOnComplete, setVoiceRecordKey, sid]);
|
|
144
|
-
}
|