zames_pro 2.34.0 → 2.36.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/commands.js +27 -0
- package/dist/i18n.js +1 -0
- package/dist/index.js +53 -20
- package/dist/input.js +31 -9
- package/dist/spinner.js +14 -13
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -35,12 +35,15 @@ export function summarizeTranscript(entries) {
|
|
|
35
35
|
toolCounts: {},
|
|
36
36
|
durationMs: 0,
|
|
37
37
|
startedAt: null,
|
|
38
|
+
autoCompacts: 0,
|
|
38
39
|
};
|
|
39
40
|
for (const e of entries) {
|
|
40
41
|
if (!e || typeof e !== 'object')
|
|
41
42
|
continue;
|
|
42
43
|
if (e.type === 'user_task')
|
|
43
44
|
stats.turns++;
|
|
45
|
+
if (e.type === 'auto_compact_done')
|
|
46
|
+
stats.autoCompacts++;
|
|
44
47
|
if (e.type === 'tool_call') {
|
|
45
48
|
stats.toolCalls++;
|
|
46
49
|
const tool = String(e.tool ?? '?');
|
|
@@ -97,6 +100,9 @@ export function renderCost(stats, transcriptFile, tokenUsage = null) {
|
|
|
97
100
|
}
|
|
98
101
|
lines.push(' tasks: ' + stats.turns);
|
|
99
102
|
lines.push(' tool calls: ' + stats.toolCalls);
|
|
103
|
+
if (stats.autoCompacts > 0) {
|
|
104
|
+
lines.push(' auto-compacts: ' + stats.autoCompacts);
|
|
105
|
+
}
|
|
100
106
|
const top = Object.entries(stats.toolCounts).sort((a, b) => b[1] - a[1]);
|
|
101
107
|
if (top.length) {
|
|
102
108
|
lines.push(' by tool:');
|
|
@@ -119,6 +125,15 @@ export function formatExport(entries, meta = {}) {
|
|
|
119
125
|
if (meta.chatId)
|
|
120
126
|
out.push('- chat: ' + meta.chatId);
|
|
121
127
|
out.push('- exported: ' + new Date().toISOString());
|
|
128
|
+
// A self-contained summary, so the exported Markdown stands on its own as a
|
|
129
|
+
// report without opening the transcript or a /cost run.
|
|
130
|
+
const stats = summarizeTranscript(entries);
|
|
131
|
+
out.push('- tasks: ' + stats.turns);
|
|
132
|
+
out.push('- tool calls: ' + stats.toolCalls);
|
|
133
|
+
out.push('- duration: ' + formatDuration(stats.durationMs));
|
|
134
|
+
if (stats.autoCompacts > 0) {
|
|
135
|
+
out.push('- auto-compacts: ' + stats.autoCompacts);
|
|
136
|
+
}
|
|
122
137
|
out.push('');
|
|
123
138
|
for (const e of entries) {
|
|
124
139
|
const type = String(e.type ?? '?');
|
|
@@ -181,6 +196,9 @@ export function renderDoctor(d) {
|
|
|
181
196
|
if (typeof d.minSendIntervalMs === 'number') {
|
|
182
197
|
row(d.minSendIntervalMs > 0, 'send pause', Math.round(d.minSendIntervalMs / 1000) + 's between agent sends');
|
|
183
198
|
}
|
|
199
|
+
if (typeof d.sshRemote === 'boolean') {
|
|
200
|
+
row(true, 'session', d.sshRemote ? 'SSH remote' : 'local terminal');
|
|
201
|
+
}
|
|
184
202
|
return 'Doctor:' + NL + rows.join(NL);
|
|
185
203
|
}
|
|
186
204
|
export function renderPermissions(p) {
|
|
@@ -482,6 +500,15 @@ export function parseGoalCommand(text) {
|
|
|
482
500
|
return { sub: 'clear' };
|
|
483
501
|
return { sub: 'set', goal: rest };
|
|
484
502
|
}
|
|
503
|
+
// True when a `/config ...` line can be handled LIVE (while the agent is busy):
|
|
504
|
+
// the text subcommands only touch config values, never the chat. The bare
|
|
505
|
+
// `/config` and `/config menu` (which pause the editor and read keys) are NOT
|
|
506
|
+
// live — they must go through the main loop, so a mid-run menu never fights
|
|
507
|
+
// the running task. Pure, so the interception is unit-tested.
|
|
508
|
+
export function isLiveConfigCommand(text) {
|
|
509
|
+
return /^\/config\s+(?!menu\b|ui\b)\S+/i.test(String(text ?? '').trim());
|
|
510
|
+
}
|
|
511
|
+
// Parse a `/queue` invocation. Returns the subcommand ('list' | 'clear') or
|
|
485
512
|
// null when the text is not a /queue command at all. This runs WHILE the agent
|
|
486
513
|
// is busy (intercepted in the input handler), because the main command loop is
|
|
487
514
|
// blocked on runTask() and a queued /queue would only run after the task —
|
package/dist/i18n.js
CHANGED
|
@@ -491,6 +491,7 @@ const CATALOG = {
|
|
|
491
491
|
'status.undo': { ru: 'Undo: {v}', en: 'Undo: {v}' },
|
|
492
492
|
'status.transcript': { ru: 'Транскрипт: {v}', en: 'Transcript: {v}' },
|
|
493
493
|
'status.locale': { ru: 'Язык: {v}', en: 'Language: {v}' },
|
|
494
|
+
'status.goal': { ru: 'Цель сессии: {v}', en: 'Session goal: {v}' },
|
|
494
495
|
// ---------- messages ----------
|
|
495
496
|
'msg.working_dir': {
|
|
496
497
|
ru: 'Рабочая директория: {v}',
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ 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, formatDuration, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
19
|
+
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, formatDuration, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
20
20
|
import { performCompact } from './compact.js';
|
|
21
21
|
import { Scheduler, parseInterval, formatInterval, parseCron, } from './scheduler.js';
|
|
22
22
|
import { renderMarkdown } from './markdown.js';
|
|
@@ -1466,6 +1466,9 @@ async function main() {
|
|
|
1466
1466
|
// The editor pulls the number on every render, so it follows the live
|
|
1467
1467
|
// DeepSeek counter (accumulated_token_usage) without a polling timer.
|
|
1468
1468
|
ed.onContextQuery = () => browser.getLastTokenUsage();
|
|
1469
|
+
// Queue badge: show "⧗N" while messages are waiting to be sent after the
|
|
1470
|
+
// current task. Pulled on every render (like the context counter).
|
|
1471
|
+
ed.onQueueQuery = () => pendingQueue.length;
|
|
1469
1472
|
// Use the configured context window for the fill percentage/color.
|
|
1470
1473
|
ed.setContextLimit(config.ui.contextLimit);
|
|
1471
1474
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
@@ -1596,6 +1599,21 @@ async function main() {
|
|
|
1596
1599
|
applyLiveToggle(tg);
|
|
1597
1600
|
return;
|
|
1598
1601
|
}
|
|
1602
|
+
// `/config <sub>` text commands (list/get/set/reset/lang) are safe
|
|
1603
|
+
// mid-run: they only touch config values, never the chat. The
|
|
1604
|
+
// interactive menu (`/config` bare or `menu`) is NOT intercepted —
|
|
1605
|
+
// it pauses the editor and would fight the running task, so it stays
|
|
1606
|
+
// queued. `setConfigRuntime` already applies hot values (locale,
|
|
1607
|
+
// toggles, context limit) to the live objects immediately.
|
|
1608
|
+
if (isLiveConfigCommand(text)) {
|
|
1609
|
+
void handleConfigCommand(text);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
// `/goal` is safe mid-run: it only sets a variable and writes a file.
|
|
1613
|
+
if (/^\/goal(\s|$)/i.test(text.trim())) {
|
|
1614
|
+
void handleGoal(text);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1599
1617
|
}
|
|
1600
1618
|
const attachments = (items || []).map((a) => ({
|
|
1601
1619
|
path: a.path,
|
|
@@ -1765,6 +1783,36 @@ async function main() {
|
|
|
1765
1783
|
else
|
|
1766
1784
|
console.log(theme.system(line));
|
|
1767
1785
|
}
|
|
1786
|
+
// Handle a `/goal` command shared by the main loop and the live interception
|
|
1787
|
+
// (so a goal can be set even while the agent is busy — it only touches a
|
|
1788
|
+
// variable and a file, never the chat). Output goes above the input line
|
|
1789
|
+
// when the editor is active, otherwise to stdout.
|
|
1790
|
+
async function handleGoal(input) {
|
|
1791
|
+
const g = parseGoalCommand(input);
|
|
1792
|
+
if (!g)
|
|
1793
|
+
return;
|
|
1794
|
+
const emit = (line) => {
|
|
1795
|
+
if (editor)
|
|
1796
|
+
editor.printAbove(line);
|
|
1797
|
+
else
|
|
1798
|
+
console.log(line);
|
|
1799
|
+
};
|
|
1800
|
+
if (g.sub === 'clear') {
|
|
1801
|
+
sessionGoal = null;
|
|
1802
|
+
await saveGoal(currentWorkdir, null);
|
|
1803
|
+
emit(theme.system(t('goal.cleared')));
|
|
1804
|
+
}
|
|
1805
|
+
else if (g.sub === 'show') {
|
|
1806
|
+
emit(sessionGoal
|
|
1807
|
+
? theme.system(t('goal.current', { goal: sessionGoal }))
|
|
1808
|
+
: theme.dim(t('goal.none')));
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
sessionGoal = g.goal;
|
|
1812
|
+
await saveGoal(currentWorkdir, g.goal);
|
|
1813
|
+
emit(theme.system(t('goal.set', { goal: g.goal })));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1768
1816
|
function configSetRaw(field, raw) {
|
|
1769
1817
|
const value = validateConfigValue(field, raw);
|
|
1770
1818
|
writeConfigValue('home', field.path, raw);
|
|
@@ -2315,25 +2363,8 @@ async function main() {
|
|
|
2315
2363
|
continue;
|
|
2316
2364
|
}
|
|
2317
2365
|
if (lower === '/goal' || lower.startsWith('/goal ')) {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
if (g.sub === 'clear') {
|
|
2321
|
-
sessionGoal = null;
|
|
2322
|
-
await saveGoal(currentWorkdir, null);
|
|
2323
|
-
console.log(theme.system(t('goal.cleared')));
|
|
2324
|
-
}
|
|
2325
|
-
else if (g.sub === 'show') {
|
|
2326
|
-
console.log(sessionGoal
|
|
2327
|
-
? theme.system(t('goal.current', { goal: sessionGoal }))
|
|
2328
|
-
: theme.dim(t('goal.none')));
|
|
2329
|
-
}
|
|
2330
|
-
else {
|
|
2331
|
-
sessionGoal = g.goal;
|
|
2332
|
-
await saveGoal(currentWorkdir, g.goal);
|
|
2333
|
-
console.log(theme.system(t('goal.set', { goal: g.goal })));
|
|
2334
|
-
}
|
|
2335
|
-
continue;
|
|
2336
|
-
}
|
|
2366
|
+
await handleGoal(trimmed);
|
|
2367
|
+
continue;
|
|
2337
2368
|
}
|
|
2338
2369
|
if (lower === '/loop' || lower.startsWith('/loop ')) {
|
|
2339
2370
|
const rest = trimmed.slice('/loop'.length).trim();
|
|
@@ -2654,6 +2685,7 @@ async function main() {
|
|
|
2654
2685
|
console.log(theme.system(t('status.transcript', {
|
|
2655
2686
|
v: transcript.file || t('common.off'),
|
|
2656
2687
|
})));
|
|
2688
|
+
console.log(theme.system(t('status.goal', { v: sessionGoal || t('common.none') })));
|
|
2657
2689
|
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
2658
2690
|
console.log(theme.system(t('status.mcp', {
|
|
2659
2691
|
v: mcpPool ? String(mcpPool.status().toolCount) : t('common.none'),
|
|
@@ -2767,6 +2799,7 @@ async function main() {
|
|
|
2767
2799
|
? (await runGit('git remote', currentWorkdir, 5000)).includes('origin')
|
|
2768
2800
|
: undefined,
|
|
2769
2801
|
minSendIntervalMs: config.browser.minSendIntervalMs,
|
|
2802
|
+
sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
|
|
2770
2803
|
})));
|
|
2771
2804
|
continue;
|
|
2772
2805
|
}
|
package/dist/input.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { theme } from './theme.js';
|
|
2
2
|
import { renderMarkdown } from './markdown.js';
|
|
3
|
-
import { randomThinkingPhrase, stripEllipsis } from './spinner.js';
|
|
3
|
+
import { randomThinkingPhrase, stripEllipsis, DOTS, renderDots, } from './spinner.js';
|
|
4
4
|
import { translate } from './i18n.js';
|
|
5
5
|
import { AttachmentStore, parseImagePaste, looksLikeFilePath, } from './attachments.js';
|
|
6
6
|
// A permanent input line at the bottom of the terminal + a status/output area above it.
|
|
@@ -75,10 +75,8 @@ export function expandPastes(pastes, text) {
|
|
|
75
75
|
}
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
|
-
// Dot animation
|
|
79
|
-
//
|
|
80
|
-
const DOTS = ['', '.', '..', '...'];
|
|
81
|
-
const DOTS_PAD = ' ';
|
|
78
|
+
// Dot animation is shared with the ora spinner (see src/spinner.ts): DOTS and
|
|
79
|
+
// renderDots live there so both status lines animate identically.
|
|
82
80
|
function safeJson(v) {
|
|
83
81
|
try {
|
|
84
82
|
return JSON.stringify(v);
|
|
@@ -310,6 +308,13 @@ export class LineEditor {
|
|
|
310
308
|
contextStatus;
|
|
311
309
|
// The raw token count behind contextStatus (for the color level).
|
|
312
310
|
contextTokens;
|
|
311
|
+
// Number of messages waiting in the queue, shown as a small "⧗N" badge in
|
|
312
|
+
// the status line so the operator sees that typed-ahead messages are still
|
|
313
|
+
// pending. 0 hides it. Updated by the caller.
|
|
314
|
+
queueLength;
|
|
315
|
+
// A callback the editor calls before every render to read the CURRENT queue
|
|
316
|
+
// length, so the badge stays live without the caller polling.
|
|
317
|
+
onQueueQuery;
|
|
313
318
|
// Whether the DeepSeek "Deep thinking" / "Smart search" toggles are ON.
|
|
314
319
|
// Shown as two colored icons before the context counter: a dim gray icon
|
|
315
320
|
// means off, a teal-green icon means on.
|
|
@@ -388,6 +393,8 @@ export class LineEditor {
|
|
|
388
393
|
this.contextLimit = CONTEXT_LIMIT;
|
|
389
394
|
this.onToggleQuery = null;
|
|
390
395
|
this.onContextQuery = null;
|
|
396
|
+
this.queueLength = 0;
|
|
397
|
+
this.onQueueQuery = null;
|
|
391
398
|
}
|
|
392
399
|
// Refresh the toggle icon states from onToggleQuery (if wired) before a
|
|
393
400
|
// render. A broken callback must never break the render.
|
|
@@ -436,6 +443,23 @@ export class LineEditor {
|
|
|
436
443
|
return theme.warn(ctx);
|
|
437
444
|
return theme.success(ctx);
|
|
438
445
|
}
|
|
446
|
+
// The queue badge ("⧗N") shown before the context counter when messages are
|
|
447
|
+
// waiting. Returns '' when the queue is empty, so it never clutters an idle
|
|
448
|
+
// status line. Refreshed from onQueueQuery before every render when wired.
|
|
449
|
+
_queueBadge() {
|
|
450
|
+
if (this.onQueueQuery) {
|
|
451
|
+
try {
|
|
452
|
+
const n = this.onQueueQuery();
|
|
453
|
+
this.queueLength = typeof n === 'number' && n > 0 ? n : 0;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// A broken callback must never break the render.
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (this.queueLength <= 0)
|
|
460
|
+
return '';
|
|
461
|
+
return theme.warn('⧗' + this.queueLength) + ' ';
|
|
462
|
+
}
|
|
439
463
|
// Icons for the DeepSeek toggles: 🧠 "Deep thinking" and 🌐 "Smart search",
|
|
440
464
|
// shown before the context counter. Only the ENABLED toggles are shown, so
|
|
441
465
|
// the state is unambiguous WITHOUT relying on the terminal coloring the
|
|
@@ -671,7 +695,7 @@ export class LineEditor {
|
|
|
671
695
|
// but the icons are still meaningful.
|
|
672
696
|
const ctxRaw = this._contextText();
|
|
673
697
|
this._refreshToggles();
|
|
674
|
-
const ctxText = this._toggleIcons() + ctxRaw;
|
|
698
|
+
const ctxText = this._queueBadge() + this._toggleIcons() + ctxRaw;
|
|
675
699
|
let statusOut = '';
|
|
676
700
|
let top = 0;
|
|
677
701
|
// Never fill a row to the FULL terminal width: on terminals with
|
|
@@ -895,9 +919,7 @@ export class LineEditor {
|
|
|
895
919
|
this._animating = false;
|
|
896
920
|
}
|
|
897
921
|
_dots(n) {
|
|
898
|
-
|
|
899
|
-
// the hint on the right "jumps" when the animation phase changes.
|
|
900
|
-
return theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
922
|
+
return renderDots(n);
|
|
901
923
|
}
|
|
902
924
|
// Status-line hint ("Esc — stop"), localized.
|
|
903
925
|
_hint() {
|
package/dist/spinner.js
CHANGED
|
@@ -15,6 +15,17 @@ export function randomThinkingPhrase(locale = 'ru') {
|
|
|
15
15
|
export function stripEllipsis(phrase) {
|
|
16
16
|
return phrase.replace(/[.…]+\s*$/, '');
|
|
17
17
|
}
|
|
18
|
+
// Dot animation shared by BOTH UIs (the ora spinner and the LineEditor): start
|
|
19
|
+
// from an empty string (0 dots), then grow. The width is padded to the maximum
|
|
20
|
+
// (3 dots) so the trailing hint does not "jump" when the phase changes. Kept in
|
|
21
|
+
// one place so the two status lines animate identically.
|
|
22
|
+
export const DOTS = ['', '.', '..', '...'];
|
|
23
|
+
export const DOTS_PAD = ' ';
|
|
24
|
+
// The colored dot string for a phase (brown, matching the base text).
|
|
25
|
+
export function renderDots(n) {
|
|
26
|
+
const d = DOTS[n] ?? '';
|
|
27
|
+
return theme.brown(d + DOTS_PAD.slice(d.length));
|
|
28
|
+
}
|
|
18
29
|
export function createSpinner(locale = 'ru') {
|
|
19
30
|
let spinner = null;
|
|
20
31
|
let dotTimer = null;
|
|
@@ -26,10 +37,6 @@ export function createSpinner(locale = 'ru') {
|
|
|
26
37
|
// zero every second and look frozen on a single dot.
|
|
27
38
|
let animBase = '';
|
|
28
39
|
let animating = false;
|
|
29
|
-
// Dot animation: start from an empty string (0 dots), then grow.
|
|
30
|
-
// We align the width to the maximum (3) so the hint doesn't shift.
|
|
31
|
-
const DOTS = ['', '.', '..', '...'];
|
|
32
|
-
const DOTS_PAD = ' ';
|
|
33
40
|
const start = (text) => {
|
|
34
41
|
if (!spinner)
|
|
35
42
|
spinner = ora(text).start();
|
|
@@ -59,10 +66,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
59
66
|
const startAnimated = (baseText, showHint = true) => {
|
|
60
67
|
animBase = theme.brown(stripEllipsis(baseText));
|
|
61
68
|
const hint = showHint ? HINT : '';
|
|
62
|
-
|
|
63
|
-
// the hint on the right "jumps" when the animation phase changes.
|
|
64
|
-
const dots = (n) => theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
65
|
-
start(animBase + dots(0) + hint);
|
|
69
|
+
start(animBase + renderDots(0) + hint);
|
|
66
70
|
dotPhase = 0;
|
|
67
71
|
if (dotTimer)
|
|
68
72
|
clearInterval(dotTimer);
|
|
@@ -70,7 +74,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
70
74
|
if (!spinner)
|
|
71
75
|
return;
|
|
72
76
|
dotPhase = (dotPhase + 1) % DOTS.length;
|
|
73
|
-
spinner.text = animBase +
|
|
77
|
+
spinner.text = animBase + renderDots(dotPhase) + hint;
|
|
74
78
|
}, 400);
|
|
75
79
|
animating = true;
|
|
76
80
|
if (dotTimer.unref)
|
|
@@ -113,10 +117,7 @@ export function createSpinner(locale = 'ru') {
|
|
|
113
117
|
// so the countdown refreshes without resetting the dots.
|
|
114
118
|
if (animating && dotTimer && spinner) {
|
|
115
119
|
animBase = theme.brown(stripEllipsis(label));
|
|
116
|
-
spinner.text =
|
|
117
|
-
animBase +
|
|
118
|
-
theme.brown(DOTS[dotPhase] + DOTS_PAD.slice(DOTS[dotPhase].length)) +
|
|
119
|
-
HINT;
|
|
120
|
+
spinner.text = animBase + renderDots(dotPhase) + HINT;
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
startAnimated(label);
|