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/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
- function stripAnsi(str) {
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
- stdout.write(`\x1b[90m${truncateDisplay(description, width)}\x1b[0m\n\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');
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(`› \x1b[90m${truncateDisplay(placeholder, promptWidth() - 3)}\x1b[0m\n`);
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(`› \x1b[90m${truncateDisplay(placeholder, promptWidth() - 3)}\x1b[0m\n`);
231
+ stdout.write(renderPromptLine(placeholder, { muted: true }));
254
232
  } else {
255
- stdout.write(`› ${getInputView().visible}\n`);
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[1B\x1b[1G\x1b[2K\r');
271
- 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);
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
- export async function selectEffortMenu(currentEffort, currentLang) {
362
- const levels = ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
363
- const descriptions = currentLang === 'cn'
364
- ? [
365
- '快速回答 · 最多 3 步 · 单次读取 150 行',
366
- '轻量任务 · 最多 6 步 · 单次读取 300 行',
367
- '日常编程(推荐)· 最多 12 步 · 单次读取 600 行',
368
- '深入分析 · 最多 25 步 · 单次读取 1200 行',
369
- '复杂任务 · 最多 50 · 单次读取 2500 行',
370
- '超长任务 · 最多 150 步 · 大文件消化前会确认'
371
- ]
372
- : [
373
- 'Quick answers · up to 3 steps · 150 lines per read',
374
- 'Light tasks · up to 6 steps · 300 lines per read',
375
- 'Daily coding (recommended) · up to 12 steps · 600 lines per read',
376
- 'Deep analysis · up to 25 steps · 1200 lines per read',
377
- 'Complex tasks · up to 50 steps · 2500 lines per read',
378
- 'Long-running tasks · up to 150 steps · confirms large-file digestion'
379
- ];
380
- const selected = await selectMenu(
381
- currentLang === 'cn' ? '调整努力模式' : 'Adjust Effort Mode',
382
- currentLang === 'cn' ? '选择速度、上下文容量与连续工具步骤之间的平衡。' : 'Balance speed, context capacity, and consecutive tool steps.',
383
- levels.map((level, index) => ({ name: level, desc: descriptions[index] })),
384
- Math.max(0, levels.indexOf(currentEffort))
385
- );
386
- return selected === -1 ? null : levels[selected];
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
+ }
@@ -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());
@@ -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
- ...profile,
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
- "temperature": 0.2
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
- export const EFFORT_PRESETS = {
253
- low: {
254
- maxLoops: 3,
255
- maxReadLines: 150,
256
- label: 'low'
257
- },
258
- medium: {
259
- maxLoops: 6,
260
- maxReadLines: 300,
261
- label: 'medium'
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 getActiveEffort() {
287
- const active = getActiveProfile();
288
- return active ? (active.effort || 'high') : 'high';
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 setActiveEffort(effort) {
292
- if (!Object.prototype.hasOwnProperty.call(EFFORT_PRESETS, effort)) return false;
293
- const active = getActiveProfile();
294
- if (active) {
295
- updateProfile(active.model, { effort });
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
  }