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/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';
|
|
@@ -55,6 +56,14 @@ function getPositional() {
|
|
|
55
56
|
}
|
|
56
57
|
return positional;
|
|
57
58
|
}
|
|
59
|
+
// /compact asks the OLD chat for a summary. If browser.ask() exhausts its own
|
|
60
|
+
// rate-limit budget, aborting the whole compaction used to force the operator
|
|
61
|
+
// to start over. We retry the summary call a few times instead (inside
|
|
62
|
+
// performCompact()).
|
|
63
|
+
// How many restored messages the LOCAL fallback summary keeps when the model
|
|
64
|
+
// could not produce one (a rate limit / server error must not lose the
|
|
65
|
+
// compaction).
|
|
66
|
+
const COMPACT_FALLBACK_LIMIT = 40;
|
|
58
67
|
const config = loadConfig();
|
|
59
68
|
// Current interface/agent language. Changed by the /config lang <ru|en> command.
|
|
60
69
|
let currentLocale = isLocale(config.ui?.locale)
|
|
@@ -279,6 +288,7 @@ ${theme.bold(t('help.commands'))}
|
|
|
279
288
|
${t('help.cmd.add_dir')}
|
|
280
289
|
${t('help.cmd.review')}
|
|
281
290
|
${t('help.cmd.compact')}
|
|
291
|
+
${t('help.cmd.queue')}
|
|
282
292
|
${t('help.cmd.config')}
|
|
283
293
|
${t('help.cmd.lang')}
|
|
284
294
|
${t('help.cmd.debug_dom')}
|
|
@@ -335,6 +345,7 @@ const SLASH_COMMANDS = [
|
|
|
335
345
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
336
346
|
{ name: '/review', key: 'help.cmd.review' },
|
|
337
347
|
{ name: '/compact', key: 'help.cmd.compact' },
|
|
348
|
+
{ name: '/queue', key: 'help.cmd.queue' },
|
|
338
349
|
{ name: '/config', key: 'help.cmd.config' },
|
|
339
350
|
{ name: '/skills', key: 'help.cmd.skills' },
|
|
340
351
|
{ name: '/memory', key: 'help.cmd.memory' },
|
|
@@ -836,7 +847,7 @@ async function resolveWorkdir() {
|
|
|
836
847
|
const dir = explicitDir ? path.resolve(explicitDir) : process.cwd();
|
|
837
848
|
const stat = await fs.stat(dir).catch(() => null);
|
|
838
849
|
if (!stat || !stat.isDirectory()) {
|
|
839
|
-
throw new Error('
|
|
850
|
+
throw new Error(t('msg.not_dir', { v: dir }));
|
|
840
851
|
}
|
|
841
852
|
return dir;
|
|
842
853
|
}
|
|
@@ -895,6 +906,16 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
895
906
|
const restoredTokens = browser.getLastTokenUsage();
|
|
896
907
|
if (typeof restoredTokens === 'number') {
|
|
897
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
|
+
}
|
|
898
919
|
}
|
|
899
920
|
for (const m of messages) {
|
|
900
921
|
if (m.role === 'user') {
|
|
@@ -914,7 +935,7 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
914
935
|
}
|
|
915
936
|
// ---------- task runner ----------
|
|
916
937
|
async function runTask(browser, tools, taskText, workdir, opts, attachments = []) {
|
|
917
|
-
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;
|
|
918
939
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
919
940
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
920
941
|
// mode (pipes) — a regular spinner + watchInput.
|
|
@@ -963,6 +984,11 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
963
984
|
// the whole pre-send phase (chat creation, throttle wait) with no
|
|
964
985
|
// generation in flight.
|
|
965
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;
|
|
966
992
|
const outcome = await mod.runAgentLoop({
|
|
967
993
|
browser,
|
|
968
994
|
tools,
|
|
@@ -976,13 +1002,27 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
976
1002
|
onThinking: () => ui.thinking(),
|
|
977
1003
|
onSendPause: (seconds) => ui.sendPause(seconds),
|
|
978
1004
|
onNotice: (msg) => ui.warning(msg),
|
|
979
|
-
onToolCall: (name, toolArgs) =>
|
|
1005
|
+
onToolCall: (name, toolArgs) => {
|
|
1006
|
+
taskTools++;
|
|
1007
|
+
ui.toolCall(name, toolArgs);
|
|
1008
|
+
},
|
|
980
1009
|
onToolResult: (result) => ui.toolResult(result),
|
|
981
1010
|
onAssistantMessage: (msg) => ui.assistant(msg),
|
|
982
1011
|
onWarning: (msg) => ui.warning(msg),
|
|
983
1012
|
onChatReady,
|
|
984
1013
|
debugLog: debug,
|
|
985
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,
|
|
986
1026
|
});
|
|
987
1027
|
// The loop may end WITHOUT a model answer: an exhausted iteration
|
|
988
1028
|
// limit or an ask() watchdog (the model stopped responding). In that
|
|
@@ -994,6 +1034,16 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
994
1034
|
ui.warning(outcome);
|
|
995
1035
|
transcript?.log('agent_no_answer', { outcome });
|
|
996
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);
|
|
997
1047
|
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
998
1048
|
// and clear it, so "stop" really stops everything.
|
|
999
1049
|
if (browser._stopped) {
|
|
@@ -1002,18 +1052,34 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
1002
1052
|
}
|
|
1003
1053
|
if (!queue.length)
|
|
1004
1054
|
break;
|
|
1005
|
-
|
|
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;
|
|
1006
1070
|
ui.stop();
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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 });
|
|
1014
1080
|
next = {
|
|
1015
|
-
text:
|
|
1016
|
-
attachments:
|
|
1081
|
+
text: merged.text,
|
|
1082
|
+
attachments: merged.attachments,
|
|
1017
1083
|
freshChat: false,
|
|
1018
1084
|
sendSystemPrompt: false,
|
|
1019
1085
|
};
|
|
@@ -1192,6 +1258,9 @@ async function main() {
|
|
|
1192
1258
|
const pendingQueue = [];
|
|
1193
1259
|
// Show the "no clipboard image" hint only once per session.
|
|
1194
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;
|
|
1195
1264
|
// ---------- review mode state ----------
|
|
1196
1265
|
// null — normal mode.
|
|
1197
1266
|
// { snapDir, snapName, originalWorkdir } — we're inside a snapshot, the chat
|
|
@@ -1206,6 +1275,10 @@ async function main() {
|
|
|
1206
1275
|
if (mcpPool)
|
|
1207
1276
|
await mcpPool.close().catch(() => { });
|
|
1208
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
|
+
})));
|
|
1209
1282
|
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
1210
1283
|
process.exit(0);
|
|
1211
1284
|
});
|
|
@@ -1220,7 +1293,9 @@ async function main() {
|
|
|
1220
1293
|
const last = loadLastSession(currentWorkdir);
|
|
1221
1294
|
if (last && last.id) {
|
|
1222
1295
|
resumeId = last.id;
|
|
1223
|
-
console.log(theme.system(
|
|
1296
|
+
console.log(theme.system(t('msg.resuming', {
|
|
1297
|
+
id: last.id + (last.title ? ` (${last.title})` : ''),
|
|
1298
|
+
})));
|
|
1224
1299
|
}
|
|
1225
1300
|
}
|
|
1226
1301
|
if (resumeId) {
|
|
@@ -1278,6 +1353,8 @@ async function main() {
|
|
|
1278
1353
|
// The editor pulls the number on every render, so it follows the live
|
|
1279
1354
|
// DeepSeek counter (accumulated_token_usage) without a polling timer.
|
|
1280
1355
|
ed.onContextQuery = () => browser.getLastTokenUsage();
|
|
1356
|
+
// Use the configured context window for the fill percentage/color.
|
|
1357
|
+
ed.setContextLimit(config.ui.contextLimit);
|
|
1281
1358
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
1282
1359
|
// counter. The editor pulls the live state on every render.
|
|
1283
1360
|
ed.onToggleQuery = () => browser.getToggleStatesSync();
|
|
@@ -1296,6 +1373,7 @@ async function main() {
|
|
|
1296
1373
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1297
1374
|
marker: att.marker,
|
|
1298
1375
|
size: formatSize(image.data.length),
|
|
1376
|
+
path: p,
|
|
1299
1377
|
})));
|
|
1300
1378
|
return att;
|
|
1301
1379
|
}
|
|
@@ -1318,6 +1396,7 @@ async function main() {
|
|
|
1318
1396
|
marker: att.marker,
|
|
1319
1397
|
name,
|
|
1320
1398
|
size: formatSize(data.length),
|
|
1399
|
+
path: filePath,
|
|
1321
1400
|
})));
|
|
1322
1401
|
return att;
|
|
1323
1402
|
};
|
|
@@ -1341,6 +1420,7 @@ async function main() {
|
|
|
1341
1420
|
marker: att.marker,
|
|
1342
1421
|
name: nm,
|
|
1343
1422
|
size: formatSize(data.length),
|
|
1423
|
+
path: fp,
|
|
1344
1424
|
}) + theme.dim(' (windows-clipboard)')));
|
|
1345
1425
|
return att;
|
|
1346
1426
|
}
|
|
@@ -1366,6 +1446,7 @@ async function main() {
|
|
|
1366
1446
|
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1367
1447
|
marker: att.marker,
|
|
1368
1448
|
size: formatSize(res.data.length),
|
|
1449
|
+
path: p,
|
|
1369
1450
|
}) + theme.dim(' (' + res.via + ')')));
|
|
1370
1451
|
return att;
|
|
1371
1452
|
};
|
|
@@ -1391,8 +1472,22 @@ async function main() {
|
|
|
1391
1472
|
};
|
|
1392
1473
|
ed.onCtrlC = () => {
|
|
1393
1474
|
if (ed.busy) {
|
|
1394
|
-
|
|
1395
|
-
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;
|
|
1396
1491
|
}
|
|
1397
1492
|
else {
|
|
1398
1493
|
// Not busy — exit. We wake takeInput() so the loop finishes.
|
|
@@ -1472,6 +1567,15 @@ async function main() {
|
|
|
1472
1567
|
if (editor)
|
|
1473
1568
|
editor.refreshStatus();
|
|
1474
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
|
+
}
|
|
1475
1579
|
}
|
|
1476
1580
|
function configSetRaw(field, raw) {
|
|
1477
1581
|
const value = validateConfigValue(field, raw);
|
|
@@ -1692,7 +1796,6 @@ async function main() {
|
|
|
1692
1796
|
continue;
|
|
1693
1797
|
}
|
|
1694
1798
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
1695
|
-
console.log(theme.system(t('msg.new_chat')));
|
|
1696
1799
|
if (editor)
|
|
1697
1800
|
editor.lock(t('msg.input_locked'));
|
|
1698
1801
|
try {
|
|
@@ -1702,6 +1805,10 @@ async function main() {
|
|
|
1702
1805
|
currentChatId = await browser.getCurrentChatId();
|
|
1703
1806
|
saveLastChat(currentChatId, currentWorkdir);
|
|
1704
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')));
|
|
1705
1812
|
console.log(theme.system(t('msg.new_chat_ok') + String.fromCharCode(10)));
|
|
1706
1813
|
}
|
|
1707
1814
|
catch (e) {
|
|
@@ -1844,7 +1951,7 @@ async function main() {
|
|
|
1844
1951
|
await mod.selfList({ config });
|
|
1845
1952
|
}
|
|
1846
1953
|
catch (e) {
|
|
1847
|
-
console.error(theme.error('
|
|
1954
|
+
console.error(theme.error(t('msg.error')), e.message);
|
|
1848
1955
|
}
|
|
1849
1956
|
continue;
|
|
1850
1957
|
}
|
|
@@ -1858,7 +1965,7 @@ async function main() {
|
|
|
1858
1965
|
await mod.selfDiff({ config, name });
|
|
1859
1966
|
}
|
|
1860
1967
|
catch (e) {
|
|
1861
|
-
console.error(theme.error('
|
|
1968
|
+
console.error(theme.error(t('msg.error')), e.message);
|
|
1862
1969
|
}
|
|
1863
1970
|
continue;
|
|
1864
1971
|
}
|
|
@@ -1872,7 +1979,7 @@ async function main() {
|
|
|
1872
1979
|
await mod.selfApply({ config, name });
|
|
1873
1980
|
}
|
|
1874
1981
|
catch (e) {
|
|
1875
|
-
console.error(theme.error('
|
|
1982
|
+
console.error(theme.error(t('msg.error')), e.message);
|
|
1876
1983
|
}
|
|
1877
1984
|
continue;
|
|
1878
1985
|
}
|
|
@@ -1886,7 +1993,7 @@ async function main() {
|
|
|
1886
1993
|
lastChats = await browser.listChats(30);
|
|
1887
1994
|
spin.stop();
|
|
1888
1995
|
if (!lastChats.length) {
|
|
1889
|
-
console.log(theme.system('
|
|
1996
|
+
console.log(theme.system(t('chats.none_hint')));
|
|
1890
1997
|
}
|
|
1891
1998
|
else {
|
|
1892
1999
|
console.log(theme.system(t('chats.recent')));
|
|
@@ -1927,7 +2034,6 @@ async function main() {
|
|
|
1927
2034
|
console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
|
|
1928
2035
|
continue;
|
|
1929
2036
|
}
|
|
1930
|
-
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1931
2037
|
if (editor)
|
|
1932
2038
|
editor.lock(t('msg.input_locked'));
|
|
1933
2039
|
try {
|
|
@@ -1937,12 +2043,14 @@ async function main() {
|
|
|
1937
2043
|
sendSystemPromptNext = promptOnResume();
|
|
1938
2044
|
saveLastChat(pick.id, currentWorkdir, pick.title);
|
|
1939
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 })));
|
|
1940
2050
|
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
1941
2051
|
theme.system(sendSystemPromptNext
|
|
1942
|
-
? String.fromCharCode(10) +
|
|
1943
|
-
|
|
1944
|
-
: String.fromCharCode(10) +
|
|
1945
|
-
' Системный промпт НЕ пересылается (он уже в начале чата). Включить: /config set browser.resendPromptOnResume true.\n'));
|
|
2052
|
+
? String.fromCharCode(10) + t('chats.prompt_will_resend')
|
|
2053
|
+
: String.fromCharCode(10) + t('chats.prompt_no_resend')));
|
|
1946
2054
|
await printRestoredHistory(browser, editor, pick.id);
|
|
1947
2055
|
}
|
|
1948
2056
|
catch (e) {
|
|
@@ -1992,6 +2100,8 @@ async function main() {
|
|
|
1992
2100
|
if (editor)
|
|
1993
2101
|
editor.lock(t('msg.input_locked'));
|
|
1994
2102
|
try {
|
|
2103
|
+
if (editor)
|
|
2104
|
+
editor.clearScreen();
|
|
1995
2105
|
console.log(theme.system(t('msg.opening_chat', { id })));
|
|
1996
2106
|
await browser.openChat(id);
|
|
1997
2107
|
currentChatId = id;
|
|
@@ -1999,7 +2109,7 @@ async function main() {
|
|
|
1999
2109
|
sendSystemPromptNext = promptOnResume();
|
|
2000
2110
|
saveLastChat(id, currentWorkdir);
|
|
2001
2111
|
transcript.log('resume_chat', { id });
|
|
2002
|
-
console.log(theme.assistant('
|
|
2112
|
+
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
2003
2113
|
theme.system(String.fromCharCode(10)));
|
|
2004
2114
|
await printRestoredHistory(browser, editor, id);
|
|
2005
2115
|
}
|
|
@@ -2016,6 +2126,37 @@ async function main() {
|
|
|
2016
2126
|
console.log(theme.system(currentWorkdir));
|
|
2017
2127
|
continue;
|
|
2018
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
|
+
}
|
|
2019
2160
|
if (lower === '/mcp') {
|
|
2020
2161
|
if (!mcpPool) {
|
|
2021
2162
|
console.log(theme.dim(t('mcp.none')));
|
|
@@ -2171,7 +2312,7 @@ async function main() {
|
|
|
2171
2312
|
console.error(theme.error(' ' + e));
|
|
2172
2313
|
}
|
|
2173
2314
|
else {
|
|
2174
|
-
console.log(theme.assistant(
|
|
2315
|
+
console.log(theme.assistant(t('msg.reloaded', { n: count })));
|
|
2175
2316
|
}
|
|
2176
2317
|
}
|
|
2177
2318
|
catch (e) {
|
|
@@ -2351,86 +2492,27 @@ async function main() {
|
|
|
2351
2492
|
// Compaction: ask DeepSeek (in the CURRENT chat) to compress the
|
|
2352
2493
|
// history into a handover summary, then start a NEW chat, resend the
|
|
2353
2494
|
// system prompt and post the summary as the carried-over context.
|
|
2354
|
-
//
|
|
2355
|
-
//
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
try {
|
|
2372
|
-
summary = await browser.ask(buildCompactPrompt(currentLocale), {
|
|
2373
|
-
agent: true,
|
|
2374
|
-
timeout: Math.max(60_000, config.browser.answerTimeoutMs),
|
|
2375
|
-
});
|
|
2376
|
-
}
|
|
2377
|
-
catch (e) {
|
|
2378
|
-
console.error(theme.error(t('compact.summary_failed', { v: e.message })));
|
|
2379
|
-
continue;
|
|
2380
|
-
}
|
|
2381
|
-
summary = String(summary || '').trim();
|
|
2382
|
-
// A model "answer" that is actually an error/abort sentinel is not a
|
|
2383
|
-
// summary - do not carry it over.
|
|
2384
|
-
if (!summary || /^\(прервано пользователем\)$/.test(summary)) {
|
|
2385
|
-
console.error(theme.error(t('compact.summary_failed', {
|
|
2386
|
-
v: summary || t('common.unknown'),
|
|
2387
|
-
})));
|
|
2388
|
-
continue;
|
|
2389
|
-
}
|
|
2390
|
-
transcript.log('compact_summary', {
|
|
2391
|
-
chars: summary.length,
|
|
2392
|
-
beforeTokens,
|
|
2393
|
-
});
|
|
2394
|
-
// 2) New chat + system prompt + the summary as the first message.
|
|
2395
|
-
await browser.newChat();
|
|
2396
|
-
await browser.ask(mod.buildSystemPrompt({
|
|
2397
|
-
workdir: currentWorkdir,
|
|
2398
|
-
tools: mod.createTools(currentWorkdir, { undo }),
|
|
2399
|
-
locale: currentLocale,
|
|
2400
|
-
}), { timeout: 60_000, agent: false });
|
|
2401
|
-
await browser.ask(buildCompactCarryover(summary, task ?? undefined), {
|
|
2402
|
-
timeout: 60_000,
|
|
2403
|
-
agent: false,
|
|
2404
|
-
});
|
|
2405
|
-
currentChatId = await browser.getCurrentChatId();
|
|
2495
|
+
// Shared with the AUTO-compact path via performCompact() so both
|
|
2496
|
+
// paths stay identical (same retries, same fallback, same resend).
|
|
2497
|
+
const res = await performCompact({
|
|
2498
|
+
browser,
|
|
2499
|
+
currentChatId,
|
|
2500
|
+
workdir: currentWorkdir,
|
|
2501
|
+
locale: currentLocale,
|
|
2502
|
+
task: task ?? undefined,
|
|
2503
|
+
transcript,
|
|
2504
|
+
ui: editor,
|
|
2505
|
+
buildSystemPrompt: mod.buildSystemPrompt,
|
|
2506
|
+
tools: mod.createTools(currentWorkdir, { undo }),
|
|
2507
|
+
answerTimeoutMs: config.browser.answerTimeoutMs,
|
|
2508
|
+
fallbackLimit: COMPACT_FALLBACK_LIMIT,
|
|
2509
|
+
});
|
|
2510
|
+
if (res.ok) {
|
|
2511
|
+
currentChatId = res.chatId;
|
|
2406
2512
|
saveLastChat(currentChatId, currentWorkdir);
|
|
2407
2513
|
freshChatNext = false;
|
|
2408
2514
|
// The new chat already carries the system prompt and the context.
|
|
2409
2515
|
sendSystemPromptNext = false;
|
|
2410
|
-
console.log(theme.assistant(t('compact.done') +
|
|
2411
|
-
String.fromCharCode(10) +
|
|
2412
|
-
t('compact.report', {
|
|
2413
|
-
chars: summary.length,
|
|
2414
|
-
tokens: beforeTokens === null
|
|
2415
|
-
? t('common.unknown')
|
|
2416
|
-
: String(beforeTokens),
|
|
2417
|
-
})));
|
|
2418
|
-
if (editor) {
|
|
2419
|
-
editor.printAbove(theme.dim(String.fromCharCode(10) +
|
|
2420
|
-
'--- compacted context ---' +
|
|
2421
|
-
String.fromCharCode(10) +
|
|
2422
|
-
summary +
|
|
2423
|
-
String.fromCharCode(10) +
|
|
2424
|
-
'--- end ---' +
|
|
2425
|
-
String.fromCharCode(10)));
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
catch (e) {
|
|
2429
|
-
console.error(theme.error(e.message));
|
|
2430
|
-
}
|
|
2431
|
-
finally {
|
|
2432
|
-
if (editor)
|
|
2433
|
-
editor.unlock();
|
|
2434
2516
|
}
|
|
2435
2517
|
continue;
|
|
2436
2518
|
}
|
|
@@ -2578,6 +2660,36 @@ async function main() {
|
|
|
2578
2660
|
saveLastChat(chatId, currentWorkdir);
|
|
2579
2661
|
}
|
|
2580
2662
|
},
|
|
2663
|
+
// Auto-compact between tool calls when the context nears the
|
|
2664
|
+
// window limit. Enabled via browser.autoCompact. It sends messages
|
|
2665
|
+
// from THIS execution context (the task owns the browser), and the
|
|
2666
|
+
// loop awaits it, so it serializes with the throttle.
|
|
2667
|
+
onAutoCompact: config.browser.autoCompact
|
|
2668
|
+
? async () => {
|
|
2669
|
+
const res = await performCompact({
|
|
2670
|
+
browser,
|
|
2671
|
+
currentChatId,
|
|
2672
|
+
workdir: currentWorkdir,
|
|
2673
|
+
locale: currentLocale,
|
|
2674
|
+
task: taskText,
|
|
2675
|
+
transcript,
|
|
2676
|
+
ui: editor,
|
|
2677
|
+
buildSystemPrompt: mod.buildSystemPrompt,
|
|
2678
|
+
tools: mod.createTools(currentWorkdir, { undo }),
|
|
2679
|
+
answerTimeoutMs: config.browser.answerTimeoutMs,
|
|
2680
|
+
quiet: true,
|
|
2681
|
+
skipUiLock: true,
|
|
2682
|
+
});
|
|
2683
|
+
if (res.ok) {
|
|
2684
|
+
currentChatId = res.chatId;
|
|
2685
|
+
saveLastChat(res.chatId, currentWorkdir);
|
|
2686
|
+
}
|
|
2687
|
+
return res.ok ? res.chatId : null;
|
|
2688
|
+
}
|
|
2689
|
+
: null,
|
|
2690
|
+
autoCompactPct: config.browser.autoCompactPct,
|
|
2691
|
+
contextLimit: config.ui.contextLimit,
|
|
2692
|
+
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
2581
2693
|
}, inputAttachments);
|
|
2582
2694
|
}
|
|
2583
2695
|
finally {
|
|
@@ -2598,6 +2710,12 @@ async function main() {
|
|
|
2598
2710
|
if (mcpPool)
|
|
2599
2711
|
await mcpPool.close().catch(() => { });
|
|
2600
2712
|
transcript.close();
|
|
2713
|
+
// One dim line so the operator can find the log and the session after
|
|
2714
|
+
// the run (previously the path was only available via /transcript).
|
|
2715
|
+
console.log(theme.dim(t('msg.exit_summary', {
|
|
2716
|
+
transcript: transcript.file || t('common.off'),
|
|
2717
|
+
chat: currentChatId || t('chats.not_created'),
|
|
2718
|
+
})));
|
|
2601
2719
|
}
|
|
2602
2720
|
main().catch((e) => {
|
|
2603
2721
|
console.error(theme.error(t('msg.critical')), e.message);
|