cvc-tui 0.1.0 → 0.4.0

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 (130) hide show
  1. package/dist/app/completion.js +4 -0
  2. package/dist/app/createGatewayEventHandler.js +508 -0
  3. package/dist/app/createSlashHandler.js +101 -0
  4. package/dist/app/delegationStore.js +51 -0
  5. package/dist/app/gatewayContext.js +17 -0
  6. package/dist/app/historyStore.js +4 -0
  7. package/dist/app/inputBuffer.js +4 -0
  8. package/dist/app/inputSelectionStore.js +8 -0
  9. package/dist/app/inputStore.js +4 -0
  10. package/dist/app/interfaces.js +6 -0
  11. package/dist/app/overlayStore.js +40 -0
  12. package/dist/app/promptStore.js +4 -0
  13. package/dist/app/queueStore.js +4 -0
  14. package/dist/app/scroll.js +44 -0
  15. package/dist/app/setupHandoff.js +28 -0
  16. package/dist/app/slash/commands/core.js +421 -234
  17. package/dist/app/slash/commands/debug.js +39 -6
  18. package/dist/app/slash/commands/ops.js +471 -136
  19. package/dist/app/slash/commands/session.js +405 -65
  20. package/dist/app/slash/commands/setup.js +16 -43
  21. package/dist/app/slash/commands/toggles.js +4 -0
  22. package/dist/app/slash/registry.js +7 -68
  23. package/dist/app/slash/types.js +1 -16
  24. package/dist/app/spawnHistoryStore.js +105 -0
  25. package/dist/app/turnController.js +650 -0
  26. package/dist/app/turnStore.js +47 -59
  27. package/dist/app/uiStore.js +34 -29
  28. package/dist/app/useComposerState.js +265 -0
  29. package/dist/app/useConfigSync.js +144 -0
  30. package/dist/app/useInputHandlers.js +403 -0
  31. package/dist/app/useLongRunToolCharms.js +50 -0
  32. package/dist/app/useMainApp.js +629 -0
  33. package/dist/app/useSessionLifecycle.js +175 -0
  34. package/dist/app/useSubmission.js +287 -0
  35. package/dist/app.js +13 -217
  36. package/dist/banner.js +40 -3
  37. package/dist/components/agentsOverlay.js +474 -0
  38. package/dist/components/appChrome.js +252 -0
  39. package/dist/components/appLayout.js +121 -22
  40. package/dist/components/appOverlays.js +65 -0
  41. package/dist/components/branding.js +97 -6
  42. package/dist/components/fpsOverlay.js +22 -0
  43. package/dist/components/helpHint.js +21 -0
  44. package/dist/components/markdown.js +501 -0
  45. package/dist/components/maskedPrompt.js +12 -0
  46. package/dist/components/messageLine.js +82 -0
  47. package/dist/components/modelPicker.js +254 -0
  48. package/dist/components/overlayControls.js +30 -0
  49. package/dist/components/overlays/helpOverlay.js +1 -0
  50. package/dist/components/overlays/historySearch.js +1 -0
  51. package/dist/components/overlays/modelPicker.js +2 -1
  52. package/dist/components/overlays/overlayUtils.js +2 -1
  53. package/dist/components/overlays/secretPrompt.js +1 -0
  54. package/dist/components/overlays/sessionPicker.js +1 -0
  55. package/dist/components/overlays/skillsHub.js +1 -0
  56. package/dist/components/prompts.js +95 -0
  57. package/dist/components/queuedMessages.js +24 -0
  58. package/dist/components/sessionPicker.js +130 -0
  59. package/dist/components/skillsHub.js +165 -0
  60. package/dist/components/streamingAssistant.js +35 -0
  61. package/dist/components/streamingMarkdown.js +110 -186
  62. package/dist/components/textInput.js +748 -218
  63. package/dist/components/themed.js +12 -0
  64. package/dist/components/thinking.js +493 -36
  65. package/dist/components/todoPanel.js +40 -0
  66. package/dist/config/env.js +18 -0
  67. package/dist/config/limits.js +22 -0
  68. package/dist/config/timing.js +4 -0
  69. package/dist/content/charms.js +5 -0
  70. package/dist/content/faces.js +21 -0
  71. package/dist/content/fortunes.js +29 -0
  72. package/dist/content/hotkeys.js +38 -0
  73. package/dist/content/placeholders.js +15 -0
  74. package/dist/content/setup.js +14 -0
  75. package/dist/content/verbs.js +41 -0
  76. package/dist/domain/details.js +53 -0
  77. package/dist/domain/messages.js +63 -0
  78. package/dist/domain/paths.js +16 -0
  79. package/dist/domain/providers.js +11 -0
  80. package/dist/domain/roles.js +6 -0
  81. package/dist/domain/slash.js +11 -0
  82. package/dist/domain/usage.js +1 -0
  83. package/dist/domain/viewport.js +33 -0
  84. package/dist/entry.js +65 -40
  85. package/dist/gatewayClient.js +574 -0
  86. package/dist/gatewayTypes.js +1 -0
  87. package/dist/hooks/useCompletion.js +86 -0
  88. package/dist/hooks/useGitBranch.js +58 -0
  89. package/dist/hooks/useInputHistory.js +12 -0
  90. package/dist/hooks/useQueue.js +57 -0
  91. package/dist/hooks/useVirtualHistory.js +401 -0
  92. package/dist/lib/circularBuffer.js +43 -0
  93. package/dist/lib/clipboard.js +126 -0
  94. package/dist/lib/editor.js +41 -0
  95. package/dist/lib/editor.test.js +58 -0
  96. package/dist/lib/emoji.js +49 -0
  97. package/dist/lib/externalCli.js +11 -0
  98. package/dist/lib/forceTruecolor.js +26 -0
  99. package/dist/lib/fpsStore.js +36 -0
  100. package/dist/lib/gracefulExit.js +29 -0
  101. package/dist/lib/history.js +69 -0
  102. package/dist/lib/inputMetrics.js +143 -0
  103. package/dist/lib/liveProgress.js +51 -0
  104. package/dist/lib/liveProgress.test.js +89 -0
  105. package/dist/lib/mathUnicode.js +685 -0
  106. package/dist/lib/memory.js +123 -0
  107. package/dist/lib/memoryMonitor.js +76 -0
  108. package/dist/lib/messages.js +3 -0
  109. package/dist/lib/messages.test.js +25 -0
  110. package/dist/lib/osc52.js +53 -0
  111. package/dist/lib/perfPane.js +94 -0
  112. package/dist/lib/platform.js +312 -0
  113. package/dist/lib/precisionWheel.js +25 -0
  114. package/dist/lib/reasoning.js +39 -0
  115. package/dist/lib/rpc.js +26 -0
  116. package/dist/lib/subagentTree.js +287 -0
  117. package/dist/lib/syntax.js +89 -0
  118. package/dist/lib/terminalModes.js +46 -0
  119. package/dist/lib/terminalParity.js +48 -0
  120. package/dist/lib/terminalSetup.js +321 -0
  121. package/dist/lib/text.js +203 -0
  122. package/dist/lib/text.test.js +18 -0
  123. package/dist/lib/todo.js +2 -0
  124. package/dist/lib/todo.test.js +22 -0
  125. package/dist/lib/viewportStore.js +82 -0
  126. package/dist/lib/virtualHeights.js +61 -0
  127. package/dist/lib/wheelAccel.js +143 -0
  128. package/dist/theme.js +398 -0
  129. package/dist/types.js +1 -7
  130. package/package.json +2 -1
