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,321 @@
|
|
|
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 { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
const DEFAULT_FILE_OPS = { copyFile, mkdir, readFile, writeFile };
|
|
9
|
+
const COPY_SEQUENCE = '\u001b[99;13u';
|
|
10
|
+
const MULTILINE_SEQUENCE = '\\\r\n';
|
|
11
|
+
const TERMINAL_META = {
|
|
12
|
+
vscode: { appName: 'Code', label: 'VS Code' },
|
|
13
|
+
cursor: { appName: 'Cursor', label: 'Cursor' },
|
|
14
|
+
windsurf: { appName: 'Windsurf', label: 'Windsurf' }
|
|
15
|
+
};
|
|
16
|
+
const MAC_COPY_BINDING = {
|
|
17
|
+
key: 'cmd+c',
|
|
18
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
19
|
+
when: 'terminalFocus && terminalTextSelected',
|
|
20
|
+
args: { text: COPY_SEQUENCE }
|
|
21
|
+
};
|
|
22
|
+
const BASE_BINDINGS = [
|
|
23
|
+
{
|
|
24
|
+
key: 'shift+enter',
|
|
25
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
26
|
+
when: 'terminalFocus',
|
|
27
|
+
args: { text: MULTILINE_SEQUENCE }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'ctrl+enter',
|
|
31
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
32
|
+
when: 'terminalFocus',
|
|
33
|
+
args: { text: MULTILINE_SEQUENCE }
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'cmd+enter',
|
|
37
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
38
|
+
when: 'terminalFocus',
|
|
39
|
+
args: { text: MULTILINE_SEQUENCE }
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'cmd+z',
|
|
43
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
44
|
+
when: 'terminalFocus',
|
|
45
|
+
args: { text: '\u001b[122;9u' }
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'shift+cmd+z',
|
|
49
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
50
|
+
when: 'terminalFocus',
|
|
51
|
+
args: { text: '\u001b[122;10u' }
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
const targetBindings = (platform) => platform === 'darwin' ? [MAC_COPY_BINDING, ...BASE_BINDINGS] : BASE_BINDINGS;
|
|
55
|
+
export function detectVSCodeLikeTerminal(env = process.env) {
|
|
56
|
+
const askpass = env['VSCODE_GIT_ASKPASS_MAIN']?.toLowerCase() ?? '';
|
|
57
|
+
if (env['CURSOR_TRACE_ID'] || askpass.includes('cursor')) {
|
|
58
|
+
return 'cursor';
|
|
59
|
+
}
|
|
60
|
+
if (askpass.includes('windsurf')) {
|
|
61
|
+
return 'windsurf';
|
|
62
|
+
}
|
|
63
|
+
if (env['TERM_PROGRAM'] === 'vscode' || env['VSCODE_GIT_IPC_HANDLE']) {
|
|
64
|
+
return 'vscode';
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Strip JSONC features (// line comments, /* block comments *\/, trailing commas)
|
|
70
|
+
* so the result is valid JSON parseable by JSON.parse().
|
|
71
|
+
* Handles comments inside strings correctly (preserves them).
|
|
72
|
+
*/
|
|
73
|
+
export function stripJsonComments(content) {
|
|
74
|
+
let result = '';
|
|
75
|
+
let i = 0;
|
|
76
|
+
const len = content.length;
|
|
77
|
+
while (i < len) {
|
|
78
|
+
const ch = content[i];
|
|
79
|
+
// String literal — copy as-is, including any comment-like chars inside
|
|
80
|
+
if (ch === '"') {
|
|
81
|
+
let j = i + 1;
|
|
82
|
+
while (j < len) {
|
|
83
|
+
if (content[j] === '\\') {
|
|
84
|
+
j += 2; // skip escaped char
|
|
85
|
+
}
|
|
86
|
+
else if (content[j] === '"') {
|
|
87
|
+
j++;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
j++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
result += content.slice(i, j);
|
|
95
|
+
i = j;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Line comment
|
|
99
|
+
if (ch === '/' && content[i + 1] === '/') {
|
|
100
|
+
const eol = content.indexOf('\n', i);
|
|
101
|
+
i = eol === -1 ? len : eol;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Block comment
|
|
105
|
+
if (ch === '/' && content[i + 1] === '*') {
|
|
106
|
+
const end = content.indexOf('*/', i + 2);
|
|
107
|
+
i = end === -1 ? len : end + 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
result += ch;
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
// Remove trailing commas before ] or }
|
|
114
|
+
return result.replace(/,(\s*[}\]])/g, '$1');
|
|
115
|
+
}
|
|
116
|
+
export function isRemoteShellSession(env) {
|
|
117
|
+
return Boolean(env['SSH_CONNECTION'] || env['SSH_TTY'] || env['SSH_CLIENT']);
|
|
118
|
+
}
|
|
119
|
+
export function getVSCodeStyleConfigDir(appName, platform = process.platform, env = process.env, homeDir = homedir()) {
|
|
120
|
+
if (platform === 'darwin') {
|
|
121
|
+
return join(homeDir, 'Library', 'Application Support', appName, 'User');
|
|
122
|
+
}
|
|
123
|
+
if (platform === 'win32') {
|
|
124
|
+
return env['APPDATA'] ? join(env['APPDATA'], appName, 'User') : null;
|
|
125
|
+
}
|
|
126
|
+
return join(homeDir, '.config', appName, 'User');
|
|
127
|
+
}
|
|
128
|
+
function isKeybinding(value) {
|
|
129
|
+
return typeof value === 'object' && value !== null;
|
|
130
|
+
}
|
|
131
|
+
function sameBinding(a, b) {
|
|
132
|
+
return a.key === b.key && a.command === b.command && a.when === b.when && a.args?.text === b.args?.text;
|
|
133
|
+
}
|
|
134
|
+
const WHEN_TOKEN_RE = /!?[A-Za-z_][\w.]*/g;
|
|
135
|
+
function parseWhenRequirements(when) {
|
|
136
|
+
const required = new Set();
|
|
137
|
+
const forbidden = new Set();
|
|
138
|
+
for (const [token] of when.matchAll(WHEN_TOKEN_RE)) {
|
|
139
|
+
if (token.startsWith('!')) {
|
|
140
|
+
forbidden.add(token.slice(1));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
required.add(token);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { forbidden, required };
|
|
147
|
+
}
|
|
148
|
+
function requirementsContradict(a, b) {
|
|
149
|
+
for (const token of a.required) {
|
|
150
|
+
if (b.forbidden.has(token)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const token of b.required) {
|
|
155
|
+
if (a.forbidden.has(token)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
function whensOverlap(a, b) {
|
|
162
|
+
if (a === b) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
// Empty when = global, overlaps every context.
|
|
166
|
+
if (!a || !b) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const left = parseWhenRequirements(a);
|
|
170
|
+
const right = parseWhenRequirements(b);
|
|
171
|
+
if (requirementsContradict(left, right)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
// This intentionally avoids a full VS Code when-clause parser. If two
|
|
175
|
+
// same-key bindings share a positive context token and don't explicitly
|
|
176
|
+
// contradict each other, they can fire together in that context.
|
|
177
|
+
for (const token of left.required) {
|
|
178
|
+
if (right.required.has(token)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
// VS Code allows multiple bindings on the same key as long as their `when`
|
|
185
|
+
// clauses don't overlap. We flag a conflict when the contexts overlap but
|
|
186
|
+
// the bindings differ — e.g. existing `terminalFocus` cmd+c overlaps with
|
|
187
|
+
// our `terminalFocus && terminalTextSelected`, so the existing binding
|
|
188
|
+
// would shadow ours when text isn't selected.
|
|
189
|
+
function bindingsConflict(existing, target) {
|
|
190
|
+
if (existing.key !== target.key) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (!whensOverlap(existing.when ?? '', target.when ?? '')) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return !sameBinding(existing, target);
|
|
197
|
+
}
|
|
198
|
+
async function backupFile(filePath, ops) {
|
|
199
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
200
|
+
await ops.copyFile(filePath, `${filePath}.backup.${stamp}`);
|
|
201
|
+
}
|
|
202
|
+
export async function configureTerminalKeybindings(terminal, options) {
|
|
203
|
+
const env = options?.env ?? process.env;
|
|
204
|
+
const platform = options?.platform ?? process.platform;
|
|
205
|
+
const homeDir = options?.homeDir ?? homedir();
|
|
206
|
+
const ops = { ...DEFAULT_FILE_OPS, ...(options?.fileOps ?? {}) };
|
|
207
|
+
const meta = TERMINAL_META[terminal];
|
|
208
|
+
if (isRemoteShellSession(env)) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
message: `${meta.label} terminal setup must be run on the local machine, not inside an SSH session.`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const configDir = getVSCodeStyleConfigDir(meta.appName, platform, env, homeDir);
|
|
215
|
+
if (!configDir) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
message: `Could not determine ${meta.label} settings path on this platform.`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const keybindingsFile = join(configDir, 'keybindings.json');
|
|
222
|
+
try {
|
|
223
|
+
await ops.mkdir(configDir, { recursive: true });
|
|
224
|
+
let keybindings = [];
|
|
225
|
+
let hasExistingFile = false;
|
|
226
|
+
try {
|
|
227
|
+
const content = await ops.readFile(keybindingsFile, 'utf8');
|
|
228
|
+
hasExistingFile = true;
|
|
229
|
+
const parsed = JSON.parse(stripJsonComments(content));
|
|
230
|
+
if (!Array.isArray(parsed)) {
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
message: `${meta.label} keybindings.json is not a JSON array: ${keybindingsFile}`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
keybindings = parsed;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const code = error?.code;
|
|
240
|
+
if (code !== 'ENOENT') {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
message: `Failed to read ${meta.label} keybindings: ${error}`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const targets = targetBindings(platform);
|
|
248
|
+
const conflicts = targets.filter(target => keybindings.some(existing => isKeybinding(existing) && bindingsConflict(existing, target)));
|
|
249
|
+
if (conflicts.length) {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
message: `Existing terminal keybindings would conflict in ${keybindingsFile}: ` + conflicts.map(c => c.key).join(', ')
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
let added = 0;
|
|
256
|
+
for (const target of targets.slice().reverse()) {
|
|
257
|
+
const exists = keybindings.some(existing => isKeybinding(existing) && sameBinding(existing, target));
|
|
258
|
+
if (!exists) {
|
|
259
|
+
keybindings.unshift(target);
|
|
260
|
+
added += 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!added) {
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
message: `${meta.label} terminal keybindings already configured.`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (hasExistingFile) {
|
|
270
|
+
await backupFile(keybindingsFile, ops);
|
|
271
|
+
}
|
|
272
|
+
await ops.writeFile(keybindingsFile, `${JSON.stringify(keybindings, null, 2)}\n`, 'utf8');
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
requiresRestart: true,
|
|
276
|
+
message: `Added ${added} ${meta.label} terminal keybinding${added === 1 ? '' : 's'} in ${keybindingsFile}`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
message: `Failed to configure ${meta.label} terminal shortcuts: ${error}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export async function configureDetectedTerminalKeybindings(options) {
|
|
287
|
+
const detected = detectVSCodeLikeTerminal(options?.env ?? process.env);
|
|
288
|
+
if (!detected) {
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
message: 'No supported IDE terminal detected. Supported: VS Code, Cursor, Windsurf.'
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return configureTerminalKeybindings(detected, options);
|
|
295
|
+
}
|
|
296
|
+
export async function shouldPromptForTerminalSetup(options) {
|
|
297
|
+
const env = options?.env ?? process.env;
|
|
298
|
+
const detected = detectVSCodeLikeTerminal(env);
|
|
299
|
+
if (!detected || isRemoteShellSession(env)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const platform = options?.platform ?? process.platform;
|
|
303
|
+
const homeDir = options?.homeDir ?? homedir();
|
|
304
|
+
const ops = { ...DEFAULT_FILE_OPS, ...(options?.fileOps ?? {}) };
|
|
305
|
+
const meta = TERMINAL_META[detected];
|
|
306
|
+
const configDir = getVSCodeStyleConfigDir(meta.appName, platform, env, homeDir);
|
|
307
|
+
if (!configDir) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const content = await ops.readFile(join(configDir, 'keybindings.json'), 'utf8');
|
|
312
|
+
const parsed = JSON.parse(stripJsonComments(content));
|
|
313
|
+
if (!Array.isArray(parsed)) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
return targetBindings(platform).some(target => !parsed.some(existing => isKeybinding(existing) && sameBinding(existing, target)));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
package/dist/lib/text.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
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 { HISTORY_RENDER_MAX_CHARS, HISTORY_RENDER_MAX_LINES, LIVE_RENDER_MAX_CHARS, LIVE_RENDER_MAX_LINES, THINKING_COT_MAX } from '../config/limits.js';
|
|
6
|
+
import { VERBS } from '../content/verbs.js';
|
|
7
|
+
const ESC = String.fromCharCode(27);
|
|
8
|
+
const ANSI_RE = new RegExp(`${ESC}\\[[0-9;]*m`, 'g');
|
|
9
|
+
const WS_RE = /\s+/g;
|
|
10
|
+
export const stripAnsi = (s) => s.replace(ANSI_RE, '');
|
|
11
|
+
export const hasAnsi = (s) => s.includes(`${ESC}[`) || s.includes(`${ESC}]`);
|
|
12
|
+
const renderEstimateLine = (line) => {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (trimmed.startsWith('|')) {
|
|
15
|
+
return trimmed
|
|
16
|
+
.split('|')
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map(cell => cell.trim())
|
|
19
|
+
.join(' ');
|
|
20
|
+
}
|
|
21
|
+
return line
|
|
22
|
+
.replace(/!\[(.*?)\]\(([^)\s]+)\)/g, '[image: $1]')
|
|
23
|
+
.replace(/\[(.+?)\]\((https?:\/\/[^\s)]+)\)/g, '$1')
|
|
24
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
25
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
26
|
+
.replace(/(?<!\w)__(.+?)__(?!\w)/g, '$1')
|
|
27
|
+
.replace(/\*(.+?)\*/g, '$1')
|
|
28
|
+
.replace(/(?<!\w)_(.+?)_(?!\w)/g, '$1')
|
|
29
|
+
.replace(/~~(.+?)~~/g, '$1')
|
|
30
|
+
.replace(/==(.+?)==/g, '$1')
|
|
31
|
+
.replace(/\[\^([^\]]+)\]/g, '[$1]')
|
|
32
|
+
.replace(/^#{1,6}\s+/, '')
|
|
33
|
+
.replace(/^\s*[-*+]\s+\[( |x|X)\]\s+/, (_m, checked) => `• [${checked.toLowerCase() === 'x' ? 'x' : ' '}] `)
|
|
34
|
+
.replace(/^\s*[-*+]\s+/, '• ')
|
|
35
|
+
.replace(/^\s*(\d+)\.\s+/, '$1. ')
|
|
36
|
+
.replace(/^\s*(?:>\s*)+/, '│ ');
|
|
37
|
+
};
|
|
38
|
+
export const compactPreview = (s, max) => {
|
|
39
|
+
const one = s.replace(WS_RE, ' ').trim();
|
|
40
|
+
return !one ? '' : one.length > max ? one.slice(0, max - 1) + '…' : one;
|
|
41
|
+
};
|
|
42
|
+
export const estimateTokensRough = (text) => (!text ? 0 : (text.length + 3) >> 2);
|
|
43
|
+
export const edgePreview = (s, head = 16, tail = 28) => {
|
|
44
|
+
const one = s.replace(WS_RE, ' ').trim().replace(/\]\]/g, '] ]');
|
|
45
|
+
return !one
|
|
46
|
+
? ''
|
|
47
|
+
: one.length <= head + tail + 4
|
|
48
|
+
? one
|
|
49
|
+
: `${one.slice(0, head).trimEnd()}.. ${one.slice(-tail).trimStart()}`;
|
|
50
|
+
};
|
|
51
|
+
export const pasteTokenLabel = (text, lineCount) => {
|
|
52
|
+
const preview = edgePreview(text);
|
|
53
|
+
if (!preview) {
|
|
54
|
+
return `[[ [${fmtK(lineCount)} lines] ]]`;
|
|
55
|
+
}
|
|
56
|
+
const [head = preview, tail = ''] = preview.split('.. ', 2);
|
|
57
|
+
return tail
|
|
58
|
+
? `[[ ${head.trimEnd()}.. [${fmtK(lineCount)} lines] .. ${tail.trimStart()} ]]`
|
|
59
|
+
: `[[ ${preview} [${fmtK(lineCount)} lines] ]]`;
|
|
60
|
+
};
|
|
61
|
+
const THINKING_STATUS_RE = new RegExp(`^(?:${VERBS.join('|')})\\.{0,3}$`, 'i');
|
|
62
|
+
const THINKING_STATUS_CHUNK_RE = new RegExp(`[^A-Za-z\n]+\\s*(?:${VERBS.join('|')})\\.{0,3}\\s*`, 'giu');
|
|
63
|
+
export const cleanThinkingText = (reasoning) => reasoning
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map(line => line.replace(THINKING_STATUS_CHUNK_RE, '').trim())
|
|
66
|
+
.filter(line => line && !THINKING_STATUS_RE.test(line.replace(/\.\.\.$/, '').trim()))
|
|
67
|
+
.join('\n')
|
|
68
|
+
.replace(/([^\n])(?=\*\*[^*\n][^\n]*?\*\*)/g, '$1\n\n')
|
|
69
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
70
|
+
.trim();
|
|
71
|
+
export const thinkingPreview = (reasoning, mode, max = THINKING_COT_MAX) => {
|
|
72
|
+
const raw = cleanThinkingText(reasoning);
|
|
73
|
+
return !raw || mode === 'collapsed' ? '' : mode === 'full' ? raw : compactPreview(raw.replace(WS_RE, ' '), max);
|
|
74
|
+
};
|
|
75
|
+
export const boundedLiveRenderText = (text, { maxChars = LIVE_RENDER_MAX_CHARS, maxLines = LIVE_RENDER_MAX_LINES } = {}) => boundedRenderText(text, 'showing live tail', { maxChars, maxLines });
|
|
76
|
+
export const boundedHistoryRenderText = (text, { maxChars = HISTORY_RENDER_MAX_CHARS, maxLines = HISTORY_RENDER_MAX_LINES } = {}) => boundedRenderText(text, 'showing tail', { maxChars, maxLines });
|
|
77
|
+
const boundedRenderText = (text, labelPrefix, { maxChars, maxLines }) => {
|
|
78
|
+
if (text.length <= maxChars && text.split('\n', maxLines + 1).length <= maxLines) {
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
let start = 0;
|
|
82
|
+
let idx = text.length;
|
|
83
|
+
for (let seen = 0; seen < maxLines && idx > 0; seen++) {
|
|
84
|
+
idx = text.lastIndexOf('\n', idx - 1);
|
|
85
|
+
start = idx < 0 ? 0 : idx + 1;
|
|
86
|
+
if (idx < 0) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const lineStart = start;
|
|
91
|
+
start = Math.max(lineStart, text.length - maxChars);
|
|
92
|
+
if (start > lineStart) {
|
|
93
|
+
const nextBreak = text.indexOf('\n', start);
|
|
94
|
+
if (nextBreak >= 0 && nextBreak < text.length - 1) {
|
|
95
|
+
start = nextBreak + 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const tail = text.slice(start).trimStart();
|
|
99
|
+
const omittedLines = countNewlines(text, start);
|
|
100
|
+
const omittedChars = Math.max(0, text.length - tail.length);
|
|
101
|
+
const label = omittedLines > 0
|
|
102
|
+
? `[${labelPrefix}; omitted ${fmtK(omittedLines)} lines / ${fmtK(omittedChars)} chars]\n`
|
|
103
|
+
: `[${labelPrefix}; omitted ${fmtK(omittedChars)} chars]\n`;
|
|
104
|
+
return `${label}${tail}`;
|
|
105
|
+
};
|
|
106
|
+
const countNewlines = (text, end) => {
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (let i = 0; i < end; i++) {
|
|
109
|
+
if (text.charCodeAt(i) === 10) {
|
|
110
|
+
count++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
};
|
|
115
|
+
export const stripTrailingPasteNewlines = (text) => (/[^\n]/.test(text) ? text.replace(/\n+$/, '') : text);
|
|
116
|
+
export const toolTrailLabel = (name) => name
|
|
117
|
+
.split('_')
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.map(p => p[0].toUpperCase() + p.slice(1))
|
|
120
|
+
.join(' ') || name;
|
|
121
|
+
export const formatToolCall = (name, context = '') => {
|
|
122
|
+
const label = toolTrailLabel(name);
|
|
123
|
+
const preview = compactPreview(context, 64);
|
|
124
|
+
return preview ? `${label}("${preview}")` : label;
|
|
125
|
+
};
|
|
126
|
+
export const buildToolTrailLine = (name, context, error, note, duration) => {
|
|
127
|
+
const detail = compactPreview(note ?? '', 72);
|
|
128
|
+
const took = duration !== undefined ? ` (${duration.toFixed(1)}s)` : '';
|
|
129
|
+
return `${formatToolCall(name, context)}${took}${detail ? ` :: ${detail}` : ''} ${error ? '✗' : '✓'}`;
|
|
130
|
+
};
|
|
131
|
+
export const isToolTrailResultLine = (line) => line.endsWith(' ✓') || line.endsWith(' ✗');
|
|
132
|
+
export const parseToolTrailResultLine = (line) => {
|
|
133
|
+
if (!isToolTrailResultLine(line)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const mark = line.endsWith(' ✗') ? '✗' : '✓';
|
|
137
|
+
const body = line.slice(0, -2);
|
|
138
|
+
const [call, detail] = body.split(' :: ', 2);
|
|
139
|
+
if (detail != null) {
|
|
140
|
+
return { call, detail, mark };
|
|
141
|
+
}
|
|
142
|
+
const legacy = body.indexOf(': ');
|
|
143
|
+
if (legacy > 0) {
|
|
144
|
+
return { call: body.slice(0, legacy), detail: body.slice(legacy + 2), mark };
|
|
145
|
+
}
|
|
146
|
+
return { call: body, detail: '', mark };
|
|
147
|
+
};
|
|
148
|
+
export const splitToolDuration = (call) => {
|
|
149
|
+
const match = call.match(/^(.*?)( \(\d+(?:\.\d)?s\))$/);
|
|
150
|
+
return match ? { label: match[1], duration: match[2] } : { label: call, duration: '' };
|
|
151
|
+
};
|
|
152
|
+
export const isTransientTrailLine = (line) => line.startsWith('drafting ') || line === 'analyzing tool output…';
|
|
153
|
+
export const sameToolTrailGroup = (label, entry) => entry === `${label} ✓` ||
|
|
154
|
+
entry === `${label} ✗` ||
|
|
155
|
+
entry.startsWith(`${label}(`) ||
|
|
156
|
+
entry.startsWith(`${label} ::`) ||
|
|
157
|
+
entry.startsWith(`${label}:`);
|
|
158
|
+
export const lastCotTrailIndex = (trail) => {
|
|
159
|
+
for (let i = trail.length - 1; i >= 0; i--) {
|
|
160
|
+
if (!isToolTrailResultLine(trail[i])) {
|
|
161
|
+
return i;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return -1;
|
|
165
|
+
};
|
|
166
|
+
export const estimateRows = (text, w, compact = false) => {
|
|
167
|
+
let fence = null;
|
|
168
|
+
let rows = 0;
|
|
169
|
+
for (const raw of text.split('\n')) {
|
|
170
|
+
const line = stripAnsi(raw);
|
|
171
|
+
const maybeFence = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
172
|
+
if (maybeFence) {
|
|
173
|
+
const marker = maybeFence[1];
|
|
174
|
+
const lang = maybeFence[2].trim();
|
|
175
|
+
if (!fence) {
|
|
176
|
+
fence = { char: marker[0], len: marker.length };
|
|
177
|
+
if (lang) {
|
|
178
|
+
rows += Math.ceil((`─ ${lang}`.length || 1) / w);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (marker[0] === fence.char && marker.length >= fence.len) {
|
|
182
|
+
fence = null;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const inCode = Boolean(fence);
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
if (!inCode && trimmed.startsWith('|') && /^[|\s:-]+$/.test(trimmed)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const rendered = inCode ? line : renderEstimateLine(line);
|
|
192
|
+
if (compact && !rendered.trim()) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
rows += Math.ceil((rendered.length || 1) / w);
|
|
196
|
+
}
|
|
197
|
+
return Math.max(1, rows);
|
|
198
|
+
};
|
|
199
|
+
export const flat = (r) => Object.values(r).flat();
|
|
200
|
+
const COMPACT_NUMBER = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, notation: 'compact' });
|
|
201
|
+
export const fmtK = (n) => COMPACT_NUMBER.format(n).replace(/[KMBT]$/, s => s.toLowerCase());
|
|
202
|
+
export const pick = (a) => a[Math.floor(Math.random() * a.length)];
|
|
203
|
+
export const isPasteBackedText = (text) => /\[\[paste:\d+(?:[^\n]*?)\]\]|\[paste #\d+ (?:attached|excerpt)(?:[^\n]*?)\]/.test(text);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { stripTrailingPasteNewlines } from './text.js';
|
|
7
|
+
describe('stripTrailingPasteNewlines', () => {
|
|
8
|
+
it('removes trailing newline runs from pasted text', () => {
|
|
9
|
+
expect(stripTrailingPasteNewlines('alpha\n')).toBe('alpha');
|
|
10
|
+
expect(stripTrailingPasteNewlines('alpha\nbeta\n\n')).toBe('alpha\nbeta');
|
|
11
|
+
});
|
|
12
|
+
it('preserves interior newlines', () => {
|
|
13
|
+
expect(stripTrailingPasteNewlines('alpha\nbeta\ngamma')).toBe('alpha\nbeta\ngamma');
|
|
14
|
+
});
|
|
15
|
+
it('preserves newline-only pastes', () => {
|
|
16
|
+
expect(stripTrailingPasteNewlines('\n\n')).toBe('\n\n');
|
|
17
|
+
});
|
|
18
|
+
});
|
package/dist/lib/todo.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
6
|
+
import { todoGlyph, todoTone } from './todo.js';
|
|
7
|
+
describe('todoGlyph', () => {
|
|
8
|
+
it('uses fixed-width ASCII markers so the active row does not render wide or emoji-like', () => {
|
|
9
|
+
expect(todoGlyph('completed')).toBe('[x]');
|
|
10
|
+
expect(todoGlyph('in_progress')).toBe('[>]');
|
|
11
|
+
expect(todoGlyph('pending')).toBe('[ ]');
|
|
12
|
+
expect(todoGlyph('cancelled')).toBe('[-]');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('todoTone', () => {
|
|
16
|
+
it('keeps todo status rows neutral instead of red/green', () => {
|
|
17
|
+
expect(todoTone('completed')).toBe('dim');
|
|
18
|
+
expect(todoTone('cancelled')).toBe('dim');
|
|
19
|
+
expect(todoTone('pending')).toBe('body');
|
|
20
|
+
expect(todoTone('in_progress')).toBe('active');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
+
const EMPTY = {
|
|
3
|
+
atBottom: true,
|
|
4
|
+
bottom: 0,
|
|
5
|
+
pending: 0,
|
|
6
|
+
scrollHeight: 0,
|
|
7
|
+
top: 0,
|
|
8
|
+
viewportHeight: 0
|
|
9
|
+
};
|
|
10
|
+
const EMPTY_SCROLLBAR = {
|
|
11
|
+
scrollHeight: 0,
|
|
12
|
+
top: 0,
|
|
13
|
+
viewportHeight: 0
|
|
14
|
+
};
|
|
15
|
+
export function getViewportSnapshot(s) {
|
|
16
|
+
if (!s) {
|
|
17
|
+
return EMPTY;
|
|
18
|
+
}
|
|
19
|
+
const pending = s.getPendingDelta();
|
|
20
|
+
const top = Math.max(0, s.getScrollTop() + pending);
|
|
21
|
+
const viewportHeight = Math.max(0, s.getViewportHeight());
|
|
22
|
+
const cachedScrollHeight = Math.max(viewportHeight, s.getScrollHeight());
|
|
23
|
+
let scrollHeight = cachedScrollHeight;
|
|
24
|
+
const bottom = top + viewportHeight;
|
|
25
|
+
let atBottom = s.isSticky() || bottom >= scrollHeight - 2;
|
|
26
|
+
if (!atBottom) {
|
|
27
|
+
scrollHeight = Math.max(viewportHeight, s.getFreshScrollHeight?.() ?? cachedScrollHeight);
|
|
28
|
+
atBottom = s.isSticky() || bottom >= scrollHeight - 2;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
atBottom,
|
|
32
|
+
bottom,
|
|
33
|
+
pending,
|
|
34
|
+
scrollHeight,
|
|
35
|
+
top,
|
|
36
|
+
viewportHeight
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function viewportSnapshotKey(v) {
|
|
40
|
+
return `${v.atBottom ? 1 : 0}:${Math.ceil(v.top / 8) * 8}:${v.viewportHeight}:${Math.ceil(v.scrollHeight / 8) * 8}:${v.pending}`;
|
|
41
|
+
}
|
|
42
|
+
export function getScrollbarSnapshot(s) {
|
|
43
|
+
if (!s) {
|
|
44
|
+
return EMPTY_SCROLLBAR;
|
|
45
|
+
}
|
|
46
|
+
const viewportHeight = Math.max(0, s.getViewportHeight());
|
|
47
|
+
const scrollHeight = Math.max(viewportHeight, s.getScrollHeight());
|
|
48
|
+
const maxTop = Math.max(0, scrollHeight - viewportHeight);
|
|
49
|
+
return {
|
|
50
|
+
scrollHeight,
|
|
51
|
+
top: Math.max(0, Math.min(maxTop, s.getScrollTop())),
|
|
52
|
+
viewportHeight
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function scrollbarSnapshotKey(v) {
|
|
56
|
+
return `${v.top}:${v.viewportHeight}:${v.scrollHeight}`;
|
|
57
|
+
}
|
|
58
|
+
export function useViewportSnapshot(scrollRef) {
|
|
59
|
+
const key = useSyncExternalStore(useCallback((cb) => scrollRef.current?.subscribe(cb) ?? (() => { }), [scrollRef]), () => viewportSnapshotKey(getViewportSnapshot(scrollRef.current)), () => viewportSnapshotKey(EMPTY));
|
|
60
|
+
return useMemo(() => {
|
|
61
|
+
const [atBottom = '1', top = '0', viewportHeight = '0', scrollHeight = '0', pending = '0'] = key.split(':');
|
|
62
|
+
return {
|
|
63
|
+
atBottom: atBottom === '1',
|
|
64
|
+
bottom: Number(top) + Number(viewportHeight),
|
|
65
|
+
pending: Number(pending),
|
|
66
|
+
scrollHeight: Number(scrollHeight),
|
|
67
|
+
top: Number(top),
|
|
68
|
+
viewportHeight: Number(viewportHeight)
|
|
69
|
+
};
|
|
70
|
+
}, [key]);
|
|
71
|
+
}
|
|
72
|
+
export function useScrollbarSnapshot(scrollRef) {
|
|
73
|
+
const key = useSyncExternalStore(useCallback((cb) => scrollRef.current?.subscribe(cb) ?? (() => { }), [scrollRef]), () => scrollbarSnapshotKey(getScrollbarSnapshot(scrollRef.current)), () => scrollbarSnapshotKey(EMPTY_SCROLLBAR));
|
|
74
|
+
return useMemo(() => {
|
|
75
|
+
const [top = '0', viewportHeight = '0', scrollHeight = '0'] = key.split(':');
|
|
76
|
+
return {
|
|
77
|
+
scrollHeight: Number(scrollHeight),
|
|
78
|
+
top: Number(top),
|
|
79
|
+
viewportHeight: Number(viewportHeight)
|
|
80
|
+
};
|
|
81
|
+
}, [key]);
|
|
82
|
+
}
|