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.
- package/dist/app/completion.js +4 -0
- package/dist/app/createGatewayEventHandler.js +508 -0
- package/dist/app/createSlashHandler.js +101 -0
- package/dist/app/delegationStore.js +51 -0
- package/dist/app/gatewayContext.js +17 -0
- package/dist/app/historyStore.js +4 -0
- package/dist/app/inputBuffer.js +4 -0
- package/dist/app/inputSelectionStore.js +8 -0
- package/dist/app/inputStore.js +4 -0
- package/dist/app/interfaces.js +6 -0
- package/dist/app/overlayStore.js +40 -0
- package/dist/app/promptStore.js +4 -0
- package/dist/app/queueStore.js +4 -0
- package/dist/app/scroll.js +44 -0
- package/dist/app/setupHandoff.js +28 -0
- package/dist/app/slash/commands/core.js +421 -234
- package/dist/app/slash/commands/debug.js +39 -6
- package/dist/app/slash/commands/ops.js +471 -136
- package/dist/app/slash/commands/session.js +405 -65
- package/dist/app/slash/commands/setup.js +16 -43
- package/dist/app/slash/commands/toggles.js +4 -0
- package/dist/app/slash/registry.js +7 -68
- package/dist/app/slash/types.js +1 -16
- package/dist/app/spawnHistoryStore.js +105 -0
- package/dist/app/turnController.js +650 -0
- package/dist/app/turnStore.js +47 -59
- package/dist/app/uiStore.js +34 -29
- package/dist/app/useComposerState.js +265 -0
- package/dist/app/useConfigSync.js +144 -0
- package/dist/app/useInputHandlers.js +403 -0
- package/dist/app/useLongRunToolCharms.js +50 -0
- package/dist/app/useMainApp.js +629 -0
- package/dist/app/useSessionLifecycle.js +175 -0
- package/dist/app/useSubmission.js +287 -0
- package/dist/app.js +13 -217
- package/dist/banner.js +40 -3
- package/dist/components/agentsOverlay.js +474 -0
- package/dist/components/appChrome.js +252 -0
- package/dist/components/appLayout.js +121 -22
- package/dist/components/appOverlays.js +65 -0
- package/dist/components/branding.js +97 -6
- package/dist/components/fpsOverlay.js +22 -0
- package/dist/components/helpHint.js +21 -0
- package/dist/components/markdown.js +501 -0
- package/dist/components/maskedPrompt.js +12 -0
- package/dist/components/messageLine.js +82 -0
- package/dist/components/modelPicker.js +254 -0
- package/dist/components/overlayControls.js +30 -0
- package/dist/components/overlays/helpOverlay.js +1 -0
- package/dist/components/overlays/historySearch.js +1 -0
- package/dist/components/overlays/modelPicker.js +2 -1
- package/dist/components/overlays/overlayUtils.js +2 -1
- package/dist/components/overlays/secretPrompt.js +1 -0
- package/dist/components/overlays/sessionPicker.js +1 -0
- package/dist/components/overlays/skillsHub.js +1 -0
- package/dist/components/prompts.js +95 -0
- package/dist/components/queuedMessages.js +24 -0
- package/dist/components/sessionPicker.js +130 -0
- package/dist/components/skillsHub.js +165 -0
- package/dist/components/streamingAssistant.js +35 -0
- package/dist/components/streamingMarkdown.js +110 -186
- package/dist/components/textInput.js +748 -218
- package/dist/components/themed.js +12 -0
- package/dist/components/thinking.js +493 -36
- package/dist/components/todoPanel.js +40 -0
- package/dist/config/env.js +18 -0
- package/dist/config/limits.js +22 -0
- package/dist/config/timing.js +4 -0
- package/dist/content/charms.js +5 -0
- package/dist/content/faces.js +21 -0
- package/dist/content/fortunes.js +29 -0
- package/dist/content/hotkeys.js +38 -0
- package/dist/content/placeholders.js +15 -0
- package/dist/content/setup.js +14 -0
- package/dist/content/verbs.js +41 -0
- package/dist/domain/details.js +53 -0
- package/dist/domain/messages.js +63 -0
- package/dist/domain/paths.js +16 -0
- package/dist/domain/providers.js +11 -0
- package/dist/domain/roles.js +6 -0
- package/dist/domain/slash.js +11 -0
- package/dist/domain/usage.js +1 -0
- package/dist/domain/viewport.js +33 -0
- package/dist/entry.js +65 -40
- package/dist/gatewayClient.js +574 -0
- package/dist/gatewayTypes.js +1 -0
- package/dist/hooks/useCompletion.js +86 -0
- package/dist/hooks/useGitBranch.js +58 -0
- package/dist/hooks/useInputHistory.js +12 -0
- package/dist/hooks/useQueue.js +57 -0
- package/dist/hooks/useVirtualHistory.js +401 -0
- package/dist/lib/circularBuffer.js +43 -0
- package/dist/lib/clipboard.js +126 -0
- package/dist/lib/editor.js +41 -0
- package/dist/lib/editor.test.js +58 -0
- package/dist/lib/emoji.js +49 -0
- package/dist/lib/externalCli.js +11 -0
- package/dist/lib/forceTruecolor.js +26 -0
- package/dist/lib/fpsStore.js +36 -0
- package/dist/lib/gracefulExit.js +29 -0
- package/dist/lib/history.js +69 -0
- package/dist/lib/inputMetrics.js +143 -0
- package/dist/lib/liveProgress.js +51 -0
- package/dist/lib/liveProgress.test.js +89 -0
- package/dist/lib/mathUnicode.js +685 -0
- package/dist/lib/memory.js +123 -0
- package/dist/lib/memoryMonitor.js +76 -0
- package/dist/lib/messages.js +3 -0
- package/dist/lib/messages.test.js +25 -0
- package/dist/lib/osc52.js +53 -0
- package/dist/lib/perfPane.js +94 -0
- package/dist/lib/platform.js +312 -0
- package/dist/lib/precisionWheel.js +25 -0
- package/dist/lib/reasoning.js +39 -0
- package/dist/lib/rpc.js +26 -0
- package/dist/lib/subagentTree.js +287 -0
- package/dist/lib/syntax.js +89 -0
- package/dist/lib/terminalModes.js +46 -0
- package/dist/lib/terminalParity.js +48 -0
- package/dist/lib/terminalSetup.js +321 -0
- package/dist/lib/text.js +203 -0
- package/dist/lib/text.test.js +18 -0
- package/dist/lib/todo.js +2 -0
- package/dist/lib/todo.test.js +22 -0
- package/dist/lib/viewportStore.js +82 -0
- package/dist/lib/virtualHeights.js +61 -0
- package/dist/lib/wheelAccel.js +143 -0
- package/dist/theme.js +398 -0
- package/dist/types.js +1 -7
- package/package.json +2 -1
|
@@ -1,292 +1,479 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 { forceRedraw } from '@cvc/ink';
|
|
6
|
+
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js';
|
|
7
|
+
import { dailyFortune, randomFortune } from '../../../content/fortunes.js';
|
|
8
|
+
import { HOTKEYS } from '../../../content/hotkeys.js';
|
|
9
|
+
import { SECTION_NAMES, isSectionName, nextDetailsMode, parseDetailsMode } from '../../../domain/details.js';
|
|
10
|
+
import { writeClipboardText } from '../../../lib/clipboard.js';
|
|
11
|
+
import { writeOsc52Clipboard } from '../../../lib/osc52.js';
|
|
12
|
+
import { configureDetectedTerminalKeybindings, configureTerminalKeybindings } from '../../../lib/terminalSetup.js';
|
|
13
|
+
import { patchOverlayState } from '../../overlayStore.js';
|
|
14
|
+
import { patchUiState } from '../../uiStore.js';
|
|
15
|
+
const flagFromArg = (arg, current) => {
|
|
16
|
+
if (!arg) {
|
|
17
|
+
return !current;
|
|
18
|
+
}
|
|
19
|
+
const mode = arg.trim().toLowerCase();
|
|
20
|
+
if (mode === 'on') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (mode === 'off') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (mode === 'toggle') {
|
|
27
|
+
return !current;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
const RESET_WORDS = new Set(['reset', 'clear', 'default']);
|
|
32
|
+
const CYCLE_WORDS = new Set(['cycle', 'toggle']);
|
|
33
|
+
const DETAILS_USAGE = 'usage: /details [hidden|collapsed|expanded|cycle] or /details <section> [hidden|collapsed|expanded|reset]';
|
|
34
|
+
const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]';
|
|
12
35
|
export const coreCommands = [
|
|
13
36
|
{
|
|
37
|
+
help: 'list commands + hotkeys',
|
|
14
38
|
name: 'help',
|
|
15
|
-
aliases: ['?'],
|
|
16
|
-
category: 'core',
|
|
17
|
-
help: 'list slash commands + hotkeys',
|
|
18
39
|
run: (_arg, ctx) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
['/history', 'view current transcript'],
|
|
35
|
-
['/save', 'save transcript to JSON'],
|
|
36
|
-
['/title <text>', 'set session title'],
|
|
37
|
-
['/redraw', 'force a full UI repaint'],
|
|
38
|
-
['/fortune [random|daily]', 'a CVC fortune'],
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
title: 'Sessions',
|
|
43
|
-
rows: [
|
|
44
|
-
['/sessions', 'browse prior sessions'],
|
|
45
|
-
['/resume [id]', 'resume a prior session'],
|
|
46
|
-
['/new [title]', 'new session (alias of /clear)'],
|
|
47
|
-
['/checkpoint', 'create a CVC checkpoint'],
|
|
48
|
-
['/rollback [hash]', 'rollback to a checkpoint'],
|
|
49
|
-
['/undo', 'undo last assistant turn'],
|
|
50
|
-
['/retry', 'retry the last user message'],
|
|
51
|
-
['/branch [name]', 'create a CVC branch'],
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
title: 'Models & Tools',
|
|
56
|
-
rows: [
|
|
57
|
-
['/model [name]', 'switch model (arrow-key picker if blank)'],
|
|
58
|
-
['/provider [name]', 'switch provider'],
|
|
59
|
-
['/skills', 'browse skills hub'],
|
|
60
|
-
['/reload-skills', 'rescan skill directory'],
|
|
61
|
-
['/agents', 'list registered agents'],
|
|
62
|
-
['/memory [show|edit|clear]', 'memory store'],
|
|
63
|
-
['/mem', 'alias of /memory'],
|
|
64
|
-
['/tools', 'list tools'],
|
|
65
|
-
['/reload', 'reload config'],
|
|
66
|
-
['/reload-mcp', 'reload MCP servers'],
|
|
67
|
-
['/compact', 'toggle compact transcript'],
|
|
68
|
-
['/compress', 'compress conversation'],
|
|
69
|
-
],
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
title: 'UI & Toggles',
|
|
73
|
-
rows: [
|
|
74
|
-
['/personality [name]', 'switch personality'],
|
|
75
|
-
['/skin [name]', 'switch skin'],
|
|
76
|
-
['/statusbar [on|off|top|bottom]', 'status bar position'],
|
|
77
|
-
['/indicator [on|off]', 'busy indicator'],
|
|
78
|
-
['/details [hidden|collapsed|expanded|cycle]', 'agent detail visibility'],
|
|
79
|
-
['/mouse [on|off]', 'mouse/wheel tracking'],
|
|
80
|
-
['/verbose, /reasoning, /fast, /yolo', 'misc toggles'],
|
|
81
|
-
['/voice, /image, /browser', 'media toggles'],
|
|
82
|
-
['/copy [n], /paste', 'clipboard'],
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
title: 'Ops & Debug',
|
|
87
|
-
rows: [
|
|
88
|
-
['/stop', 'interrupt current turn'],
|
|
89
|
-
['/steer <hint>', 'steer the in-flight turn'],
|
|
90
|
-
['/logs [n]', 'tail gateway logs'],
|
|
91
|
-
['/heapdump', 'capture a heap snapshot'],
|
|
92
|
-
['/replay [id]', 'replay a session'],
|
|
93
|
-
['/replay-diff <a> <b>', 'diff two replays'],
|
|
94
|
-
['/setup', 'run setup wizard'],
|
|
95
|
-
['/terminal-setup [auto|cursor|vscode|windsurf]', 'IDE keybindings'],
|
|
96
|
-
],
|
|
97
|
-
},
|
|
40
|
+
const sections = (ctx.local.catalog?.categories ?? []).map(cat => ({
|
|
41
|
+
rows: cat.pairs,
|
|
42
|
+
title: cat.name
|
|
43
|
+
}));
|
|
44
|
+
if (ctx.local.catalog?.skillCount) {
|
|
45
|
+
sections.push({ text: `${ctx.local.catalog.skillCount} skill commands available — /skills to browse` });
|
|
46
|
+
}
|
|
47
|
+
sections.push({
|
|
48
|
+
rows: [
|
|
49
|
+
['/details [hidden|collapsed|expanded|cycle]', 'set global agent detail visibility mode'],
|
|
50
|
+
[
|
|
51
|
+
'/details <section> [hidden|collapsed|expanded|reset]',
|
|
52
|
+
'override one section (thinking/tools/subagents/activity)'
|
|
53
|
+
],
|
|
54
|
+
['/fortune [random|daily]', 'show a random or daily local fortune']
|
|
98
55
|
],
|
|
99
|
-
|
|
100
|
-
|
|
56
|
+
title: 'TUI'
|
|
57
|
+
}, { rows: HOTKEYS, title: 'Hotkeys' });
|
|
58
|
+
ctx.transcript.panel(ctx.ui.theme.brand.helpHeader, sections);
|
|
59
|
+
}
|
|
101
60
|
},
|
|
102
61
|
{
|
|
103
|
-
name: 'quit',
|
|
104
62
|
aliases: ['exit', 'q'],
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
run: (_arg, ctx) => ctx.
|
|
63
|
+
help: 'exit hermes',
|
|
64
|
+
name: 'quit',
|
|
65
|
+
run: (_arg, ctx) => ctx.session.die()
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
aliases: ['scroll'],
|
|
69
|
+
help: 'toggle mouse/wheel tracking [on|off|toggle]',
|
|
70
|
+
name: 'mouse',
|
|
71
|
+
run: (arg, ctx) => {
|
|
72
|
+
const current = ctx.ui.mouseTracking;
|
|
73
|
+
const next = flagFromArg(arg, current);
|
|
74
|
+
if (next === null) {
|
|
75
|
+
return ctx.transcript.sys('usage: /mouse [on|off|toggle]');
|
|
76
|
+
}
|
|
77
|
+
patchUiState({ mouseTracking: next });
|
|
78
|
+
ctx.gateway.rpc('config.set', { key: 'mouse', value: next ? 'on' : 'off' }).catch(() => { });
|
|
79
|
+
queueMicrotask(() => ctx.transcript.sys(`mouse tracking ${next ? 'on' : 'off'}`));
|
|
80
|
+
}
|
|
108
81
|
},
|
|
109
82
|
{
|
|
110
|
-
name: 'clear',
|
|
111
83
|
aliases: ['new'],
|
|
112
|
-
category: 'core',
|
|
113
84
|
help: 'start a new session',
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
85
|
+
name: 'clear',
|
|
86
|
+
run: (arg, ctx, cmd) => {
|
|
87
|
+
if (ctx.session.guardBusySessionSwitch('switch sessions')) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const isNew = cmd.startsWith('/new');
|
|
91
|
+
const requestedTitle = isNew ? arg.trim() : '';
|
|
92
|
+
const commit = () => {
|
|
93
|
+
patchUiState({ status: 'forging session…' });
|
|
94
|
+
ctx.session.newSession(isNew ? 'new session started' : undefined, requestedTitle || undefined);
|
|
95
|
+
};
|
|
96
|
+
if (NO_CONFIRM_DESTRUCTIVE) {
|
|
97
|
+
return commit();
|
|
98
|
+
}
|
|
99
|
+
patchOverlayState({
|
|
100
|
+
confirm: {
|
|
101
|
+
cancelLabel: 'No, keep going',
|
|
102
|
+
confirmLabel: isNew ? 'Yes, start a new session' : 'Yes, clear the session',
|
|
103
|
+
danger: true,
|
|
104
|
+
detail: 'This ends the current conversation and clears the transcript.',
|
|
105
|
+
onConfirm: commit,
|
|
106
|
+
title: isNew ? 'Start a new session?' : 'Clear the current session?'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
121
110
|
},
|
|
122
111
|
{
|
|
123
|
-
name: 'redraw',
|
|
124
|
-
category: 'core',
|
|
125
112
|
help: 'force a full UI repaint',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
name: 'redraw',
|
|
114
|
+
run: (_arg, ctx) => {
|
|
115
|
+
forceRedraw(process.stdout);
|
|
116
|
+
ctx.transcript.sys('ui redrawn');
|
|
117
|
+
}
|
|
130
118
|
},
|
|
131
119
|
{
|
|
132
|
-
name: 'status',
|
|
133
|
-
category: 'core',
|
|
134
120
|
help: 'show live session info',
|
|
135
|
-
|
|
121
|
+
name: 'status',
|
|
122
|
+
run: (_arg, ctx) => {
|
|
136
123
|
if (!ctx.sid) {
|
|
137
|
-
ctx.
|
|
124
|
+
return ctx.transcript.sys('no active session');
|
|
125
|
+
}
|
|
126
|
+
ctx.gateway
|
|
127
|
+
.rpc('session.status', { session_id: ctx.sid })
|
|
128
|
+
.then(ctx.guarded(r => ctx.transcript.page(r.output || '(no status)', 'Status')))
|
|
129
|
+
.catch(ctx.guardedErr);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
help: 'resume a prior session',
|
|
134
|
+
name: 'resume',
|
|
135
|
+
run: (arg, ctx) => {
|
|
136
|
+
if (ctx.session.guardBusySessionSwitch('switch sessions')) {
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
|
-
ctx.
|
|
141
|
-
}
|
|
139
|
+
arg ? ctx.session.resumeById(arg) : patchOverlayState({ picker: true });
|
|
140
|
+
}
|
|
142
141
|
},
|
|
143
142
|
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
help: 'set or show current session title',
|
|
144
|
+
name: 'title',
|
|
145
|
+
run: (arg, ctx) => {
|
|
146
|
+
if (!ctx.sid) {
|
|
147
|
+
return ctx.transcript.sys('no active session');
|
|
148
|
+
}
|
|
149
|
+
const title = arg.trim();
|
|
150
|
+
if (!arg) {
|
|
151
|
+
ctx.gateway
|
|
152
|
+
.rpc('session.title', { session_id: ctx.sid })
|
|
153
|
+
.then(ctx.guarded(r => {
|
|
154
|
+
const current = (r?.title ?? '').trim();
|
|
155
|
+
ctx.transcript.sys(current ? `title: ${current}` : 'no title set');
|
|
156
|
+
}))
|
|
157
|
+
.catch(ctx.guardedErr);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!title) {
|
|
161
|
+
return ctx.transcript.sys('usage: /title <your session title>');
|
|
162
|
+
}
|
|
163
|
+
ctx.gateway
|
|
164
|
+
.rpc('session.title', { session_id: ctx.sid, title })
|
|
165
|
+
.then(ctx.guarded(r => {
|
|
166
|
+
const next = (r?.title ?? title).trim();
|
|
167
|
+
const suffix = r?.pending ? ' (queued while session initializes)' : '';
|
|
168
|
+
ctx.transcript.sys(`session title set: ${next}${suffix}`);
|
|
169
|
+
}))
|
|
170
|
+
.catch(ctx.guardedErr);
|
|
171
|
+
}
|
|
148
172
|
},
|
|
149
173
|
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
help: 'view current transcript',
|
|
174
|
+
help: 'toggle compact transcript',
|
|
175
|
+
name: 'compact',
|
|
153
176
|
run: (arg, ctx) => {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
return ctx.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return `[${tag}]\n${clip}`;
|
|
163
|
-
});
|
|
164
|
-
ctx.emit({ kind: 'page', title: 'History', text: lines.join('\n\n') });
|
|
165
|
-
},
|
|
177
|
+
const next = flagFromArg(arg, ctx.ui.compact);
|
|
178
|
+
if (next === null) {
|
|
179
|
+
return ctx.transcript.sys('usage: /compact [on|off|toggle]');
|
|
180
|
+
}
|
|
181
|
+
patchUiState({ compact: next });
|
|
182
|
+
ctx.gateway.rpc('config.set', { key: 'compact', value: next ? 'on' : 'off' }).catch(() => { });
|
|
183
|
+
queueMicrotask(() => ctx.transcript.sys(`compact ${next ? 'on' : 'off'}`));
|
|
184
|
+
}
|
|
166
185
|
},
|
|
167
186
|
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
run: (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
aliases: ['detail'],
|
|
188
|
+
help: 'control agent detail visibility (global or per-section)',
|
|
189
|
+
name: 'details',
|
|
190
|
+
run: (arg, ctx) => {
|
|
191
|
+
const { gateway, transcript, ui } = ctx;
|
|
192
|
+
if (!arg) {
|
|
193
|
+
gateway
|
|
194
|
+
.rpc('config.get', { key: 'details_mode' })
|
|
195
|
+
.then(r => {
|
|
196
|
+
if (ctx.stale()) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const mode = parseDetailsMode(r?.value) ?? ui.detailsMode;
|
|
200
|
+
patchUiState({ detailsMode: mode, detailsModeCommandOverride: false });
|
|
201
|
+
const overrides = SECTION_NAMES.filter(s => ui.sections[s])
|
|
202
|
+
.map(s => `${s}=${ui.sections[s]}`)
|
|
203
|
+
.join(' ');
|
|
204
|
+
transcript.sys(`details: ${mode}${overrides ? ` (${overrides})` : ''}`);
|
|
205
|
+
})
|
|
206
|
+
.catch(() => !ctx.stale() && transcript.sys(`details: ${ui.detailsMode}`));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const [first, second] = arg.trim().toLowerCase().split(/\s+/);
|
|
210
|
+
if (second && isSectionName(first)) {
|
|
211
|
+
const reset = RESET_WORDS.has(second);
|
|
212
|
+
const mode = reset ? null : parseDetailsMode(second);
|
|
213
|
+
if (!reset && !mode) {
|
|
214
|
+
return transcript.sys(DETAILS_SECTION_USAGE);
|
|
215
|
+
}
|
|
216
|
+
const { [first]: _drop, ...rest } = ui.sections;
|
|
217
|
+
patchUiState({ sections: mode ? { ...rest, [first]: mode } : rest });
|
|
218
|
+
gateway
|
|
219
|
+
.rpc('config.set', { key: `details_mode.${first}`, value: mode ?? '' })
|
|
220
|
+
.catch(() => { });
|
|
221
|
+
transcript.sys(`details ${first}: ${mode ?? 'reset'}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const next = CYCLE_WORDS.has(first ?? '') ? nextDetailsMode(ui.detailsMode) : parseDetailsMode(first);
|
|
225
|
+
if (!next) {
|
|
226
|
+
return transcript.sys(DETAILS_USAGE);
|
|
227
|
+
}
|
|
228
|
+
const sections = Object.fromEntries(SECTION_NAMES.map(section => [section, next]));
|
|
229
|
+
patchUiState({ detailsMode: next, detailsModeCommandOverride: true, sections });
|
|
230
|
+
gateway.rpc('config.set', { key: 'details_mode', value: next }).catch(() => { });
|
|
231
|
+
transcript.sys(`details: ${next}`);
|
|
232
|
+
}
|
|
176
233
|
},
|
|
177
234
|
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
help: 'set or show session title',
|
|
235
|
+
help: 'local fortune',
|
|
236
|
+
name: 'fortune',
|
|
181
237
|
run: (arg, ctx) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
238
|
+
const key = arg.trim().toLowerCase();
|
|
239
|
+
if (!arg || key === 'random') {
|
|
240
|
+
return ctx.transcript.sys(randomFortune());
|
|
241
|
+
}
|
|
242
|
+
if (['daily', 'stable', 'today'].includes(key)) {
|
|
243
|
+
return ctx.transcript.sys(dailyFortune(ctx.sid));
|
|
244
|
+
}
|
|
245
|
+
ctx.transcript.sys('usage: /fortune [random|daily]');
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
help: 'copy selection or assistant message',
|
|
250
|
+
name: 'copy',
|
|
251
|
+
run: async (arg, ctx) => {
|
|
252
|
+
const { sys } = ctx.transcript;
|
|
253
|
+
if (!arg && ctx.composer.hasSelection) {
|
|
254
|
+
const text = await ctx.composer.selection.copySelection();
|
|
255
|
+
if (text) {
|
|
256
|
+
return sys(`copied ${text.length} characters`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return sys('clipboard copy failed — try CVC_TUI_FORCE_OSC52=1 to force the escape sequence; CVC_TUI_DEBUG_CLIPBOARD=1 for details');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (arg && Number.isNaN(parseInt(arg, 10))) {
|
|
263
|
+
return sys('usage: /copy [number]');
|
|
264
|
+
}
|
|
265
|
+
const all = ctx.local.getHistoryItems().filter(m => m.role === 'assistant');
|
|
266
|
+
const target = all[arg ? Math.min(parseInt(arg, 10), all.length) - 1 : all.length - 1];
|
|
267
|
+
if (!target) {
|
|
268
|
+
return sys('nothing to copy — start a conversation first');
|
|
269
|
+
}
|
|
270
|
+
void writeClipboardText(target.text)
|
|
271
|
+
.then(nativeOk => {
|
|
272
|
+
if (ctx.stale()) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (nativeOk) {
|
|
276
|
+
sys('copied to clipboard');
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
writeOsc52Clipboard(target.text);
|
|
280
|
+
sys('sent OSC52 copy sequence (terminal support required)');
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
.catch(error => {
|
|
284
|
+
if (!ctx.stale()) {
|
|
285
|
+
sys(`copy failed: ${String(error)}`);
|
|
286
|
+
}
|
|
189
287
|
});
|
|
190
|
-
|
|
191
|
-
ctx.emit({ kind: 'sys', text: `session title set: ${t}` });
|
|
192
|
-
},
|
|
288
|
+
}
|
|
193
289
|
},
|
|
194
290
|
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
run: (arg, ctx) => {
|
|
199
|
-
const cur = Boolean(ctx.ui.compact);
|
|
200
|
-
const next = flagFromArg(arg, cur);
|
|
201
|
-
if (next === null)
|
|
202
|
-
return ctx.emit({ kind: 'sys', text: 'usage: /compact [on|off|toggle]' });
|
|
203
|
-
ctx.emit({ kind: 'patchUi', ui: { compact: next } });
|
|
204
|
-
ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'compact', value: next ? 'on' : 'off' } });
|
|
205
|
-
ctx.emit({ kind: 'sys', text: `compact ${next ? 'on' : 'off'}` });
|
|
206
|
-
},
|
|
291
|
+
help: 'attach clipboard image',
|
|
292
|
+
name: 'paste',
|
|
293
|
+
run: (arg, ctx) => (arg ? ctx.transcript.sys('usage: /paste') : ctx.composer.paste())
|
|
207
294
|
},
|
|
208
295
|
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
296
|
+
help: 'configure IDE terminal keybindings for multiline + undo/redo',
|
|
297
|
+
name: 'terminal-setup',
|
|
298
|
+
run: (arg, ctx) => {
|
|
299
|
+
const target = arg.trim().toLowerCase();
|
|
300
|
+
if (target && !['auto', 'cursor', 'vscode', 'windsurf'].includes(target)) {
|
|
301
|
+
return ctx.transcript.sys('usage: /terminal-setup [auto|vscode|cursor|windsurf]');
|
|
302
|
+
}
|
|
303
|
+
const runner = !target || target === 'auto'
|
|
304
|
+
? configureDetectedTerminalKeybindings()
|
|
305
|
+
: configureTerminalKeybindings(target);
|
|
306
|
+
void runner
|
|
307
|
+
.then(result => {
|
|
308
|
+
if (ctx.stale()) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
ctx.transcript.sys(result.message);
|
|
312
|
+
if (result.success && result.requiresRestart) {
|
|
313
|
+
ctx.transcript.sys('restart the IDE terminal for the new keybindings to take effect');
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
.catch(error => {
|
|
317
|
+
if (!ctx.stale()) {
|
|
318
|
+
ctx.transcript.sys(`terminal setup failed: ${String(error)}`);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
213
322
|
},
|
|
214
323
|
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
help: 'create a CVC commit from the current transcript',
|
|
324
|
+
help: 'view gateway logs',
|
|
325
|
+
name: 'logs',
|
|
218
326
|
run: (arg, ctx) => {
|
|
219
|
-
const
|
|
220
|
-
ctx.
|
|
221
|
-
|
|
222
|
-
},
|
|
327
|
+
const text = ctx.gateway.gw.getLogTail(Math.min(80, Math.max(1, parseInt(arg, 10) || 20)));
|
|
328
|
+
text ? ctx.transcript.page(text, 'Logs') : ctx.transcript.sys('no gateway logs');
|
|
329
|
+
}
|
|
223
330
|
},
|
|
224
331
|
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
help: 'a local CVC fortune',
|
|
332
|
+
help: 'view current transcript (user + assistant messages)',
|
|
333
|
+
name: 'history',
|
|
228
334
|
run: (arg, ctx) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
335
|
+
// The CLI-side `/history` runs in a detached slash-worker subprocess
|
|
336
|
+
// that never sees the TUI's turns — it only surfaces whatever was
|
|
337
|
+
// persisted before this process started. Render the TUI's own
|
|
338
|
+
// transcript so `/history` actually reflects what the user just did.
|
|
339
|
+
const items = ctx.local.getHistoryItems().filter(m => m.role === 'user' || m.role === 'assistant');
|
|
340
|
+
if (!items.length) {
|
|
341
|
+
return ctx.transcript.sys('no conversation yet');
|
|
234
342
|
}
|
|
235
|
-
|
|
236
|
-
|
|
343
|
+
const preview = Math.max(80, parseInt(arg, 10) || 400);
|
|
344
|
+
const lines = items.map((m, i) => {
|
|
345
|
+
const tag = m.role === 'user' ? `You #${i + 1}` : `Hermes #${i + 1}`;
|
|
346
|
+
const body = m.text.trim() || (m.tools?.length ? `(${m.tools.length} tool calls)` : '(empty)');
|
|
347
|
+
const clipped = body.length > preview ? `${body.slice(0, preview).trimEnd()}…` : body;
|
|
348
|
+
return `[${tag}]\n${clipped}`;
|
|
349
|
+
});
|
|
350
|
+
ctx.transcript.page(lines.join('\n\n'), 'History');
|
|
351
|
+
}
|
|
237
352
|
},
|
|
238
353
|
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
ctx.
|
|
249
|
-
|
|
250
|
-
|
|
354
|
+
help: 'save the current transcript to JSON',
|
|
355
|
+
name: 'save',
|
|
356
|
+
run: (_arg, ctx) => {
|
|
357
|
+
const hasConversation = ctx.local
|
|
358
|
+
.getHistoryItems()
|
|
359
|
+
.some(m => m.role === 'user' || m.role === 'assistant' || m.role === 'tool');
|
|
360
|
+
if (!hasConversation) {
|
|
361
|
+
return ctx.transcript.sys('no conversation yet');
|
|
362
|
+
}
|
|
363
|
+
if (!ctx.sid) {
|
|
364
|
+
return ctx.transcript.sys('no active session — nothing to save');
|
|
365
|
+
}
|
|
366
|
+
ctx.gateway
|
|
367
|
+
.rpc('session.save', { session_id: ctx.sid })
|
|
368
|
+
.then(ctx.guarded(r => {
|
|
369
|
+
const file = r?.file;
|
|
370
|
+
if (file) {
|
|
371
|
+
ctx.transcript.sys(`conversation saved to: ${file}`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
ctx.transcript.sys('failed to save');
|
|
375
|
+
}
|
|
376
|
+
}))
|
|
377
|
+
.catch(ctx.guardedErr);
|
|
378
|
+
}
|
|
251
379
|
},
|
|
252
380
|
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
run: (
|
|
381
|
+
aliases: ['sb'],
|
|
382
|
+
help: 'status bar position (on|off|top|bottom)',
|
|
383
|
+
name: 'statusbar',
|
|
384
|
+
run: (arg, ctx) => {
|
|
385
|
+
const mode = arg.trim().toLowerCase();
|
|
386
|
+
const toggle = ctx.ui.statusBar === 'off' ? 'top' : 'off';
|
|
387
|
+
const next = !mode || mode === 'toggle'
|
|
388
|
+
? toggle
|
|
389
|
+
: mode === 'on' || mode === 'top'
|
|
390
|
+
? 'top'
|
|
391
|
+
: mode === 'off' || mode === 'bottom'
|
|
392
|
+
? mode
|
|
393
|
+
: null;
|
|
394
|
+
if (!next) {
|
|
395
|
+
return ctx.transcript.sys('usage: /statusbar [on|off|top|bottom|toggle]');
|
|
396
|
+
}
|
|
397
|
+
patchUiState({ statusBar: next });
|
|
398
|
+
ctx.gateway.rpc('config.set', { key: 'statusbar', value: next }).catch(() => { });
|
|
399
|
+
queueMicrotask(() => ctx.transcript.sys(`status bar ${next}`));
|
|
400
|
+
}
|
|
257
401
|
},
|
|
258
402
|
{
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
help: 'tail gateway logs',
|
|
403
|
+
help: 'inspect or enqueue a message',
|
|
404
|
+
name: 'queue',
|
|
262
405
|
run: (arg, ctx) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
406
|
+
if (!arg) {
|
|
407
|
+
return ctx.transcript.sys(`${ctx.composer.queueRef.current.length} queued message(s)`);
|
|
408
|
+
}
|
|
409
|
+
ctx.composer.enqueue(arg);
|
|
410
|
+
ctx.transcript.sys(`queued: "${arg.slice(0, 50)}${arg.length > 50 ? '…' : ''}"`);
|
|
411
|
+
}
|
|
266
412
|
},
|
|
267
413
|
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
category: 'core',
|
|
271
|
-
help: 'control agent detail visibility',
|
|
414
|
+
help: 'inject a message after the next tool call (no interrupt)',
|
|
415
|
+
name: 'steer',
|
|
272
416
|
run: (arg, ctx) => {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
ctx.emit({ kind: 'sys', text: `details: ${ctx.ui.detailsMode ?? 'collapsed'}` });
|
|
277
|
-
return;
|
|
417
|
+
const payload = arg?.trim() ?? '';
|
|
418
|
+
if (!payload) {
|
|
419
|
+
return ctx.transcript.sys('usage: /steer <prompt>');
|
|
278
420
|
}
|
|
279
|
-
|
|
280
|
-
|
|
421
|
+
// If the agent isn't running, fall back to the queue so the user's
|
|
422
|
+
// message isn't lost — identical semantics to the gateway handler.
|
|
423
|
+
if (!ctx.ui.busy || !ctx.sid) {
|
|
424
|
+
ctx.composer.enqueue(payload);
|
|
425
|
+
ctx.transcript.sys(`no active turn — queued for next: "${payload.slice(0, 50)}${payload.length > 50 ? '…' : ''}"`);
|
|
426
|
+
return;
|
|
281
427
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
428
|
+
ctx.gateway
|
|
429
|
+
.rpc('session.steer', { session_id: ctx.sid, text: payload })
|
|
430
|
+
.then(ctx.guarded(r => {
|
|
431
|
+
if (r?.status === 'queued') {
|
|
432
|
+
ctx.transcript.sys(`steer queued — arrives after next tool call: "${payload.slice(0, 50)}${payload.length > 50 ? '…' : ''}"`);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
ctx.transcript.sys('steer rejected');
|
|
436
|
+
}
|
|
437
|
+
}))
|
|
438
|
+
.catch(ctx.guardedErr);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
help: 'undo last exchange',
|
|
443
|
+
name: 'undo',
|
|
444
|
+
run: (_arg, ctx) => {
|
|
445
|
+
if (!ctx.sid) {
|
|
446
|
+
return ctx.transcript.sys('nothing to undo');
|
|
287
447
|
}
|
|
288
|
-
ctx.
|
|
289
|
-
|
|
290
|
-
|
|
448
|
+
ctx.gateway.rpc('session.undo', { session_id: ctx.sid }).then(ctx.guarded(r => {
|
|
449
|
+
if ((r.removed ?? 0) > 0) {
|
|
450
|
+
ctx.transcript.setHistoryItems((prev) => ctx.transcript.trimLastExchange(prev));
|
|
451
|
+
ctx.transcript.sys(`undid ${r.removed} messages`);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
ctx.transcript.sys('nothing to undo');
|
|
455
|
+
}
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
291
458
|
},
|
|
459
|
+
{
|
|
460
|
+
help: 'retry last user message',
|
|
461
|
+
name: 'retry',
|
|
462
|
+
run: (_arg, ctx) => {
|
|
463
|
+
const last = ctx.local.getLastUserMsg();
|
|
464
|
+
if (!last) {
|
|
465
|
+
return ctx.transcript.sys('nothing to retry');
|
|
466
|
+
}
|
|
467
|
+
if (!ctx.sid) {
|
|
468
|
+
return ctx.transcript.send(last);
|
|
469
|
+
}
|
|
470
|
+
ctx.gateway.rpc('session.undo', { session_id: ctx.sid }).then(ctx.guarded(r => {
|
|
471
|
+
if ((r.removed ?? 0) <= 0) {
|
|
472
|
+
return ctx.transcript.sys('nothing to retry');
|
|
473
|
+
}
|
|
474
|
+
ctx.transcript.setHistoryItems((prev) => ctx.transcript.trimLastExchange(prev));
|
|
475
|
+
ctx.transcript.send(last);
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
292
479
|
];
|