@@ -1,91 +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
+ };
1
25
  export const sessionCommands = [
2
26
  {
3
- name: 'sessions',
4
- category: 'session',
5
- help: 'browse prior sessions',
6
- run: (_a, ctx) => ctx.emit({ kind: 'overlay', overlay: 'sessionPicker' }),
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
+ }
7
42
  },
8
43
  {
9
- name: 'resume',
10
- category: 'session',
11
- help: 'resume a prior session by id',
44
+ help: 'change or show model',
45
+ name: 'model',
12
46
  run: (arg, ctx) => {
13
- const id = arg.trim();
14
- if (!id)
15
- return ctx.emit({ kind: 'overlay', overlay: 'sessionPicker' });
16
- ctx.emit({ kind: 'rpc', method: 'session.resume', params: { session_id: id } });
17
- ctx.emit({ kind: 'sys', text: `resuming session ${id}…` });
18
- },
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
+ }
19
67
  },
20
68
  {
21
- name: 'checkpoint',
22
- category: 'session',
23
- help: 'create a CVC checkpoint commit',
69
+ help: 'browse and resume previous sessions',
70
+ name: 'sessions',
24
71
  run: (arg, ctx) => {
25
- const msg = arg.trim() || 'tui checkpoint';
26
- ctx.emit({ kind: 'rpc', method: 'cvc.commit', params: { message: msg, commit_type: 'checkpoint' } });
27
- ctx.emit({ kind: 'sys', text: `checkpoint: ${msg}` });
28
- },
72
+ if (ctx.session.guardBusySessionSwitch('switch sessions')) {
73
+ return;
74
+ }
75
+ if (!arg.trim()) {
76
+ return patchOverlayState({ picker: true });
77
+ }
78
+ }
29
79
  },
