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 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
- const g = parseGoalCommand(trimmed);
2319
- if (g) {
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: start from an empty string (0 dots), then grow.
79
- // We align the width to the maximum (3) so the hint doesn't shift.
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
- // The dots are the same color as the base and of fixed width — otherwise
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
- // The dots are the same color as the base and of fixed width — otherwise
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 + dots(dotPhase) + hint;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.34.0",
3
+ "version": "2.36.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {