dave-code 1.0.4 → 1.2.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/README.md +39 -5
- package/bin/aiClient.js +878 -159
- package/bin/check.js +11 -0
- package/bin/cliMenu.js +251 -172
- package/bin/commandRouter.js +70 -0
- package/bin/configManager.js +153 -64
- package/bin/contextManager.js +167 -0
- package/bin/index.js +2103 -573
- package/bin/markdownRenderer.js +264 -0
- package/bin/memoryManager.js +182 -0
- package/bin/planManager.js +291 -0
- package/bin/projectNotebookManager.js +839 -0
- package/bin/runtimeEvents.js +104 -0
- package/bin/scanManager.js +561 -0
- package/bin/sessionManager.js +182 -0
- package/bin/terminalRenderer.js +701 -0
- package/bin/textWidth.js +194 -0
- package/bin/thunderManager.js +302 -0
- package/bin/thunderOrchestrator.js +263 -0
- package/bin/thunderPrompts.js +53 -0
- package/bin/thunderRenderer.js +200 -0
- package/bin/toolRuntime.js +1607 -0
- package/package.json +6 -5
package/bin/check.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const files = fs.readdirSync(directory).filter(file => file.endsWith('.js')).sort();
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
const result = spawnSync(process.execPath, ['--check', path.join(directory, file)], { stdio: 'inherit' });
|
|
10
|
+
if (result.status !== 0) process.exit(result.status || 1);
|
|
11
|
+
}
|
package/bin/cliMenu.js
CHANGED
|
@@ -8,7 +8,23 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import readline from 'readline';
|
|
11
|
-
import
|
|
11
|
+
import { formatConversationMessage } from './terminalRenderer.js';
|
|
12
|
+
import { displayWidth as getStringWidth, truncateEnd, padEnd } from './textWidth.js';
|
|
13
|
+
|
|
14
|
+
const truncateDisplay = (str, maxWidth) => truncateEnd(str, maxWidth, '...');
|
|
15
|
+
|
|
16
|
+
function enterMenuScreen(stdout) {
|
|
17
|
+
stdout.write('\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function redrawMenuScreen(stdout, draw) {
|
|
21
|
+
stdout.write('\x1b[H\x1b[2J');
|
|
22
|
+
draw();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exitMenuScreen(stdout) {
|
|
26
|
+
stdout.write('\x1b[?25h\x1b[?1049l');
|
|
27
|
+
}
|
|
12
28
|
|
|
13
29
|
export function selectMenu(title, description, options, defaultIndex = 0) {
|
|
14
30
|
return new Promise((resolve) => {
|
|
@@ -23,51 +39,57 @@ export function selectMenu(title, description, options, defaultIndex = 0) {
|
|
|
23
39
|
}
|
|
24
40
|
stdin.resume();
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
stdout.write('\x1b[?25l');
|
|
42
|
+
enterMenuScreen(stdout);
|
|
28
43
|
|
|
29
44
|
function draw() {
|
|
30
|
-
stdout.
|
|
31
|
-
|
|
45
|
+
const width = Math.max(24, Math.min(stdout.columns || 100, 120));
|
|
46
|
+
const compact = width < 64;
|
|
47
|
+
const nameWidth = Math.max(12, Math.floor(width * 0.42));
|
|
48
|
+
const descWidth = Math.max(10, width - nameWidth - 10);
|
|
49
|
+
|
|
50
|
+
stdout.write(`\x1b[1;36m${truncateDisplay(title, width)}\x1b[0m\n`);
|
|
51
|
+
for (const line of String(description || '').split(/\r?\n/).slice(0, 18)) {
|
|
52
|
+
stdout.write(`\x1b[90m${truncateDisplay(line, width)}\x1b[0m\n`);
|
|
53
|
+
}
|
|
54
|
+
stdout.write('\n');
|
|
32
55
|
|
|
33
56
|
for (let i = 0; i < options.length; i++) {
|
|
34
57
|
const opt = options[i];
|
|
58
|
+
const name = truncateDisplay(opt.name, compact ? width - 8 : nameWidth);
|
|
59
|
+
const desc = truncateDisplay(opt.desc, compact ? width - 7 : descWidth);
|
|
35
60
|
if (i === cursor) {
|
|
36
|
-
stdout.write(` \x1b[32m›\x1b[0m \x1b[1;32m${i + 1}. ${
|
|
61
|
+
stdout.write(` \x1b[32m›\x1b[0m \x1b[1;32m${i + 1}. ${name}\x1b[0m${compact ? '\n \x1b[90m' + desc + '\x1b[0m' : ' \x1b[90m' + desc + '\x1b[0m'}\n`);
|
|
37
62
|
} else {
|
|
38
|
-
stdout.write(` ${i + 1}. ${
|
|
63
|
+
stdout.write(` ${i + 1}. ${name}${compact ? '\n \x1b[90m' + desc + '\x1b[0m' : ' \x1b[90m' + desc + '\x1b[0m'}\n`);
|
|
39
64
|
}
|
|
40
65
|
}
|
|
41
|
-
stdout.write('\n\x1b[90mUse
|
|
66
|
+
stdout.write('\n\x1b[90mUse ↑/↓ or number keys, Enter to select, Esc to cancel\x1b[0m\n');
|
|
42
67
|
}
|
|
43
68
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
stdout.write('\x1b[1A\x1b[2K');
|
|
47
|
-
}
|
|
69
|
+
function redrawMenu() {
|
|
70
|
+
redrawMenuScreen(stdout, draw);
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
draw();
|
|
73
|
+
redrawMenu();
|
|
52
74
|
|
|
53
75
|
function onKeypress(str, key) {
|
|
54
76
|
if (!key) return;
|
|
55
77
|
|
|
56
78
|
if (key.name === 'up') {
|
|
57
79
|
cursor = (cursor - 1 + options.length) % options.length;
|
|
58
|
-
|
|
59
|
-
draw();
|
|
80
|
+
redrawMenu();
|
|
60
81
|
} else if (key.name === 'down') {
|
|
61
82
|
cursor = (cursor + 1) % options.length;
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
redrawMenu();
|
|
84
|
+
} else if (/^[1-9]$/.test(str || '') && Number(str) <= options.length) {
|
|
85
|
+
cursor = Number(str) - 1;
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve(cursor);
|
|
64
88
|
} else if (key.name === 'return') {
|
|
65
89
|
cleanup();
|
|
66
|
-
stdout.write('\x1b[?25h'); // Show cursor
|
|
67
90
|
resolve(cursor);
|
|
68
91
|
} else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
69
92
|
cleanup();
|
|
70
|
-
stdout.write('\x1b[?25h');
|
|
71
93
|
resolve(-1);
|
|
72
94
|
}
|
|
73
95
|
}
|
|
@@ -78,6 +100,7 @@ export function selectMenu(title, description, options, defaultIndex = 0) {
|
|
|
78
100
|
stdin.setRawMode(oldRawMode);
|
|
79
101
|
}
|
|
80
102
|
stdin.pause();
|
|
103
|
+
exitMenuScreen(stdout);
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
stdin.on('keypress', onKeypress);
|
|
@@ -153,12 +176,21 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
153
176
|
const stdout = process.stdout;
|
|
154
177
|
|
|
155
178
|
const footerText = currentLang === 'cn'
|
|
156
|
-
? '
|
|
157
|
-
: '
|
|
179
|
+
? '/help 获取帮助 · Ctrl+C 退出'
|
|
180
|
+
: '/help for help · Ctrl+C to exit';
|
|
181
|
+
|
|
182
|
+
const promptWidth = () => Math.max(24, Math.min(stdout.columns || 96, 120));
|
|
183
|
+
function renderPromptLine(value, { muted = false } = {}) {
|
|
184
|
+
const inner = promptWidth() - 2;
|
|
185
|
+
const visible = padEnd(truncateDisplay(value, inner), inner);
|
|
186
|
+
if (process.env.NO_COLOR) return `> ${visible}\n`;
|
|
187
|
+
const foreground = muted ? '\x1b[90m' : '\x1b[37m';
|
|
188
|
+
return `\x1b[48;5;236m\x1b[36m>\x1b[39m ${foreground}${visible}\x1b[0m\n`;
|
|
189
|
+
}
|
|
158
190
|
|
|
159
|
-
stdout.write(
|
|
160
|
-
stdout.write(
|
|
161
|
-
stdout.write(`\x1b[90m${footerText}\x1b[0m`);
|
|
191
|
+
stdout.write(`\x1b[90m${'─'.repeat(promptWidth())}\x1b[0m\n`);
|
|
192
|
+
stdout.write(renderPromptLine(placeholder, { muted: true }));
|
|
193
|
+
stdout.write(`\x1b[90m${truncateDisplay(footerText, promptWidth())}\x1b[0m`);
|
|
162
194
|
|
|
163
195
|
stdout.write('\x1b[1A\x1b[3G');
|
|
164
196
|
|
|
@@ -172,23 +204,22 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
172
204
|
let chars = [];
|
|
173
205
|
let cursorIndex = 0;
|
|
174
206
|
|
|
175
|
-
function
|
|
176
|
-
const
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
width += 2;
|
|
187
|
-
} else {
|
|
188
|
-
width += 1;
|
|
189
|
-
}
|
|
207
|
+
function getInputView() {
|
|
208
|
+
const available = Math.max(8, promptWidth() - 3);
|
|
209
|
+
let start = 0;
|
|
210
|
+
while (start < cursorIndex && getStringWidth(chars.slice(start, cursorIndex).join('')) > available - 1) {
|
|
211
|
+
start++;
|
|
212
|
+
}
|
|
213
|
+
const prefix = start > 0 ? '…' : '';
|
|
214
|
+
let visible = prefix;
|
|
215
|
+
for (let i = start; i < chars.length; i++) {
|
|
216
|
+
if (getStringWidth(visible + chars[i]) > available) break;
|
|
217
|
+
visible += chars[i];
|
|
190
218
|
}
|
|
191
|
-
return
|
|
219
|
+
return {
|
|
220
|
+
visible,
|
|
221
|
+
cursorLeft: prefix + chars.slice(start, cursorIndex).join('')
|
|
222
|
+
};
|
|
192
223
|
}
|
|
193
224
|
|
|
194
225
|
function redraw() {
|
|
@@ -197,16 +228,15 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
197
228
|
|
|
198
229
|
const valueStr = chars.join('');
|
|
199
230
|
if (valueStr.length === 0) {
|
|
200
|
-
stdout.write(
|
|
231
|
+
stdout.write(renderPromptLine(placeholder, { muted: true }));
|
|
201
232
|
} else {
|
|
202
|
-
stdout.write(
|
|
233
|
+
stdout.write(renderPromptLine(getInputView().visible));
|
|
203
234
|
}
|
|
204
235
|
|
|
205
|
-
stdout.write(`\x1b[90m${footerText}\x1b[0m`);
|
|
236
|
+
stdout.write(`\x1b[90m${truncateDisplay(footerText, promptWidth())}\x1b[0m`);
|
|
206
237
|
stdout.write('\x1b[1A');
|
|
207
238
|
|
|
208
|
-
const
|
|
209
|
-
const cursorCol = 3 + (chars.length === 0 ? 0 : getStringWidth(leftPart));
|
|
239
|
+
const cursorCol = 3 + (chars.length === 0 ? 0 : getStringWidth(getInputView().cursorLeft));
|
|
210
240
|
stdout.write(`\x1b[${cursorCol}G`);
|
|
211
241
|
}
|
|
212
242
|
|
|
@@ -214,9 +244,16 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
214
244
|
if (!key) return;
|
|
215
245
|
|
|
216
246
|
if (key.name === 'return') {
|
|
247
|
+
const submitted = chars.join('').trim();
|
|
217
248
|
cleanup();
|
|
218
|
-
stdout.write('\x1b[
|
|
219
|
-
|
|
249
|
+
stdout.write('\x1b[1G\x1b[J');
|
|
250
|
+
if (submitted) {
|
|
251
|
+
stdout.write(`${formatConversationMessage(
|
|
252
|
+
{ role: 'user', content: submitted },
|
|
253
|
+
{ color: Boolean(stdout.isTTY && !process.env.NO_COLOR), width: promptWidth() }
|
|
254
|
+
)}\n`);
|
|
255
|
+
}
|
|
256
|
+
resolve(submitted);
|
|
220
257
|
} else if (key.name === 'backspace') {
|
|
221
258
|
if (cursorIndex > 0) {
|
|
222
259
|
chars.splice(cursorIndex - 1, 1);
|
|
@@ -287,6 +324,10 @@ export function waitForEnter(promptText) {
|
|
|
287
324
|
cleanup();
|
|
288
325
|
stdout.write('\n');
|
|
289
326
|
resolve();
|
|
327
|
+
} else if (key && (key.name === 'escape' || (key.ctrl && key.name === 'c'))) {
|
|
328
|
+
cleanup();
|
|
329
|
+
stdout.write('\n');
|
|
330
|
+
resolve();
|
|
290
331
|
}
|
|
291
332
|
}
|
|
292
333
|
|
|
@@ -302,146 +343,155 @@ export function waitForEnter(promptText) {
|
|
|
302
343
|
});
|
|
303
344
|
}
|
|
304
345
|
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}).start();
|
|
346
|
+
function centerAt(text, center, width) {
|
|
347
|
+
const value = String(text || '');
|
|
348
|
+
const start = Math.max(0, Math.min(width - getStringWidth(value), Math.round(center - getStringWidth(value) / 2)));
|
|
349
|
+
return { start, value };
|
|
350
|
+
}
|
|
311
351
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
352
|
+
function overlayLine(width, entries) {
|
|
353
|
+
const cells = Array(width).fill(' ');
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
let offset = entry.start;
|
|
356
|
+
for (const char of entry.value) {
|
|
357
|
+
if (offset >= width) break;
|
|
358
|
+
cells[offset] = char;
|
|
359
|
+
offset += getStringWidth(char);
|
|
360
|
+
if (getStringWidth(char) === 2 && offset - 1 < width) cells[offset - 1] = '';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return cells.join('');
|
|
315
364
|
}
|
|
316
365
|
|
|
317
|
-
export function
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
366
|
+
export function renderWorkModeSelector(selectedMode = 'highway', currentLang = 'cn', { width = 86, color = true } = {}) {
|
|
367
|
+
const safeWidth = Math.max(48, Math.min(120, Math.floor(width || 86)));
|
|
368
|
+
const leftCenter = Math.floor(safeWidth * 0.25);
|
|
369
|
+
const rightCenter = Math.floor(safeWidth * 0.75);
|
|
370
|
+
const selectedCenter = selectedMode === 'thunder' ? rightCenter : leftCenter;
|
|
371
|
+
const green = color ? '\x1b[1;38;2;74;222;128m' : '';
|
|
372
|
+
const cyan = color ? '\x1b[1;36m' : '';
|
|
373
|
+
const muted = color ? '\x1b[90m' : '';
|
|
374
|
+
const white = color ? '\x1b[1;37m' : '';
|
|
375
|
+
const reset = color ? '\x1b[0m' : '';
|
|
376
|
+
const active = selectedMode === 'thunder' ? green : cyan;
|
|
377
|
+
const labels = currentLang === 'cn'
|
|
378
|
+
? { left: '更快速', right: '更强大', highway: 'Highway', thunder: 'Thunder', highwayDesc: '专注的单 Agent 工作流', thunderDesc: 'PM 领导的办公室团队' }
|
|
379
|
+
: { left: 'Faster', right: 'Smarter', highway: 'Highway', thunder: 'Thunder', highwayDesc: 'Focused single-agent workflow', thunderDesc: 'PM-led office agent team' };
|
|
380
|
+
const title = overlayLine(safeWidth, [centerAt(labels.left, leftCenter, safeWidth), centerAt(labels.right, rightCenter, safeWidth)]);
|
|
381
|
+
const marker = overlayLine(safeWidth, [{ start: selectedCenter, value: '▲' }]);
|
|
382
|
+
const divider = Math.floor(safeWidth / 2);
|
|
383
|
+
const ruleChars = Array(safeWidth).fill('─');
|
|
384
|
+
ruleChars[divider] = '┊';
|
|
385
|
+
const names = overlayLine(safeWidth, [centerAt(labels.highway, leftCenter, safeWidth), centerAt(labels.thunder, rightCenter, safeWidth)]);
|
|
386
|
+
const desc = overlayLine(safeWidth, [centerAt(labels.highwayDesc, leftCenter, safeWidth), centerAt(labels.thunderDesc, rightCenter, safeWidth)]);
|
|
387
|
+
const coloredNames = selectedMode === 'thunder'
|
|
388
|
+
? names.replace(labels.thunder, `${green}${labels.thunder}${reset}`)
|
|
389
|
+
: names.replace(labels.highway, `${cyan}${labels.highway}${reset}`);
|
|
390
|
+
return [
|
|
391
|
+
`${white}${title}${reset}`,
|
|
392
|
+
`${active}${marker}${reset}`,
|
|
393
|
+
`${muted}${ruleChars.join('')}${reset}`,
|
|
394
|
+
coloredNames,
|
|
395
|
+
`${muted}${desc}${reset}`,
|
|
396
|
+
'',
|
|
397
|
+
`${muted}${currentLang === 'cn' ? '←/→ 选择 · Enter 确认 · Esc 取消' : '←/→ select · Enter confirm · Esc cancel'}${reset}`
|
|
398
|
+
].join('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function selectWorkModeMenu(currentMode = 'highway', currentLang = 'cn', { stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
402
|
+
if (!stdin?.isTTY || !stdout?.isTTY) return Promise.resolve(currentMode);
|
|
403
|
+
return new Promise(resolve => {
|
|
404
|
+
let selected = currentMode === 'thunder' ? 'thunder' : 'highway';
|
|
405
|
+
const oldRawMode = Boolean(stdin.isRaw);
|
|
326
406
|
readline.emitKeypressEvents(stdin);
|
|
327
|
-
|
|
328
|
-
if (stdin.setRawMode) {
|
|
329
|
-
stdin.setRawMode(true);
|
|
330
|
-
}
|
|
407
|
+
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
331
408
|
stdin.resume();
|
|
332
|
-
|
|
333
|
-
// Hide terminal cursor
|
|
334
|
-
stdout.write('\x1b[?25l');
|
|
335
|
-
|
|
336
|
-
const title = currentLang === 'cn' ? '调整努力模式 (Adjust Effort Mode)' : 'Adjust Effort Mode';
|
|
337
|
-
const description = currentLang === 'cn'
|
|
338
|
-
? '选择模型的思考努力程度。努力程度越高,循环上限和阅读长度越大;ultracode 模式会执行大文件消化工作流以完全读完。'
|
|
339
|
-
: 'Select thinking effort level. Higher levels increase loop limits and file read sizes; ultracode triggers digestion workflows to read large files fully.';
|
|
340
|
-
|
|
341
|
-
const footer = currentLang === 'cn'
|
|
342
|
-
? '使用左右方向键 (←/→) 移动滑块, 回车 (Enter) 确认, Esc/Ctrl+C 取消'
|
|
343
|
-
: 'Use Arrow Keys (←/→) to navigate, Enter to select, Esc/Ctrl+C to cancel';
|
|
409
|
+
enterMenuScreen(stdout);
|
|
344
410
|
|
|
345
411
|
function draw() {
|
|
346
|
-
stdout.
|
|
347
|
-
stdout.
|
|
348
|
-
|
|
349
|
-
stdout.write(
|
|
350
|
-
stdout.write(`───────────────────────────────────────────────────┋───────────────────────\n`);
|
|
351
|
-
|
|
352
|
-
// Calculate Pointer line (triangle)
|
|
353
|
-
const centers = [2, 12, 23, 36, 46, 60];
|
|
354
|
-
const selectedCenter = centers[cursor];
|
|
355
|
-
|
|
356
|
-
let pointerChar = '▲';
|
|
357
|
-
// Triangle color: purple if ultracode, otherwise yellow
|
|
358
|
-
let pointerColor = cursor === 5 ? '\x1b[1;38;2;140;90;240m' : '\x1b[1;33m';
|
|
359
|
-
|
|
360
|
-
let rawPointerLine = ' '.repeat(selectedCenter) + '▲' + ' '.repeat(76 - selectedCenter - 1);
|
|
361
|
-
let rawPointerLineChars = [...rawPointerLine];
|
|
362
|
-
rawPointerLineChars[50] = '┋';
|
|
363
|
-
|
|
364
|
-
let coloredPointerLine = '';
|
|
365
|
-
for (let i = 0; i < rawPointerLineChars.length; i++) {
|
|
366
|
-
if (i === selectedCenter) {
|
|
367
|
-
coloredPointerLine += pointerColor + '▲\x1b[0m';
|
|
368
|
-
} else if (i === 50) {
|
|
369
|
-
coloredPointerLine += '\x1b[90m┋\x1b[0m';
|
|
370
|
-
} else {
|
|
371
|
-
coloredPointerLine += rawPointerLineChars[i];
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
stdout.write(`${coloredPointerLine}\n`);
|
|
375
|
-
|
|
376
|
-
// Calculate Labels line
|
|
377
|
-
const optColors = levels.map((lvl, idx) => {
|
|
378
|
-
if (idx === cursor) {
|
|
379
|
-
return idx === 5 ? '\x1b[1;38;2;140;90;240m' : '\x1b[1;33m';
|
|
380
|
-
}
|
|
381
|
-
return '\x1b[90m';
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
stdout.write(` `);
|
|
385
|
-
stdout.write(`${optColors[0]}low\x1b[0m` + ` `);
|
|
386
|
-
stdout.write(`${optColors[1]}medium\x1b[0m` + ` `);
|
|
387
|
-
stdout.write(`${optColors[2]}high\x1b[0m` + ` `);
|
|
388
|
-
stdout.write(`${optColors[3]}xhigh\x1b[0m` + ` `);
|
|
389
|
-
stdout.write(`${optColors[4]}max\x1b[0m` + ` \x1b[90m┋\x1b[0m `);
|
|
390
|
-
stdout.write(`${optColors[5]}ultracode\x1b[0m\n`);
|
|
391
|
-
|
|
392
|
-
// Print subtext line (spacing with divider)
|
|
393
|
-
stdout.write(' '.repeat(50) + '\x1b[90m┋\x1b[0m\n\n');
|
|
394
|
-
|
|
395
|
-
stdout.write(`\x1b[90m${footer}\x1b[0m\n`);
|
|
412
|
+
const width = Math.max(48, Math.min(stdout.columns || 86, 110));
|
|
413
|
+
const topPadding = Math.max(1, Math.floor(((stdout.rows || 20) - 7) / 3));
|
|
414
|
+
stdout.write('\n'.repeat(topPadding));
|
|
415
|
+
stdout.write(renderWorkModeSelector(selected, currentLang, { width, color: !process.env.NO_COLOR }));
|
|
396
416
|
}
|
|
397
|
-
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
function redraw() { redrawMenuScreen(stdout, draw); }
|
|
418
|
+
function cleanup() {
|
|
419
|
+
stdin.removeListener('keypress', onKeypress);
|
|
420
|
+
if (stdin.setRawMode) stdin.setRawMode(oldRawMode);
|
|
421
|
+
stdin.pause();
|
|
422
|
+
exitMenuScreen(stdout);
|
|
402
423
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (cursor > 0) {
|
|
412
|
-
cursor--;
|
|
413
|
-
clearLines(totalLines);
|
|
414
|
-
draw();
|
|
415
|
-
}
|
|
416
|
-
} else if (key.name === 'right') {
|
|
417
|
-
if (cursor < 5) {
|
|
418
|
-
cursor++;
|
|
419
|
-
clearLines(totalLines);
|
|
420
|
-
draw();
|
|
421
|
-
}
|
|
424
|
+
function onKeypress(str, key = {}) {
|
|
425
|
+
if (['left', 'right', 'up', 'down', 'tab'].includes(key.name)) {
|
|
426
|
+
selected = selected === 'highway' ? 'thunder' : 'highway';
|
|
427
|
+
redraw();
|
|
428
|
+
} else if (str === '1') {
|
|
429
|
+
selected = 'highway'; cleanup(); resolve(selected);
|
|
430
|
+
} else if (str === '2') {
|
|
431
|
+
selected = 'thunder'; cleanup(); resolve(selected);
|
|
422
432
|
} else if (key.name === 'return') {
|
|
423
|
-
cleanup();
|
|
424
|
-
stdout.write('\x1b[?25h'); // Show cursor
|
|
425
|
-
resolve(levels[cursor]);
|
|
433
|
+
cleanup(); resolve(selected);
|
|
426
434
|
} else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
427
|
-
cleanup();
|
|
428
|
-
stdout.write('\x1b[?25h');
|
|
429
|
-
resolve(null);
|
|
435
|
+
cleanup(); resolve(null);
|
|
430
436
|
}
|
|
431
437
|
}
|
|
432
|
-
|
|
433
|
-
function cleanup() {
|
|
434
|
-
stdin.removeListener('keypress', onKeypress);
|
|
435
|
-
if (stdin.setRawMode) {
|
|
436
|
-
stdin.setRawMode(oldRawMode);
|
|
437
|
-
}
|
|
438
|
-
stdin.pause();
|
|
439
|
-
}
|
|
440
|
-
|
|
441
438
|
stdin.on('keypress', onKeypress);
|
|
439
|
+
redraw();
|
|
442
440
|
});
|
|
443
441
|
}
|
|
444
442
|
|
|
443
|
+
export function renderThunderActivationFrame(frame = 0, { width = 86, height = 16, currentLang = 'cn', color = true } = {}) {
|
|
444
|
+
const safeWidth = Math.max(40, Math.min(140, Math.floor(width || 86)));
|
|
445
|
+
const safeHeight = Math.max(10, Math.min(36, Math.floor(height || 16)));
|
|
446
|
+
const shades = color
|
|
447
|
+
? ['\x1b[48;2;5;32;20m', '\x1b[48;2;8;55;31m', '\x1b[48;2;11;78;43m', '\x1b[48;2;16;104;57m', '\x1b[48;2;24;132;73m']
|
|
448
|
+
: ['', '', '', '', ''];
|
|
449
|
+
const reset = color ? '\x1b[0m' : '';
|
|
450
|
+
const lines = [];
|
|
451
|
+
for (let row = 0; row < safeHeight; row++) {
|
|
452
|
+
let line = '';
|
|
453
|
+
for (let col = 0; col < safeWidth - 1; col += 2) {
|
|
454
|
+
const wave = Math.sin((col + frame * 5) / 8) + Math.cos((row * 3 - frame * 2) / 5);
|
|
455
|
+
const pulse = Math.sin((col + row * 5 + frame * 7) / 11);
|
|
456
|
+
const shade = Math.max(0, Math.min(shades.length - 1, Math.floor((wave + pulse + 3) / 1.4)));
|
|
457
|
+
line += `${shades[shade]} ${reset}`;
|
|
458
|
+
}
|
|
459
|
+
if (safeWidth % 2 === 1) line += `${shades[(row + frame) % shades.length]} ${reset}`;
|
|
460
|
+
lines.push(line);
|
|
461
|
+
}
|
|
462
|
+
const titleRow = Math.floor(safeHeight / 2) - 1;
|
|
463
|
+
const title = '⚡ T H U N D E R ⚡';
|
|
464
|
+
const subtitle = currentLang === 'cn' ? '办公室团队正在上线' : 'Office team coming online';
|
|
465
|
+
const place = (row, text, tone) => {
|
|
466
|
+
const plain = ' '.repeat(Math.max(0, Math.floor((safeWidth - getStringWidth(text)) / 2))) + text;
|
|
467
|
+
lines[row] = `${color ? tone : ''}${plain}${reset}` + ' '.repeat(Math.max(0, safeWidth - getStringWidth(plain)));
|
|
468
|
+
};
|
|
469
|
+
place(titleRow, title, '\x1b[1;97;48;2;14;86;48m');
|
|
470
|
+
place(titleRow + 2, subtitle, '\x1b[1;38;2;177;255;204;48;2;8;55;31m');
|
|
471
|
+
return lines.join('\n');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function runThunderActivationAnimation({ stdout = process.stdout, currentLang = 'cn', durationMs = 950, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
|
|
475
|
+
if (!stdout || (stdout === process.stdout && (!stdout.isTTY || process.env.NO_COLOR || process.env.TERM === 'dumb'))) return;
|
|
476
|
+
const frameMs = 55;
|
|
477
|
+
const frames = Math.max(6, Math.floor(durationMs / frameMs));
|
|
478
|
+
enterMenuScreen(stdout);
|
|
479
|
+
try {
|
|
480
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
481
|
+
stdout.write('\x1b[H');
|
|
482
|
+
stdout.write(renderThunderActivationFrame(frame, {
|
|
483
|
+
width: stdout.columns || 86,
|
|
484
|
+
height: Math.max(10, (stdout.rows || 20) - 1),
|
|
485
|
+
currentLang,
|
|
486
|
+
color: true
|
|
487
|
+
}));
|
|
488
|
+
await sleep(frameMs);
|
|
489
|
+
}
|
|
490
|
+
} finally {
|
|
491
|
+
exitMenuScreen(stdout);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
445
495
|
export function confirmPrompt(promptText) {
|
|
446
496
|
return new Promise((resolve) => {
|
|
447
497
|
const stdin = process.stdin;
|
|
@@ -487,4 +537,33 @@ export function confirmPrompt(promptText) {
|
|
|
487
537
|
});
|
|
488
538
|
}
|
|
489
539
|
|
|
490
|
-
|
|
540
|
+
export function permissionPrompt(promptText) {
|
|
541
|
+
return new Promise(resolve => {
|
|
542
|
+
const stdin = process.stdin;
|
|
543
|
+
const stdout = process.stdout;
|
|
544
|
+
stdout.write(promptText);
|
|
545
|
+
readline.emitKeypressEvents(stdin);
|
|
546
|
+
const oldRawMode = stdin.isRawMode;
|
|
547
|
+
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
548
|
+
stdin.resume();
|
|
549
|
+
function cleanup() {
|
|
550
|
+
stdin.removeListener('keypress', onKeypress);
|
|
551
|
+
if (stdin.setRawMode) stdin.setRawMode(oldRawMode);
|
|
552
|
+
stdin.pause();
|
|
553
|
+
}
|
|
554
|
+
function onKeypress(str, key) {
|
|
555
|
+
if (!key) return;
|
|
556
|
+
const char = String(str || '').toLowerCase();
|
|
557
|
+
if (char === 'y' || char === 'a') {
|
|
558
|
+
cleanup();
|
|
559
|
+
stdout.write(`${char}\n`);
|
|
560
|
+
resolve(char === 'a' ? 'always' : true);
|
|
561
|
+
} else if (char === 'n' || key.name === 'return' || key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
562
|
+
cleanup();
|
|
563
|
+
stdout.write('n\n');
|
|
564
|
+
resolve(false);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
stdin.on('keypress', onKeypress);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function exactCommand(input, command) {
|
|
2
|
+
return input === command || input.startsWith(`${command} `);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function parsePlanCreation(argument) {
|
|
6
|
+
const separator = argument.indexOf(':');
|
|
7
|
+
if (separator === -1) {
|
|
8
|
+
return { error: 'Use /plan <name>: <request>.' };
|
|
9
|
+
}
|
|
10
|
+
const name = argument.slice(0, separator).trim();
|
|
11
|
+
const request = argument.slice(separator + 1).trim();
|
|
12
|
+
if (!name || !request) return { error: 'Plan name and request are both required.' };
|
|
13
|
+
if ([...name].length > 60) return { error: 'Plan name must be 60 characters or fewer.' };
|
|
14
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) return { error: 'Plan name cannot contain control characters.' };
|
|
15
|
+
return { name, request };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseInputCommand(input, { findPlan = () => null } = {}) {
|
|
19
|
+
const trimmed = String(input || '').trim();
|
|
20
|
+
if (trimmed === '/mode') return { type: 'mode.menu' };
|
|
21
|
+
if (exactCommand(trimmed, '/mode')) {
|
|
22
|
+
const mode = trimmed.slice('/mode'.length).trim().toLowerCase();
|
|
23
|
+
return ['highway', 'thunder'].includes(mode)
|
|
24
|
+
? { type: 'mode.set', mode }
|
|
25
|
+
: { type: 'error', error: 'Use /mode highway or /mode thunder.' };
|
|
26
|
+
}
|
|
27
|
+
if (trimmed === '/thunder') return { type: 'thunder.usage' };
|
|
28
|
+
if (exactCommand(trimmed, '/thunder')) {
|
|
29
|
+
const prompt = trimmed.slice('/thunder'.length).trim();
|
|
30
|
+
return prompt ? { type: 'agent', mode: 'chat', workMode: 'thunder', prompt, plan: null } : { type: 'thunder.usage' };
|
|
31
|
+
}
|
|
32
|
+
if (trimmed === '/agents') return { type: 'agents.menu' };
|
|
33
|
+
if (trimmed === '/note') return { type: 'note', action: 'status' };
|
|
34
|
+
if (exactCommand(trimmed, '/note')) {
|
|
35
|
+
const action = trimmed.slice('/note'.length).trim().toLowerCase();
|
|
36
|
+
return ['status', 'refresh', 'rebuild', 'clear'].includes(action)
|
|
37
|
+
? { type: 'note', action }
|
|
38
|
+
: { type: 'error', error: 'Use /note status, refresh, rebuild, or clear.' };
|
|
39
|
+
}
|
|
40
|
+
if (trimmed === '/plan') return { type: 'plan.menu' };
|
|
41
|
+
if (exactCommand(trimmed, '/plan')) {
|
|
42
|
+
const parsed = parsePlanCreation(trimmed.slice('/plan'.length).trim());
|
|
43
|
+
return parsed.error ? { type: 'error', error: parsed.error } : { type: 'plan.create', ...parsed };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (trimmed === '/code') return { type: 'code.usage' };
|
|
47
|
+
if (exactCommand(trimmed, '/code')) {
|
|
48
|
+
let argument = trimmed.slice('/code'.length).trim();
|
|
49
|
+
if (!argument) return { type: 'code.usage' };
|
|
50
|
+
if (argument.startsWith('--prompt ')) {
|
|
51
|
+
argument = argument.slice('--prompt '.length).trim();
|
|
52
|
+
return argument
|
|
53
|
+
? { type: 'agent', mode: 'code', prompt: argument, plan: null }
|
|
54
|
+
: { type: 'code.usage' };
|
|
55
|
+
}
|
|
56
|
+
if (argument.startsWith('@')) {
|
|
57
|
+
const name = argument.slice(1).trim();
|
|
58
|
+
const plan = findPlan(name);
|
|
59
|
+
return plan
|
|
60
|
+
? { type: 'agent', mode: 'code', prompt: plan.content, plan }
|
|
61
|
+
: { type: 'error', error: `Plan not found: ${name}` };
|
|
62
|
+
}
|
|
63
|
+
const plan = findPlan(argument);
|
|
64
|
+
return plan
|
|
65
|
+
? { type: 'agent', mode: 'code', prompt: plan.content, plan }
|
|
66
|
+
: { type: 'agent', mode: 'code', prompt: argument, plan: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { type: 'agent', mode: 'chat', prompt: trimmed, plan: null };
|
|
70
|
+
}
|