cvc-tui 0.4.4 → 0.4.7
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/NOTICES.md +13 -0
- package/dist/app/completion.js +102 -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 +123 -0
- package/dist/app/inputBuffer.js +120 -0
- package/dist/app/inputSelectionStore.js +8 -0
- package/dist/app/inputStore.js +28 -0
- package/dist/app/interfaces.js +6 -0
- package/dist/app/overlayStore.js +40 -0
- package/dist/app/promptStore.js +44 -0
- package/dist/app/queueStore.js +25 -0
- package/dist/app/scroll.js +44 -0
- package/dist/app/setupHandoff.js +28 -0
- package/dist/app/slash/commands/core.js +479 -0
- package/dist/app/slash/commands/debug.js +44 -0
- package/dist/app/slash/commands/ops.js +512 -0
- package/dist/app/slash/commands/session.js +431 -0
- package/dist/app/slash/commands/setup.js +20 -0
- package/dist/app/slash/commands/toggles.js +40 -0
- package/dist/app/slash/registry.js +18 -0
- package/dist/app/slash/types.js +1 -0
- package/dist/app/spawnHistoryStore.js +105 -0
- package/dist/app/turnController.js +650 -0
- package/dist/app/turnStore.js +48 -0
- package/dist/app/uiStore.js +36 -0
- 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 +638 -0
- package/dist/app/useSessionLifecycle.js +175 -0
- package/dist/app/useSubmission.js +287 -0
- package/dist/app.js +15 -0
- package/dist/banner.js +63 -0
- package/dist/components/agentsOverlay.js +474 -0
- package/dist/components/appChrome.js +252 -0
- package/dist/components/appLayout.js +122 -0
- package/dist/components/appOverlays.js +65 -0
- package/dist/components/branding.js +97 -0
- 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/confirmPrompt.js +25 -0
- package/dist/components/overlays/helpOverlay.js +76 -0
- package/dist/components/overlays/historySearch.js +49 -0
- package/dist/components/overlays/modelPicker.js +60 -0
- package/dist/components/overlays/overlayUtils.js +19 -0
- package/dist/components/overlays/secretPrompt.js +36 -0
- package/dist/components/overlays/sessionPicker.js +93 -0
- package/dist/components/overlays/skillsHub.js +71 -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 +144 -0
- package/dist/components/textInput.js +794 -0
- package/dist/components/themed.js +12 -0
- package/dist/components/thinking.js +496 -0
- package/dist/components/todoPanel.js +40 -0
- package/dist/components/transcript.js +22 -0
- package/dist/config/env.js +18 -0
- package/dist/config/limits.js +22 -0
- package/dist/config/timing.js +25 -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 +64 -70236
- package/dist/gateway/client.js +312 -0
- 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/localSessionInfo.js +116 -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/react-devtools-stub.js +12 -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/protocol/interpolation.js +4 -0
- package/dist/protocol/paste.js +3 -0
- package/dist/theme.js +398 -0
- package/dist/types.js +1 -0
- package/dist/vendor/cvc-ink/dist/entry-exports.js +52737 -0
- package/dist/vendor/cvc-ink/index.js +1 -0
- package/dist/vendor/cvc-ink/package.json +9 -0
- package/dist/vendor/cvc-ink/text-input.js +1 -0
- package/package.json +9 -9
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
5
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
6
|
+
import { Box, Text, useInput, useStdout } from '../vendor/cvc-ink/index.js';
|
|
7
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import { providerDisplayNames } from '../domain/providers.js';
|
|
9
|
+
import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js';
|
|
10
|
+
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js';
|
|
11
|
+
import { OverlayHint, useOverlayKeys, windowItems } from './overlayControls.js';
|
|
12
|
+
const VISIBLE = 12;
|
|
13
|
+
const MIN_WIDTH = 40;
|
|
14
|
+
const MAX_WIDTH = 90;
|
|
15
|
+
export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }) {
|
|
16
|
+
const [providers, setProviders] = useState([]);
|
|
17
|
+
const [currentModel, setCurrentModel] = useState('');
|
|
18
|
+
const [err, setErr] = useState('');
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [persistGlobal, setPersistGlobal] = useState(false);
|
|
21
|
+
const [providerIdx, setProviderIdx] = useState(0);
|
|
22
|
+
const [modelIdx, setModelIdx] = useState(0);
|
|
23
|
+
const [stage, setStage] = useState('provider');
|
|
24
|
+
const [keyInput, setKeyInput] = useState('');
|
|
25
|
+
const [keySaving, setKeySaving] = useState(false);
|
|
26
|
+
const [keyError, setKeyError] = useState('');
|
|
27
|
+
const { stdout } = useStdout();
|
|
28
|
+
// Pin the picker to a stable width so the FloatBox parent (which shrinks-
|
|
29
|
+
// to-fit with alignSelf="flex-start") doesn't resize as long provider /
|
|
30
|
+
// model names scroll into view, and so `wrap="truncate-end"` on each row
|
|
31
|
+
// has an actual constraint to truncate against.
|
|
32
|
+
const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6));
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
gw.request('model.options', sessionId ? { session_id: sessionId } : {})
|
|
35
|
+
.then(raw => {
|
|
36
|
+
const r = asRpcResult(raw);
|
|
37
|
+
if (!r) {
|
|
38
|
+
setErr('invalid response: model.options');
|
|
39
|
+
setLoading(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const next = r.providers ?? [];
|
|
43
|
+
setProviders(next);
|
|
44
|
+
setCurrentModel(String(r.model ?? ''));
|
|
45
|
+
setProviderIdx(Math.max(0, next.findIndex(p => p.is_current)));
|
|
46
|
+
setModelIdx(0);
|
|
47
|
+
setStage('provider');
|
|
48
|
+
setErr('');
|
|
49
|
+
setLoading(false);
|
|
50
|
+
})
|
|
51
|
+
.catch((e) => {
|
|
52
|
+
setErr(rpcErrorMessage(e));
|
|
53
|
+
setLoading(false);
|
|
54
|
+
});
|
|
55
|
+
}, [gw, sessionId]);
|
|
56
|
+
const provider = providers[providerIdx];
|
|
57
|
+
const models = provider?.models ?? [];
|
|
58
|
+
const names = useMemo(() => providerDisplayNames(providers), [providers]);
|
|
59
|
+
const back = () => {
|
|
60
|
+
if (stage === 'model' || stage === 'key' || stage === 'disconnect') {
|
|
61
|
+
setStage('provider');
|
|
62
|
+
setModelIdx(0);
|
|
63
|
+
setKeyInput('');
|
|
64
|
+
setKeyError('');
|
|
65
|
+
setKeySaving(false);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
onCancel();
|
|
69
|
+
};
|
|
70
|
+
useOverlayKeys({ onBack: back, onClose: onCancel });
|
|
71
|
+
useInput((ch, key) => {
|
|
72
|
+
// Key entry stage handles its own input
|
|
73
|
+
if (stage === 'key') {
|
|
74
|
+
if (keySaving) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (key.return) {
|
|
78
|
+
if (!keyInput.trim()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setKeySaving(true);
|
|
82
|
+
setKeyError('');
|
|
83
|
+
gw.request('model.save_key', {
|
|
84
|
+
slug: provider?.slug,
|
|
85
|
+
api_key: keyInput.trim(),
|
|
86
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
87
|
+
})
|
|
88
|
+
.then(raw => {
|
|
89
|
+
const r = asRpcResult(raw);
|
|
90
|
+
if (!r?.provider) {
|
|
91
|
+
setKeyError('failed to save key');
|
|
92
|
+
setKeySaving(false);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Update the provider in our list with fresh data
|
|
96
|
+
setProviders(prev => prev.map(p => p.slug === r.provider.slug ? r.provider : p));
|
|
97
|
+
setKeyInput('');
|
|
98
|
+
setKeySaving(false);
|
|
99
|
+
setStage('model');
|
|
100
|
+
setModelIdx(0);
|
|
101
|
+
})
|
|
102
|
+
.catch((e) => {
|
|
103
|
+
setKeyError(rpcErrorMessage(e));
|
|
104
|
+
setKeySaving(false);
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (key.backspace || key.delete) {
|
|
109
|
+
setKeyInput(v => v.slice(0, -1));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// ctrl+u clears input
|
|
113
|
+
if (ch === '\u0015') {
|
|
114
|
+
setKeyInput('');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (ch && !key.ctrl && !key.meta) {
|
|
118
|
+
setKeyInput(v => v + ch);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Disconnect confirmation stage
|
|
123
|
+
if (stage === 'disconnect') {
|
|
124
|
+
if (ch.toLowerCase() === 'y' || key.return) {
|
|
125
|
+
if (!provider) {
|
|
126
|
+
setStage('provider');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
setKeySaving(true);
|
|
130
|
+
gw.request('model.disconnect', {
|
|
131
|
+
slug: provider.slug,
|
|
132
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
133
|
+
})
|
|
134
|
+
.then(raw => {
|
|
135
|
+
const r = asRpcResult(raw);
|
|
136
|
+
if (r?.disconnected) {
|
|
137
|
+
// Mark provider as unauthenticated in local state
|
|
138
|
+
setProviders(prev => prev.map(p => p.slug === provider.slug
|
|
139
|
+
? { ...p, authenticated: false, models: [], total_models: 0, warning: p.key_env ? `paste ${p.key_env} to activate` : 'run `hermes model` to configure' }
|
|
140
|
+
: p));
|
|
141
|
+
}
|
|
142
|
+
setKeySaving(false);
|
|
143
|
+
setStage('provider');
|
|
144
|
+
})
|
|
145
|
+
.catch(() => {
|
|
146
|
+
setKeySaving(false);
|
|
147
|
+
setStage('provider');
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (ch.toLowerCase() === 'n' || key.escape) {
|
|
152
|
+
setStage('provider');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const count = stage === 'provider' ? providers.length : models.length;
|
|
158
|
+
const sel = stage === 'provider' ? providerIdx : modelIdx;
|
|
159
|
+
const setSel = stage === 'provider' ? setProviderIdx : setModelIdx;
|
|
160
|
+
if (key.upArrow && sel > 0) {
|
|
161
|
+
setSel(v => v - 1);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.downArrow && sel < count - 1) {
|
|
165
|
+
setSel(v => v + 1);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (key.return) {
|
|
169
|
+
if (stage === 'provider') {
|
|
170
|
+
if (!provider) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (provider.authenticated === false) {
|
|
174
|
+
// api_key providers: prompt for key inline
|
|
175
|
+
if (provider.auth_type === 'api_key' && provider.key_env) {
|
|
176
|
+
setStage('key');
|
|
177
|
+
setKeyInput('');
|
|
178
|
+
setKeyError('');
|
|
179
|
+
}
|
|
180
|
+
// Other auth types: no-op (warning shown tells them to run hermes model)
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
setStage('model');
|
|
184
|
+
setModelIdx(0);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const model = models[modelIdx];
|
|
188
|
+
if (provider && model) {
|
|
189
|
+
onSelect(`${model} --provider ${provider.slug}${persistGlobal ? ' --global' : ` ${TUI_SESSION_MODEL_FLAG}`}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
setStage('provider');
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (ch.toLowerCase() === 'g') {
|
|
197
|
+
setPersistGlobal(v => !v);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Disconnect: only in provider stage, only for authenticated providers
|
|
201
|
+
if (ch.toLowerCase() === 'd' && stage === 'provider' && provider?.authenticated !== false) {
|
|
202
|
+
setStage('disconnect');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
if (loading) {
|
|
207
|
+
return _jsx(Text, { color: t.color.muted, children: "loading models\u2026" });
|
|
208
|
+
}
|
|
209
|
+
if (err) {
|
|
210
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: t.color.label, children: ["error: ", err] }), _jsx(OverlayHint, { t: t, children: "Esc/q cancel" })] }));
|
|
211
|
+
}
|
|
212
|
+
if (!providers.length) {
|
|
213
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: t.color.muted, children: "no providers available" }), _jsx(OverlayHint, { t: t, children: "Esc/q cancel" })] }));
|
|
214
|
+
}
|
|
215
|
+
// ── Key entry stage ──────────────────────────────────────────────────
|
|
216
|
+
if (stage === 'key' && provider) {
|
|
217
|
+
const masked = keyInput ? '•'.repeat(Math.min(keyInput.length, 40)) : '';
|
|
218
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { bold: true, color: t.color.accent, wrap: "truncate-end", children: ["Configure ", provider.name] }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: "Paste your API key below (saved to ~/.hermes/.env)" }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: " " }), _jsxs(Text, { color: t.color.muted, wrap: "truncate-end", children: [provider.key_env, ":"] }), _jsxs(Text, { color: t.color.accent, wrap: "truncate-end", children: [' ', masked || '(empty)', keySaving ? '' : '▎'] }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: " " }), keyError ? (_jsxs(Text, { color: t.color.label, wrap: "truncate-end", children: ["error: ", keyError] })) : keySaving ? (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: "saving\u2026" })) : (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: " " })), _jsx(OverlayHint, { t: t, children: "Enter save \u00B7 Ctrl+U clear \u00B7 Esc back" })] }));
|
|
219
|
+
}
|
|
220
|
+
// ── Disconnect confirmation stage ─────────────────────────────────────
|
|
221
|
+
if (stage === 'disconnect' && provider) {
|
|
222
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { bold: true, color: t.color.accent, wrap: "truncate-end", children: ["Disconnect ", provider.name, "?"] }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: " " }), _jsxs(Text, { color: t.color.muted, wrap: "truncate-end", children: ["This removes saved credentials for ", provider.name, "."] }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: "You can re-authenticate later by selecting it again." }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: " " }), keySaving ? (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: "disconnecting\u2026" })) : (_jsx(OverlayHint, { t: t, children: "y/Enter confirm \u00B7 n/Esc cancel" }))] }));
|
|
223
|
+
}
|
|
224
|
+
// ── Provider selection stage ─────────────────────────────────────────
|
|
225
|
+
if (stage === 'provider') {
|
|
226
|
+
const rows = providers.map((p, i) => {
|
|
227
|
+
const authMark = p.authenticated === false ? '○' : p.is_current ? '*' : '●';
|
|
228
|
+
const modelCount = p.total_models ?? p.models?.length ?? 0;
|
|
229
|
+
const suffix = p.authenticated === false
|
|
230
|
+
? (p.auth_type === 'api_key' ? '(no key)' : '(needs setup)')
|
|
231
|
+
: `${modelCount} models`;
|
|
232
|
+
return `${authMark} ${names[i]} · ${suffix}`;
|
|
233
|
+
});
|
|
234
|
+
const { items, offset } = windowItems(rows, providerIdx, VISIBLE);
|
|
235
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(Text, { bold: true, color: t.color.accent, wrap: "truncate-end", children: "Select provider (step 1/2)" }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: "Full model IDs on the next step \u00B7 Enter to continue" }), _jsxs(Text, { color: t.color.muted, wrap: "truncate-end", children: ["Current: ", currentModel || '(unknown)'] }), _jsx(Text, { color: t.color.label, wrap: "truncate-end", children: provider?.warning ? `warning: ${provider.warning}` : ' ' }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: offset > 0 ? ` ↑ ${offset} more` : ' ' }), Array.from({ length: VISIBLE }, (_, i) => {
|
|
236
|
+
const row = items[i];
|
|
237
|
+
const idx = offset + i;
|
|
238
|
+
const p = providers[idx];
|
|
239
|
+
const dimmed = p?.authenticated === false;
|
|
240
|
+
return row ? (_jsxs(Text, { bold: providerIdx === idx, color: providerIdx === idx ? t.color.accent : dimmed ? t.color.label : t.color.muted, inverse: providerIdx === idx, wrap: "truncate-end", children: [providerIdx === idx ? '▸ ' : ' ', idx + 1, ". ", row] }, providers[idx]?.slug ?? `row-${idx}`)) : (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: ' ' }, `pad-${i}`));
|
|
241
|
+
}), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: offset + VISIBLE < rows.length ? ` ↓ ${rows.length - offset - VISIBLE} more` : ' ' }), _jsxs(Text, { color: t.color.muted, wrap: "truncate-end", children: ["persist: ", persistGlobal ? 'global' : 'session', " \u00B7 g toggle"] }), _jsx(OverlayHint, { t: t, children: "\u2191/\u2193 select \u00B7 Enter choose \u00B7 d disconnect \u00B7 Esc/q cancel" })] }));
|
|
242
|
+
}
|
|
243
|
+
// ── Model selection stage ────────────────────────────────────────────
|
|
244
|
+
const { items, offset } = windowItems(models, modelIdx, VISIBLE);
|
|
245
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(Text, { bold: true, color: t.color.accent, wrap: "truncate-end", children: "Select model (step 2/2)" }), _jsxs(Text, { color: t.color.muted, wrap: "truncate-end", children: [names[providerIdx] || '(unknown provider)', " \u00B7 Esc back"] }), _jsx(Text, { color: t.color.label, wrap: "truncate-end", children: provider?.warning ? `warning: ${provider.warning}` : ' ' }), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: offset > 0 ? ` ↑ ${offset} more` : ' ' }), Array.from({ length: VISIBLE }, (_, i) => {
|
|
246
|
+
const row = items[i];
|
|
247
|
+
const idx = offset + i;
|
|
248
|
+
if (!row) {
|
|
249
|
+
return !models.length && i === 0 ? (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: "no models listed for this provider" }, "empty")) : (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: ' ' }, `pad-${i}`));
|
|
250
|
+
}
|
|
251
|
+
const prefix = modelIdx === idx ? '▸ ' : row === currentModel ? '* ' : ' ';
|
|
252
|
+
return (_jsxs(Text, { bold: modelIdx === idx, color: modelIdx === idx ? t.color.accent : t.color.muted, inverse: modelIdx === idx, wrap: "truncate-end", children: [prefix, idx + 1, ". ", row] }, `${provider?.slug ?? 'prov'}:${idx}:${row}`));
|
|
253
|
+
}), _jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: offset + VISIBLE < models.length ? ` ↓ ${models.length - offset - VISIBLE} more` : ' ' }), _jsxs(Text, { color: t.color.muted, wrap: "truncate-end", children: ["persist: ", persistGlobal ? 'global' : 'session', " \u00B7 g toggle"] }), _jsx(OverlayHint, { t: t, children: models.length ? '↑/↓ select · Enter switch · Esc back · q close' : 'Enter/Esc back · q close' })] }));
|
|
254
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
5
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
6
|
+
import { Text, useInput } from '../vendor/cvc-ink/index.js';
|
|
7
|
+
export function useOverlayKeys({ disabled = false, onBack, onClose }) {
|
|
8
|
+
useInput((ch, key) => {
|
|
9
|
+
if (disabled) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (ch === 'q') {
|
|
13
|
+
return onClose();
|
|
14
|
+
}
|
|
15
|
+
if (key.escape) {
|
|
16
|
+
return onBack ? onBack() : onClose();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function OverlayHint({ children, t }) {
|
|
21
|
+
return (_jsx(Text, { color: t.color.muted, wrap: "truncate-end", children: children }));
|
|
22
|
+
}
|
|
23
|
+
export const windowOffset = (count, selected, visible) => Math.max(0, Math.min(selected - Math.floor(visible / 2), count - visible));
|
|
24
|
+
export function windowItems(items, selected, visible) {
|
|
25
|
+
const offset = windowOffset(items.length, selected, visible);
|
|
26
|
+
return {
|
|
27
|
+
items: items.slice(offset, offset + visible),
|
|
28
|
+
offset
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { useStore } from '@nanostores/react';
|
|
4
|
+
import { $prompt, resolveActivePrompt } from '../../app/promptStore.js';
|
|
5
|
+
export const ConfirmPrompt = () => {
|
|
6
|
+
const cur = useStore($prompt);
|
|
7
|
+
if (!cur || cur.kind !== 'confirm')
|
|
8
|
+
return null;
|
|
9
|
+
const destructive = !!cur.destructive;
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (key.escape) {
|
|
12
|
+
resolveActivePrompt(null);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (input === 'y' || input === 'Y') {
|
|
16
|
+
resolveActivePrompt(true);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (input === 'n' || input === 'N' || key.return) {
|
|
20
|
+
resolveActivePrompt(false);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: destructive ? '#e63946' : '#4a9eff', bold: true, children: destructive ? '⚠ ' : '? ' }), _jsx(Text, { children: cur.prompt })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "[y/N] \u00B7 Enter = no \u00B7 Esc = cancel" }) })] }));
|
|
25
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// CVC helpOverlay — full-screen /help overlay listing every slash command
|
|
4
|
+
// from the central registry, grouped by category. Arrow-key scrollable.
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
|
+
import { Box, Text, useInput } from 'ink';
|
|
7
|
+
import { CVC_THEME } from '../../types.js';
|
|
8
|
+
import { SLASH_COMMANDS } from '../../app/slash/registry.js';
|
|
9
|
+
import { windowItems } from './overlayUtils.js';
|
|
10
|
+
const VISIBLE = 18;
|
|
11
|
+
const ORDER = [
|
|
12
|
+
'core',
|
|
13
|
+
'session',
|
|
14
|
+
'ops',
|
|
15
|
+
'setup',
|
|
16
|
+
'toggles',
|
|
17
|
+
'debug',
|
|
18
|
+
'other',
|
|
19
|
+
];
|
|
20
|
+
const TITLES = {
|
|
21
|
+
core: 'Core',
|
|
22
|
+
session: 'Sessions',
|
|
23
|
+
ops: 'Models · Skills · Tools',
|
|
24
|
+
setup: 'Setup',
|
|
25
|
+
toggles: 'Toggles',
|
|
26
|
+
debug: 'Debug',
|
|
27
|
+
other: 'Other',
|
|
28
|
+
};
|
|
29
|
+
function groupCommands() {
|
|
30
|
+
const buckets = new Map();
|
|
31
|
+
for (const c of SLASH_COMMANDS) {
|
|
32
|
+
if (c.hidden)
|
|
33
|
+
continue;
|
|
34
|
+
const k = (c.category ?? 'other');
|
|
35
|
+
if (!buckets.has(k))
|
|
36
|
+
buckets.set(k, []);
|
|
37
|
+
buckets.get(k).push(c);
|
|
38
|
+
}
|
|
39
|
+
const rows = [];
|
|
40
|
+
for (const k of ORDER) {
|
|
41
|
+
const list = buckets.get(k);
|
|
42
|
+
if (!list || list.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
rows.push({ kind: 'header', text: TITLES[k] ?? String(k) });
|
|
45
|
+
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
46
|
+
for (const c of list) {
|
|
47
|
+
const aliases = c.aliases?.length ? ` (${c.aliases.map(a => `/${a}`).join(', ')})` : '';
|
|
48
|
+
rows.push({ kind: 'cmd', text: `/${c.name}${aliases} — ${c.help}`, cmd: c });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
export const HelpOverlay = ({ onClose }) => {
|
|
54
|
+
const rows = useMemo(groupCommands, []);
|
|
55
|
+
const [top, setTop] = useState(0);
|
|
56
|
+
useInput((input, key) => {
|
|
57
|
+
if (key.escape || input === 'q' || (key.ctrl && input === 'c'))
|
|
58
|
+
return onClose();
|
|
59
|
+
if (key.upArrow && top > 0)
|
|
60
|
+
return setTop(top - 1);
|
|
61
|
+
if (key.downArrow && top < Math.max(0, rows.length - VISIBLE))
|
|
62
|
+
return setTop(top + 1);
|
|
63
|
+
if (key.pageUp)
|
|
64
|
+
return setTop(Math.max(0, top - VISIBLE));
|
|
65
|
+
if (key.pageDown)
|
|
66
|
+
return setTop(Math.min(Math.max(0, rows.length - VISIBLE), top + VISIBLE));
|
|
67
|
+
});
|
|
68
|
+
const { items, offset } = windowItems(rows, top + Math.floor(VISIBLE / 2), VISIBLE);
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CVC_THEME.primary, bold: true, children: ["CVC \u2014 slash commands (", rows.filter(r => r.kind === 'cmd').length, ")"] }), offset > 0 ? _jsx(Text, { dimColor: true, children: ` ↑ ${offset} more` }) : _jsx(Text, { children: " " }), items.map((row, i) => {
|
|
70
|
+
const k = `${offset + i}-${row.kind}`;
|
|
71
|
+
if (row.kind === 'header') {
|
|
72
|
+
return (_jsx(Text, { color: CVC_THEME.primary, bold: true, children: row.text }, k));
|
|
73
|
+
}
|
|
74
|
+
return (_jsxs(Text, { dimColor: false, children: [' ', row.text] }, k));
|
|
75
|
+
}), offset + VISIBLE < rows.length ? (_jsx(Text, { dimColor: true, children: ` ↓ ${rows.length - offset - VISIBLE} more` })) : (_jsx(Text, { children: " " })), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 scroll \u00B7 PgUp/PgDn page \u00B7 Esc/q close" })] }));
|
|
76
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// Reverse-incremental history search overlay (Ctrl+R).
|
|
4
|
+
// Keystrokes refine the query; Up/Down moves through matches; Enter accepts;
|
|
5
|
+
// Esc cancels.
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { Box, Text, useInput } from 'ink';
|
|
8
|
+
import { searchHistory } from '../../app/historyStore.js';
|
|
9
|
+
export const HistorySearch = ({ onAccept, onCancel }) => {
|
|
10
|
+
const [query, setQuery] = useState('');
|
|
11
|
+
const [index, setIndex] = useState(0);
|
|
12
|
+
const matches = searchHistory(query, 50);
|
|
13
|
+
const current = matches[index] ?? '';
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.escape || (key.ctrl && input === 'g')) {
|
|
16
|
+
onCancel();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (key.return) {
|
|
20
|
+
if (current)
|
|
21
|
+
onAccept(current);
|
|
22
|
+
else
|
|
23
|
+
onCancel();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.upArrow || (key.ctrl && input === 'r')) {
|
|
27
|
+
if (matches.length > 0)
|
|
28
|
+
setIndex(Math.min(matches.length - 1, index + 1));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (key.downArrow || (key.ctrl && input === 's')) {
|
|
32
|
+
setIndex(Math.max(0, index - 1));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (key.backspace || key.delete) {
|
|
36
|
+
setQuery(query.slice(0, -1));
|
|
37
|
+
setIndex(0);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
41
|
+
setQuery(query + input);
|
|
42
|
+
setIndex(0);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#e63946", children: "(reverse-i-search)" }), _jsx(Text, { children: `'${query}': ` }), _jsx(Text, { children: current })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: matches.length > 0
|
|
47
|
+
? `${index + 1}/${matches.length} match${matches.length === 1 ? '' : 'es'} · ↑/↓ navigate · Enter accept · Esc cancel`
|
|
48
|
+
: 'no matches · Esc cancel' }) })] }));
|
|
49
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// CVC modelPicker overlay — arrow-key navigable, two-stage (provider → model).
|
|
4
|
+
// Behavior parity with CVC ui-tui modelPicker, but consumes preloaded data
|
|
5
|
+
// via props so it stays trivial to test and wire.
|
|
6
|
+
import { useMemo, useState } from 'react';
|
|
7
|
+
import { Box, Text, useInput } from 'ink';
|
|
8
|
+
import { CVC_THEME } from '../../types.js';
|
|
9
|
+
import { windowItems } from './overlayUtils.js';
|
|
10
|
+
const VISIBLE = 10;
|
|
11
|
+
export const ModelPicker = ({ providers, currentModel, onSelect, onCancel, }) => {
|
|
12
|
+
const [stage, setStage] = useState('provider');
|
|
13
|
+
const [pIdx, setPIdx] = useState(() => Math.max(0, providers.findIndex(p => p.is_current)));
|
|
14
|
+
const [mIdx, setMIdx] = useState(0);
|
|
15
|
+
const provider = providers[pIdx];
|
|
16
|
+
const models = provider?.models ?? [];
|
|
17
|
+
useInput((_input, key) => {
|
|
18
|
+
if (key.escape) {
|
|
19
|
+
if (stage === 'model') {
|
|
20
|
+
setStage('provider');
|
|
21
|
+
setMIdx(0);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
onCancel();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const count = stage === 'provider' ? providers.length : models.length;
|
|
28
|
+
const sel = stage === 'provider' ? pIdx : mIdx;
|
|
29
|
+
const setSel = stage === 'provider' ? setPIdx : setMIdx;
|
|
30
|
+
if (key.upArrow && sel > 0)
|
|
31
|
+
return setSel(sel - 1);
|
|
32
|
+
if (key.downArrow && sel < count - 1)
|
|
33
|
+
return setSel(sel + 1);
|
|
34
|
+
if (key.return) {
|
|
35
|
+
if (stage === 'provider') {
|
|
36
|
+
if (provider) {
|
|
37
|
+
setStage('model');
|
|
38
|
+
setMIdx(0);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const m = models[mIdx];
|
|
43
|
+
if (provider && m)
|
|
44
|
+
onSelect(provider.slug, m);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const rows = useMemo(() => {
|
|
48
|
+
if (stage === 'provider') {
|
|
49
|
+
return providers.map(p => `${p.is_current ? '*' : '●'} ${p.name} · ${p.models.length} models`);
|
|
50
|
+
}
|
|
51
|
+
return models.map(m => `${m === currentModel ? '*' : ' '} ${m}`);
|
|
52
|
+
}, [stage, providers, models, currentModel]);
|
|
53
|
+
const sel = stage === 'provider' ? pIdx : mIdx;
|
|
54
|
+
const { items, offset } = windowItems(rows, sel, VISIBLE);
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: CVC_THEME.primary, bold: true, children: stage === 'provider' ? 'Select provider (1/2)' : `Select model — ${provider?.name ?? ''} (2/2)` }), _jsx(Text, { dimColor: true, children: currentModel ? `current: ${currentModel}` : ' ' }), offset > 0 ? _jsx(Text, { dimColor: true, children: ` ↑ ${offset} more` }) : _jsx(Text, { children: " " }), items.map((row, i) => {
|
|
56
|
+
const idx = offset + i;
|
|
57
|
+
const active = sel === idx;
|
|
58
|
+
return (_jsxs(Text, { color: active ? CVC_THEME.primary : undefined, inverse: active, bold: active, children: [active ? '▸ ' : ' ', row] }, `${stage}-${idx}`));
|
|
59
|
+
}), offset + VISIBLE < rows.length ? (_jsx(Text, { dimColor: true, children: ` ↓ ${rows.length - offset - VISIBLE} more` })) : (_jsx(Text, { children: " " })), _jsxs(Text, { dimColor: true, children: ["\u2191/\u2193 select \u00B7 Enter choose \u00B7 Esc ", stage === 'model' ? 'back' : 'cancel'] })] }));
|
|
60
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Tiny shared helpers for CVC arrow-key navigable overlays.
|
|
3
|
+
// Keeps modelPicker / sessionPicker / skillsHub / helpOverlay consistent
|
|
4
|
+
// without dragging in the heavier CVC overlayControls surface.
|
|
5
|
+
/** Window a list around the selected index so a cursor at any depth stays visible. */
|
|
6
|
+
export function windowItems(all, selected, visible) {
|
|
7
|
+
if (all.length <= visible)
|
|
8
|
+
return { items: all, offset: 0 };
|
|
9
|
+
const half = Math.floor(visible / 2);
|
|
10
|
+
let offset = Math.max(0, Math.min(all.length - visible, selected - half));
|
|
11
|
+
return { items: all.slice(offset, offset + visible), offset };
|
|
12
|
+
}
|
|
13
|
+
/** Filter a list of strings by a case-insensitive substring (q is the query). */
|
|
14
|
+
export function filterRows(rows, q, key) {
|
|
15
|
+
const needle = q.trim().toLowerCase();
|
|
16
|
+
if (!needle)
|
|
17
|
+
return rows;
|
|
18
|
+
return rows.filter(r => key(r).toLowerCase().includes(needle));
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// Secret prompt overlay — masked input. Resolves with the entered string,
|
|
4
|
+
// or null on Esc.
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { Box, Text, useInput } from 'ink';
|
|
7
|
+
import { useStore } from '@nanostores/react';
|
|
8
|
+
import { $prompt, resolveActivePrompt } from '../../app/promptStore.js';
|
|
9
|
+
export const SecretPrompt = () => {
|
|
10
|
+
const cur = useStore($prompt);
|
|
11
|
+
const [value, setValue] = useState('');
|
|
12
|
+
if (!cur || cur.kind !== 'secret')
|
|
13
|
+
return null;
|
|
14
|
+
const mask = cur.mask !== false;
|
|
15
|
+
useInput((input, key) => {
|
|
16
|
+
if (key.escape) {
|
|
17
|
+
resolveActivePrompt(null);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (key.return) {
|
|
21
|
+
resolveActivePrompt(value);
|
|
22
|
+
setValue('');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (key.backspace || key.delete) {
|
|
26
|
+
setValue(value.slice(0, -1));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
30
|
+
setValue(value + input);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const display = mask ? '●'.repeat(value.length) : value;
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#e63946", bold: true, children: "\uD83D\uDD12 " }), _jsxs(Text, { children: [cur.prompt, " "] }), _jsx(Text, { children: display })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" }) })] }));
|
|
36
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// CVC sessionPicker overlay — lists ~/.cvc/sessions/*.json with timestamp +
|
|
4
|
+
// first-message preview, arrow-key navigable, filter input.
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { Box, Text, useInput } from 'ink';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import { CVC_THEME } from '../../types.js';
|
|
11
|
+
import { filterRows, windowItems } from './overlayUtils.js';
|
|
12
|
+
const VISIBLE = 10;
|
|
13
|
+
/** Load sessions from ~/.cvc/sessions/*.json. Resilient to malformed files. */
|
|
14
|
+
export function loadSessions(dir = path.join(os.homedir(), '.cvc', 'sessions')) {
|
|
15
|
+
if (!fs.existsSync(dir))
|
|
16
|
+
return [];
|
|
17
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const f of files) {
|
|
20
|
+
const fp = path.join(dir, f);
|
|
21
|
+
try {
|
|
22
|
+
const stat = fs.statSync(fp);
|
|
23
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
24
|
+
const j = JSON.parse(raw);
|
|
25
|
+
const first = (j.messages ?? []).find(m => m.role === 'user');
|
|
26
|
+
const preview = (first?.content ?? '').replace(/\s+/g, ' ').trim().slice(0, 80) || '(empty)';
|
|
27
|
+
out.push({
|
|
28
|
+
id: j.id ?? f.replace(/\.json$/, ''),
|
|
29
|
+
path: fp,
|
|
30
|
+
mtime: stat.mtimeMs,
|
|
31
|
+
preview,
|
|
32
|
+
title: j.title,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// skip malformed
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
40
|
+
}
|
|
41
|
+
const fmtAge = (ms) => {
|
|
42
|
+
const d = Math.max(0, Date.now() - ms);
|
|
43
|
+
const m = Math.floor(d / 60000);
|
|
44
|
+
if (m < 1)
|
|
45
|
+
return 'just now';
|
|
46
|
+
if (m < 60)
|
|
47
|
+
return `${m}m ago`;
|
|
48
|
+
const h = Math.floor(m / 60);
|
|
49
|
+
if (h < 24)
|
|
50
|
+
return `${h}h ago`;
|
|
51
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
52
|
+
};
|
|
53
|
+
export const SessionPicker = ({ sessions, onSelect, onCancel }) => {
|
|
54
|
+
const [all, setAll] = useState(sessions ?? []);
|
|
55
|
+
const [query, setQuery] = useState('');
|
|
56
|
+
const [idx, setIdx] = useState(0);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!sessions)
|
|
59
|
+
setAll(loadSessions());
|
|
60
|
+
}, [sessions]);
|
|
61
|
+
const filtered = useMemo(() => filterRows(all, query, s => `${s.title ?? ''} ${s.preview} ${s.id}`), [all, query]);
|
|
62
|
+
const safeIdx = Math.min(idx, Math.max(0, filtered.length - 1));
|
|
63
|
+
useInput((input, key) => {
|
|
64
|
+
if (key.escape)
|
|
65
|
+
return onCancel();
|
|
66
|
+
if (key.upArrow && safeIdx > 0)
|
|
67
|
+
return setIdx(safeIdx - 1);
|
|
68
|
+
if (key.downArrow && safeIdx < filtered.length - 1)
|
|
69
|
+
return setIdx(safeIdx + 1);
|
|
70
|
+
if (key.return) {
|
|
71
|
+
const s = filtered[safeIdx];
|
|
72
|
+
if (s)
|
|
73
|
+
onSelect(s.id);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (key.backspace || key.delete) {
|
|
77
|
+
setQuery(q => q.slice(0, -1));
|
|
78
|
+
setIdx(0);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
82
|
+
setQuery(q => q + input);
|
|
83
|
+
setIdx(0);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const rows = filtered.map(s => `${(s.title ?? s.id).slice(0, 24).padEnd(24)} · ${fmtAge(s.mtime).padEnd(10)} · ${s.preview}`);
|
|
87
|
+
const { items, offset } = windowItems(rows, safeIdx, VISIBLE);
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CVC_THEME.primary, bold: true, children: ["Sessions (", filtered.length, "/", all.length, ")"] }), _jsxs(Text, { dimColor: true, children: ["filter: ", query || '(none)'] }), offset > 0 ? _jsx(Text, { dimColor: true, children: ` ↑ ${offset} more` }) : _jsx(Text, { children: " " }), items.length === 0 ? (_jsx(Text, { dimColor: true, children: "no sessions" })) : (items.map((row, i) => {
|
|
89
|
+
const k = offset + i;
|
|
90
|
+
const active = safeIdx === k;
|
|
91
|
+
return (_jsxs(Text, { color: active ? CVC_THEME.primary : undefined, inverse: active, bold: active, children: [active ? '▸ ' : ' ', row] }, filtered[k]?.id ?? `s-${k}`));
|
|
92
|
+
})), offset + VISIBLE < rows.length ? (_jsx(Text, { dimColor: true, children: ` ↓ ${rows.length - offset - VISIBLE} more` })) : (_jsx(Text, { children: " " })), _jsx(Text, { dimColor: true, children: "type to filter \u00B7 \u2191/\u2193 select \u00B7 Enter resume \u00B7 Esc cancel" })] }));
|
|
93
|
+
};
|