zames_pro 2.23.0 → 2.25.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.
@@ -35,6 +35,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
35
35
  const safeToolResult = safe(onToolResult);
36
36
  const safeAssistantMessage = safe(onAssistantMessage);
37
37
  const safeWarning = safe(onWarning);
38
+ // One AbortController per task. A tool gets `toolAbort.signal`; the loop
39
+ // aborts it as soon as the operator presses Esc/Ctrl+C (browser._abort /
40
+ // _stopped). This is what actually kills a running `npm test` instead of
41
+ // just abandoning it.
42
+ const toolAbort = new AbortController();
43
+ const syncToolAbort = () => {
44
+ if ((browser._abort || browser._stopped) && !toolAbort.signal.aborted) {
45
+ toolAbort.abort();
46
+ }
47
+ };
38
48
  if (freshChat) {
39
49
  await browser.newChat();
40
50
  transcript?.log('new_chat');
@@ -546,21 +556,43 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
546
556
  results.push({ tool: call.tool, result: err });
547
557
  continue;
548
558
  }
559
+ // The operator may have pressed Esc while we were parsing/among the
560
+ // previous calls — make sure the signal reflects it before we start.
561
+ syncToolAbort();
549
562
  safeToolCall(call.tool, call.args);
550
563
  transcript?.log('tool_call', { tool: call.tool, args: call.args });
551
564
  let result;
565
+ // While the tool runs, poll for an Esc/Ctrl+C: the abort flag is a plain
566
+ // boolean set by stopGeneration(), so the only way to turn it into a
567
+ // child-process kill is to check it periodically. 100ms is cheap and
568
+ // makes Esc feel immediate.
569
+ const poll = setInterval(syncToolAbort, 100);
570
+ if (typeof poll.unref === 'function')
571
+ poll.unref();
552
572
  try {
553
- result = await tool.fn(call.args);
573
+ result = await tool.fn(call.args, { signal: toolAbort.signal });
554
574
  }
555
575
  catch (e) {
556
576
  result = `Error: ${e.message}`;
557
577
  }
578
+ finally {
579
+ clearInterval(poll);
580
+ }
558
581
  safeToolResult(result);
559
582
  transcript?.log('tool_result', {
560
583
  tool: call.tool,
561
584
  result: String(result),
562
585
  });
563
586
  results.push({ tool: call.tool, result });
587
+ // The operator pressed Esc/Ctrl+C while the tool was running. The tool
588
+ // itself has finished (we cannot kill an arbitrary child process from
589
+ // here), but we must NOT feed its result back to the model and must NOT
590
+ // run the remaining calls in this batch — that would keep the agent
591
+ // going after an explicit stop.
592
+ if (browser._abort || browser._stopped) {
593
+ transcript?.log('user_aborted');
594
+ return '(прервано пользователем)';
595
+ }
564
596
  }
565
597
  // A tool just ran — the next answer is expected to be a fresh tool call.
566
598
  // Reset the watchdog so the next empty/repeated answer is nudged.
package/dist/browser.js CHANGED
@@ -3,7 +3,6 @@ import { extractAnswer, extractTokenUsage, dumpNetBody } from './net-capture.js'
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import fs from 'fs/promises';
6
- import { execSync } from 'child_process';
7
6
  import { theme } from './theme.js';
8
7
  import { translate, DEFAULT_LOCALE } from './i18n.js';
9
8
  // Muted terminal input for passwords. We do NOT use readline here: readline
@@ -212,15 +211,6 @@ export class ServerBusyError extends Error {
212
211
  }
213
212
  }
214
213
  // ---------- profile cleanup ----------
