zames_pro 2.29.15 → 2.31.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/dist/agent-loop.js +138 -12
- package/dist/browser.js +469 -169
- package/dist/commands.js +106 -2
- package/dist/compact.js +178 -0
- package/dist/config-menu.js +22 -5
- package/dist/config.js +52 -0
- package/dist/confirm.js +30 -6
- package/dist/context.js +3 -2
- package/dist/gitTools.js +3 -2
- package/dist/i18n.js +106 -8
- package/dist/index.js +223 -105
- package/dist/input.js +86 -11
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { isImageName } from './attachments.js';
|
|
2
3
|
// Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
|
|
3
4
|
// /permissions, /review, /add-dir). Pure functions, unit-tested without a
|
|
4
5
|
// live agent/browser. index.ts only renders their output.
|
|
@@ -342,7 +343,9 @@ export function buildCompactPrompt(locale = 'ru') {
|
|
|
342
343
|
'(3) the exact current state (files changed, commands run, their results); ' +
|
|
343
344
|
'(4) open questions and the next concrete steps. ' +
|
|
344
345
|
'Keep file paths, function/identifier names, commands and error texts verbatim. ' +
|
|
345
|
-
'Be concise but complete — no small talk, no code dumps beyond short essential snippets.'
|
|
346
|
+
'Be concise but complete — no small talk, no code dumps beyond short essential snippets. ' +
|
|
347
|
+
'Reply with the summary text ONLY. Do NOT call any tools and do NOT output tool-call ' +
|
|
348
|
+
'JSON or DSML — this is a handover summary, not a continuation of the work.');
|
|
346
349
|
}
|
|
347
350
|
return ('Сожми историю диалога в краткое резюме для НОВОГО чата. ' +
|
|
348
351
|
'Это резюме будет ЕДИНСТВЕННЫМ контекстом, с которым новый чат начнёт работу, поэтому оно должно быть самодостаточным. ' +
|
|
@@ -350,7 +353,40 @@ export function buildCompactPrompt(locale = 'ru') {
|
|
|
350
353
|
'(3) точное текущее состояние (изменённые файлы, выполненные команды и их результаты); ' +
|
|
351
354
|
'(4) открытые вопросы и следующие конкретные шаги. ' +
|
|
352
355
|
'Пути к файлам, имена функций/идентификаторов, команды и тексты ошибок сохраняй дословно. ' +
|
|
353
|
-
'Пиши кратко, но полно — без воды и без больших дампов кода (только короткие важные фрагменты).'
|
|
356
|
+
'Пиши кратко, но полно — без воды и без больших дампов кода (только короткие важные фрагменты). ' +
|
|
357
|
+
'В ответе верни ТОЛЬКО текст резюме. НЕ вызывай инструменты и НЕ выводи JSON/DSML вызова ' +
|
|
358
|
+
'инструмента — это резюме для передачи, а не продолжение работы.');
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Is the model's /compact answer a usable handover summary?
|
|
362
|
+
*
|
|
363
|
+
* The summary request is sent into the OLD chat as a normal turn, so in
|
|
364
|
+
* reasoning mode the model can answer it with a TOOL CALL instead of prose
|
|
365
|
+
* (observed live: it emitted a Bash call to "gather the current state" and
|
|
366
|
+
* never produced a summary — only a THINK fragment). A tool-call JSON is
|
|
367
|
+
* protocol noise and must NOT be carried into the new chat, otherwise the new
|
|
368
|
+
* chat starts by re-running an old command. An empty answer / the abort
|
|
369
|
+
* sentinel is unusable too.
|
|
370
|
+
*/
|
|
371
|
+
export function isUsableCompactSummary(text) {
|
|
372
|
+
const t = String(text || '').trim();
|
|
373
|
+
if (!t)
|
|
374
|
+
return false;
|
|
375
|
+
if (t.length < 40)
|
|
376
|
+
return false;
|
|
377
|
+
if (/^\(прервано пользователем\)$/.test(t))
|
|
378
|
+
return false;
|
|
379
|
+
if (parseAssistantToolCall(t))
|
|
380
|
+
return false;
|
|
381
|
+
if (/DSML/i.test(t))
|
|
382
|
+
return false;
|
|
383
|
+
// A tool-call body without a recognizable head (a cut-off stream) still
|
|
384
|
+
// carries "args": { ... }.
|
|
385
|
+
if (/"args"\s*:\s*\{/.test(t))
|
|
386
|
+
return false;
|
|
387
|
+
if (/<ds_safety>/i.test(t))
|
|
388
|
+
return false;
|
|
389
|
+
return true;
|
|
354
390
|
}
|
|
355
391
|
/**
|
|
356
392
|
* Wrap the model's summary into the text posted as the first message of the
|
|
@@ -370,6 +406,74 @@ export function buildCompactCarryover(summary, task) {
|
|
|
370
406
|
return out;
|
|
371
407
|
}
|
|
372
408
|
// ---------- /review ----------
|
|
409
|
+
// ---------- Ctrl+C escalation ----------
|
|
410
|
+
// Decide what a Ctrl+C does while the agent is busy. The FIRST press aborts
|
|
411
|
+
// the currently running TOOL; a SECOND press within the window stops the WHOLE
|
|
412
|
+
// run (including the queue). Outside the window it is a fresh first press.
|
|
413
|
+
// Returns 'tool' | 'run'.
|
|
414
|
+
export function ctrlCEscalation(sinceLastMs, windowMs = 2000) {
|
|
415
|
+
return sinceLastMs < windowMs ? 'run' : 'tool';
|
|
416
|
+
}
|
|
417
|
+
// True when the queued message is a slash-command. It must NOT be merged into
|
|
418
|
+
// a task: the main loop has to execute it (e.g. /compact, /new).
|
|
419
|
+
export function isSlashCommand(text) {
|
|
420
|
+
return String(text ?? '')
|
|
421
|
+
.trim()
|
|
422
|
+
.startsWith('/');
|
|
423
|
+
}
|
|
424
|
+
function queuedIsImage(a) {
|
|
425
|
+
return (String(a.mime || '').startsWith('image/') ||
|
|
426
|
+
isImageName(String(a.name || '')));
|
|
427
|
+
}
|
|
428
|
+
// Merge a batch of queued messages into a single task. Marker numbering is
|
|
429
|
+
// PER-MESSAGE ([image#1]/[file#1] restart in every message), so a naive
|
|
430
|
+
// concatenation would produce duplicate markers for different files. We
|
|
431
|
+
// RENUMBER the markers across the batch and rebuild the attachments array in
|
|
432
|
+
// the same order, keeping the text markers the source of truth.
|
|
433
|
+
export function mergeMessages(msgs) {
|
|
434
|
+
const list = (msgs || []).filter((m) => String(m?.text ?? '').trim() !== '');
|
|
435
|
+
if (list.length === 0)
|
|
436
|
+
return { text: '', attachments: [] };
|
|
437
|
+
if (list.length === 1) {
|
|
438
|
+
return {
|
|
439
|
+
text: list[0].text,
|
|
440
|
+
attachments: (list[0].attachments || []).slice(),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
let imgOffset = 0;
|
|
444
|
+
let fileOffset = 0;
|
|
445
|
+
const attachments = [];
|
|
446
|
+
const parts = [];
|
|
447
|
+
for (const m of list) {
|
|
448
|
+
const atts = m.attachments || [];
|
|
449
|
+
const images = atts.filter(queuedIsImage);
|
|
450
|
+
const files = atts.filter((a) => !queuedIsImage(a));
|
|
451
|
+
let text = String(m.text);
|
|
452
|
+
text = text.replace(/\[image#(\d+)\]/g, (whole, n) => {
|
|
453
|
+
const i = Number(n);
|
|
454
|
+
if (!Number.isFinite(i) || i < 1 || i > images.length)
|
|
455
|
+
return whole;
|
|
456
|
+
return '[image#' + (imgOffset + i) + ']';
|
|
457
|
+
});
|
|
458
|
+
text = text.replace(/\[file#(\d+)\]/g, (whole, n) => {
|
|
459
|
+
const i = Number(n);
|
|
460
|
+
if (!Number.isFinite(i) || i < 1 || i > files.length)
|
|
461
|
+
return whole;
|
|
462
|
+
return '[file#' + (fileOffset + i) + ']';
|
|
463
|
+
});
|
|
464
|
+
imgOffset += images.length;
|
|
465
|
+
fileOffset += files.length;
|
|
466
|
+
attachments.push(...atts);
|
|
467
|
+
parts.push(text.trim());
|
|
468
|
+
}
|
|
469
|
+
const header = 'The operator sent ' +
|
|
470
|
+
list.length +
|
|
471
|
+
' messages while you were working. They are listed below in order — ' +
|
|
472
|
+
'treat them as one instruction.';
|
|
473
|
+
const text = header + NL + NL + parts.join(NL + NL + '---' + NL + NL);
|
|
474
|
+
return { text, attachments };
|
|
475
|
+
}
|
|
476
|
+
// ---------- /review ----------
|
|
373
477
|
export function buildReviewPrompt(focus, hasStaged = false) {
|
|
374
478
|
const scope = hasStaged ? 'staged' : 'uncommitted';
|
|
375
479
|
const f = String(focus ?? '').trim();
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { translate } from './i18n.js';
|
|
2
|
+
import { buildCompactPrompt, buildCompactCarryover, isUsableCompactSummary, trimRestoredMessages, formatRestoredHistory, } from './commands.js';
|
|
3
|
+
const NL = String.fromCharCode(10);
|
|
4
|
+
/** How many times the summary call is retried before the local fallback. */
|
|
5
|
+
export const COMPACT_SUMMARY_ATTEMPTS = 3;
|
|
6
|
+
/** How many restored messages the local fallback keeps. */
|
|
7
|
+
export const COMPACT_FALLBACK_LIMIT = 40;
|
|
8
|
+
/**
|
|
9
|
+
* Compact the CURRENT chat: summarize it (retrying a few times, with a LOCAL
|
|
10
|
+
* history fallback), open a NEW chat, resend the system prompt and post the
|
|
11
|
+
* summary as the carried-over context. Shared by the manual `/compact` command
|
|
12
|
+
* and the automatic between-tools trigger, so both paths stay identical.
|
|
13
|
+
*
|
|
14
|
+
* The caller owns the browser and the throttle; this function only sends.
|
|
15
|
+
*/
|
|
16
|
+
export async function performCompact(opts) {
|
|
17
|
+
const { browser, workdir, locale, task, transcript = null, ui = null, buildSystemPrompt, tools, answerTimeoutMs, fallbackLimit = COMPACT_FALLBACK_LIMIT, skipUiLock = false, quiet = false, } = opts;
|
|
18
|
+
const t = translate(locale);
|
|
19
|
+
const log = (msg) => {
|
|
20
|
+
if (!quiet)
|
|
21
|
+
console.error(msg);
|
|
22
|
+
};
|
|
23
|
+
let currentChatId = opts.currentChatId;
|
|
24
|
+
if (!currentChatId) {
|
|
25
|
+
currentChatId = await browser.getCurrentChatId();
|
|
26
|
+
}
|
|
27
|
+
if (!currentChatId) {
|
|
28
|
+
log(t('compact.no_chat'));
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
chatId: null,
|
|
32
|
+
summaryChars: 0,
|
|
33
|
+
beforeTokens: null,
|
|
34
|
+
error: t('compact.no_chat'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const beforeTokens = browser.getLastTokenUsage
|
|
38
|
+
? browser.getLastTokenUsage()
|
|
39
|
+
: null;
|
|
40
|
+
if (ui && !skipUiLock)
|
|
41
|
+
ui.lock(t('msg.input_locked'));
|
|
42
|
+
try {
|
|
43
|
+
if (!quiet)
|
|
44
|
+
log(t('compact.start'));
|
|
45
|
+
// 1) Ask the OLD chat to summarize itself.
|
|
46
|
+
let summary = '';
|
|
47
|
+
let summaryErr = '';
|
|
48
|
+
for (let attempt = 1; attempt <= COMPACT_SUMMARY_ATTEMPTS; attempt++) {
|
|
49
|
+
let prompt = buildCompactPrompt(locale);
|
|
50
|
+
if (attempt > 1) {
|
|
51
|
+
prompt +=
|
|
52
|
+
locale === 'en'
|
|
53
|
+
? '\n\nIMPORTANT: reply with the summary as PLAIN TEXT only. Do NOT call any tools and do NOT output tool-call JSON/DSML.'
|
|
54
|
+
: '\n\nВАЖНО: ответь ТОЛЬКО текстом резюме. НЕ вызывай инструменты и НЕ выводи JSON/DSML вызова инструмента.';
|
|
55
|
+
}
|
|
56
|
+
let answer = '';
|
|
57
|
+
try {
|
|
58
|
+
answer = await browser.ask(prompt, {
|
|
59
|
+
agent: true,
|
|
60
|
+
timeout: Math.max(60_000, answerTimeoutMs),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
summaryErr = e.message;
|
|
65
|
+
}
|
|
66
|
+
const trimmed = String(answer || '').trim();
|
|
67
|
+
if (/^\(прервано пользователем\)$/.test(trimmed)) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
chatId: currentChatId,
|
|
71
|
+
summaryChars: 0,
|
|
72
|
+
beforeTokens,
|
|
73
|
+
error: trimmed,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (!summaryErr && isUsableCompactSummary(trimmed)) {
|
|
77
|
+
summary = trimmed;
|
|
78
|
+
summaryErr = '';
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!summaryErr)
|
|
82
|
+
summaryErr = trimmed || t('common.unknown');
|
|
83
|
+
if (attempt < COMPACT_SUMMARY_ATTEMPTS) {
|
|
84
|
+
log(t('compact.summary_retry', {
|
|
85
|
+
v: summaryErr,
|
|
86
|
+
attempt: String(attempt),
|
|
87
|
+
max: String(COMPACT_SUMMARY_ATTEMPTS),
|
|
88
|
+
}));
|
|
89
|
+
await new Promise((r) => setTimeout(r, 3000 * attempt));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!summary) {
|
|
93
|
+
let fallback = '';
|
|
94
|
+
try {
|
|
95
|
+
const all = browser.fetchChatMessages && currentChatId
|
|
96
|
+
? await browser.fetchChatMessages(currentChatId).catch(() => [])
|
|
97
|
+
: [];
|
|
98
|
+
const msgs = trimRestoredMessages(all, 0);
|
|
99
|
+
if (msgs.length) {
|
|
100
|
+
fallback = formatRestoredHistory(msgs, { limit: fallbackLimit });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
fallback = String(fallback || '').trim();
|
|
105
|
+
if (fallback) {
|
|
106
|
+
summary = fallback;
|
|
107
|
+
log(t('compact.summary_fallback', { v: String(fallbackLimit) }));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const err = summaryErr || t('common.unknown');
|
|
111
|
+
log(t('compact.summary_failed', { v: err }));
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
chatId: currentChatId,
|
|
115
|
+
summaryChars: 0,
|
|
116
|
+
beforeTokens,
|
|
117
|
+
error: err,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
transcript?.log('compact_summary', {
|
|
122
|
+
chars: summary.length,
|
|
123
|
+
beforeTokens,
|
|
124
|
+
});
|
|
125
|
+
// 2) New chat + system prompt + the summary as the first message.
|
|
126
|
+
await browser.newChat();
|
|
127
|
+
await browser.ask(buildSystemPrompt({ workdir, tools, locale }), {
|
|
128
|
+
timeout: 60_000,
|
|
129
|
+
agent: false,
|
|
130
|
+
});
|
|
131
|
+
await browser.ask(buildCompactCarryover(summary, task), {
|
|
132
|
+
timeout: 60_000,
|
|
133
|
+
agent: false,
|
|
134
|
+
});
|
|
135
|
+
const newChatId = browser.getCurrentChatId
|
|
136
|
+
? await browser.getCurrentChatId()
|
|
137
|
+
: null;
|
|
138
|
+
if (!quiet) {
|
|
139
|
+
log(t('compact.done') +
|
|
140
|
+
NL +
|
|
141
|
+
t('compact.report', {
|
|
142
|
+
chars: summary.length,
|
|
143
|
+
tokens: beforeTokens === null
|
|
144
|
+
? t('common.unknown')
|
|
145
|
+
: String(beforeTokens),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
if (ui && !quiet) {
|
|
149
|
+
ui.printAbove(NL +
|
|
150
|
+
'--- compacted context ---' +
|
|
151
|
+
NL +
|
|
152
|
+
summary +
|
|
153
|
+
NL +
|
|
154
|
+
'--- end ---' +
|
|
155
|
+
NL);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
chatId: newChatId,
|
|
160
|
+
summaryChars: summary.length,
|
|
161
|
+
beforeTokens,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
log(e.message);
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
chatId: currentChatId,
|
|
169
|
+
summaryChars: 0,
|
|
170
|
+
beforeTokens,
|
|
171
|
+
error: e.message,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (ui && !skipUiLock)
|
|
176
|
+
ui.unlock();
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/config-menu.js
CHANGED
|
@@ -105,11 +105,26 @@ export function runConfigMenu(opts) {
|
|
|
105
105
|
output.write(lines.join('\n') + '\n');
|
|
106
106
|
lastRows = lines.reduce((n, l) => n + rowsOf(l), 0);
|
|
107
107
|
};
|
|
108
|
-
|
|
108
|
+
// Erase the rendered menu block from the screen. Used on exit so the config
|
|
109
|
+
// list does not stay in the terminal history / scrollback (the operator
|
|
110
|
+
// asked for the menu to disappear after Esc).
|
|
111
|
+
const clearBlock = () => {
|
|
112
|
+
if (lastRows > 0) {
|
|
113
|
+
output.write(ESC + '[' + lastRows + 'A');
|
|
114
|
+
for (let i = 0; i < lastRows; i++) {
|
|
115
|
+
output.write(ESC + '[2K' + ESC + '[1B');
|
|
116
|
+
}
|
|
117
|
+
output.write(ESC + '[' + lastRows + 'A');
|
|
118
|
+
lastRows = 0;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const cleanup = (clearOnExit = false) => {
|
|
109
122
|
if (exited)
|
|
110
123
|
return;
|
|
111
124
|
exited = true;
|
|
112
125
|
input.removeListener('data', onData);
|
|
126
|
+
if (clearOnExit)
|
|
127
|
+
clearBlock();
|
|
113
128
|
if (input.setRawMode)
|
|
114
129
|
input.setRawMode(wasRaw || false);
|
|
115
130
|
output.write(ESC + '[?25h'); // restore the cursor
|
|
@@ -167,13 +182,14 @@ export function runConfigMenu(opts) {
|
|
|
167
182
|
break;
|
|
168
183
|
}
|
|
169
184
|
if (ch === ESC) {
|
|
185
|
+
// Cancel the edit (do NOT exit the menu) — Esc in the field is
|
|
186
|
+
// "abandon this value", not "close the config".
|
|
170
187
|
editing = false;
|
|
171
188
|
editBuf = '';
|
|
172
189
|
break;
|
|
173
190
|
}
|
|
174
191
|
if (ch === '\x03') {
|
|
175
|
-
cleanup();
|
|
176
|
-
output.write('\n');
|
|
192
|
+
cleanup(true);
|
|
177
193
|
return;
|
|
178
194
|
}
|
|
179
195
|
if (ch === '\x7f' || ch === '\b') {
|
|
@@ -205,8 +221,9 @@ export function runConfigMenu(opts) {
|
|
|
205
221
|
}
|
|
206
222
|
const ch = s[i];
|
|
207
223
|
if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
// Erase the menu block from the screen so the config list does not
|
|
225
|
+
// linger in the terminal history after Esc.
|
|
226
|
+
cleanup(true);
|
|
210
227
|
return;
|
|
211
228
|
}
|
|
212
229
|
if (ch === 'k') {
|
package/dist/config.js
CHANGED
|
@@ -47,6 +47,11 @@ export const DEFAULTS = {
|
|
|
47
47
|
maxIncompleteRetries: 4,
|
|
48
48
|
incompleteWaitMs: 2000,
|
|
49
49
|
autoContinue: true,
|
|
50
|
+
continueMinGapMs: 1500,
|
|
51
|
+
askDeadlineMs: 240000,
|
|
52
|
+
maxAfterToolRetries: 6,
|
|
53
|
+
autoCompact: false,
|
|
54
|
+
autoCompactPct: 95,
|
|
50
55
|
resendPromptOnResume: false,
|
|
51
56
|
deepThinking: false,
|
|
52
57
|
webSearch: true,
|
|
@@ -58,6 +63,7 @@ export const DEFAULTS = {
|
|
|
58
63
|
},
|
|
59
64
|
ui: {
|
|
60
65
|
locale: DEFAULT_LOCALE,
|
|
66
|
+
contextLimit: 1_000_000,
|
|
61
67
|
},
|
|
62
68
|
};
|
|
63
69
|
export function loadConfig() {
|
|
@@ -265,6 +271,44 @@ export const CONFIG_SCHEMA = [
|
|
|
265
271
|
labelKey: 'cfg.f.browser_autoContinue',
|
|
266
272
|
groupKey: 'cfg.group.browser',
|
|
267
273
|
},
|
|
274
|
+
{
|
|
275
|
+
path: 'browser.continueMinGapMs',
|
|
276
|
+
type: 'number',
|
|
277
|
+
min: 0,
|
|
278
|
+
max: 600000,
|
|
279
|
+
labelKey: 'cfg.f.browser_continueMinGapMs',
|
|
280
|
+
groupKey: 'cfg.group.browser',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
path: 'browser.askDeadlineMs',
|
|
284
|
+
type: 'number',
|
|
285
|
+
min: 30000,
|
|
286
|
+
max: 1800000,
|
|
287
|
+
labelKey: 'cfg.f.browser_askDeadlineMs',
|
|
288
|
+
groupKey: 'cfg.group.browser',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
path: 'browser.maxAfterToolRetries',
|
|
292
|
+
type: 'number',
|
|
293
|
+
min: 0,
|
|
294
|
+
max: 50,
|
|
295
|
+
labelKey: 'cfg.f.browser_maxAfterToolRetries',
|
|
296
|
+
groupKey: 'cfg.group.browser',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
path: 'browser.autoCompact',
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
labelKey: 'cfg.f.browser_autoCompact',
|
|
302
|
+
groupKey: 'cfg.group.browser',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
path: 'browser.autoCompactPct',
|
|
306
|
+
type: 'number',
|
|
307
|
+
min: 50,
|
|
308
|
+
max: 100,
|
|
309
|
+
labelKey: 'cfg.f.browser_autoCompactPct',
|
|
310
|
+
groupKey: 'cfg.group.browser',
|
|
311
|
+
},
|
|
268
312
|
{
|
|
269
313
|
path: 'browser.resendPromptOnResume',
|
|
270
314
|
type: 'boolean',
|
|
@@ -283,6 +327,14 @@ export const CONFIG_SCHEMA = [
|
|
|
283
327
|
labelKey: 'cfg.f.browser_webSearch',
|
|
284
328
|
groupKey: 'cfg.group.browser',
|
|
285
329
|
},
|
|
330
|
+
{
|
|
331
|
+
path: 'ui.contextLimit',
|
|
332
|
+
type: 'number',
|
|
333
|
+
min: 10_000,
|
|
334
|
+
max: 10_000_000,
|
|
335
|
+
labelKey: 'cfg.f.ui_contextLimit',
|
|
336
|
+
groupKey: 'cfg.group.ui',
|
|
337
|
+
},
|
|
286
338
|
];
|
|
287
339
|
export function getConfigField(path) {
|
|
288
340
|
return CONFIG_SCHEMA.find((f) => f.path === path);
|
package/dist/confirm.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import * as readlinePromises from 'readline/promises';
|
|
2
2
|
import { theme } from './theme.js';
|
|
3
3
|
import { unifiedDiff, colorDiff } from './diff.js';
|
|
4
|
+
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
4
5
|
export class ConfirmManager {
|
|
5
6
|
settings;
|
|
6
7
|
alwaysConfirm;
|
|
7
8
|
allowedForSession;
|
|
8
|
-
|
|
9
|
+
locale;
|
|
10
|
+
constructor({ config, locale = DEFAULT_LOCALE, } = {}) {
|
|
9
11
|
const c = config || {};
|
|
12
|
+
this.locale = locale;
|
|
10
13
|
this.settings = {
|
|
11
14
|
write: c.write !== false,
|
|
12
15
|
edit: c.edit !== false,
|
|
13
16
|
bash: c.bash !== false,
|
|
14
17
|
};
|
|
15
|
-
|
|
18
|
+
// An invalid operator-supplied regex must NEVER crash the run. Fall back to
|
|
19
|
+
// a literal substring match (case-insensitive) for that pattern.
|
|
20
|
+
this.alwaysConfirm = (c.alwaysConfirm || []).map((p) => {
|
|
21
|
+
try {
|
|
22
|
+
return new RegExp(p, 'i');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return new RegExp(escapeRegExp(p), 'i');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
16
28
|
this.allowedForSession = new Set();
|
|
17
29
|
}
|
|
18
30
|
_matchesAlwaysConfirm(command) {
|
|
@@ -28,21 +40,27 @@ export class ConfirmManager {
|
|
|
28
40
|
return false;
|
|
29
41
|
return true;
|
|
30
42
|
}
|
|
43
|
+
// Hot-update the language (from /config lang). The manager holds the locale
|
|
44
|
+
// so its prompt follows the interface language without a restart.
|
|
45
|
+
setLocale(locale) {
|
|
46
|
+
this.locale = locale;
|
|
47
|
+
}
|
|
31
48
|
async ask({ kind, target, preview = null, message, }) {
|
|
32
49
|
if (!this.shouldAsk(kind, target ?? null))
|
|
33
50
|
return true;
|
|
34
51
|
if (preview) {
|
|
35
52
|
console.log(preview);
|
|
36
53
|
}
|
|
37
|
-
const
|
|
54
|
+
const t = translate(this.locale);
|
|
55
|
+
const label = message || t('confirm.ask_label') + ' ' + kind + '?';
|
|
38
56
|
const question = theme.warn(`${label} `) +
|
|
39
57
|
theme.system('[') +
|
|
40
58
|
theme.assistant('y') +
|
|
41
|
-
theme.system(' =
|
|
59
|
+
theme.system(' = ' + t('common.yes') + ', ') +
|
|
42
60
|
theme.error('n') +
|
|
43
|
-
theme.system(' =
|
|
61
|
+
theme.system(' = ' + t('common.no') + ', ') +
|
|
44
62
|
theme.user('a') +
|
|
45
|
-
theme.system(' =
|
|
63
|
+
theme.system(' = ' + t('confirm.hint') + '] ');
|
|
46
64
|
const answer = await this._prompt(question);
|
|
47
65
|
if (answer === 'a') {
|
|
48
66
|
if (kind !== 'bash' || !this._matchesAlwaysConfirm(target ?? '')) {
|
|
@@ -75,6 +93,12 @@ export class ConfirmManager {
|
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
95
|
}
|
|
96
|
+
// Escape a string so it can be used as a literal regex (used when an
|
|
97
|
+
// operator-supplied alwaysConfirm pattern is invalid and we fall back to a
|
|
98
|
+
// substring match).
|
|
99
|
+
function escapeRegExp(s) {
|
|
100
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, (m) => '\\' + m);
|
|
101
|
+
}
|
|
78
102
|
export function formatDiffPreview(oldText, newText, { label } = {}) {
|
|
79
103
|
const diff = unifiedDiff(oldText, newText, { label });
|
|
80
104
|
return colorDiff(diff);
|
package/dist/context.js
CHANGED
|
@@ -25,9 +25,10 @@ async function readIfFile(p, budget) {
|
|
|
25
25
|
if (!st.isFile())
|
|
26
26
|
return null;
|
|
27
27
|
const raw = await fs.readFile(p, 'utf-8');
|
|
28
|
+
// An EXISTING but empty file is still present: keep it so the prompt
|
|
29
|
+
// shows an (empty) AGENTS.md/MEMORY.md section instead of silently
|
|
30
|
+
// dropping the file. Only a read failure returns null.
|
|
28
31
|
const content = clipIf(path.basename(p), raw, budget);
|
|
29
|
-
if (!content)
|
|
30
|
-
return null;
|
|
31
32
|
return { path: p, content };
|
|
32
33
|
}
|
|
33
34
|
catch {
|
package/dist/gitTools.js
CHANGED
|
@@ -177,8 +177,9 @@ export function createGitTools(workdir) {
|
|
|
177
177
|
}
|
|
178
178
|
// We stage everything only if the index is empty (as the description promises).
|
|
179
179
|
const staged = await runGit('git diff --cached --name-only', workdir, 10_000);
|
|
180
|
-
const
|
|
181
|
-
|
|
180
|
+
const stagedTrimmed = staged.trim();
|
|
181
|
+
const hasStaged = stagedTrimmed !== '' &&
|
|
182
|
+
stagedTrimmed !== '(command produced no output)';
|
|
182
183
|
const addResult = hasStaged
|
|
183
184
|
? '(index is not empty — add -A skipped)'
|
|
184
185
|
: await runGit('git add -A', workdir, 20_000);
|