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.
- package/dist/agent-loop.js +24 -1
- package/dist/browser.js +50 -18
- package/dist/index.js +3 -0
- package/dist/input.js +60 -5
- package/dist/spinner.js +8 -3
- package/dist/theme.js +2 -0
- package/dist/tools.js +9 -3
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) +
|
|
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) +
|
|
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
|
{
|