215
- async function killStaleChrome() {
216
- if (process.platform !== 'win32')
217
- return;
218
- try {
219
- execSync(`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='chrome.exe'\\" | Where-Object { $_.CommandLine -like '*\\.zames\\profile*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`, { stdio: 'ignore', timeout: 5000 });
220
- await new Promise((r) => setTimeout(r, 800));
221
- }
222
- catch { }
223
- }
224
214
  async function cleanSingletonFiles() {
225
215
  const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
226
216
  for (const name of names) {
@@ -361,7 +351,11 @@ export class DeepSeekBrowser {
361
351
  return translate(this.locale)(key, params);
362
352
  }
363
353
  async launch() {
364
- await killStaleChrome();
354
+ // NOTE: we deliberately do NOT kill stray chrome processes here. Per the
355
+ // project rules, an agent must never kill a browser it did not start —
356
+ // another zames instance may be using the same profile. A locked profile
357
+ // is handled by removing the Singleton files (below) and, if it is still
358
+ // busy, by a clear error in _launchOnce().
365
359
  if (await profileLooksLocked()) {
366
360
  if (this.debug)
367
361
  console.error('profile: удаляю Singleton-файлы');
@@ -418,7 +412,10 @@ export class DeepSeekBrowser {
418
412
  if (/Target page, context or browser has been closed|profile.*in use/i.test(e.message)) {
419
413
  if (this.debug)
420
414
  console.error('profile: занят, перезапускаю после очистки');
421
- await killStaleChrome();
415
+ // Do NOT kill a foreign chrome (see AGENTS.md): only clear the
416
+ // Singleton files and retry. If the profile is genuinely in use by a
417
+ // live browser, nukeProfile() below fails safely and the error
418
+ // surfaces instead of silently killing someone else's session.
422
419
  await cleanSingletonFiles();
423
420
  try {
424
421
  this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
@@ -509,11 +506,11 @@ export class DeepSeekBrowser {
509
506
  await this.context.close();
510
507
  }
511
508
  catch { }
512
- // A restart must REUSE the existing profile. `launch()` would call
513
- // killStaleChrome() and clean Singleton files, which can race with a
514
- // sibling agent on the same profile; here we just relaunch the persistent
515
- // context in place. A fresh window appears only because the old context was
516
- // actually closed — no second window is created while one is alive.
509
+ // A restart must REUSE the existing profile. `launch()` would clean
510
+ // Singleton files, which can race with a sibling agent on the same
511
+ // profile; here we just relaunch the persistent context in place. A fresh
512
+ // window appears only because the old context was actually closed — no
513
+ // second window is created while one is alive.
517
514
  await this._launchOnce();
518
515
  await this._fixHeadlessUserAgent();
519
516
  }
@@ -2033,6 +2030,5 @@ export class DeepSeekBrowser {
2033
2030
  await this.context.close();
2034
2031
  }
2035
2032
  catch { }
2036
- await killStaleChrome();
2037
2033
  }
2038
2034
  }
package/dist/index.js CHANGED
@@ -871,6 +871,12 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
871
871
  browser._stopped = false;
872
872
  browser._abort = false;
873
873
  const ui = editor || mod.createSpinner(currentLocale);
874
+ // Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
875
+ // Esc / Ctrl+C abort the current generation only while busy; without this a
876
+ // long-running tool (Bash, npm, MCP) could not be interrupted — Esc did
877
+ // nothing. Cleared in the finally below.
878
+ if (editor)
879
+ editor.busy = true;
874
880
  const stopWatching = editor
875
881
  ? () => { }
876
882
  : watchInput({
@@ -971,6 +977,8 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
971
977
  finally {
972
978
  stopWatching();
973
979
  ui.stop();
980
+ if (editor)
981
+ editor.busy = false;
974
982
  // Detach the send hooks so a later browser.ask() outside this task cannot
975
983
  // start a stale spinner.
976
984
  browser.onSendStart = null;
package/dist/input.js CHANGED
@@ -221,9 +221,9 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
221
221
  }
222
222
  const pct = Math.max(0, (n / limit) * 100);
223
223
  const pctStr = pct >= 10 ? String(Math.round(pct)) : pct.toFixed(1);
224
- // "ctx" marks this as the USED context size, so the operator does not have
224
+ // "ctx:" marks this as the USED context size, so the operator does not have
225
225
  // to guess what "302k · 30%" means.
226
- return 'ctx ' + compact + ' · ' + pctStr + '%';
226
+ return 'ctx: ' + compact + ' · ' + pctStr + '%';
227
227
  }
228
228
  export class LineEditor {
229
229
  promptStr;
@@ -242,6 +242,9 @@ export class LineEditor {
242
242
  // True while the dot-animation timer is running. Lets sendPause() update
243
243
  // only the base text (remaining seconds) without restarting the timer.
244
244
  _animating;
245
+ // Whether the animated status shows the trailing "Esc — стоп" hint. Off
246
+ // while a tool runs (its process cannot be cancelled).
247
+ _showHint;
245
248
  _dotPhase;
246
249
  _thinkBase;
247
250
  // The last rendered status row(s) (status + right-aligned context). Cached so
@@ -305,6 +308,7 @@ export class LineEditor {
305
308
  this._inPaste = false;
306
309
  this._dotTimer = null;
307
310
  this._animating = false;
311
+ this._showHint = true;
308
312
  this._dotPhase = 0;
309
313
  this._thinkBase = '';
310
314
  this._lastStatusBlock = '';
@@ -699,6 +703,8 @@ export class LineEditor {
699
703
  }
700
704
  // Status-line hint ("Esc — stop"), localized.
701
705
  _hint() {
706
+ if (!this._showHint)
707
+ return '';
702
708
  return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
703
709
  }
704
710
  _startThinking() {
@@ -712,8 +718,13 @@ export class LineEditor {
712
718
  // Shared by the thinking spinner and the send-pause indicator, so the pause
713
719
  // is animated too (previously the pause was a static console line and the
714
720
  // dots stayed frozen).
715
- _startAnimated(baseText) {
721
+ //
722
+ // `showHint` (default true) controls the trailing "· Esc — стоп". While a
723
+ // TOOL runs we hide it: the tool's child process cannot be killed, so
724
+ // promising "Esc to stop" would be misleading.
725
+ _startAnimated(baseText, showHint = true) {
716
726
  this._thinkBase = theme.brown(stripEllipsis(baseText));
727
+ this._showHint = showHint;
717
728
  this._dotPhase = 0;
718
729
  this.setStatus(this._thinkBase + this._dots(0) + this._hint());
719
730
  this._stopDots();
@@ -764,7 +775,9 @@ export class LineEditor {
764
775
  // A tool may run for a long time (Bash, npm test, MCP). Without an active
765
776
  // animation the operator sees a frozen screen and cannot tell work is in
766
777
  // progress. Start the animated status AFTER printAbove (which stops the
767
- // dots) and keep it running until toolResult()/assistant()/stop().
778
+ // dots) and keep it running until toolResult()/assistant()/stop(). Esc now
779
+ // really aborts the tool (the Bash child process is killed via the
780
+ // AbortSignal), so the "Esc — стоп" hint is shown.
768
781
  this._startAnimated(translate(this.locale)('spinner.running_tool', { name }));
769
782
  }
770
783
  toolResult(result) {
package/dist/spinner.js CHANGED
@@ -52,12 +52,17 @@ export function createSpinner(locale = 'ru') {
52
52
  // "running" dot sequence. Shared by the thinking spinner and the send-pause
53
53
  // indicator, so the pause is animated too (before, it was a static line and
54
54
  // the dots did not move — the operator saw a frozen spinner).
55
- const startAnimated = (baseText) => {
55
+ //
56
+ // `showHint` (default true) controls the trailing "· Esc — стоп". While a
57
+ // tool runs we hide it: its process cannot be cancelled, so promising a stop
58
+ // would be misleading.
59
+ const startAnimated = (baseText, showHint = true) => {
56
60
  animBase = theme.brown(stripEllipsis(baseText));
61
+ const hint = showHint ? HINT : '';
57
62
  // The dots are the same color as the base and of fixed width — otherwise
58
63
  // the hint on the right "jumps" when the animation phase changes.
59
64
  const dots = (n) => theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
60
- start(animBase + dots(0) + HINT);
65
+ start(animBase + dots(0) + hint);
61
66
  dotPhase = 0;
62
67
  if (dotTimer)
63
68
  clearInterval(dotTimer);
@@ -65,7 +70,7 @@ export function createSpinner(locale = 'ru') {
65
70
  if (!spinner)
66
71
  return;
67
72
  dotPhase = (dotPhase + 1) % DOTS.length;
68
- spinner.text = animBase + dots(dotPhase) + HINT;
73
+ spinner.text = animBase + dots(dotPhase) + hint;
69
74
  }, 400);
70
75
  animating = true;
71
76
  if (dotTimer.unref)
package/dist/tools.js CHANGED
@@ -37,13 +37,16 @@ export function createTools(workdir, { undo } = {}) {
37
37
  }
38
38
  }
39
39
  };
40
- const runShell = (command, timeout = 30_000) => new Promise((resolve) => {
40
+ const runShell = (command, timeout = 30_000, signal) => new Promise((resolve) => {
41
41
  const options = {
42
42
  cwd: workdir,
43
43
  timeout,
44
44
  maxBuffer: 1024 * 1024 * 8,
45
45
  windowsHide: true,
46
46
  env: { ...process.env },
47
+ // Abort the child process when the operator pressed Esc/Ctrl+C.
48
+ // Without it a long `npm test` kept running even after the stop.
49
+ signal,
47
50
  };
48
51
  if (process.platform === 'win32') {
49
52
  options.shell = process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
@@ -63,6 +66,9 @@ export function createTools(workdir, { undo } = {}) {
63
66
  if (err.killed) {
64
67
  parts.push(`⏱ Timeout after ${timeout}ms — process killed.`);
65
68
  }
69
+ else if (err.code === 'ABORT_ERR') {
70
+ parts.push('(aborted by the operator)');
71
+ }
66
72
  else if (err.code !== undefined && err.code !== null) {
67
73
  parts.push(`Exit code: ${err.code}`);
68
74
  }
@@ -195,9 +201,9 @@ export function createTools(workdir, { undo } = {}) {
195
201
  'Do not use for commands that require interactive input. ' +
196
202
  'For git use the Git* tools. For the web use WebFetch / WebSearch.',
197
203
  parameters: { command: 'string', timeout: 'number?' },
198
- fn: async ({ command, timeout }) => {
204
+ fn: async ({ command, timeout }, ctx) => {
199
205
  assertCommandInsideRoot(req(command, 'command'));
200
- return runShell(req(command, 'command'), timeout);
206
+ return runShell(req(command, 'command'), timeout, ctx?.signal);
201
207
  },
202
208
  },
203
209
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.23.0",
3
+ "version": "2.25.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {