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