zames_pro 2.29.16 → 2.32.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 +422 -145
- package/dist/commands.js +78 -0
- package/dist/compact.js +178 -0
- package/dist/config-menu.js +22 -5
- package/dist/config.js +43 -0
- package/dist/confirm.js +30 -6
- package/dist/context.js +3 -2
- package/dist/gitTools.js +3 -2
- package/dist/i18n.js +94 -8
- package/dist/index.js +222 -174
- package/dist/input.js +104 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,8 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
|
|
|
16
16
|
import { Transcript } from './transcript.js';
|
|
17
17
|
import { UndoStore } from './undo.js';
|
|
18
18
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
19
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT,
|
|
19
|
+
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, isSlashCommand, ctrlCEscalation, } from './commands.js';
|
|
20
|
+
import { performCompact } from './compact.js';
|
|
20
21
|
import { renderMarkdown } from './markdown.js';
|
|
21
22
|
import { closeWeb } from './web.js';
|
|
22
23
|
import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
|
|
@@ -57,8 +58,8 @@ function getPositional() {
|
|
|
57
58
|
}
|
|
58
59
|
// /compact asks the OLD chat for a summary. If browser.ask() exhausts its own
|
|
59
60
|
// rate-limit budget, aborting the whole compaction used to force the operator
|
|
60
|
-
// to start over. We retry the summary call a few times instead
|
|
61
|
-
|
|
61
|
+
// to start over. We retry the summary call a few times instead (inside
|
|
62
|
+
// performCompact()).
|
|
62
63
|
// How many restored messages the LOCAL fallback summary keeps when the model
|
|
63
64
|
// could not produce one (a rate limit / server error must not lose the
|
|
64
65
|
// compaction).
|
|
@@ -287,6 +288,7 @@ ${theme.bold(t('help.commands'))}
|
|
|
287
288
|
${t('help.cmd.add_dir')}
|
|
288
289
|
${t('help.cmd.review')}
|
|
289
290
|
${t('help.cmd.compact')}
|
|
291
|
+
${t('help.cmd.queue')}
|
|
290
292
|
${t('help.cmd.config')}
|
|
291
293
|
${t('help.cmd.lang')}
|
|
292
294
|
${t('help.cmd.debug_dom')}
|
|
@@ -343,6 +345,7 @@ const SLASH_COMMANDS = [
|
|
|
343
345
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
344
346
|
{ name: '/review', key: 'help.cmd.review' },
|
|
345
347
|
{ name: '/compact', key: 'help.cmd.compact' },
|
|
348
|
+
{ name: '/queue', key: 'help.cmd.queue' },
|
|
346
349
|
{ name: '/config', key: 'help.cmd.config' },
|
|
347
350
|
{ name: '/skills', key: 'help.cmd.skills' },
|
|
348
351
|
{ name: '/memory', key: 'help.cmd.memory' },
|
|
@@ -844,7 +847,7 @@ async function resolveWorkdir() {
|
|
|
844
847
|
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
|
|
845
848
|
const stat = await fs.stat(dir).catch(() => null);
|
|
846
849
|
if (!stat || !stat.isDirectory()) {
|
|
847
|
-
throw new Error('
|
|
850
|
+
throw new Error(t('msg.not_dir', { v: dir }));
|
|
848
851
|
}
|
|
849
852
|
return dir;
|
|
850
853
|
}
|
|
@@ -903,6 +906,16 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
903
906
|
const restoredTokens = browser.getLastTokenUsage();
|
|
904
907
|
if (typeof restoredTokens === 'number') {
|
|
905
908
|
out(theme.dim(t('chats.history_tokens', { v: String(restoredTokens) })));
|
|
909
|
+
// Warn when the restored chat is near the context limit, and suggest
|
|
910
|
+
// /compact. The threshold mirrors the auto-compact one so the operator
|
|
911
|
+
// sees the same signal whether or not auto-compact is enabled.
|
|
912
|
+
const limit = config.ui.contextLimit;
|
|
913
|
+
if (limit > 0) {
|
|
914
|
+
const pct = (restoredTokens / limit) * 100;
|
|
915
|
+
if (pct >= 80) {
|
|
916
|
+
out(theme.warn(t('chats.history_near_limit', { pct: String(Math.round(pct)) })));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
906
919
|
}
|
|
907
920
|
for (const m of messages) {
|
|
908
921
|
if (m.role === 'user') {
|
|
@@ -922,7 +935,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
922
935
|
}
|
|
923
936
|
// ---------- task runner ----------
|
|
924
937
|
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
925
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, } = opts;
|
|
938
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, } = opts;
|
|
926
939
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
927
940
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
928
941
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -971,6 +984,11 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
971
984
|
// the whole pre-send phase (chat creation, throttle wait) with no
|
|
972
985
|
// generation in flight.
|
|
973
986
|
while (true) {
|
|
987
|
+
// T7: per-task summary (duration + number of tool calls). Counts tools
|
|
988
|
+
// via the UI callback and reports ONE dim line after the task, so the
|
|
989
|
+
// operator sees how long it took and how much work happened.
|
|
990
|
+
const taskStart = Date.now();
|
|
991
|
+
let taskTools = 0;
|
|
974
992
|
const outcome = await mod.runAgentLoop({
|
|
975
993
|
browser,
|
|
976
994
|
tools,
|
|
@@ -984,13 +1002,27 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
984
1002
|
onThinking: () => ui.thinking(),
|
|
985
1003
|
onSendPause: (seconds) => ui.sendPause(seconds),
|
|
986
1004
|
onNotice: (msg) => ui.warning(msg),
|
|
987
|
-
onToolCall: (name, toolArgs) =>
|
|
1005
|
+
onToolCall: (name, toolArgs) => {
|
|
1006
|
+
taskTools++;
|
|
1007
|
+
ui.toolCall(name, toolArgs);
|
|
1008
|
+
},
|
|
988
1009
|
onToolResult: (result) => ui.toolResult(result),
|
|
989
1010
|
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
990
1011
|
onWarning: (msg) => ui.warning(msg),
|
|
991
1012
|
onChatReady,
|
|
992
1013
|
debugLog: debug,
|
|
993
1014
|
locale: currentLocale,
|
|
1015
|
+
askDeadlineMs: config.browser.askDeadlineMs,
|
|
1016
|
+
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
1017
|
+
// Auto-compact between tool calls when the context nears the
|
|
1018
|
+
// window limit. The callback is provided by the caller (runTask) so it
|
|
1019
|
+
// can refresh the outer currentChatId. It runs in the SAME execution
|
|
1020
|
+
// context (this task owns the browser send loop) and the loop awaits
|
|
1021
|
+
// it, so it serializes with the throttle.
|
|
1022
|
+
onAutoCompact,
|
|
1023
|
+
autoCompactPct,
|
|
1024
|
+
contextLimit,
|
|
1025
|
+
getTokenUsage,
|
|
994
1026
|
});
|
|
995
1027
|
// The loop may end WITHOUT a model answer: an exhausted iteration
|
|
996
1028
|
// limit or an ask() watchdog (the model stopped responding). In that
|
|
@@ -1002,6 +1034,16 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1002
1034
|
ui.warning(outcome);
|
|
1003
1035
|
transcript?.log('agent_no_answer', { outcome });
|
|
1004
1036
|
}
|
|
1037
|
+
// T7: one dim summary line per task (duration + tool calls). Printed
|
|
1038
|
+
// even on an abort, so the operator sees what happened.
|
|
1039
|
+
const summary = theme.dim(t('msg.task_summary', {
|
|
1040
|
+
sec: ((Date.now() - taskStart) / 1000).toFixed(1),
|
|
1041
|
+
tools: String(taskTools),
|
|
1042
|
+
}));
|
|
1043
|
+
if (editor)
|
|
1044
|
+
editor.printAbove(summary);
|
|
1045
|
+
else
|
|
1046
|
+
console.log(summary);
|
|
1005
1047
|
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
1006
1048
|
// and clear it, so "stop" really stops everything.
|
|
1007
1049
|
if (browser._stopped) {
|
|
@@ -1010,18 +1052,34 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1010
1052
|
}
|
|
1011
1053
|
if (!queue.length)
|
|
1012
1054
|
break;
|
|
1013
|
-
|
|
1055
|
+
// Merge the LEADING plain-text messages into ONE batch (fewer sends ->
|
|
1056
|
+
// less rate-limit risk and the operator's thoughts arrive together). We
|
|
1057
|
+
// stop at the first slash-command: it must NOT be sent as a task — it is
|
|
1058
|
+
// handled by the main loop, so we leave it (and anything after it) in
|
|
1059
|
+
// the queue and break out of this task's drain loop.
|
|
1060
|
+
let batchLen = 0;
|
|
1061
|
+
while (batchLen < queue.length && !isSlashCommand(queue[batchLen].text)) {
|
|
1062
|
+
batchLen++;
|
|
1063
|
+
}
|
|
1064
|
+
if (batchLen === 0)
|
|
1065
|
+
break;
|
|
1066
|
+
const batch = queue.splice(0, batchLen);
|
|
1067
|
+
const merged = mergeMessages(batch);
|
|
1068
|
+
if (!merged.text.trim())
|
|
1069
|
+
break;
|
|
1014
1070
|
ui.stop();
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1071
|
+
const banner = batch.length === 1
|
|
1072
|
+
? theme.user(t('msg.from_queue')) + theme.assistant(batch[0].text)
|
|
1073
|
+
: theme.user(t('msg.from_queue_batch', { n: batch.length })) +
|
|
1074
|
+
theme.assistant(t('msg.batch_joined', { n: batch.length }));
|
|
1075
|
+
if (editor)
|
|
1076
|
+
editor.printAbove(banner);
|
|
1077
|
+
else
|
|
1078
|
+
console.log(banner);
|
|
1079
|
+
transcript?.log('queued_task', { task: merged.text, count: batch.length });
|
|
1022
1080
|
next = {
|
|
1023
|
-
text:
|
|
1024
|
-
attachments:
|
|
1081
|
+
text: merged.text,
|
|
1082
|
+
attachments: merged.attachments,
|
|
1025
1083
|
freshChat: false,
|
|
1026
1084
|
sendSystemPrompt: false,
|
|
1027
1085
|
};
|
|
@@ -1200,6 +1258,9 @@ async function main() {
|
|
|
1200
1258
|
const pendingQueue = [];
|
|
1201
1259
|
// Show the "no clipboard image" hint only once per session.
|
|
1202
1260
|
let clipboardWarned = false;
|
|
1261
|
+
// Timestamp of the last Ctrl+C while the agent was busy. Two presses
|
|
1262
|
+
// within 2s escalate from "abort the running tool" to "stop the whole run".
|
|
1263
|
+
let lastCtrlCAt = 0;
|
|
1203
1264
|
// ---------- review mode state ----------
|
|
1204
1265
|
// null — normal mode.
|
|
1205
1266
|
// { snapDir, snapName, originalWorkdir } — we're inside a snapshot, the chat
|
|
@@ -1214,6 +1275,10 @@ async function main() {
|
|
|
1214
1275
|
if (mcpPool)
|
|
1215
1276
|
await mcpPool.close().catch(() => { });
|
|
1216
1277
|
transcript.close();
|
|
1278
|
+
console.log(theme.dim(t('msg.exit_summary', {
|
|
1279
|
+
transcript: transcript.file || t('common.off'),
|
|
1280
|
+
chat: currentChatId || t('chats.not_created'),
|
|
1281
|
+
})));
|
|
1217
1282
|
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
1218
1283
|
process.exit(0);
|
|
1219
1284
|
});
|
|
@@ -1228,7 +1293,9 @@ async function main() {
|
|
|
1228
1293
|
const last = loadLastSession(currentWorkdir);
|
|
1229
1294
|
if (last && last.id) {
|
|
1230
1295
|
resumeId = last.id;
|
|
1231
|
-
console.log(theme.system(
|
|
1296
|
+
console.log(theme.system(t('msg.resuming', {
|
|
1297
|
+
id: last.id + (last.title ? ` (${last.title})` : ''),
|
|
1298
|
+
})));
|
|
1232
1299
|
}
|
|
1233
1300
|
}
|
|
1234
1301
|
if (resumeId) {
|
|
@@ -1286,6 +1353,8 @@ async function main() {
|
|
|
1286
1353
|
// The editor pulls the number on every render, so it follows the live
|
|
1287
1354
|
// DeepSeek counter (accumulated_token_usage) without a polling timer.
|
|
1288
1355
|
ed.onContextQuery = () => browser.getLastTokenUsage();
|
|
1356
|
+
// Use the configured context window for the fill percentage/color.
|
|
1357
|
+
ed.setContextLimit(config.ui.contextLimit);
|
|
1289
1358
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
1290
1359
|
// counter. The editor pulls the live state on every render.
|
|
1291
1360
|
ed.onToggleQuery = () => browser.getToggleStatesSync();
|
|
@@ -1304,6 +1373,7 @@ async function main() {
|
|
|
1304
1373
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1305
1374
|
marker: att.marker,
|
|
1306
1375
|
size: formatSize(image.data.length),
|
|
1376
|
+
path: p,
|
|
1307
1377
|
})));
|
|
1308
1378
|
return att;
|
|
1309
1379
|
}
|
|
@@ -1326,6 +1396,7 @@ async function main() {
|
|
|
1326
1396
|
marker: att.marker,
|
|
1327
1397
|
name,
|
|
1328
1398
|
size: formatSize(data.length),
|
|
1399
|
+
path: filePath,
|
|
1329
1400
|
})));
|
|
1330
1401
|
return att;
|
|
1331
1402
|
};
|
|
@@ -1349,6 +1420,7 @@ async function main() {
|
|
|
1349
1420
|
marker: att.marker,
|
|
1350
1421
|
name: nm,
|
|
1351
1422
|
size: formatSize(data.length),
|
|
1423
|
+
path: fp,
|
|
1352
1424
|
}) + theme.dim(' (windows-clipboard)')));
|
|
1353
1425
|
return att;
|
|
1354
1426
|
}
|
|
@@ -1374,6 +1446,7 @@ async function main() {
|
|
|
1374
1446
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1375
1447
|
marker: att.marker,
|
|
1376
1448
|
size: formatSize(res.data.length),
|
|
1449
|
+
path: p,
|
|
1377
1450
|
}) + theme.dim(' (' + res.via + ')')));
|
|
1378
1451
|
return att;
|
|
1379
1452
|
};
|
|
@@ -1399,8 +1472,22 @@ async function main() {
|
|
|
1399
1472
|
};
|
|
1400
1473
|
ed.onCtrlC = () => {
|
|
1401
1474
|
if (ed.busy) {
|
|
1402
|
-
|
|
1403
|
-
browser.
|
|
1475
|
+
// A running tool can be long (npm test, a big build). The FIRST
|
|
1476
|
+
// Ctrl+C aborts just the TOOL (sets browser._abort, which the tool's
|
|
1477
|
+
// AbortSignal follows and which makes the loop drop the tool result).
|
|
1478
|
+
// A SECOND Ctrl+C within 2s is an explicit "stop the whole run"
|
|
1479
|
+
// (sets _stopped, which also halts the queue). This makes the two
|
|
1480
|
+
// intents distinguishable instead of one press doing everything.
|
|
1481
|
+
const now = Date.now();
|
|
1482
|
+
if (ctrlCEscalation(now - lastCtrlCAt) === 'run') {
|
|
1483
|
+
ed.printAbove(theme.warn(t('msg.abort_ctrlc_short')));
|
|
1484
|
+
browser.stopGeneration().catch(() => { });
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
ed.printAbove(theme.warn(t('msg.abort_tool_short')));
|
|
1488
|
+
browser._abort = true;
|
|
1489
|
+
}
|
|
1490
|
+
lastCtrlCAt = now;
|
|
1404
1491
|
}
|
|
1405
1492
|
else {
|
|
1406
1493
|
// Not busy — exit. We wake takeInput() so the loop finishes.
|
|
@@ -1480,6 +1567,15 @@ async function main() {
|
|
|
1480
1567
|
if (editor)
|
|
1481
1568
|
editor.refreshStatus();
|
|
1482
1569
|
}
|
|
1570
|
+
// The context window size feeds the status-bar percentage and its color.
|
|
1571
|
+
// Apply it to the live editor immediately (the value is a plain number,
|
|
1572
|
+
// no restart needed).
|
|
1573
|
+
if (path === 'ui.contextLimit' && typeof value === 'number') {
|
|
1574
|
+
if (editor) {
|
|
1575
|
+
editor.setContextLimit(value);
|
|
1576
|
+
editor.refreshStatus();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1483
1579
|
}
|
|
1484
1580
|
function configSetRaw(field, raw) {
|
|
1485
1581
|
const value = validateConfigValue(field, raw);
|
|
@@ -1700,7 +1796,6 @@ async function main() {
|
|
|
1700
1796
|
continue;
|
|
1701
1797
|
}
|
|
1702
1798
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
1703
|
-
console.log(theme.system(t('msg.new_chat')));
|
|
1704
1799
|
if (editor)
|
|
1705
1800
|
editor.lock(t('msg.input_locked'));
|
|
1706
1801
|
try {
|
|
@@ -1710,6 +1805,10 @@ async function main() {
|
|
|
1710
1805
|
currentChatId = await browser.getCurrentChatId();
|
|
1711
1806
|
saveLastChat(currentChatId, currentWorkdir);
|
|
1712
1807
|
transcript.log('new_chat');
|
|
1808
|
+
// Start the new chat on a clean screen (like /clear in a shell).
|
|
1809
|
+
if (editor)
|
|
1810
|
+
editor.clearScreen();
|
|
1811
|
+
console.log(theme.system(t('msg.new_chat')));
|
|
1713
1812
|
console.log(theme.system(t('msg.new_chat_ok') + String.fromCharCode(10)));
|
|
1714
1813
|
}
|
|
1715
1814
|
catch (e) {
|
|
@@ -1852,7 +1951,7 @@ async function main() {
|
|
|
1852
1951
|
await mod.selfList({ config });
|
|
1853
1952
|
}
|
|
1854
1953
|
catch (e) {
|
|
1855
|
-
console.error(theme.error('
|
|
1954
|
+
console.error(theme.error(t('msg.error')), e.message);
|
|
1856
1955
|
}
|
|
1857
1956
|
continue;
|
|
1858
1957
|
}
|
|
@@ -1866,7 +1965,7 @@ async function main() {
|
|
|
1866
1965
|
await mod.selfDiff({ config, name });
|
|
1867
1966
|
}
|
|
1868
1967
|
catch (e) {
|
|
1869
|
-
console.error(theme.error('
|
|
1968
|
+
console.error(theme.error(t('msg.error')), e.message);
|
|
1870
1969
|
}
|
|
1871
1970
|
continue;
|
|
1872
1971
|
}
|
|
@@ -1880,7 +1979,7 @@ async function main() {
|
|
|
1880
1979
|
await mod.selfApply({ config, name });
|
|
1881
1980
|
}
|
|
1882
1981
|
catch (e) {
|
|
1883
|
-
console.error(theme.error('
|
|
1982
|
+
console.error(theme.error(t('msg.error')), e.message);
|
|
1884
1983
|
}
|
|
1885
1984
|
continue;
|
|
1886
1985
|
}
|
|
@@ -1894,7 +1993,7 @@ async function main() {
|
|
|
1894
1993
|
lastChats = await browser.listChats(30);
|
|
1895
1994
|
spin.stop();
|
|
1896
1995
|
if (!lastChats.length) {
|
|
1897
|
-
console.log(theme.system('
|
|
1996
|
+
console.log(theme.system(t('chats.none_hint')));
|
|
1898
1997
|
}
|
|
1899
1998
|
else {
|
|
1900
1999
|
console.log(theme.system(t('chats.recent')));
|
|
@@ -1935,7 +2034,6 @@ async function main() {
|
|
|
1935
2034
|
console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
|
|
1936
2035
|
continue;
|
|
1937
2036
|
}
|
|
1938
|
-
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1939
2037
|
if (editor)
|
|
1940
2038
|
editor.lock(t('msg.input_locked'));
|
|
1941
2039
|
try {
|
|
@@ -1945,12 +2043,14 @@ async function main() {
|
|
|
1945
2043
|
sendSystemPromptNext = promptOnResume();
|
|
1946
2044
|
saveLastChat(pick.id, currentWorkdir, pick.title);
|
|
1947
2045
|
transcript.log('resume_chat', { id: pick.id, title: pick.title });
|
|
2046
|
+
// Resume on a clean screen so the restored dialogue is readable.
|
|
2047
|
+
if (editor)
|
|
2048
|
+
editor.clearScreen();
|
|
2049
|
+
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1948
2050
|
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
1949
2051
|
theme.system(sendSystemPromptNext
|
|
1950
|
-
? String.fromCharCode(10) +
|
|
1951
|
-
|
|
1952
|
-
: String.fromCharCode(10) +
|
|
1953
|
-
' Системный промпт НЕ пересылается (он уже в начале чата). Включить: /config set browser.resendPromptOnResume true.\n'));
|
|
2052
|
+
? String.fromCharCode(10) + t('chats.prompt_will_resend')
|
|
2053
|
+
: String.fromCharCode(10) + t('chats.prompt_no_resend')));
|
|
1954
2054
|
await printRestoredHistory(browser, editor, pick.id);
|
|
1955
2055
|
}
|
|
1956
2056
|
catch (e) {
|
|
@@ -2000,6 +2100,8 @@ async function main() {
|
|
|
2000
2100
|
if (editor)
|
|
2001
2101
|
editor.lock(t('msg.input_locked'));
|
|
2002
2102
|
try {
|
|
2103
|
+
if (editor)
|
|
2104
|
+
editor.clearScreen();
|
|
2003
2105
|
console.log(theme.system(t('msg.opening_chat', { id })));
|
|
2004
2106
|
await browser.openChat(id);
|
|
2005
2107
|
currentChatId = id;
|
|
@@ -2007,7 +2109,7 @@ async function main() {
|
|
|
2007
2109
|
sendSystemPromptNext = promptOnResume();
|
|
2008
2110
|
saveLastChat(id, currentWorkdir);
|
|
2009
2111
|
transcript.log('resume_chat', { id });
|
|
2010
|
-
console.log(theme.assistant('
|
|
2112
|
+
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
2011
2113
|
theme.system(String.fromCharCode(10)));
|
|
2012
2114
|
await printRestoredHistory(browser, editor, id);
|
|
2013
2115
|
}
|
|
@@ -2024,6 +2126,37 @@ async function main() {
|
|
|
2024
2126
|
console.log(theme.system(currentWorkdir));
|
|
2025
2127
|
continue;
|
|
2026
2128
|
}
|
|
2129
|
+
if (lower === '/queue' || lower.startsWith('/queue ')) {
|
|
2130
|
+
const sub = trimmed.slice('/queue'.length).trim().toLowerCase();
|
|
2131
|
+
if (sub === 'clear' || sub === 'c' || sub === 'clean') {
|
|
2132
|
+
const n = pendingQueue.length;
|
|
2133
|
+
pendingQueue.length = 0;
|
|
2134
|
+
console.log(theme.system(t('msg.queue_cleared') + ' (' + n + ')'));
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
if (sub && sub !== 'list' && sub !== 'ls') {
|
|
2138
|
+
console.error(theme.error(t('msg.queue_usage')));
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if (!pendingQueue.length) {
|
|
2142
|
+
console.log(theme.dim(t('msg.queue_empty')));
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
console.log(theme.system(t('msg.queue_title')));
|
|
2146
|
+
pendingQueue.forEach((m, i) => {
|
|
2147
|
+
const one = String(m.text || '')
|
|
2148
|
+
.replace(/\s+/g, ' ')
|
|
2149
|
+
.trim();
|
|
2150
|
+
const shown = one.length > 80 ? one.slice(0, 80) + ' …' : one;
|
|
2151
|
+
const att = m.attachments?.length
|
|
2152
|
+
? ' [+' + m.attachments.length + ']'
|
|
2153
|
+
: '';
|
|
2154
|
+
console.log(theme.system(' ') +
|
|
2155
|
+
theme.assistant(t('msg.queue_item', { n: i + 1, text: shown + att })));
|
|
2156
|
+
});
|
|
2157
|
+
console.log(theme.dim(t('msg.queue_cleared_hint')));
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2027
2160
|
if (lower === '/mcp') {
|
|
2028
2161
|
if (!mcpPool) {
|
|
2029
2162
|
console.log(theme.dim(t('mcp.none')));
|
|
@@ -2179,7 +2312,7 @@ async function main() {
|
|
|
2179
2312
|
console.error(theme.error(' ' + e));
|
|
2180
2313
|
}
|
|
2181
2314
|
else {
|
|
2182
|
-
console.log(theme.assistant(
|
|
2315
|
+
console.log(theme.assistant(t('msg.reloaded', { n: count })));
|
|
2183
2316
|
}
|
|
2184
2317
|
}
|
|
2185
2318
|
catch (e) {
|
|
@@ -2328,6 +2461,11 @@ async function main() {
|
|
|
2328
2461
|
mcpTools: mcpStatus.toolCount,
|
|
2329
2462
|
transcriptOk: !!transcript.file,
|
|
2330
2463
|
authSaved: authMarkerExists(),
|
|
2464
|
+
contextLimit: config.ui.contextLimit,
|
|
2465
|
+
hasOrigin: gitOk
|
|
2466
|
+
? (await runGit('git remote', currentWorkdir, 5000)).includes('origin')
|
|
2467
|
+
: undefined,
|
|
2468
|
+
minSendIntervalMs: config.browser.minSendIntervalMs,
|
|
2331
2469
|
})));
|
|
2332
2470
|
continue;
|
|
2333
2471
|
}
|
|
@@ -2359,153 +2497,27 @@ async function main() {
|
|
|
2359
2497
|
// Compaction: ask DeepSeek (in the CURRENT chat) to compress the
|
|
2360
2498
|
// history into a handover summary, then start a NEW chat, resend the
|
|
2361
2499
|
// system prompt and post the summary as the carried-over context.
|
|
2362
|
-
//
|
|
2363
|
-
//
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
// The call is retried a few times: browser.ask() already retries the
|
|
2380
|
-
// "Messages too frequent" rate limit internally, but if it exhausts
|
|
2381
|
-
// its own budget the whole /compact used to abort and the operator
|
|
2382
|
-
// had to start over. A transient server error must NOT lose the
|
|
2383
|
-
// compaction, so we keep trying with a growing pause before giving up.
|
|
2384
|
-
let summary = '';
|
|
2385
|
-
let summaryErr = '';
|
|
2386
|
-
for (let attempt = 1; attempt <= COMPACT_SUMMARY_ATTEMPTS; attempt++) {
|
|
2387
|
-
// On a retry after an unusable answer we append an explicit
|
|
2388
|
-
// "plain text only" note. In reasoning mode DeepSeek answered the
|
|
2389
|
-
// /compact prompt with a Bash TOOL CALL ("gather the current state")
|
|
2390
|
-
// instead of a summary — protocol noise that must NOT be carried
|
|
2391
|
-
// into the new chat.
|
|
2392
|
-
let prompt = buildCompactPrompt(currentLocale);
|
|
2393
|
-
if (attempt > 1) {
|
|
2394
|
-
prompt +=
|
|
2395
|
-
currentLocale === 'en'
|
|
2396
|
-
? '\n\nIMPORTANT: reply with the summary as PLAIN TEXT only. Do NOT call any tools and do NOT output tool-call JSON/DSML.'
|
|
2397
|
-
: '\n\nВАЖНО: ответь ТОЛЬКО текстом резюме. НЕ вызывай инструменты и НЕ выводи JSON/DSML вызова инструмента.';
|
|
2398
|
-
}
|
|
2399
|
-
let answer = '';
|
|
2400
|
-
try {
|
|
2401
|
-
answer = await browser.ask(prompt, {
|
|
2402
|
-
agent: true,
|
|
2403
|
-
timeout: Math.max(60_000, config.browser.answerTimeoutMs),
|
|
2404
|
-
});
|
|
2405
|
-
}
|
|
2406
|
-
catch (e) {
|
|
2407
|
-
summaryErr = e.message;
|
|
2408
|
-
}
|
|
2409
|
-
const trimmed = String(answer || '').trim();
|
|
2410
|
-
// The user aborted (Esc) — do not keep retrying.
|
|
2411
|
-
if (/^\(прервано пользователем\)$/.test(trimmed)) {
|
|
2412
|
-
summaryErr = trimmed;
|
|
2413
|
-
break;
|
|
2414
|
-
}
|
|
2415
|
-
if (!summaryErr && isUsableCompactSummary(trimmed)) {
|
|
2416
|
-
summary = trimmed;
|
|
2417
|
-
summaryErr = '';
|
|
2418
|
-
break;
|
|
2419
|
-
}
|
|
2420
|
-
// The turn may have succeeded yet produced an unusable answer (a
|
|
2421
|
-
// tool call, empty text). Record it and retry with the note.
|
|
2422
|
-
if (!summaryErr)
|
|
2423
|
-
summaryErr = trimmed || t('common.unknown');
|
|
2424
|
-
if (attempt < COMPACT_SUMMARY_ATTEMPTS) {
|
|
2425
|
-
console.error(theme.warn(t('compact.summary_retry', {
|
|
2426
|
-
v: summaryErr,
|
|
2427
|
-
attempt: String(attempt),
|
|
2428
|
-
max: String(COMPACT_SUMMARY_ATTEMPTS),
|
|
2429
|
-
})));
|
|
2430
|
-
await new Promise((r) => setTimeout(r, 3000 * attempt));
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
// A model summary that never arrived (a server error, or only
|
|
2434
|
-
// tool-call answers) must NOT lose the compaction: fall back to a
|
|
2435
|
-
// LOCAL summary built from the restored chat history (the same
|
|
2436
|
-
// displayable dialogue /resume prints).
|
|
2437
|
-
if (!summary) {
|
|
2438
|
-
let fallback = '';
|
|
2439
|
-
try {
|
|
2440
|
-
const all = currentChatId && browser.fetchChatMessages
|
|
2441
|
-
? await browser.fetchChatMessages(currentChatId).catch(() => [])
|
|
2442
|
-
: [];
|
|
2443
|
-
const msgs = trimRestoredMessages(all, 0);
|
|
2444
|
-
if (msgs.length) {
|
|
2445
|
-
fallback = formatRestoredHistory(msgs, {
|
|
2446
|
-
limit: COMPACT_FALLBACK_LIMIT,
|
|
2447
|
-
});
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
catch { }
|
|
2451
|
-
fallback = String(fallback || '').trim();
|
|
2452
|
-
if (fallback) {
|
|
2453
|
-
summary = fallback;
|
|
2454
|
-
console.error(theme.warn(t('compact.summary_fallback', {
|
|
2455
|
-
v: String(COMPACT_FALLBACK_LIMIT),
|
|
2456
|
-
})));
|
|
2457
|
-
}
|
|
2458
|
-
else {
|
|
2459
|
-
console.error(theme.error(t('compact.summary_failed', {
|
|
2460
|
-
v: summaryErr || t('common.unknown'),
|
|
2461
|
-
})));
|
|
2462
|
-
continue;
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
transcript.log('compact_summary', {
|
|
2466
|
-
chars: summary.length,
|
|
2467
|
-
beforeTokens,
|
|
2468
|
-
});
|
|
2469
|
-
// 2) New chat + system prompt + the summary as the first message.
|
|
2470
|
-
await browser.newChat();
|
|
2471
|
-
await browser.ask(mod.buildSystemPrompt({
|
|
2472
|
-
workdir: currentWorkdir,
|
|
2473
|
-
tools: mod.createTools(currentWorkdir, { undo }),
|
|
2474
|
-
locale: currentLocale,
|
|
2475
|
-
}), { timeout: 60_000, agent: false });
|
|
2476
|
-
await browser.ask(buildCompactCarryover(summary, task ?? undefined), {
|
|
2477
|
-
timeout: 60_000,
|
|
2478
|
-
agent: false,
|
|
2479
|
-
});
|
|
2480
|
-
currentChatId = await browser.getCurrentChatId();
|
|
2500
|
+
// Shared with the AUTO-compact path via performCompact() so both
|
|
2501
|
+
// paths stay identical (same retries, same fallback, same resend).
|
|
2502
|
+
const res = await performCompact({
|
|
2503
|
+
browser,
|
|
2504
|
+
currentChatId,
|
|
2505
|
+
workdir: currentWorkdir,
|
|
2506
|
+
locale: currentLocale,
|
|
2507
|
+
task: task ?? undefined,
|
|
2508
|
+
transcript,
|
|
2509
|
+
ui: editor,
|
|
2510
|
+
buildSystemPrompt: mod.buildSystemPrompt,
|
|
2511
|
+
tools: mod.createTools(currentWorkdir, { undo }),
|
|
2512
|
+
answerTimeoutMs: config.browser.answerTimeoutMs,
|
|
2513
|
+
fallbackLimit: COMPACT_FALLBACK_LIMIT,
|
|
2514
|
+
});
|
|
2515
|
+
if (res.ok) {
|
|
2516
|
+
currentChatId = res.chatId;
|
|
2481
2517
|
saveLastChat(currentChatId, currentWorkdir);
|
|
2482
2518
|
freshChatNext = false;
|
|
2483
2519
|
// The new chat already carries the system prompt and the context.
|
|
2484
2520
|
sendSystemPromptNext = false;
|
|
2485
|
-
console.log(theme.assistant(t('compact.done') +
|
|
2486
|
-
String.fromCharCode(10) +
|
|
2487
|
-
t('compact.report', {
|
|
2488
|
-
chars: summary.length,
|
|
2489
|
-
tokens: beforeTokens === null
|
|
2490
|
-
? t('common.unknown')
|
|
2491
|
-
: String(beforeTokens),
|
|
2492
|
-
})));
|
|
2493
|
-
if (editor) {
|
|
2494
|
-
editor.printAbove(theme.dim(String.fromCharCode(10) +
|
|
2495
|
-
'--- compacted context ---' +
|
|
2496
|
-
String.fromCharCode(10) +
|
|
2497
|
-
summary +
|
|
2498
|
-
String.fromCharCode(10) +
|
|
2499
|
-
'--- end ---' +
|
|
2500
|
-
String.fromCharCode(10)));
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
catch (e) {
|
|
2504
|
-
console.error(theme.error(e.message));
|
|
2505
|
-
}
|
|
2506
|
-
finally {
|
|
2507
|
-
if (editor)
|
|
2508
|
-
editor.unlock();
|
|
2509
2521
|
}
|
|
2510
2522
|
continue;
|
|
2511
2523
|
}
|
|
@@ -2653,6 +2665,36 @@ async function main() {
|
|
|
2653
2665
|
saveLastChat(chatId, currentWorkdir);
|
|
2654
2666
|
}
|
|
2655
2667
|
},
|
|
2668
|
+
// Auto-compact between tool calls when the context nears the
|
|
2669
|
+
// window limit. Enabled via browser.autoCompact. It sends messages
|
|
2670
|
+
// from THIS execution context (the task owns the browser), and the
|
|
2671
|
+
// loop awaits it, so it serializes with the throttle.
|
|
2672
|
+
onAutoCompact: config.browser.autoCompact
|
|
2673
|
+
? async () => {
|
|
2674
|
+
const res = await performCompact({
|
|
2675
|
+
browser,
|
|
2676
|
+
currentChatId,
|
|
2677
|
+
workdir: currentWorkdir,
|
|
2678
|
+
locale: currentLocale,
|
|
2679
|
+
task: taskText,
|
|
2680
|
+
transcript,
|
|
2681
|
+
ui: editor,
|
|
2682
|
+
buildSystemPrompt: mod.buildSystemPrompt,
|
|
2683
|
+
tools: mod.createTools(currentWorkdir, { undo }),
|
|
2684
|
+
answerTimeoutMs: config.browser.answerTimeoutMs,
|
|
2685
|
+
quiet: true,
|
|
2686
|
+
skipUiLock: true,
|
|
2687
|
+
});
|
|
2688
|
+
if (res.ok) {
|
|
2689
|
+
currentChatId = res.chatId;
|
|
2690
|
+
saveLastChat(res.chatId, currentWorkdir);
|
|
2691
|
+
}
|
|
2692
|
+
return res.ok ? res.chatId : null;
|
|
2693
|
+
}
|
|
2694
|
+
: null,
|
|
2695
|
+
autoCompactPct: config.browser.autoCompactPct,
|
|
2696
|
+
contextLimit: config.ui.contextLimit,
|
|
2697
|
+
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
2656
2698
|
}, inputAttachments);
|
|
2657
2699
|
}
|
|
2658
2700
|
finally {
|
|
@@ -2673,6 +2715,12 @@ async function main() {
|
|
|
2673
2715
|
if (mcpPool)
|
|
2674
2716
|
await mcpPool.close().catch(() => { });
|
|
2675
2717
|
transcript.close();
|
|
2718
|
+
// One dim line so the operator can find the log and the session after
|
|
2719
|
+
// the run (previously the path was only available via /transcript).
|
|
2720
|
+
console.log(theme.dim(t('msg.exit_summary', {
|
|
2721
|
+
transcript: transcript.file || t('common.off'),
|
|
2722
|
+
chat: currentChatId || t('chats.not_created'),
|
|
2723
|
+
})));
|
|
2676
2724
|
}
|
|
2677
2725
|
main().catch((e) => {
|
|
2678
2726
|
console.error(theme.error(t('msg.critical')), e.message);
|