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/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 ora from 'ora';
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
- // Hide terminal cursor
27
- stdout.write('\x1b[?25l');
42
+ enterMenuScreen(stdout);
28
43
 
29
44
  function draw() {
30
- stdout.write(`\n\x1b[1;36m${title}\x1b[0m\n`);
31
- stdout.write(`\x1b[90m${description}\x1b[0m\n\n`);
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}. ${opt.name}\x1b[0m \x1b[90m${opt.desc}\x1b[0m\n`);
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}. ${opt.name} \x1b[90m${opt.desc}\x1b[0m\n`);
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 Arrow Keys (↑/↓) to navigate, Enter to select, Esc to cancel\x1b[0m\n');
66
+ stdout.write('\n\x1b[90mUse ↑/↓ or number keys, Enter to select, Esc to cancel\x1b[0m\n');
42
67
  }
43
68
 
44
- function clearLines(count) {
45
- for (let i = 0; i < count; i++) {
46
- stdout.write('\x1b[1A\x1b[2K');
47
- }
69
+ function redrawMenu() {
70
+ redrawMenuScreen(stdout, draw);
48
71
  }
49
72
 
50
- const totalLines = options.length + 6;
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
- clearLines(totalLines);
59
- draw();
80
+ redrawMenu();
60
81
  } else if (key.name === 'down') {
61
82
  cursor = (cursor + 1) % options.length;
62
- clearLines(totalLines);
63
- draw();
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
- ? '? 获取帮助 · Ctrl+C 退出'
157
- : '? for help · Ctrl+C to exit';
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('\x1b[90m───────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\n');
160
- stdout.write(`› \x1b[90m${placeholder}\x1b[0m\n`);
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 getStringWidth(str) {
176
- const cleanStr = str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
177
- let width = 0;
178
- for (let i = 0; i < cleanStr.length; i++) {
179
- const code = cleanStr.charCodeAt(i);
180
- if (
181
- (code >= 0x4e00 && code <= 0x9fff) ||
182
- (code >= 0x3400 && code <= 0x4dbf) ||
183
- (code >= 0x3000 && code <= 0x303f) ||
184
- (code >= 0xff00 && code <= 0xffef)
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 width;
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(`› \x1b[90m${placeholder}\x1b[0m\n`);
231
+ stdout.write(renderPromptLine(placeholder, { muted: true }));
201
232
  } else {
202
- stdout.write(`› ${valueStr}\n`);
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 leftPart = chars.slice(0, cursorIndex).join('');
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[1B\x1b[1G\x1b[2K\r');
219
- resolve(chars.join('').trim());
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
- export function startSpinner(prefixText) {
306
- const spinner = ora({
307
- text: prefixText,
308
- color: 'cyan',
309
- spinner: 'dots'
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
- return () => {
313
- spinner.stop();
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 selectEffortMenu(currentEffort, currentLang) {
318
- const levels = ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
319
- let cursor = levels.indexOf(currentEffort);
320
- if (cursor === -1) cursor = 2; // Default to 'high'
321
-
322
- return new Promise((resolve) => {
323
- const stdout = process.stdout;
324
- const stdin = process.stdin;
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
- const oldRawMode = stdin.isRawMode;
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.write(`\n\x1b[1;36m${title}\x1b[0m\n`);
347
- stdout.write(`\x1b[90m${description}\x1b[0m\n\n`);
348
-
349
- stdout.write(`Faster Smarter\n`);
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 clearLines(count) {
399
- for (let i = 0; i < count; i++) {
400
- stdout.write('\x1b[1A\x1b[2K');
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
- const totalLines = 10;
405
- draw();
406
-
407
- function onKeypress(str, key) {
408
- if (!key) return;
409
-
410
- if (key.name === 'left') {
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
+ }