cvc-tui 0.4.3 → 0.4.5

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 (141) hide show
  1. package/NOTICES.md +13 -0
  2. package/dist/app/completion.js +102 -0
  3. package/dist/app/createGatewayEventHandler.js +508 -0
  4. package/dist/app/createSlashHandler.js +101 -0
  5. package/dist/app/delegationStore.js +51 -0
  6. package/dist/app/gatewayContext.js +17 -0
  7. package/dist/app/historyStore.js +123 -0
  8. package/dist/app/inputBuffer.js +120 -0
  9. package/dist/app/inputSelectionStore.js +8 -0
  10. package/dist/app/inputStore.js +28 -0
  11. package/dist/app/interfaces.js +6 -0
  12. package/dist/app/overlayStore.js +40 -0
  13. package/dist/app/promptStore.js +44 -0
  14. package/dist/app/queueStore.js +25 -0
  15. package/dist/app/scroll.js +44 -0
  16. package/dist/app/setupHandoff.js +28 -0
  17. package/dist/app/slash/commands/core.js +479 -0
  18. package/dist/app/slash/commands/debug.js +44 -0
  19. package/dist/app/slash/commands/ops.js +498 -0
  20. package/dist/app/slash/commands/session.js +431 -0
  21. package/dist/app/slash/commands/setup.js +20 -0
  22. package/dist/app/slash/commands/toggles.js +40 -0
  23. package/dist/app/slash/registry.js +18 -0
  24. package/dist/app/slash/types.js +1 -0
  25. package/dist/app/spawnHistoryStore.js +105 -0
  26. package/dist/app/turnController.js +650 -0
  27. package/dist/app/turnStore.js +48 -0
  28. package/dist/app/uiStore.js +36 -0
  29. package/dist/app/useComposerState.js +265 -0
  30. package/dist/app/useConfigSync.js +144 -0
  31. package/dist/app/useInputHandlers.js +403 -0
  32. package/dist/app/useLongRunToolCharms.js +50 -0
  33. package/dist/app/useMainApp.js +629 -0
  34. package/dist/app/useSessionLifecycle.js +175 -0
  35. package/dist/app/useSubmission.js +287 -0
  36. package/dist/app.js +15 -0
  37. package/dist/banner.js +63 -0
  38. package/dist/components/agentsOverlay.js +474 -0
  39. package/dist/components/appChrome.js +252 -0
  40. package/dist/components/appLayout.js +121 -0
  41. package/dist/components/appOverlays.js +65 -0
  42. package/dist/components/branding.js +97 -0
  43. package/dist/components/fpsOverlay.js +22 -0
  44. package/dist/components/helpHint.js +21 -0
  45. package/dist/components/markdown.js +501 -0
  46. package/dist/components/maskedPrompt.js +12 -0
  47. package/dist/components/messageLine.js +82 -0
  48. package/dist/components/modelPicker.js +254 -0
  49. package/dist/components/overlayControls.js +30 -0
  50. package/dist/components/overlays/confirmPrompt.js +25 -0
  51. package/dist/components/overlays/helpOverlay.js +76 -0
  52. package/dist/components/overlays/historySearch.js +49 -0
  53. package/dist/components/overlays/modelPicker.js +60 -0
  54. package/dist/components/overlays/overlayUtils.js +19 -0
  55. package/dist/components/overlays/secretPrompt.js +36 -0
  56. package/dist/components/overlays/sessionPicker.js +93 -0
  57. package/dist/components/overlays/skillsHub.js +71 -0
  58. package/dist/components/prompts.js +95 -0
  59. package/dist/components/queuedMessages.js +24 -0
  60. package/dist/components/sessionPicker.js +130 -0
  61. package/dist/components/skillsHub.js +165 -0
  62. package/dist/components/streamingAssistant.js +35 -0
  63. package/dist/components/streamingMarkdown.js +144 -0
  64. package/dist/components/textInput.js +794 -0
  65. package/dist/components/themed.js +12 -0
  66. package/dist/components/thinking.js +496 -0
  67. package/dist/components/todoPanel.js +40 -0
  68. package/dist/components/transcript.js +22 -0
  69. package/dist/config/env.js +18 -0
  70. package/dist/config/limits.js +22 -0
  71. package/dist/config/timing.js +25 -0
  72. package/dist/content/charms.js +5 -0
  73. package/dist/content/faces.js +21 -0
  74. package/dist/content/fortunes.js +29 -0
  75. package/dist/content/hotkeys.js +38 -0
  76. package/dist/content/placeholders.js +15 -0
  77. package/dist/content/setup.js +14 -0
  78. package/dist/content/verbs.js +41 -0
  79. package/dist/domain/details.js +53 -0
  80. package/dist/domain/messages.js +63 -0
  81. package/dist/domain/paths.js +16 -0
  82. package/dist/domain/providers.js +11 -0
  83. package/dist/domain/roles.js +6 -0
  84. package/dist/domain/slash.js +11 -0
  85. package/dist/domain/usage.js +1 -0
  86. package/dist/domain/viewport.js +33 -0
  87. package/dist/entry.js +64 -71149
  88. package/dist/gateway/client.js +312 -0
  89. package/dist/gatewayClient.js +574 -0
  90. package/dist/gatewayTypes.js +1 -0
  91. package/dist/hooks/useCompletion.js +86 -0
  92. package/dist/hooks/useGitBranch.js +58 -0
  93. package/dist/hooks/useInputHistory.js +12 -0
  94. package/dist/hooks/useQueue.js +57 -0
  95. package/dist/hooks/useVirtualHistory.js +401 -0
  96. package/dist/lib/circularBuffer.js +43 -0
  97. package/dist/lib/clipboard.js +126 -0
  98. package/dist/lib/editor.js +41 -0
  99. package/dist/lib/editor.test.js +58 -0
  100. package/dist/lib/emoji.js +49 -0
  101. package/dist/lib/externalCli.js +11 -0
  102. package/dist/lib/forceTruecolor.js +26 -0
  103. package/dist/lib/fpsStore.js +36 -0
  104. package/dist/lib/gracefulExit.js +29 -0
  105. package/dist/lib/history.js +69 -0
  106. package/dist/lib/inputMetrics.js +143 -0
  107. package/dist/lib/liveProgress.js +51 -0
  108. package/dist/lib/liveProgress.test.js +89 -0
  109. package/dist/lib/mathUnicode.js +685 -0
  110. package/dist/lib/memory.js +123 -0
  111. package/dist/lib/memoryMonitor.js +76 -0
  112. package/dist/lib/messages.js +3 -0
  113. package/dist/lib/messages.test.js +25 -0
  114. package/dist/lib/osc52.js +53 -0
  115. package/dist/lib/perfPane.js +94 -0
  116. package/dist/lib/platform.js +312 -0
  117. package/dist/lib/precisionWheel.js +25 -0
  118. package/dist/lib/react-devtools-stub.js +12 -0
  119. package/dist/lib/reasoning.js +39 -0
  120. package/dist/lib/rpc.js +26 -0
  121. package/dist/lib/subagentTree.js +287 -0
  122. package/dist/lib/syntax.js +89 -0
  123. package/dist/lib/terminalModes.js +46 -0
  124. package/dist/lib/terminalParity.js +48 -0
  125. package/dist/lib/terminalSetup.js +321 -0
  126. package/dist/lib/text.js +203 -0
  127. package/dist/lib/text.test.js +18 -0
  128. package/dist/lib/todo.js +2 -0
  129. package/dist/lib/todo.test.js +22 -0
  130. package/dist/lib/viewportStore.js +82 -0
  131. package/dist/lib/virtualHeights.js +61 -0
  132. package/dist/lib/wheelAccel.js +143 -0
  133. package/dist/protocol/interpolation.js +4 -0
  134. package/dist/protocol/paste.js +3 -0
  135. package/dist/theme.js +398 -0
  136. package/dist/types.js +1 -0
  137. package/dist/vendor/cvc-ink/dist/entry-exports.js +52737 -0
  138. package/dist/vendor/cvc-ink/index.js +1 -0
  139. package/dist/vendor/cvc-ink/package.json +9 -0
  140. package/dist/vendor/cvc-ink/text-input.js +1 -0
  141. package/package.json +9 -9