30
80
  {
31
- name: 'rollback',
32
- category: 'session',
33
- help: 'rollback to a checkpoint',
81
+ help: 'attach an image',
82
+ name: 'image',
34
83
  run: (arg, ctx) => {
35
- const h = arg.trim();
36
- if (!h)
37
- return ctx.emit({ kind: 'sys', text: 'usage: /rollback <commit-hash>' });
38
- ctx.emit({ kind: 'rpc', method: 'cvc.restore', params: { commit_hash: h } });
39
- ctx.emit({ kind: 'sys', text: `rolling back to ${h}…` });
40
- },
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
+ }
41
91
  },
42
92
  {
43
- name: 'undo',
44
- category: 'session',
45
- help: 'undo the last turn',
46
- run: (_a, ctx) => ctx.emit({ kind: 'rpc', method: 'session.undo', params: ctx.sid ? { session_id: ctx.sid } : {} }),
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
+ }
47
107
  },
48
108
  {
49
- name: 'retry',
50
- category: 'session',
51
- help: 'retry the last user message',
52
- run: (_a, ctx) => ctx.emit({ kind: 'rpc', method: 'session.retry', params: ctx.sid ? { session_id: ctx.sid } : {} }),
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
+ }
53
146
  },
54
147
  {
148
+ aliases: ['fork'],
149
+ help: 'branch the session',
55
150
  name: 'branch',
56
- category: 'session',
57
- help: 'create a CVC cognitive branch',
58
151
  run: (arg, ctx) => {
59
- const name = arg.trim();
60
- if (!name)
61
- return ctx.emit({ kind: 'sys', text: 'usage: /branch <name>' });
62
- ctx.emit({ kind: 'rpc', method: 'cvc.branch', params: { name } });
63
- ctx.emit({ kind: 'sys', text: `branch created: ${name}` });
64
- },
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
+ }
65
164
  },
66
165
  {
67
- name: 'stop',
68
- category: 'session',
69
- help: 'interrupt the current turn',
70
- run: (_a, ctx) => {
71
- ctx.emit({ kind: 'rpc', method: 'session.stop', params: ctx.sid ? { session_id: ctx.sid } : {} });
72
- ctx.emit({ kind: 'sys', text: 'interrupt requested' });
73
- },
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
+ }
74
237
  },
75
238
  {
76
- name: 'steer',
77
- category: 'session',
78
- help: 'steer the in-flight turn',
239
+ help: 'switch theme skin (fires skin.changed)',
240
+ name: 'skin',
79
241
  run: (arg, ctx) => {
80
- const hint = arg.trim();
81
- if (!hint)
82
- return ctx.emit({ kind: 'sys', text: 'usage: /steer <hint>' });
83
- ctx.emit({
84
- kind: 'rpc',
85
- method: 'session.steer',
86
- params: { session_id: ctx.sid, hint },
87
- });
88
- ctx.emit({ kind: 'sys', text: 'steer queued' });
89
- },
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
+ }
90
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
+ }
91
431
  ];
@@ -1,47 +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 '@cvc/ink';
6
+ import { launchHermesCommand } from '../../../lib/externalCli.js';
7
+ import { runExternalSetup } from '../../setupHandoff.js';
1
8
  export const setupCommands = [
2
9
  {
10
+ help: 'run full setup wizard (launches `hermes setup`)',
3
11
  name: 'setup',
4
- category: 'setup',
5
- help: 'launch the CVC setup wizard',
6
- run: (_a, ctx) => {
7
- ctx.emit({ kind: 'rpc', method: 'cli.setup' });
8
- ctx.emit({ kind: 'sys', text: 'launching setup wizard…' });
9
- },
10
- },
11
- {
12
- name: 'terminal-setup',
13
- category: 'setup',
14
- help: 'configure IDE terminal keybindings (auto|cursor|vscode|windsurf)',
15
- run: (arg, ctx) => {
16
- const target = arg.trim().toLowerCase() || 'auto';
17
- const ok = ['auto', 'cursor', 'vscode', 'windsurf'];
18
- if (!ok.includes(target)) {
19
- return ctx.emit({ kind: 'sys', text: 'usage: /terminal-setup [auto|cursor|vscode|windsurf]' });
20
- }
21
- ctx.emit({ kind: 'rpc', method: 'cli.terminalSetup', params: { target } });
22
- ctx.emit({ kind: 'sys', text: `terminal-setup (${target})…` });
23
- },
24
- },
25
- {
26
- name: 'replay',
27
- category: 'setup',
28
- help: 'replay a prior session by id',
29
- run: (arg, ctx) => {
30
- const id = arg.trim();
31
- if (!id)
32
- return ctx.emit({ kind: 'sys', text: 'usage: /replay <session-id>' });
33
- ctx.emit({ kind: 'rpc', method: 'session.replay', params: { session_id: id } });
34
- },
35
- },
36
- {
37
- name: 'replay-diff',
38
- category: 'setup',
39
- help: 'diff two replays /replay-diff <a> <b>',
40
- run: (arg, ctx) => {
41
- const [a, b] = arg.trim().split(/\s+/);
42
- if (!a || !b)
43
- return ctx.emit({ kind: 'sys', text: 'usage: /replay-diff <a> <b>' });
44
- ctx.emit({ kind: 'rpc', method: 'session.replayDiff', params: { a, b } });
45
- },
46
- },
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
+ }
47
20
  ];
