dave-code 1.1.0 → 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 +25 -6
- package/bin/aiClient.js +292 -67
- package/bin/check.js +11 -0
- package/bin/cliMenu.js +205 -68
- package/bin/commandRouter.js +20 -0
- package/bin/configManager.js +71 -47
- package/bin/contextManager.js +107 -91
- package/bin/index.js +1413 -243
- package/bin/markdownRenderer.js +264 -0
- package/bin/memoryManager.js +182 -0
- package/bin/planManager.js +61 -9
- package/bin/projectNotebookManager.js +839 -0
- package/bin/runtimeEvents.js +42 -0
- package/bin/scanManager.js +561 -0
- package/bin/sessionManager.js +17 -1
- package/bin/terminalRenderer.js +543 -125
- 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 +688 -133
- package/package.json +3 -5
package/bin/cliMenu.js
CHANGED
|
@@ -8,43 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import readline from 'readline';
|
|
11
|
+
import { formatConversationMessage } from './terminalRenderer.js';
|
|
12
|
+
import { displayWidth as getStringWidth, truncateEnd, padEnd } from './textWidth.js';
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
return String(str || '').replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function getStringWidth(str) {
|
|
17
|
-
const cleanStr = stripAnsi(str);
|
|
18
|
-
let width = 0;
|
|
19
|
-
for (let i = 0; i < cleanStr.length; i++) {
|
|
20
|
-
const code = cleanStr.charCodeAt(i);
|
|
21
|
-
if (
|
|
22
|
-
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
23
|
-
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
24
|
-
(code >= 0x3000 && code <= 0x303f) ||
|
|
25
|
-
(code >= 0xff00 && code <= 0xffef)
|
|
26
|
-
) {
|
|
27
|
-
width += 2;
|
|
28
|
-
} else {
|
|
29
|
-
width += 1;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return width;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function truncateDisplay(str, maxWidth) {
|
|
36
|
-
const text = String(str || '');
|
|
37
|
-
if (getStringWidth(text) <= maxWidth) return text;
|
|
38
|
-
let width = 0;
|
|
39
|
-
let output = '';
|
|
40
|
-
for (const char of text) {
|
|
41
|
-
const charWidth = getStringWidth(char);
|
|
42
|
-
if (width + charWidth > maxWidth - 3) break;
|
|
43
|
-
output += char;
|
|
44
|
-
width += charWidth;
|
|
45
|
-
}
|
|
46
|
-
return output + '...';
|
|
47
|
-
}
|
|
14
|
+
const truncateDisplay = (str, maxWidth) => truncateEnd(str, maxWidth, '...');
|
|
48
15
|
|
|
49
16
|
function enterMenuScreen(stdout) {
|
|
50
17
|
stdout.write('\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J');
|
|
@@ -81,7 +48,10 @@ export function selectMenu(title, description, options, defaultIndex = 0) {
|
|
|
81
48
|
const descWidth = Math.max(10, width - nameWidth - 10);
|
|
82
49
|
|
|
83
50
|
stdout.write(`\x1b[1;36m${truncateDisplay(title, width)}\x1b[0m\n`);
|
|
84
|
-
|
|
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');
|
|
85
55
|
|
|
86
56
|
for (let i = 0; i < options.length; i++) {
|
|
87
57
|
const opt = options[i];
|
|
@@ -210,8 +180,16 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
210
180
|
: '/help for help · Ctrl+C to exit';
|
|
211
181
|
|
|
212
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
|
+
}
|
|
190
|
+
|
|
213
191
|
stdout.write(`\x1b[90m${'─'.repeat(promptWidth())}\x1b[0m\n`);
|
|
214
|
-
stdout.write(
|
|
192
|
+
stdout.write(renderPromptLine(placeholder, { muted: true }));
|
|
215
193
|
stdout.write(`\x1b[90m${truncateDisplay(footerText, promptWidth())}\x1b[0m`);
|
|
216
194
|
|
|
217
195
|
stdout.write('\x1b[1A\x1b[3G');
|
|
@@ -250,9 +228,9 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
250
228
|
|
|
251
229
|
const valueStr = chars.join('');
|
|
252
230
|
if (valueStr.length === 0) {
|
|
253
|
-
stdout.write(
|
|
231
|
+
stdout.write(renderPromptLine(placeholder, { muted: true }));
|
|
254
232
|
} else {
|
|
255
|
-
stdout.write(
|
|
233
|
+
stdout.write(renderPromptLine(getInputView().visible));
|
|
256
234
|
}
|
|
257
235
|
|
|
258
236
|
stdout.write(`\x1b[90m${truncateDisplay(footerText, promptWidth())}\x1b[0m`);
|
|
@@ -266,9 +244,16 @@ export function chatInputPrompt(placeholder, currentLang) {
|
|
|
266
244
|
if (!key) return;
|
|
267
245
|
|
|
268
246
|
if (key.name === 'return') {
|
|
247
|
+
const submitted = chars.join('').trim();
|
|
269
248
|
cleanup();
|
|
270
|
-
stdout.write('\x1b[
|
|
271
|
-
|
|
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);
|
|
272
257
|
} else if (key.name === 'backspace') {
|
|
273
258
|
if (cursorIndex > 0) {
|
|
274
259
|
chars.splice(cursorIndex - 1, 1);
|
|
@@ -358,32 +343,153 @@ export function waitForEnter(promptText) {
|
|
|
358
343
|
});
|
|
359
344
|
}
|
|
360
345
|
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
}
|
|
351
|
+
|
|
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('');
|
|
364
|
+
}
|
|
365
|
+
|
|
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);
|
|
406
|
+
readline.emitKeypressEvents(stdin);
|
|
407
|
+
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
408
|
+
stdin.resume();
|
|
409
|
+
enterMenuScreen(stdout);
|
|
410
|
+
|
|
411
|
+
function draw() {
|
|
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 }));
|
|
416
|
+
}
|
|
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);
|
|
423
|
+
}
|
|
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);
|
|
432
|
+
} else if (key.name === 'return') {
|
|
433
|
+
cleanup(); resolve(selected);
|
|
434
|
+
} else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
435
|
+
cleanup(); resolve(null);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
stdin.on('keypress', onKeypress);
|
|
439
|
+
redraw();
|
|
440
|
+
});
|
|
441
|
+
}
|
|
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
|
+
}
|
|
387
493
|
}
|
|
388
494
|
|
|
389
495
|
export function confirmPrompt(promptText) {
|
|
@@ -430,3 +536,34 @@ export function confirmPrompt(promptText) {
|
|
|
430
536
|
stdin.on('keypress', onKeypress);
|
|
431
537
|
});
|
|
432
538
|
}
|
|
539
|
+
|
|
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
|
+
}
|
package/bin/commandRouter.js
CHANGED
|
@@ -17,6 +17,26 @@ export function parsePlanCreation(argument) {
|
|
|
17
17
|
|
|
18
18
|
export function parseInputCommand(input, { findPlan = () => null } = {}) {
|
|
19
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
|
+
}
|
|
20
40
|
if (trimmed === '/plan') return { type: 'plan.menu' };
|
|
21
41
|
if (exactCommand(trimmed, '/plan')) {
|
|
22
42
|
const parsed = parsePlanCreation(trimmed.slice('/plan'.length).trim());
|
package/bin/configManager.js
CHANGED
|
@@ -54,14 +54,37 @@ function normalizeProfile(profile, index) {
|
|
|
54
54
|
}
|
|
55
55
|
const maxOutputTokens = Number(profile.maxOutputTokens ?? 4096);
|
|
56
56
|
const temperature = Number(profile.temperature ?? 0.2);
|
|
57
|
+
const contextWindowTokens = profile.contextWindowTokens === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: Number(profile.contextWindowTokens);
|
|
57
60
|
if (!Number.isInteger(maxOutputTokens) || maxOutputTokens < 1 || maxOutputTokens > 200000) {
|
|
58
61
|
throw new Error(`Profile ${index + 1} has invalid maxOutputTokens.`);
|
|
59
62
|
}
|
|
60
63
|
if (!Number.isFinite(temperature) || temperature < 0 || temperature > 2) {
|
|
61
64
|
throw new Error(`Profile ${index + 1} has invalid temperature.`);
|
|
62
65
|
}
|
|
66
|
+
if (contextWindowTokens !== undefined && (!Number.isInteger(contextWindowTokens) || contextWindowTokens < 8192 || contextWindowTokens > 4000000)) {
|
|
67
|
+
throw new Error(`Profile ${index + 1} has invalid contextWindowTokens.`);
|
|
68
|
+
}
|
|
69
|
+
const { effort: _legacyEffort, ...profileWithoutEffort } = profile;
|
|
70
|
+
const rawThunder = profile.thunder && typeof profile.thunder === 'object' && !Array.isArray(profile.thunder)
|
|
71
|
+
? profile.thunder
|
|
72
|
+
: {};
|
|
73
|
+
const rawPermission = profile.permissionPolicy && typeof profile.permissionPolicy === 'object' && !Array.isArray(profile.permissionPolicy)
|
|
74
|
+
? profile.permissionPolicy
|
|
75
|
+
: {};
|
|
76
|
+
const permissionMode = ['ask', 'acceptEdits', 'allowlist'].includes(rawPermission.mode) ? rawPermission.mode : 'ask';
|
|
77
|
+
const permissionRules = Array.isArray(rawPermission.rules)
|
|
78
|
+
? rawPermission.rules.map(String).map(rule => rule.trim()).filter(Boolean).slice(0, 100)
|
|
79
|
+
: [];
|
|
80
|
+
const roleProfiles = {};
|
|
81
|
+
for (const [role, modelId] of Object.entries(rawThunder.roleProfiles || {})) {
|
|
82
|
+
if (!['pm', 'techLead', 'engineer', 'reviewer'].includes(role)) continue;
|
|
83
|
+
const value = String(modelId || '').trim();
|
|
84
|
+
if (value) roleProfiles[role] = value;
|
|
85
|
+
}
|
|
63
86
|
return {
|
|
64
|
-
...
|
|
87
|
+
...profileWithoutEffort,
|
|
65
88
|
model,
|
|
66
89
|
apiKey: String(profile.apiKey || ''),
|
|
67
90
|
apiBase: String(profile.apiBase || '').replace(/\/+$/, ''),
|
|
@@ -69,7 +92,13 @@ function normalizeProfile(profile, index) {
|
|
|
69
92
|
...(provider ? { provider } : {}),
|
|
70
93
|
toolMode,
|
|
71
94
|
maxOutputTokens,
|
|
72
|
-
temperature
|
|
95
|
+
temperature,
|
|
96
|
+
...(contextWindowTokens ? { contextWindowTokens } : {}),
|
|
97
|
+
permissionPolicy: { mode: permissionMode, rules: permissionRules },
|
|
98
|
+
thunder: {
|
|
99
|
+
roleProfiles,
|
|
100
|
+
defaultTier: rawThunder.defaultTier === 'performance' ? 'performance' : 'balanced'
|
|
101
|
+
}
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
104
|
|
|
@@ -99,7 +128,16 @@ const TEMPLATE_CONFIG = [
|
|
|
99
128
|
"provider": "openai",
|
|
100
129
|
"toolMode": "native",
|
|
101
130
|
"maxOutputTokens": 4096,
|
|
102
|
-
"
|
|
131
|
+
"contextWindowTokens": 32768,
|
|
132
|
+
"temperature": 0.2,
|
|
133
|
+
"permissionPolicy": {
|
|
134
|
+
"mode": "ask",
|
|
135
|
+
"rules": []
|
|
136
|
+
},
|
|
137
|
+
"thunder": {
|
|
138
|
+
"defaultTier": "balanced",
|
|
139
|
+
"roleProfiles": {}
|
|
140
|
+
}
|
|
103
141
|
}
|
|
104
142
|
];
|
|
105
143
|
|
|
@@ -137,6 +175,17 @@ export function getActiveProfile() {
|
|
|
137
175
|
return profiles.length > 0 ? profiles[0] : null;
|
|
138
176
|
}
|
|
139
177
|
|
|
178
|
+
export function getThunderProfile(role) {
|
|
179
|
+
const profiles = getProfiles();
|
|
180
|
+
const active = profiles[0] || null;
|
|
181
|
+
if (!active) return null;
|
|
182
|
+
const group = ['pm', 'techLead'].includes(role)
|
|
183
|
+
? role
|
|
184
|
+
: ['qa', 'designer', 'securityData'].includes(role) ? 'reviewer' : 'engineer';
|
|
185
|
+
const requested = active.thunder?.roleProfiles?.[group];
|
|
186
|
+
return profiles.find(profile => profile.model === requested) || active;
|
|
187
|
+
}
|
|
188
|
+
|
|
140
189
|
export function setActiveProfile(modelId) {
|
|
141
190
|
const profiles = getProfiles();
|
|
142
191
|
const idx = profiles.findIndex(p => p.model === modelId);
|
|
@@ -249,51 +298,26 @@ export function getApiBase() {
|
|
|
249
298
|
return active ? active.apiBase : '';
|
|
250
299
|
}
|
|
251
300
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
},
|
|
263
|
-
high: {
|
|
264
|
-
maxLoops: 12,
|
|
265
|
-
maxReadLines: 600,
|
|
266
|
-
label: 'high'
|
|
267
|
-
},
|
|
268
|
-
xhigh: {
|
|
269
|
-
maxLoops: 25,
|
|
270
|
-
maxReadLines: 1200,
|
|
271
|
-
label: 'xhigh'
|
|
272
|
-
},
|
|
273
|
-
max: {
|
|
274
|
-
maxLoops: 50,
|
|
275
|
-
maxReadLines: 2500,
|
|
276
|
-
label: 'max'
|
|
277
|
-
},
|
|
278
|
-
ultracode: {
|
|
279
|
-
maxLoops: 150,
|
|
280
|
-
maxReadLines: 8000,
|
|
281
|
-
label: 'ultracode',
|
|
282
|
-
useWorkflows: true
|
|
283
|
-
}
|
|
284
|
-
};
|
|
301
|
+
const MODEL_CONTEXT_WINDOWS = [
|
|
302
|
+
[/claude-(?:opus|sonnet|haiku)-4/i, 200000],
|
|
303
|
+
[/claude-3/i, 200000],
|
|
304
|
+
[/gemini-(?:2\.5|3)/i, 1000000],
|
|
305
|
+
[/gpt-5|codex/i, 400000],
|
|
306
|
+
[/gpt-4\.1/i, 1000000],
|
|
307
|
+
[/gpt-4o/i, 128000],
|
|
308
|
+
[/deepseek/i, 64000],
|
|
309
|
+
[/qwen/i, 128000]
|
|
310
|
+
];
|
|
285
311
|
|
|
286
|
-
export function
|
|
287
|
-
|
|
288
|
-
|
|
312
|
+
export function inferContextWindowTokens(profile = getActiveProfile()) {
|
|
313
|
+
if (Number.isInteger(profile?.contextWindowTokens)) return profile.contextWindowTokens;
|
|
314
|
+
const model = String(profile?.model || '');
|
|
315
|
+
return MODEL_CONTEXT_WINDOWS.find(([pattern]) => pattern.test(model))?.[1] || 32768;
|
|
289
316
|
}
|
|
290
317
|
|
|
291
|
-
export function
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
return false;
|
|
318
|
+
export function getUsableContextTokens(profile = getActiveProfile()) {
|
|
319
|
+
const windowTokens = inferContextWindowTokens(profile);
|
|
320
|
+
const outputReserve = Number(profile?.maxOutputTokens) || 4096;
|
|
321
|
+
const protocolReserve = Math.max(2048, Math.ceil(windowTokens * 0.08));
|
|
322
|
+
return Math.max(4096, windowTokens - outputReserve - protocolReserve);
|
|
299
323
|
}
|