@@ -0,0 +1,431 @@
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 { attachedImageNotice, introMsg, toTranscriptMessages } from '../../../domain/messages.js';
6
+ import { TUI_SESSION_MODEL_FLAG } from '../../../domain/slash.js';
7
+ import { formatVoiceRecordKey, parseVoiceRecordKey } from '../../../lib/platform.js';
8
+ import { fmtK } from '../../../lib/text.js';
9
+ import { DEFAULT_INDICATOR_STYLE, INDICATOR_STYLES } from '../../interfaces.js';
10
+ import { patchOverlayState } from '../../overlayStore.js';
11
+ import { patchUiState } from '../../uiStore.js';
12
+ const TUI_SESSION_MODEL_RE = new RegExp(`(?:^|\\s)${TUI_SESSION_MODEL_FLAG}(?:\\s|$)`);
13
+ const TUI_SESSION_STRIP_RE = new RegExp(`\\s*${TUI_SESSION_MODEL_FLAG}\\b\\s*`, 'g');
14
+ const stripTuiSessionFlag = (trimmed) => trimmed.replace(TUI_SESSION_STRIP_RE, ' ').replace(/\s+/g, ' ').trim();
15
+ const modelValueForConfigSet = (arg) => {
16
+ const trimmed = arg.trim();
17
+ if (!trimmed) {
18
+ return trimmed;
19
+ }
20
+ if (TUI_SESSION_MODEL_RE.test(trimmed)) {
21
+ return stripTuiSessionFlag(trimmed);
22
+ }
23
+ return trimmed;
24
+ };
25
+ export const sessionCommands = [
26
+ {
27
+ aliases: ['bg', 'btw'],
28
+ help: 'launch a background prompt',
29
+ name: 'background',
30
+ run: (arg, ctx) => {
31
+ if (!arg) {
32
+ return ctx.transcript.sys('/background <prompt>');
33
+ }
34
+ ctx.gateway.rpc('prompt.background', { session_id: ctx.sid, text: arg }).then(ctx.guarded(r => {
35
+ if (!r.task_id) {
36
+ return;
37
+ }
38
+ patchUiState(state => ({ ...state, bgTasks: new Set(state.bgTasks).add(r.task_id) }));
39
+ ctx.transcript.sys(`bg ${r.task_id} started`);
40
+ }));
41
+ }
42
+ },
43
+ {
44
+ help: 'change or show model',
45
+ name: 'model',
46
+ run: (arg, ctx) => {
47
+ if (ctx.session.guardBusySessionSwitch('change models')) {
48
+ return;
49
+ }
50
+ if (!arg.trim()) {
51
+ return patchOverlayState({ modelPicker: true });
52
+ }
53
+ ctx.gateway
54
+ .rpc('config.set', { key: 'model', session_id: ctx.sid, value: modelValueForConfigSet(arg) })
55
+ .then(ctx.guarded(r => {
56
+ if (!r.value) {
57
+ return ctx.transcript.sys('error: invalid response: model switch');
58
+ }
59
+ ctx.transcript.sys(`model → ${r.value}`);
60
+ ctx.local.maybeWarn(r);
61
+ patchUiState(state => ({
62
+ ...state,
63
+ info: state.info ? { ...state.info, model: r.value } : { model: r.value, skills: {}, tools: {} }
64
+ }));
65
+ }));
66
+ }
67
+ },
68
+ {
69
+ help: 'browse and resume previous sessions',
70
+ name: 'sessions',
71
+ run: (arg, ctx) => {
72
+ if (ctx.session.guardBusySessionSwitch('switch sessions')) {
73
+ return;
74
+ }
75
+ if (!arg.trim()) {
76
+ return patchOverlayState({ picker: true });
77
+ }
78
+ }
79
+ },
80
+ {
81
+ help: 'attach an image',
82
+ name: 'image',
83
+ run: (arg, ctx) => {
84
+ ctx.gateway.rpc('image.attach', { path: arg, session_id: ctx.sid }).then(ctx.guarded(r => {
85
+ ctx.transcript.sys(attachedImageNotice(r));
86
+ if (r.remainder) {
87
+ ctx.composer.setInput(r.remainder);
88
+ }
89
+ }));
90
+ }
91
+ },
92
+ {
93
+ help: 'switch personality for this session',
94
+ name: 'personality',
95
+ run: (arg, ctx) => {
96
+ if (!arg) {
97
+ return;
98
+ }
99
+ ctx.gateway.rpc('config.set', { key: 'personality', session_id: ctx.sid, value: arg }).then(ctx.guarded(r => {
100
+ if (r.history_reset) {
101
+ ctx.session.resetVisibleHistory(r.info ?? null);
102
+ }
103
+ ctx.transcript.sys(`personality: ${r.value || 'default'}${r.history_reset ? ' · transcript cleared' : ''}`);
104
+ ctx.local.maybeWarn(r);
105
+ }));
106
+ }
107
+ },
108
+ {
109
+ help: 'compress transcript',
110
+ name: 'compress',
111
+ run: (arg, ctx) => {
112
+ ctx.gateway
113
+ .rpc('session.compress', {
114
+ session_id: ctx.sid,
115
+ ...(arg ? { focus_topic: arg } : {})
116
+ })
117
+ .then(ctx.guarded(r => {
118
+ if (Array.isArray(r.messages)) {
119
+ const rows = toTranscriptMessages(r.messages);
120
+ ctx.transcript.setHistoryItems(r.info ? [introMsg(r.info), ...rows] : rows);
121
+ }
122
+ if (r.info) {
123
+ patchUiState({ info: r.info });
124
+ }
125
+ if (r.usage) {
126
+ patchUiState(state => ({ ...state, usage: { ...state.usage, ...r.usage } }));
127
+ }
128
+ if (r.summary?.headline) {
129
+ const prefix = r.summary.noop ? '' : '✓ ';
130
+ ctx.transcript.sys(`${prefix}${r.summary.headline}`);
131
+ if (r.summary.token_line) {
132
+ ctx.transcript.sys(` ${r.summary.token_line}`);
133
+ }
134
+ if (r.summary.note) {
135
+ ctx.transcript.sys(` ${r.summary.note}`);
136
+ }
137
+ return;
138
+ }
139
+ if ((r.removed ?? 0) <= 0) {
140
+ return ctx.transcript.sys('nothing to compress');
141
+ }
142
+ ctx.transcript.sys(`compressed ${r.removed} messages${r.usage?.total ? ` · ${fmtK(r.usage.total)} tok` : ''}`);
143
+ }))
144
+ .catch(ctx.guardedErr);
145
+ }
146
+ },
147
+ {
148
+ aliases: ['fork'],
149
+ help: 'branch the session',
150
+ name: 'branch',
151
+ run: (arg, ctx) => {
152
+ const prevSid = ctx.sid;
153
+ ctx.gateway.rpc('session.branch', { name: arg, session_id: ctx.sid }).then(ctx.guarded(r => {
154
+ if (!r.session_id) {
155
+ return;
156
+ }
157
+ void ctx.session.closeSession(prevSid);
158
+ patchUiState({ sid: r.session_id });
159
+ ctx.session.setSessionStartedAt(Date.now());
160
+ ctx.transcript.setHistoryItems([]);
161
+ ctx.transcript.sys(`branched → ${r.title ?? ''}`);
162
+ }));
163
+ }
164
+ },
165
+ {
166
+ help: 'voice mode: [on|off|tts|status]',
167
+ name: 'voice',
168
+ run: (arg, ctx) => {
169
+ const normalized = (arg ?? '').trim().toLowerCase();
170
+ const action = normalized === 'on' || normalized === 'off' || normalized === 'tts' || normalized === 'status'
171
+ ? normalized
172
+ : 'status';
173
+ ctx.gateway.rpc('voice.toggle', { action }).then(ctx.guarded(r => {
174
+ ctx.voice.setVoiceEnabled(!!r.enabled);
175
+ // Render the configured record key (config.yaml ``voice.record_key``)
176
+ // instead of hardcoded "Ctrl+B" — the gateway response carries the
177
+ // current value so /voice status and /voice on stay in sync with
178
+ // both the CLI and the TUI's actual binding (#18994).
179
+ //
180
+ // Copilot review on #19835 caught that rendering from the fresh
181
+ // backend response WITHOUT updating the frontend ``voice.recordKey``
182
+ // state would skew display and binding between config-edit and
183
+ // the next ``mtime`` poll (~5s). Parse once, push into state so
184
+ // ``useInputHandlers()`` picks up the new binding immediately.
185
+ //
186
+ // Round-2 follow-up: only push state when the response actually
187
+ // carries ``record_key`` — otherwise an older gateway (or a future
188
+ // branch that forgets to include it) would clobber a custom user
189
+ // binding back to the default on every /voice invocation. The
190
+ // label still falls back to the documented default for display.
191
+ const parsed = r.record_key ? parseVoiceRecordKey(r.record_key) : undefined;
192
+ if (parsed) {
193
+ ctx.voice.setVoiceRecordKey(parsed);
194
+ }
195
+ const recordKeyLabel = formatVoiceRecordKey(parsed ?? parseVoiceRecordKey('ctrl+b'));
196
+ // Match CLI's _show_voice_status / _enable_voice_mode /
197
+ // _toggle_voice_tts output shape so users don't have to learn
198
+ // two vocabularies.
199
+ if (action === 'status') {
200
+ const mode = r.enabled ? 'ON' : 'OFF';
201
+ const tts = r.tts ? 'ON' : 'OFF';
202
+ ctx.transcript.sys('Voice Mode Status');
203
+ ctx.transcript.sys(` Mode: ${mode}`);
204
+ ctx.transcript.sys(` TTS: ${tts}`);
205
+ ctx.transcript.sys(` Record key: ${recordKeyLabel}`);
206
+ // CLI's "Requirements:" block — surfaces STT/audio setup issues
207
+ // so the user sees "STT provider: MISSING ..." instead of
208
+ // silently failing on every record-key press.
209
+ if (r.details) {
210
+ ctx.transcript.sys('');
211
+ ctx.transcript.sys(' Requirements:');
212
+ for (const line of r.details.split('\n')) {
213
+ if (line.trim()) {
214
+ ctx.transcript.sys(` ${line}`);
215
+ }
216
+ }
217
+ }
218
+ return;
219
+ }
220
+ if (action === 'tts') {
221
+ ctx.transcript.sys(`Voice TTS ${r.tts ? 'enabled' : 'disabled'}.`);
222
+ return;
223
+ }
224
+ // on/off — mirror cli.py:_enable_voice_mode's 3-line output
225
+ if (r.enabled) {
226
+ const tts = r.tts ? ' (TTS enabled)' : '';
227
+ ctx.transcript.sys(`Voice mode enabled${tts}`);
228
+ ctx.transcript.sys(` ${recordKeyLabel} to start/stop recording`);
229
+ ctx.transcript.sys(' /voice tts to toggle speech output');
230
+ ctx.transcript.sys(' /voice off to disable voice mode');
231
+ }
232
+ else {
233
+ ctx.transcript.sys('Voice mode disabled.');
234
+ }
235
+ }));
236
+ }
237
+ },
238
+ {
239
+ help: 'switch theme skin (fires skin.changed)',
240
+ name: 'skin',
241
+ run: (arg, ctx) => {
242
+ if (!arg) {
243
+ return ctx.gateway
244
+ .rpc('config.get', { key: 'skin' })
245
+ .then(ctx.guarded(r => ctx.transcript.sys(`skin: ${r.value || 'default'}`)));
246
+ }
247
+ ctx.gateway
248
+ .rpc('config.set', { key: 'skin', value: arg })
249
+ .then(ctx.guarded(r => r.value && ctx.transcript.sys(`skin → ${r.value}`)));
250
+ }
251
+ },
252
+ {
253
+ help: 'pick the busy indicator: kaomoji (default), emoji, unicode (braille), or ascii',
254
+ name: 'indicator',
255
+ usage: `/indicator [${INDICATOR_STYLES.join('|')}]`,
256
+ run: (arg, ctx) => {
257
+ const value = arg.trim().toLowerCase();
258
+ if (!value) {
259
+ return ctx.gateway
260
+ .rpc('config.get', { key: 'indicator' })
261
+ .then(ctx.guarded(r => ctx.transcript.sys(`indicator: ${r.value || DEFAULT_INDICATOR_STYLE}`)));
262
+ }
263
+ if (!INDICATOR_STYLES.includes(value)) {
264
+ return ctx.transcript.sys(`usage: /indicator [${INDICATOR_STYLES.join('|')}]`);
265
+ }
266
+ ctx.gateway.rpc('config.set', { key: 'indicator', value }).then(ctx.guarded(r => {
267
+ if (!r.value) {
268
+ return;
269
+ }
270
+ // Hot-swap the running TUI immediately so the next render
271
+ // uses the new style without waiting for the 5s mtime poll
272
+ // to re-apply config.full.
273
+ patchUiState({ indicatorStyle: value });
274
+ ctx.transcript.sys(`indicator → ${r.value}`);
275
+ }));
276
+ }
277
+ },
278
+ {
279
+ help: 'toggle yolo mode (per-session approvals)',
280
+ name: 'yolo',
281
+ run: (_arg, ctx) => {
282
+ ctx.gateway
283
+ .rpc('config.set', { key: 'yolo', session_id: ctx.sid })
284
+ .then(ctx.guarded(r => ctx.transcript.sys(`yolo ${r.value === '1' ? 'on' : 'off'}`)));
285
+ }
286
+ },
287
+ {
288
+ help: 'inspect or set reasoning effort (updates live agent)',
289
+ name: 'reasoning',
290
+ run: (arg, ctx) => {
291
+ if (!arg) {
292
+ return ctx.gateway
293
+ .rpc('config.get', { key: 'reasoning' })
294
+ .then(ctx.guarded(r => r.value && ctx.transcript.sys(`reasoning: ${r.value} · display ${r.display || 'hide'}`)));
295
+ }
296
+ ctx.gateway
297
+ .rpc('config.set', { key: 'reasoning', session_id: ctx.sid, value: arg })
298
+ .then(ctx.guarded(r => {
299
+ if (!r.value) {
300
+ return;
301
+ }
302
+ if (r.value === 'hide') {
303
+ patchUiState(state => ({
304
+ ...state,
305
+ sections: { ...state.sections, thinking: 'hidden' },
306
+ showReasoning: false
307
+ }));
308
+ }
309
+ else if (r.value === 'show') {
310
+ patchUiState(state => ({
311
+ ...state,
312
+ sections: { ...state.sections, thinking: 'expanded' },
313
+ showReasoning: true
314
+ }));
315
+ }
316
+ ctx.transcript.sys(`reasoning: ${r.value}`);
317
+ }));
318
+ }
319
+ },
320
+ {
321
+ help: 'toggle fast mode [normal|fast|status|on|off|toggle]',
322
+ name: 'fast',
323
+ run: (arg, ctx) => {
324
+ const mode = arg.trim().toLowerCase();
325
+ const valid = new Set(['', 'status', 'normal', 'fast', 'on', 'off', 'toggle']);
326
+ if (!valid.has(mode)) {
327
+ return ctx.transcript.sys('usage: /fast [normal|fast|status|on|off|toggle]');
328
+ }
329
+ if (!mode || mode === 'status') {
330
+ return ctx.gateway
331
+ .rpc('config.get', { key: 'fast', session_id: ctx.sid })
332
+ .then(ctx.guarded(r => ctx.transcript.sys(`fast mode: ${r.value === 'fast' ? 'fast' : 'normal'}`)))
333
+ .catch(ctx.guardedErr);
334
+ }
335
+ ctx.gateway
336
+ .rpc('config.set', { key: 'fast', session_id: ctx.sid, value: mode })
337
+ .then(ctx.guarded(r => {
338
+ const next = r.value === 'fast' ? 'fast' : 'normal';
339
+ ctx.transcript.sys(`fast mode: ${next}`);
340
+ patchUiState(state => ({
341
+ ...state,
342
+ info: state.info
343
+ ? {
344
+ ...state.info,
345
+ fast: next === 'fast',
346
+ service_tier: next === 'fast' ? 'priority' : ''
347
+ }
348
+ : state.info
349
+ }));
350
+ }))
351
+ .catch(ctx.guardedErr);
352
+ }
353
+ },
354
+ {
355
+ help: 'control busy enter mode [queue|steer|interrupt|status]',
356
+ name: 'busy',
357
+ run: (arg, ctx) => {
358
+ const mode = arg.trim().toLowerCase();
359
+ const valid = new Set(['', 'status', 'queue', 'steer', 'interrupt']);
360
+ if (!valid.has(mode)) {
361
+ return ctx.transcript.sys('usage: /busy [queue|steer|interrupt|status]');
362
+ }
363
+ if (!mode || mode === 'status') {
364
+ return ctx.gateway
365
+ .rpc('config.get', { key: 'busy' })
366
+ .then(ctx.guarded(r => {
367
+ const current = r.value || 'interrupt';
368
+ ctx.transcript.sys(`busy input mode: ${current}`);
369
+ }))
370
+ .catch(ctx.guardedErr);
371
+ }
372
+ ctx.gateway
373
+ .rpc('config.set', { key: 'busy', value: mode })
374
+ .then(ctx.guarded(r => {
375
+ const next = r.value || mode;
376
+ ctx.transcript.sys(`busy input mode: ${next}`);
377
+ }))
378
+ .catch(ctx.guardedErr);
379
+ }
380
+ },
381
+ {
382
+ help: 'cycle verbose tool-output mode (updates live agent)',
383
+ name: 'verbose',
384
+ run: (arg, ctx) => {
385
+ ctx.gateway
386
+ .rpc('config.set', { key: 'verbose', session_id: ctx.sid, value: arg || 'cycle' })
387
+ .then(ctx.guarded(r => r.value && ctx.transcript.sys(`verbose: ${r.value}`)));
388
+ }
389
+ },
390
+ {
391
+ help: 'session usage (live counts — worker sees zeros)',
392
+ name: 'usage',
393
+ run: (_arg, ctx) => {
394
+ ctx.gateway.rpc('session.usage', { session_id: ctx.sid }).then(r => {
395
+ if (ctx.stale()) {
396
+ return;
397
+ }
398
+ if (r) {
399
+ patchUiState({
400
+ usage: { calls: r.calls ?? 0, input: r.input ?? 0, output: r.output ?? 0, total: r.total ?? 0 }
401
+ });
402
+ }
403
+ if (!r?.calls) {
404
+ return ctx.transcript.sys('no API calls yet');
405
+ }
406
+ const f = (v) => (v ?? 0).toLocaleString();
407
+ const cost = r.cost_usd != null ? `${r.cost_status === 'estimated' ? '~' : ''}$${r.cost_usd.toFixed(4)}` : null;
408
+ const rows = [
409
+ ['Model', r.model ?? ''],
410
+ ['Input tokens', f(r.input)],
411
+ ['Cache read tokens', f(r.cache_read)],
412
+ ['Cache write tokens', f(r.cache_write)],
413
+ ['Output tokens', f(r.output)],
414
+ ['Total tokens', f(r.total)],
415
+ ['API calls', f(r.calls)]
416
+ ];
417
+ if (cost) {
418
+ rows.push(['Cost', cost]);
419
+ }
420
+ const sections = [{ rows }];
421
+ if (r.context_max) {
422
+ sections.push({ text: `Context: ${f(r.context_used)} / ${f(r.context_max)} (${r.context_percent}%)` });
423
+ }
424
+ if (r.compressions) {
425
+ sections.push({ text: `Compressions: ${r.compressions}` });
426
+ }
427
+ ctx.transcript.panel('Usage', sections);
428
+ });
429
+ }
430
+ }
431
+ ];
@@ -0,0 +1,20 @@
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 { withInkSuspended } from '../../../vendor/cvc-ink/index.js';
6
+ import { launchHermesCommand } from '../../../lib/externalCli.js';
7
+ import { runExternalSetup } from '../../setupHandoff.js';
8
+ export const setupCommands = [
9
+ {
10
+ help: 'run full setup wizard (launches `hermes setup`)',
11
+ name: 'setup',
12
+ run: (arg, ctx) => void runExternalSetup({
13
+ args: ['setup', ...arg.split(/\s+/).filter(Boolean)],
14
+ ctx,
15
+ done: 'setup complete — starting session…',
16
+ launcher: launchHermesCommand,
17
+ suspend: withInkSuspended
18
+ })
19
+ }
20
+ ];
@@ -0,0 +1,40 @@
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
+ // Trivial toggle-style commands — boolean UI flags routed through patchUi
6
+ // + a config.set RPC so the gateway can persist them.
7
+ import { flagFromArg } from '../types.js';
8
+ const TOGGLES = [
9
+ { name: 'verbose', help: 'toggle verbose logging', key: 'verbose' },
10
+ { name: 'reasoning', help: 'toggle reasoning trace display', key: 'reasoning' },
11
+ { name: 'fast', help: 'toggle fast mode (skip extras)', key: 'fast' },
12
+ { name: 'yolo', help: 'toggle YOLO mode (auto-approve)', key: 'yolo' },
13
+ { name: 'busy', help: 'toggle busy indicator', key: 'busy' },
14
+ { name: 'queue', help: 'toggle queued-input mode', key: 'queue' },
15
+ { name: 'background', help: 'toggle background-task display', key: 'background' },
16
+ { name: 'browser', help: 'toggle browser tool', key: 'browserEnabled', aliases: ['br'] },
17
+ { name: 'voice', help: 'toggle voice input/output', key: 'voice' },
18
+ { name: 'image', help: 'toggle inline image rendering', key: 'image' },
19
+ ];
20
+ export const toggleCommands = TOGGLES.map(t => ({
21
+ name: t.name,
22
+ aliases: t.aliases,
23
+ category: 'toggles',
24
+ help: t.help,
25
+ run: (arg, ctx) => {
26
+ const cur = Boolean(ctx.ui[t.key]);
27
+ const next = flagFromArg(arg, cur);
28
+ if (next === null) {
29
+ ctx.emit({ kind: 'sys', text: `usage: /${t.name} [on|off|toggle]` });
30
+ return;
31
+ }
32
+ ctx.emit({ kind: 'patchUi', ui: { [t.key]: next } });
33
+ ctx.emit({
34
+ kind: 'rpc',
35
+ method: 'config.set',
36
+ params: { key: t.configKey ?? t.name, value: next ? 'on' : 'off' },
37
+ });
38
+ ctx.emit({ kind: 'sys', text: `${t.name} ${next ? 'on' : 'off'}` });
39
+ },
40
+ }));
@@ -0,0 +1,18 @@
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 { coreCommands } from './commands/core.js';
6
+ import { debugCommands } from './commands/debug.js';
7
+ import { opsCommands } from './commands/ops.js';
8
+ import { sessionCommands } from './commands/session.js';
9
+ import { setupCommands } from './commands/setup.js';
10
+ export const SLASH_COMMANDS = [
11
+ ...coreCommands,
12
+ ...sessionCommands,
13
+ ...opsCommands,
14
+ ...setupCommands,
15
+ ...debugCommands
16
+ ];
17
+ const byName = new Map(SLASH_COMMANDS.flatMap(cmd => [cmd.name, ...(cmd.aliases ?? [])].map(name => [name, cmd])));
18
+ export const findSlashCommand = (name) => byName.get(name.toLowerCase());
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: MIT
3
+ // Ported from CVC Agent (https://github.com/NousResearch/cvc)
4
+ // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
5
+ import { atom } from 'nanostores';
6
+ const HISTORY_LIMIT = 10;
7
+ export const $spawnHistory = atom([]);
8
+ export const $spawnDiff = atom(null);
9
+ export const getSpawnHistory = () => $spawnHistory.get();
10
+ export const getSpawnDiff = () => $spawnDiff.get();
11
+ export const clearSpawnHistory = () => $spawnHistory.set([]);
12
+ export const clearDiffPair = () => $spawnDiff.set(null);
13
+ export const setDiffPair = (pair) => $spawnDiff.set(pair);
14
+ /**
15
+ * Commit a finished turn's spawn tree to history. Keeps the last 10
16
+ * non-empty snapshots — empty turns (no subagents) are dropped.
17
+ *
18
+ * Why in-memory? The primary investigation loop is "I just ran a fan-out,
19
+ * it misbehaved, let me look at what happened" — same-session debugging.
20
+ * Disk persistence across process restarts is a natural extension but
21
+ * adds RPC surface for a less-common path.
22
+ */
23
+ export const pushSnapshot = (subagents, meta) => {
24
+ if (!subagents.length) {
25
+ return;
26
+ }
27
+ const now = Date.now();
28
+ const started = meta.startedAt ?? Math.min(...subagents.map(s => s.startedAt ?? now));
29
+ const snap = {
30
+ finishedAt: now,
31
+ id: `snap-${now.toString(36)}`,
32
+ label: summarizeLabel(subagents),
33
+ sessionId: meta.sessionId ?? null,
34
+ startedAt: Number.isFinite(started) ? started : now,
35
+ subagents: subagents.map(item => ({ ...item }))
36
+ };
37
+ const next = [snap, ...$spawnHistory.get()].slice(0, HISTORY_LIMIT);
38
+ $spawnHistory.set(next);
39
+ };
40
+ function summarizeLabel(subagents) {
41
+ const top = subagents
42
+ .filter(s => s.parentId == null || subagents.every(o => o.id !== s.parentId))
43
+ .slice(0, 2)
44
+ .map(s => s.goal || 'subagent')
45
+ .join(' · ');
46
+ return top || `${subagents.length} agent${subagents.length === 1 ? '' : 's'}`;
47
+ }
48
+ /**
49
+ * Push a disk-loaded snapshot onto the front of the history stack so the
50
+ * overlay can pick it up at index 1 via /replay load. Normalises the
51
+ * server payload (arbitrary list) into the same SubagentProgress shape
52
+ * used for live data — defensive against cross-version reads.
53
+ */
54
+ export const pushDiskSnapshot = (r, path) => {
55
+ const raw = Array.isArray(r.subagents) ? r.subagents : [];
56
+ const normalised = raw.map(normaliseSubagent);
57
+ if (!normalised.length) {
58
+ return;
59
+ }
60
+ const snap = {
61
+ finishedAt: (r.finished_at ?? Date.now() / 1000) * 1000,
62
+ fromDisk: true,
63
+ id: `disk-${path}`,
64
+ label: r.label || `${normalised.length} subagents`,
65
+ path,
66
+ sessionId: r.session_id ?? null,
67
+ startedAt: (r.started_at ?? r.finished_at ?? Date.now() / 1000) * 1000,
68
+ subagents: normalised
69
+ };
70
+ const next = [snap, ...$spawnHistory.get()].slice(0, HISTORY_LIMIT);
71
+ $spawnHistory.set(next);
72
+ };
73
+ function normaliseSubagent(raw) {
74
+ const o = raw;
75
+ const s = (v) => (typeof v === 'string' ? v : undefined);
76
+ const n = (v) => (typeof v === 'number' ? v : undefined);
77
+ const arr = (v) => (Array.isArray(v) ? v : undefined);
78
+ return {
79
+ apiCalls: n(o.apiCalls),
80
+ costUsd: n(o.costUsd),
81
+ depth: typeof o.depth === 'number' ? o.depth : 0,
82
+ durationSeconds: n(o.durationSeconds),
83
+ filesRead: arr(o.filesRead),
84
+ filesWritten: arr(o.filesWritten),
85
+ goal: s(o.goal) ?? 'subagent',
86
+ id: s(o.id) ?? `sa-${Math.random().toString(36).slice(2, 8)}`,
87
+ index: typeof o.index === 'number' ? o.index : 0,
88
+ inputTokens: n(o.inputTokens),
89
+ iteration: n(o.iteration),
90
+ model: s(o.model),
91
+ notes: (arr(o.notes) ?? []).filter(x => typeof x === 'string'),
92
+ outputTail: arr(o.outputTail),
93
+ outputTokens: n(o.outputTokens),
94
+ parentId: s(o.parentId) ?? null,
95
+ reasoningTokens: n(o.reasoningTokens),
96
+ startedAt: n(o.startedAt),
97
+ status: s(o.status) ?? 'completed',
98
+ summary: s(o.summary),
99
+ taskCount: typeof o.taskCount === 'number' ? o.taskCount : 1,
100
+ thinking: (arr(o.thinking) ?? []).filter(x => typeof x === 'string'),
101
+ toolCount: typeof o.toolCount === 'number' ? o.toolCount : 0,
102
+ tools: (arr(o.tools) ?? []).filter(x => typeof x === 'string'),
103
+ toolsets: arr(o.toolsets)
104
+ };
105
+ }