@@ -1,3 +1,7 @@
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.
1
5
  // Trivial toggle-style commands — boolean UI flags routed through patchUi
2
6
  // + a config.set RPC so the gateway can persist them.
3
7
  import { flagFromArg } from '../types.js';
@@ -1,79 +1,18 @@
1
- // Central CVC slash command registry. Mirrors Hermes ui-tui surface so
2
- // users coming from Hermes feel at home, but every command is owned by CVC.
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.
3
5
  import { coreCommands } from './commands/core.js';
4
6
  import { debugCommands } from './commands/debug.js';
5
7
  import { opsCommands } from './commands/ops.js';
6
8
  import { sessionCommands } from './commands/session.js';
7
9
  import { setupCommands } from './commands/setup.js';
8
- import { toggleCommands } from './commands/toggles.js';
9
10
  export const SLASH_COMMANDS = [
10
11
  ...coreCommands,
11
12
  ...sessionCommands,
12
13
  ...opsCommands,
13
14
  ...setupCommands,
14
- ...toggleCommands,
15
- ...debugCommands,
15
+ ...debugCommands
16
16
  ];
17
- const byName = new Map(SLASH_COMMANDS.flatMap(cmd => [cmd.name, ...(cmd.aliases ?? [])].map(n => [n.toLowerCase(), cmd])));
18
- export function findSlashCommand(name) {
19
- return byName.get(name.replace(/^\//, '').toLowerCase());
20
- }
21
- export function listSlashNames() {
22
- return SLASH_COMMANDS.map(c => `/${c.name}`).sort();
23
- }
24
- /**
25
- * Returns "/"-prefixed canonical names + aliases. Used by completion.ts to
26
- * populate slash candidates. Keeps both names ("listSlashNames" and
27
- * "listCommandNames") exported so older slice modules don't drift.
28
- */
29
- export function listCommandNames() {
30
- const all = new Set();
31
- for (const c of SLASH_COMMANDS) {
32
- all.add(`/${c.name}`);
33
- for (const a of c.aliases ?? [])
34
- all.add(`/${a}`);
35
- }
36
- return Array.from(all).sort();
37
- }
38
- /**
39
- * Parse a raw input line and dispatch it. Returns true if it WAS a slash
40
- * command (handled or not), false if the line should be treated as a normal
41
- * user message.
42
- *
43
- * Strip a single matched layer of surrounding quotes from `arg`. This is the
44
- * same affordance Hermes ui-tui applies to inputs like `/title 'foo bar'` or
45
- * `/title "foo bar"` — users coming from a shell expect quoting to "just
46
- * work" for arguments containing spaces.
47
- */
48
- export function unquoteArg(arg) {
49
- const t = arg.trim();
50
- if (t.length < 2)
51
- return t;
52
- const first = t.charCodeAt(0);
53
- const last = t.charCodeAt(t.length - 1);
54
- if (first === last && (first === 34 /* " */ || first === 39 /* ' */)) {
55
- return t.slice(1, -1);
56
- }
57
- return t;
58
- }
59
- export async function dispatchSlash(line, ctx) {
60
- if (!line.startsWith('/'))
61
- return false;
62
- const trimmed = line.trim();
63
- const sp = trimmed.indexOf(' ');
64
- const head = sp === -1 ? trimmed.slice(1) : trimmed.slice(1, sp);
65
- const rawArg = sp === -1 ? '' : trimmed.slice(sp + 1);
66
- const arg = unquoteArg(rawArg);
67
- const cmd = findSlashCommand(head);
68
- if (!cmd) {
69
- ctx.emit({ kind: 'sys', text: `unknown command: /${head} — try /help` });
70
- return true;
71
- }
72
- try {
73
- await cmd.run(arg, ctx, trimmed);
74
- }
75
- catch (err) {
76
- ctx.emit({ kind: 'sys', text: `command failed: ${String(err)}` });
77
- }
78
- return true;
79
- }
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());