cvc-tui 0.1.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/app/completion.js +98 -0
- package/dist/app/historyStore.js +119 -0
- package/dist/app/inputBuffer.js +116 -0
- package/dist/app/inputStore.js +24 -0
- package/dist/app/promptStore.js +40 -0
- package/dist/app/queueStore.js +21 -0
- package/dist/app/slash/commands/core.js +292 -0
- package/dist/app/slash/commands/debug.js +11 -0
- package/dist/app/slash/commands/ops.js +163 -0
- package/dist/app/slash/commands/session.js +91 -0
- package/dist/app/slash/commands/setup.js +47 -0
- package/dist/app/slash/commands/toggles.js +36 -0
- package/dist/app/slash/registry.js +79 -0
- package/dist/app/slash/types.js +16 -0
- package/dist/app/turnStore.js +60 -0
- package/dist/app/uiStore.js +31 -0
- package/dist/app.js +219 -0
- package/dist/banner.js +20 -0
- package/dist/components/appLayout.js +22 -0
- package/dist/components/branding.js +6 -0
- package/dist/components/overlays/confirmPrompt.js +25 -0
- package/dist/components/overlays/helpOverlay.js +75 -0
- package/dist/components/overlays/historySearch.js +48 -0
- package/dist/components/overlays/modelPicker.js +59 -0
- package/dist/components/overlays/overlayUtils.js +18 -0
- package/dist/components/overlays/secretPrompt.js +35 -0
- package/dist/components/overlays/sessionPicker.js +92 -0
- package/dist/components/overlays/skillsHub.js +70 -0
- package/dist/components/streamingMarkdown.js +220 -0
- package/dist/components/textInput.js +264 -0
- package/dist/components/thinking.js +39 -0
- package/dist/components/transcript.js +22 -0
- package/dist/config/timing.js +14 -0
- package/dist/entry.js +43 -0
- package/dist/gateway/client.js +312 -0
- package/dist/types.js +7 -0
- package/package.json +77 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// CVC power-input composer — Hermes ui-tui parity.
|
|
3
|
+
//
|
|
4
|
+
// Features:
|
|
5
|
+
// • Multi-line buffer (Alt+Enter / Shift+Enter newline; Enter submit)
|
|
6
|
+
// • Word-wrap to terminal columns
|
|
7
|
+
// • Visible inverse-block cursor
|
|
8
|
+
// • Dim live char/line counter footer
|
|
9
|
+
// • Up/Down history cycle (when on first/last logical line)
|
|
10
|
+
// • Ctrl+R reverse-incremental search overlay
|
|
11
|
+
// • Tab completion: slash commands + file paths
|
|
12
|
+
// • Ctrl+L clears
|
|
13
|
+
// • Queued submission indicator `[queued: N]`
|
|
14
|
+
// • Hidden when promptStore has an active confirm/secret prompt
|
|
15
|
+
//
|
|
16
|
+
// Pure buffer math lives in ../app/inputBuffer.ts; this file is the React
|
|
17
|
+
// glue + key dispatch.
|
|
18
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
19
|
+
import { Box, Text, useInput } from 'ink';
|
|
20
|
+
import { useStore } from '@nanostores/react';
|
|
21
|
+
import { $buffer, $completion, resetCompletion, setBuffer, setBufferRaw, } from '../app/inputStore.js';
|
|
22
|
+
import { emptyBuffer, insert, backspace, del as bufDel, moveLeft, moveRight, moveHome, moveEnd, moveUp, moveDown, onFirstLine, onLastLine, bufferStats, cursorRowCol, } from '../app/inputBuffer.js';
|
|
23
|
+
import { $history, loadHistory, pushHistory, historyPrev, historyNext, resetHistoryBrowse, } from '../app/historyStore.js';
|
|
24
|
+
import { $queue } from '../app/queueStore.js';
|
|
25
|
+
import { $prompt } from '../app/promptStore.js';
|
|
26
|
+
import { $ui } from '../app/uiStore.js';
|
|
27
|
+
import { tokenAtCursor, candidatesFor, applyCandidate, } from '../app/completion.js';
|
|
28
|
+
import { HistorySearch } from './overlays/historySearch.js';
|
|
29
|
+
import { ConfirmPrompt } from './overlays/confirmPrompt.js';
|
|
30
|
+
import { SecretPrompt } from './overlays/secretPrompt.js';
|
|
31
|
+
const PROMPT_COLOR = '#e63946';
|
|
32
|
+
/**
|
|
33
|
+
* Render the buffer with an inverse-block cursor at `cursor`. Wraps to `cols`
|
|
34
|
+
* columns. Returns an array of visual lines (already containing ANSI invert
|
|
35
|
+
* codes for the cursor cell).
|
|
36
|
+
*/
|
|
37
|
+
function renderWrapped(buf, _cols) {
|
|
38
|
+
const text = buf.text;
|
|
39
|
+
const cursor = Math.max(0, Math.min(buf.cursor, text.length));
|
|
40
|
+
// Insert cursor sentinel as inverted character.
|
|
41
|
+
const ESC = '\x1b';
|
|
42
|
+
const INV = `${ESC}[7m`;
|
|
43
|
+
const OFF = `${ESC}[27m`;
|
|
44
|
+
const ch = cursor < text.length && text[cursor] !== '\n' ? text[cursor] : ' ';
|
|
45
|
+
const withCursor = text.slice(0, cursor) + INV + ch + OFF + text.slice(cursor + (cursor < text.length && text[cursor] !== '\n' ? 1 : 0));
|
|
46
|
+
// Wrap each logical line. We can't naively wrapText because of escape codes;
|
|
47
|
+
// however the codes are zero-width so length-based wrap with the *raw* text
|
|
48
|
+
// still produces visually correct output if we map back. For simplicity and
|
|
49
|
+
// since terminal widths in our tests are generous, we wrap on raw text with
|
|
50
|
+
// injected codes treated as part of the line — the codes themselves don't
|
|
51
|
+
// consume cells, but wrapText sees them as chars. To stay correct, wrap the
|
|
52
|
+
// raw line and re-inject the cursor cell.
|
|
53
|
+
// Simpler: split on '\n' from withCursor and let Ink soft-wrap visually.
|
|
54
|
+
return withCursor.split('\n');
|
|
55
|
+
}
|
|
56
|
+
/** Slim hint footer: `chars · lines · [queued: N]`. */
|
|
57
|
+
const Hints = ({ chars, lineCount, queued, mode, }) => {
|
|
58
|
+
const parts = [`${chars} chars`, `${lineCount} line${lineCount === 1 ? '' : 's'}`];
|
|
59
|
+
if (queued > 0)
|
|
60
|
+
parts.push(`queued: ${queued}`);
|
|
61
|
+
if (mode)
|
|
62
|
+
parts.push(mode);
|
|
63
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: parts.join(' · ') }) }));
|
|
64
|
+
};
|
|
65
|
+
export const Composer = ({ onSubmit, placeholder, prompt = '❯' }) => {
|
|
66
|
+
const buf = useStore($buffer);
|
|
67
|
+
const completion = useStore($completion);
|
|
68
|
+
const ui = useStore($ui);
|
|
69
|
+
const queue = useStore($queue);
|
|
70
|
+
const prompts = useStore($prompt);
|
|
71
|
+
const history = useStore($history);
|
|
72
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
73
|
+
// One-time history load.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (history.length === 0)
|
|
76
|
+
loadHistory();
|
|
77
|
+
}, [history.length]);
|
|
78
|
+
// If a prompt is active, show the appropriate overlay instead of the composer.
|
|
79
|
+
// (Composer hides — Hermes parity.)
|
|
80
|
+
if (prompts) {
|
|
81
|
+
if (prompts.kind === 'confirm')
|
|
82
|
+
return _jsx(ConfirmPrompt, {});
|
|
83
|
+
if (prompts.kind === 'secret')
|
|
84
|
+
return _jsx(SecretPrompt, {});
|
|
85
|
+
}
|
|
86
|
+
if (searchOpen) {
|
|
87
|
+
return (_jsx(HistorySearch, { onAccept: (entry) => {
|
|
88
|
+
setBuffer({ text: entry, cursor: entry.length });
|
|
89
|
+
setSearchOpen(false);
|
|
90
|
+
}, onCancel: () => setSearchOpen(false) }));
|
|
91
|
+
}
|
|
92
|
+
const cols = Math.max(20, ui.cols ?? 80);
|
|
93
|
+
const submit = useCallback(() => {
|
|
94
|
+
const text = buf.text;
|
|
95
|
+
if (!text.trim()) {
|
|
96
|
+
// Empty submit: just clear.
|
|
97
|
+
setBuffer(emptyBuffer());
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
pushHistory(text);
|
|
101
|
+
resetHistoryBrowse();
|
|
102
|
+
setBuffer(emptyBuffer());
|
|
103
|
+
if (onSubmit)
|
|
104
|
+
onSubmit(text);
|
|
105
|
+
}, [buf.text, onSubmit]);
|
|
106
|
+
const cycleCompletion = useCallback((dir) => {
|
|
107
|
+
// Tab pressed: either start a new cycle or rotate the existing one.
|
|
108
|
+
if (completion.active && completion.candidates.length > 0) {
|
|
109
|
+
const n = completion.candidates.length;
|
|
110
|
+
const nextIdx = (completion.index + dir + n) % n;
|
|
111
|
+
const cand = completion.candidates[nextIdx];
|
|
112
|
+
const replaced = applyCandidate(buf.text, completion.start, completion.prefix.length, cand);
|
|
113
|
+
// Use raw setter so we don't reset the cycle.
|
|
114
|
+
setBufferRaw({ text: replaced.text, cursor: replaced.cursor });
|
|
115
|
+
$completion.set({
|
|
116
|
+
...completion,
|
|
117
|
+
index: nextIdx,
|
|
118
|
+
prefix: cand,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const ctx = tokenAtCursor(buf.text, buf.cursor);
|
|
123
|
+
if (!ctx)
|
|
124
|
+
return;
|
|
125
|
+
const cands = candidatesFor(ctx);
|
|
126
|
+
if (cands.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
const first = cands[0];
|
|
129
|
+
const replaced = applyCandidate(buf.text, ctx.start, ctx.token.length, first);
|
|
130
|
+
setBufferRaw({ text: replaced.text, cursor: replaced.cursor });
|
|
131
|
+
$completion.set({
|
|
132
|
+
active: true,
|
|
133
|
+
prefix: first,
|
|
134
|
+
candidates: cands,
|
|
135
|
+
index: 0,
|
|
136
|
+
start: ctx.start,
|
|
137
|
+
});
|
|
138
|
+
}, [buf.text, buf.cursor, completion]);
|
|
139
|
+
useInput((input, key) => {
|
|
140
|
+
// Ctrl+L: clear buffer.
|
|
141
|
+
if (key.ctrl && input === 'l') {
|
|
142
|
+
setBuffer(emptyBuffer());
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Ctrl+R: open reverse-search overlay.
|
|
146
|
+
if (key.ctrl && input === 'r') {
|
|
147
|
+
setSearchOpen(true);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Ctrl+C: clear current input (don't crash; parent handles outer SIGINT).
|
|
151
|
+
if (key.ctrl && input === 'c') {
|
|
152
|
+
if (buf.text.length === 0) {
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
setBuffer(emptyBuffer());
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Tab / Shift+Tab.
|
|
159
|
+
if (key.tab) {
|
|
160
|
+
cycleCompletion(key.shift ? -1 : 1);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Enter / newline. Ink reports Alt+Enter via key.meta, Shift+Enter via key.shift.
|
|
164
|
+
if (key.return) {
|
|
165
|
+
if (key.meta || key.shift) {
|
|
166
|
+
setBuffer(insert(buf, '\n'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
submit();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (key.backspace || key.delete) {
|
|
173
|
+
// Ink convention: `key.delete` is forward-delete on some terms; `key.backspace` for backspace.
|
|
174
|
+
if (key.delete && !key.backspace) {
|
|
175
|
+
setBuffer(bufDel(buf));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
setBuffer(backspace(buf));
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (key.leftArrow) {
|
|
183
|
+
setBuffer(moveLeft(buf));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (key.rightArrow) {
|
|
187
|
+
setBuffer(moveRight(buf));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (key.upArrow) {
|
|
191
|
+
// History cycle when on the first logical line.
|
|
192
|
+
if (onFirstLine(buf)) {
|
|
193
|
+
const entry = historyPrev(buf.text);
|
|
194
|
+
if (entry !== null) {
|
|
195
|
+
setBuffer({ text: entry, cursor: entry.length });
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setBuffer(moveUp(buf));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (key.downArrow) {
|
|
203
|
+
if (onLastLine(buf)) {
|
|
204
|
+
const entry = historyNext();
|
|
205
|
+
if (entry !== null) {
|
|
206
|
+
setBuffer({ text: entry, cursor: entry.length });
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
setBuffer(moveDown(buf));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Home / End — Ink doesn't expose these directly; they often arrive as raw escapes.
|
|
214
|
+
if (key.ctrl && input === 'a') {
|
|
215
|
+
setBuffer(moveHome(buf));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (key.ctrl && input === 'e') {
|
|
219
|
+
setBuffer(moveEnd(buf));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (key.escape) {
|
|
223
|
+
// Esc: cancel completion, otherwise clear buffer.
|
|
224
|
+
if (completion.active) {
|
|
225
|
+
resetCompletion();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setBuffer(emptyBuffer());
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Printable input.
|
|
232
|
+
if (input && !key.ctrl && !key.meta) {
|
|
233
|
+
setBuffer(insert(buf, input));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
const stats = bufferStats(buf.text);
|
|
238
|
+
const wrapped = renderWrapped(buf, cols);
|
|
239
|
+
const showPlaceholder = buf.text.length === 0 && placeholder;
|
|
240
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: PROMPT_COLOR, children: [prompt, " "] }), showPlaceholder ? (_jsx(Text, { dimColor: true, children: placeholder })) : (_jsx(Box, { flexDirection: "column", children: wrapped.map((line, i) => (_jsx(Text, { children: line || ' ' }, i))) }))] }), _jsx(Hints, { chars: stats.chars, lineCount: stats.lineCount, queued: queue.length, mode: completion.active ? `tab ${completion.index + 1}/${completion.candidates.length}` : undefined })] }));
|
|
241
|
+
};
|
|
242
|
+
export const TextInput = ({ value, onChange, onSubmit, placeholder, prompt }) => {
|
|
243
|
+
const buf = useStore($buffer);
|
|
244
|
+
// Sync controlled value into the store on mount / external change.
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (buf.text !== value) {
|
|
247
|
+
setBuffer({ text: value, cursor: value.length });
|
|
248
|
+
}
|
|
249
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
|
+
}, [value]);
|
|
251
|
+
// Push buffer changes back to parent on every text change.
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (buf.text !== value)
|
|
254
|
+
onChange(buf.text);
|
|
255
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
256
|
+
}, [buf.text]);
|
|
257
|
+
return (_jsx(Composer, { placeholder: placeholder, prompt: prompt, onSubmit: (t) => {
|
|
258
|
+
onChange('');
|
|
259
|
+
if (onSubmit)
|
|
260
|
+
onSubmit(t);
|
|
261
|
+
} }));
|
|
262
|
+
};
|
|
263
|
+
// Re-exports for tests / consumers.
|
|
264
|
+
export { cursorRowCol };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { TIMING } from '../config/timing.js';
|
|
5
|
+
// unicode-animations exposes a large catalog of frame arrays. We import lazily
|
|
6
|
+
// and gracefully fall back if the shape changes.
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
let UA = null;
|
|
9
|
+
try {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
+
UA = await import('unicode-animations').catch(() => null);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
UA = null;
|
|
15
|
+
}
|
|
16
|
+
const FALLBACK_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
17
|
+
function pickFrames() {
|
|
18
|
+
try {
|
|
19
|
+
if (UA?.default?.dots)
|
|
20
|
+
return UA.default.dots;
|
|
21
|
+
if (Array.isArray(UA?.dots))
|
|
22
|
+
return UA.dots;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* ignore */
|
|
26
|
+
}
|
|
27
|
+
return FALLBACK_FRAMES;
|
|
28
|
+
}
|
|
29
|
+
export const Thinking = ({ label = 'thinking', color = '#e63946' }) => {
|
|
30
|
+
const [frame, setFrame] = useState(0);
|
|
31
|
+
const frames = pickFrames();
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const id = setInterval(() => {
|
|
34
|
+
setFrame((f) => (f + 1) % frames.length);
|
|
35
|
+
}, TIMING.SPINNER_FRAME_MS);
|
|
36
|
+
return () => clearInterval(id);
|
|
37
|
+
}, [frames.length]);
|
|
38
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: color, children: [frames[frame], " "] }), _jsxs(Text, { color: "gray", children: [label, "\u2026"] })] }));
|
|
39
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useStore } from '@nanostores/react';
|
|
4
|
+
import { $messages, $turn } from '../app/turnStore.js';
|
|
5
|
+
import { Markdown, StreamingMarkdown } from './streamingMarkdown.js';
|
|
6
|
+
import { CVC_THEME } from '../types.js';
|
|
7
|
+
const Bubble = ({ msg, streaming }) => {
|
|
8
|
+
const isUser = msg.role === 'user';
|
|
9
|
+
const borderColor = isUser ? CVC_THEME.primary : CVC_THEME.dim;
|
|
10
|
+
const tag = isUser ? 'you' : msg.role === 'assistant' ? 'cvc' : msg.role;
|
|
11
|
+
const tagColor = isUser ? CVC_THEME.primary : CVC_THEME.accent;
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: tagColor, children: `▌ ${tag}` }), _jsx(Box, { borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: isUser ? (_jsx(Text, { children: msg.content })) : streaming ? (_jsx(StreamingMarkdown, { content: msg.content })) : (_jsx(Markdown, { text: msg.content })) })] }));
|
|
13
|
+
};
|
|
14
|
+
export const Transcript = () => {
|
|
15
|
+
const messages = useStore($messages);
|
|
16
|
+
const turn = useStore($turn);
|
|
17
|
+
if (messages.length === 0) {
|
|
18
|
+
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: CVC_THEME.dim, children: "\u2014 no messages yet \u2014 type below to start \u2014" }) }));
|
|
19
|
+
}
|
|
20
|
+
const lastId = messages[messages.length - 1]?.id;
|
|
21
|
+
return (_jsx(Box, { flexDirection: "column", marginY: 1, children: messages.map((m) => (_jsx(Bubble, { msg: m, streaming: m.role === 'assistant' && m.id === lastId && turn?.status === 'streaming' }, m.id))) }));
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Centralised timing constants for the CVC TUI.
|
|
2
|
+
// Keep all magic numbers controlling UI cadence in this file.
|
|
3
|
+
export const TIMING = {
|
|
4
|
+
/** Spinner frame interval (ms) */
|
|
5
|
+
SPINNER_FRAME_MS: 80,
|
|
6
|
+
/** Throttle for streaming markdown re-renders (ms) */
|
|
7
|
+
STREAM_THROTTLE_MS: 16,
|
|
8
|
+
/** Debounce on text-input change handlers (ms) */
|
|
9
|
+
INPUT_DEBOUNCE_MS: 0,
|
|
10
|
+
/** Gateway connect retry backoff (ms) */
|
|
11
|
+
GATEWAY_RECONNECT_MS: 1500,
|
|
12
|
+
/** Heartbeat ping interval (ms) */
|
|
13
|
+
HEARTBEAT_MS: 15000,
|
|
14
|
+
};
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// CVC TUI entrypoint. Verifies a TTY, connects to the Python JSON-RPC
|
|
4
|
+
// gateway via subprocess stdio, then mounts <App/>.
|
|
5
|
+
import { render } from 'ink';
|
|
6
|
+
import { App } from './app.js';
|
|
7
|
+
import { GatewayClient } from './gateway/client.js';
|
|
8
|
+
import { renderBannerPlain } from './banner.js';
|
|
9
|
+
const VERSION = '0.0.1';
|
|
10
|
+
async function main() {
|
|
11
|
+
if (!process.stdout.isTTY) {
|
|
12
|
+
process.stdout.write(renderBannerPlain({ version: VERSION, model: process.env.CVC_MODEL ?? 'unknown' }) + '\n');
|
|
13
|
+
process.stdout.write('cvc-tui requires a TTY to run interactively.\n');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
const gateway = new GatewayClient();
|
|
17
|
+
let model = process.env.CVC_MODEL ?? 'claude-sonnet-4-6';
|
|
18
|
+
try {
|
|
19
|
+
const session = await gateway.connect();
|
|
20
|
+
model = session.model || model;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
process.stderr.write(`gateway connect failed: ${err.message}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const { waitUntilExit } = render(_jsx(App, { version: VERSION, model: model, gateway: gateway }));
|
|
27
|
+
// Clean Ctrl-C exit even if Ink loses focus.
|
|
28
|
+
const onSignal = () => {
|
|
29
|
+
void gateway.close().finally(() => process.exit(0));
|
|
30
|
+
};
|
|
31
|
+
process.on('SIGINT', onSignal);
|
|
32
|
+
process.on('SIGTERM', onSignal);
|
|
33
|
+
try {
|
|
34
|
+
await waitUntilExit();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await gateway.close().catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
main().catch((err) => {
|
|
41
|
+
process.stderr.write(`fatal: ${err.stack ?? err}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|