zames_pro 2.24.0 → 2.26.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,15 +556,28 @@ 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,
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) {
@@ -290,6 +280,10 @@ export class DeepSeekBrowser {
290
280
  // completion stream and from /api/v0/chat/history_messages. Null until the
291
281
  // first answer (or history fetch) delivers it. Shown by /cost and /status.
292
282
  _lastTokenUsage;
283
+ // Last known DeepSeek toggle states (deep thinking / web search), kept in
284
+ // sync by _applyToggles()/getToggleStates(). Exposed synchronously so the
285
+ // editor can draw the toggle icons on every render without awaiting.
286
+ _toggles;
293
287
  // Authorization / PoW headers sniffed from DeepSeek's own API requests, so
294
288
  // fetchChatMessages can replay them (a bare fetch does not get them).
295
289
  _apiAuth;
@@ -342,6 +336,7 @@ export class DeepSeekBrowser {
342
336
  this._netCaptureAt = 0;
343
337
  this._netChatId = null;
344
338
  this._lastTokenUsage = null;
339
+ this._toggles = { deepThinking, webSearch };
345
340
  this._apiAuth = '';
346
341
  this._apiPow = '';
347
342
  this._lastHistoryError = '';
@@ -361,7 +356,11 @@ export class DeepSeekBrowser {
361
356
  return translate(this.locale)(key, params);
362
357
  }
363
358
  async launch() {
364
- await killStaleChrome();
359
+ // NOTE: we deliberately do NOT kill stray chrome processes here. Per the
360
+ // project rules, an agent must never kill a browser it did not start —
361
+ // another zames instance may be using the same profile. A locked profile
362
+ // is handled by removing the Singleton files (below) and, if it is still
363
+ // busy, by a clear error in _launchOnce().
365
364
  if (await profileLooksLocked()) {
366
365
  if (this.debug)
367
366
  console.error('profile: удаляю Singleton-файлы');
@@ -418,7 +417,10 @@ export class DeepSeekBrowser {
418
417
  if (/Target page, context or browser has been closed|profile.*in use/i.test(e.message)) {
419
418
  if (this.debug)
420
419
  console.error('profile: занят, перезапускаю после очистки');
421
- await killStaleChrome();
420
+ // Do NOT kill a foreign chrome (see AGENTS.md): only clear the
421
+ // Singleton files and retry. If the profile is genuinely in use by a
422
+ // live browser, nukeProfile() below fails safely and the error
423
+ // surfaces instead of silently killing someone else's session.
422
424
  await cleanSingletonFiles();
423
425
  try {
424
426
  this.context = await chromium.launchPersistentContext(USER_DATA_DIR, options);
@@ -509,11 +511,11 @@ export class DeepSeekBrowser {
509
511
  await this.context.close();
510
512
  }
511
513
  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.
514
+ // A restart must REUSE the existing profile. `launch()` would clean
515
+ // Singleton files, which can race with a sibling agent on the same
516
+ // profile; here we just relaunch the persistent context in place. A fresh
517
+ // window appears only because the old context was actually closed — no
518
+ // second window is created while one is alive.
517
519
  await this._launchOnce();
518
520
  await this._fixHeadlessUserAgent();
519
521
  }
@@ -1042,6 +1044,37 @@ export class DeepSeekBrowser {
1042
1044
  async _applyToggles() {
1043
1045
  await this._setToggle(/глубок|deep\s*think/i, this.deepThinking);
1044
1046
  await this._setToggle(/поиск|search/i, this.webSearch);
1047
+ this._toggles = await this.getToggleStates();
1048
+ }
1049
+ // Synchronous snapshot of the last known toggle states, for the editor's
1050
+ // status icons (called on every render, must not await).
1051
+ getToggleStatesSync() {
1052
+ return this._toggles;
1053
+ }
1054
+ // The CURRENT state of the two DeepSeek toggles, read from the DOM
1055
+ // (aria-pressed) so the status line reflects what is actually on, not what
1056
+ // the config asked for. Falls back to the configured values when the buttons
1057
+ // are not found.
1058
+ async getToggleStates() {
1059
+ const read = async (labelRe, fallback) => {
1060
+ try {
1061
+ const btns = this.page.locator('.ds-toggle-button');
1062
+ const count = await btns.count().catch(() => 0);
1063
+ for (let i = 0; i < count; i++) {
1064
+ const b = btns.nth(i);
1065
+ const txt = ((await b.textContent().catch(() => '')) || '').trim();
1066
+ if (!labelRe.test(txt))
1067
+ continue;
1068
+ return ((await b.getAttribute('aria-pressed').catch(() => null)) === 'true');
1069
+ }
1070
+ }
1071
+ catch { }
1072
+ return fallback;
1073
+ };
1074
+ return {
1075
+ deepThinking: await read(/глубок|deep\s*think/i, this.deepThinking),
1076
+ webSearch: await read(/поиск|search/i, this.webSearch),
1077
+ };
1045
1078
  }
1046
1079
  async stopGeneration() {
1047
1080
  this._abort = true;
@@ -2033,6 +2066,5 @@ export class DeepSeekBrowser {
2033
2066
  await this.context.close();
2034
2067
  }
2035
2068
  catch { }
2036
- await killStaleChrome();
2037
2069
  }
2038
2070
  }
package/dist/index.js CHANGED
@@ -1219,6 +1219,9 @@ async function main() {
1219
1219
  // The editor pulls the number on every render, so it follows the live
1220
1220
  // DeepSeek counter (accumulated_token_usage) without a polling timer.
1221
1221
  ed.onContextQuery = () => browser.getLastTokenUsage();
1222
+ // Toggle icons (🧠 deep thinking, 🌐 web search) before the context
1223
+ // counter. The editor pulls the live state on every render.
1224
+ ed.onToggleQuery = () => browser.getToggleStatesSync();
1222
1225
  ed.onAttach = async (raw) => {
1223
1226
  // Case 1: the paste is the image data itself (data URL / base64 blob).
1224
1227
  const image = parseImagePaste(raw);
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
@@ -286,6 +289,14 @@ export class LineEditor {
286
289
  contextStatus;
287
290
  // The raw token count behind contextStatus (for the color level).
288
291
  contextTokens;
292
+ // Whether the DeepSeek "Deep thinking" / "Smart search" toggles are ON.
293
+ // Shown as two colored icons before the context counter: a dim gray icon
294
+ // means off, a teal-green icon means on.
295
+ thinkingEnabled;
296
+ searchEnabled;
297
+ // A callback the editor calls to fetch the CURRENT toggle states before
298
+ // every render, so the icons follow the live chat state.
299
+ onToggleQuery;
289
300
  // A callback the editor calls to fetch the CURRENT token count before every
290
301
  // status render, so the status line stays fresh without the caller polling.
291
302
  onContextQuery;
@@ -305,6 +316,7 @@ export class LineEditor {
305
316
  this._inPaste = false;
306
317
  this._dotTimer = null;
307
318
  this._animating = false;
319
+ this._showHint = true;
308
320
  this._dotPhase = 0;
309
321
  this._thinkBase = '';
310
322
  this._lastStatusBlock = '';
@@ -324,8 +336,27 @@ export class LineEditor {
324
336
  this.locked = false;
325
337
  this.contextStatus = null;
326
338
  this.contextTokens = null;
339
+ this.thinkingEnabled = false;
340
+ this.searchEnabled = false;
341
+ this.onToggleQuery = null;
327
342
  this.onContextQuery = null;
328
343
  }
344
+ // Refresh the toggle icon states from onToggleQuery (if wired) before a
345
+ // render. A broken callback must never break the render.
346
+ _refreshToggles() {
347
+ if (!this.onToggleQuery)
348
+ return;
349
+ try {
350
+ const s = this.onToggleQuery();
351
+ if (s) {
352
+ this.thinkingEnabled = !!s.deepThinking;
353
+ this.searchEnabled = !!s.webSearch;
354
+ }
355
+ }
356
+ catch {
357
+ // ignore
358
+ }
359
+ }
329
360
  // The token status for the CURRENT render: refreshed from onContextQuery
330
361
  // when wired, otherwise the last value passed to setContextStatus().
331
362
  _contextForRender() {
@@ -355,6 +386,16 @@ export class LineEditor {
355
386
  return theme.warn(ctx);
356
387
  return theme.success(ctx);
357
388
  }
389
+ // Two colored icons for the DeepSeek toggles: 🧠 "Deep thinking" and 🌐 "Smart
390
+ // search". A dim gray icon = off, a teal-green icon = on. Placed just before
391
+ // the context counter, so the operator sees the live chat state at a glance.
392
+ _toggleIcons() {
393
+ const on = (s) => theme.toggleOn(s);
394
+ const off = (s) => theme.toggleOff(s);
395
+ const brain = this.thinkingEnabled ? on('🧠') : off('🧠');
396
+ const globe = this.searchEnabled ? on('🌐') : off('🌐');
397
+ return brain + ' ' + globe + ' ';
398
+ }
358
399
  // Read the OS clipboard for an image and insert its marker. Used when the
359
400
  // terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
360
401
  async _tryClipboard() {
@@ -541,7 +582,12 @@ export class LineEditor {
541
582
  // though it PRINTS `status + ' '.repeat(space) + ctxText` with a DIFFERENT
542
583
  // total width, so at certain widths `top` disagreed with the printed rows.
543
584
  // Both branches below now derive `top` from the exact string they print.
544
- const ctxText = this._contextText();
585
+ // Toggle icons (always shown) + the context counter (when known). The
586
+ // icons reflect the live DeepSeek toggles; a fresh chat has no token count
587
+ // but the icons are still meaningful.
588
+ const ctxRaw = this._contextText();
589
+ this._refreshToggles();
590
+ const ctxText = this._toggleIcons() + ctxRaw;
545
591
  let statusOut = '';
546
592
  let top = 0;
547
593
  // Never fill a row to the FULL terminal width: on terminals with
@@ -699,6 +745,8 @@ export class LineEditor {
699
745
  }
700
746
  // Status-line hint ("Esc — stop"), localized.
701
747
  _hint() {
748
+ if (!this._showHint)
749
+ return '';
702
750
  return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
703
751
  }
704
752
  _startThinking() {
@@ -712,8 +760,13 @@ export class LineEditor {
712
760
  // Shared by the thinking spinner and the send-pause indicator, so the pause
713
761
  // is animated too (previously the pause was a static console line and the
714
762
  // dots stayed frozen).
715
- _startAnimated(baseText) {
763
+ //
764
+ // `showHint` (default true) controls the trailing "· Esc — стоп". While a
765
+ // TOOL runs we hide it: the tool's child process cannot be killed, so
766
+ // promising "Esc to stop" would be misleading.
767
+ _startAnimated(baseText, showHint = true) {
716
768
  this._thinkBase = theme.brown(stripEllipsis(baseText));
769
+ this._showHint = showHint;
717
770
  this._dotPhase = 0;
718
771
  this.setStatus(this._thinkBase + this._dots(0) + this._hint());
719
772
  this._stopDots();
@@ -764,7 +817,9 @@ export class LineEditor {
764
817
  // A tool may run for a long time (Bash, npm test, MCP). Without an active
765
818
  // animation the operator sees a frozen screen and cannot tell work is in
766
819
  // progress. Start the animated status AFTER printAbove (which stops the
767
- // dots) and keep it running until toolResult()/assistant()/stop().
820
+ // dots) and keep it running until toolResult()/assistant()/stop(). Esc now
821
+ // really aborts the tool (the Bash child process is killed via the
822
+ // AbortSignal), so the "Esc — стоп" hint is shown.
768
823
  this._startAnimated(translate(this.locale)('spinner.running_tool', { name }));
769
824
  }
770
825
  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/theme.js CHANGED
@@ -27,6 +27,8 @@ export const theme = {
27
27
  error: chalk.hex('#c98a80'), // muted terracotta
28
28
  success: chalk.hex('#a9c08c'), // soft sage
29
29
  brown: chalk.hex('#a1723f'), // brown (status/spinner)
30
+ toggleOn: chalk.hex('#6fd0b0'), // teal-green — a toggle is ON
31
+ toggleOff: chalk.hex('#4a4f5a'), // dim gray — a toggle is OFF
30
32
  bold: chalk.bold,
31
33
  };
32
34
  export default theme;
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.24.0",
3
+ "version": "2.26.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {