zames_pro 2.19.0 → 2.21.1
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/README.md +3 -2
- package/dist/agent-loop.js +20 -12
- package/dist/attachments.js +80 -17
- package/dist/browser.js +115 -25
- package/dist/commands.js +7 -2
- package/dist/config-menu.js +3 -1
- package/dist/config.js +157 -23
- package/dist/context.js +68 -13
- package/dist/extraTools.js +8 -2
- package/dist/i18n.js +745 -187
- package/dist/index.js +37 -16
- package/dist/input.js +189 -24
- package/dist/mcp.js +16 -4
- package/dist/path-token.js +4 -3
- package/dist/self-review.js +6 -6
- package/dist/spinner.js +8 -2
- package/dist/system-prompt.js +19 -5
- package/dist/tools.js +2 -6
- package/dist/web.js +1 -1
- package/dist/xml-toolcall.js +10 -5
- package/package.json +14 -3
- package/scripts/eslint-ts6.mjs +51 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { DeepSeekBrowser } from './browser.js';
|
|
|
8
8
|
import { createTools } from './tools.js';
|
|
9
9
|
import { runAgentLoop } from './agent-loop.js';
|
|
10
10
|
import { createSpinner } from './spinner.js';
|
|
11
|
-
import { LineEditor, expandPastes, pasteReplacement } from './input.js';
|
|
11
|
+
import { LineEditor, expandPastes, pasteReplacement, } from './input.js';
|
|
12
12
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
|
|
13
13
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
14
14
|
import { runConfigMenu } from './config-menu.js';
|
|
@@ -370,7 +370,9 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
370
370
|
body = '';
|
|
371
371
|
}
|
|
372
372
|
const header = 'Follow the skill "' + skill.name + '" (from ' + skill.path + ').';
|
|
373
|
-
const extra = rest
|
|
373
|
+
const extra = rest
|
|
374
|
+
? '\n\nAdditional instructions from the operator: ' + rest
|
|
375
|
+
: '';
|
|
374
376
|
return header + '\n\n' + body + extra;
|
|
375
377
|
}
|
|
376
378
|
}
|
|
@@ -403,7 +405,8 @@ async function resolveAttachPath(workdir, raw) {
|
|
|
403
405
|
let s = String(raw || '').trim();
|
|
404
406
|
if (!s)
|
|
405
407
|
return null;
|
|
406
|
-
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
408
|
+
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
409
|
+
(s.startsWith("'") && s.endsWith("'"))) {
|
|
407
410
|
s = s.slice(1, -1);
|
|
408
411
|
}
|
|
409
412
|
s = s.replace(/\\ /g, ' ');
|
|
@@ -1016,7 +1019,10 @@ async function main() {
|
|
|
1016
1019
|
mcpPool = await mod.createMcpPool({ workdir: currentWorkdir });
|
|
1017
1020
|
const st = mcpPool.status();
|
|
1018
1021
|
if (st.toolCount > 0) {
|
|
1019
|
-
const names = st.servers
|
|
1022
|
+
const names = st.servers
|
|
1023
|
+
.filter((x) => !x.error)
|
|
1024
|
+
.map((x) => x.name)
|
|
1025
|
+
.join(', ');
|
|
1020
1026
|
console.log(theme.system(t('mcp.loaded', { n: String(st.toolCount), servers: names })));
|
|
1021
1027
|
}
|
|
1022
1028
|
for (const srv of st.servers) {
|
|
@@ -1117,8 +1123,6 @@ async function main() {
|
|
|
1117
1123
|
// Messages the user typed while the agent worked. runTask takes them one
|
|
1118
1124
|
// by one after the current task finishes.
|
|
1119
1125
|
const pendingQueue = [];
|
|
1120
|
-
// Attachments for the next submit (filled by the editor's onAttachments).
|
|
1121
|
-
let pendingAttachments = [];
|
|
1122
1126
|
// Show the "no clipboard image" hint only once per session.
|
|
1123
1127
|
let clipboardWarned = false;
|
|
1124
1128
|
// ---------- review mode state ----------
|
|
@@ -1185,7 +1189,9 @@ async function main() {
|
|
|
1185
1189
|
let tail;
|
|
1186
1190
|
if (reviewMode) {
|
|
1187
1191
|
tail =
|
|
1188
|
-
theme.warn(t('prompt.review')) +
|
|
1192
|
+
theme.warn(t('prompt.review')) +
|
|
1193
|
+
theme.dim(':') +
|
|
1194
|
+
theme.dir(reviewMode.snapName);
|
|
1189
1195
|
}
|
|
1190
1196
|
else {
|
|
1191
1197
|
tail = theme.dir(dirLabel(currentWorkdir));
|
|
@@ -1217,7 +1223,10 @@ async function main() {
|
|
|
1217
1223
|
mime: image.mime,
|
|
1218
1224
|
size: image.data.length,
|
|
1219
1225
|
});
|
|
1220
|
-
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1226
|
+
ed.printAbove(theme.system(t('msg.attached_image', {
|
|
1227
|
+
marker: att.marker,
|
|
1228
|
+
size: formatSize(image.data.length),
|
|
1229
|
+
})));
|
|
1221
1230
|
return att;
|
|
1222
1231
|
}
|
|
1223
1232
|
// Case 2: the paste is a path to a local file (drag&drop or copy path).
|
|
@@ -1235,7 +1244,11 @@ async function main() {
|
|
|
1235
1244
|
mime,
|
|
1236
1245
|
size: data.length,
|
|
1237
1246
|
});
|
|
1238
|
-
ed.printAbove(theme.system(t('msg.attached_file', {
|
|
1247
|
+
ed.printAbove(theme.system(t('msg.attached_file', {
|
|
1248
|
+
marker: att.marker,
|
|
1249
|
+
name,
|
|
1250
|
+
size: formatSize(data.length),
|
|
1251
|
+
})));
|
|
1239
1252
|
return att;
|
|
1240
1253
|
};
|
|
1241
1254
|
ed.onClipboard = async () => {
|
|
@@ -1932,10 +1945,16 @@ async function main() {
|
|
|
1932
1945
|
console.log(theme.system(t('mcp.title', { n: String(st.toolCount) })));
|
|
1933
1946
|
for (const srv of st.servers) {
|
|
1934
1947
|
if (srv.error) {
|
|
1935
|
-
console.log(' ' +
|
|
1948
|
+
console.log(' ' +
|
|
1949
|
+
theme.user(srv.name) +
|
|
1950
|
+
' ' +
|
|
1951
|
+
theme.dim(t('mcp.status_error', { v: srv.error })));
|
|
1936
1952
|
}
|
|
1937
1953
|
else {
|
|
1938
|
-
console.log(' ' +
|
|
1954
|
+
console.log(' ' +
|
|
1955
|
+
theme.user(srv.name) +
|
|
1956
|
+
' ' +
|
|
1957
|
+
theme.dim('(' + String(srv.tools.length) + ')'));
|
|
1939
1958
|
}
|
|
1940
1959
|
}
|
|
1941
1960
|
continue;
|
|
@@ -2132,7 +2151,7 @@ async function main() {
|
|
|
2132
2151
|
continue;
|
|
2133
2152
|
}
|
|
2134
2153
|
if (lower === '/diff' || lower.startsWith('/diff ')) {
|
|
2135
|
-
const staged = lower.indexOf(
|
|
2154
|
+
const staged = lower.indexOf('--staged') !== -1;
|
|
2136
2155
|
const { runGit } = await import('./gitTools.js');
|
|
2137
2156
|
const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
|
|
2138
2157
|
if (probe.trim() !== 'true') {
|
|
@@ -2196,14 +2215,14 @@ async function main() {
|
|
|
2196
2215
|
}
|
|
2197
2216
|
}
|
|
2198
2217
|
catch {
|
|
2199
|
-
gitOk
|
|
2218
|
+
// git is not available here; gitOk stays false.
|
|
2200
2219
|
}
|
|
2201
2220
|
let clipboardTool = null;
|
|
2202
2221
|
try {
|
|
2203
2222
|
clipboardTool = hasClipboardTool() ? 'available' : null;
|
|
2204
2223
|
}
|
|
2205
2224
|
catch {
|
|
2206
|
-
clipboardTool
|
|
2225
|
+
// not available; clipboardTool stays null
|
|
2207
2226
|
}
|
|
2208
2227
|
const mcpStatus = mcpPool
|
|
2209
2228
|
? mcpPool.status()
|
|
@@ -2283,7 +2302,9 @@ async function main() {
|
|
|
2283
2302
|
// A model "answer" that is actually an error/abort sentinel is not a
|
|
2284
2303
|
// summary - do not carry it over.
|
|
2285
2304
|
if (!summary || /^\(прервано пользователем\)$/.test(summary)) {
|
|
2286
|
-
console.error(theme.error(t('compact.summary_failed', {
|
|
2305
|
+
console.error(theme.error(t('compact.summary_failed', {
|
|
2306
|
+
v: summary || t('common.unknown'),
|
|
2307
|
+
})));
|
|
2287
2308
|
continue;
|
|
2288
2309
|
}
|
|
2289
2310
|
transcript.log('compact_summary', {
|
|
@@ -2335,7 +2356,7 @@ async function main() {
|
|
|
2335
2356
|
}
|
|
2336
2357
|
if (lower === '/review' || lower.startsWith('/review ')) {
|
|
2337
2358
|
const rest = trimmed.slice('/review'.length).trim();
|
|
2338
|
-
const staged = rest.indexOf(
|
|
2359
|
+
const staged = rest.indexOf('--staged') !== -1;
|
|
2339
2360
|
const focus = rest.replace(/--staged/g, '').trim();
|
|
2340
2361
|
const reviewTask = buildReviewPrompt(focus, staged);
|
|
2341
2362
|
const reviewTools = mod.createTools(currentWorkdir, { undo });
|
package/dist/input.js
CHANGED
|
@@ -36,7 +36,11 @@ const PASTE_END = ESC + '[201~';
|
|
|
36
36
|
export const PASTE_MIN_LINES = 3;
|
|
37
37
|
// Number of lines in a pasted block (the trailing newline doesn't add a line).
|
|
38
38
|
export function countPasteLines(raw) {
|
|
39
|
-
const normalized = String(raw)
|
|
39
|
+
const normalized = String(raw)
|
|
40
|
+
.split(CR + NL)
|
|
41
|
+
.join(NL)
|
|
42
|
+
.split(CR)
|
|
43
|
+
.join(NL);
|
|
40
44
|
const trimmed = normalized.replace(/\n+$/, '');
|
|
41
45
|
if (trimmed === '')
|
|
42
46
|
return 1;
|
|
@@ -49,7 +53,11 @@ export function formatPasteMarker(lines) {
|
|
|
49
53
|
// otherwise { marker, text }: the marker goes into the input line, and the
|
|
50
54
|
// original text is expanded back on submit.
|
|
51
55
|
export function pasteReplacement(raw) {
|
|
52
|
-
const text = String(raw)
|
|
56
|
+
const text = String(raw)
|
|
57
|
+
.split(CR + NL)
|
|
58
|
+
.join(NL)
|
|
59
|
+
.split(CR)
|
|
60
|
+
.join(NL);
|
|
53
61
|
const lines = countPasteLines(text);
|
|
54
62
|
if (lines < PASTE_MIN_LINES)
|
|
55
63
|
return null;
|
|
@@ -174,6 +182,24 @@ export function layoutInput(promptStr, buf, cursor, cols) {
|
|
|
174
182
|
// percentage of CONTEXT_LIMIT. Exported so it is unit-tested without a live
|
|
175
183
|
// editor. A null/undefined/NaN count renders an empty string (no status).
|
|
176
184
|
export const CONTEXT_LIMIT = 1_000_000;
|
|
185
|
+
// Thresholds for coloring the context counter. Below YELLOW the value is
|
|
186
|
+
// green (plenty of room), between YELLOW and RED it is yellow (getting full),
|
|
187
|
+
// above RED it is red (nearly exhausted). Percent of CONTEXT_LIMIT.
|
|
188
|
+
export const CONTEXT_YELLOW_PCT = 50;
|
|
189
|
+
export const CONTEXT_RED_PCT = 80;
|
|
190
|
+
// The color role for a context fill level, as a stable string so it can be
|
|
191
|
+
// unit-tested without a terminal: 'ok' | 'warn' | 'high'. Null tokens -> null.
|
|
192
|
+
export function tokenStatusLevel(tokens, limit = CONTEXT_LIMIT) {
|
|
193
|
+
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const pct = (tokens / limit) * 100;
|
|
197
|
+
if (pct >= CONTEXT_RED_PCT)
|
|
198
|
+
return 'high';
|
|
199
|
+
if (pct >= CONTEXT_YELLOW_PCT)
|
|
200
|
+
return 'warn';
|
|
201
|
+
return 'ok';
|
|
202
|
+
}
|
|
177
203
|
export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
178
204
|
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
179
205
|
return '';
|
|
@@ -186,7 +212,9 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
|
186
212
|
}
|
|
187
213
|
else if (n >= 1_000) {
|
|
188
214
|
const k = n / 1_000;
|
|
189
|
-
compact =
|
|
215
|
+
compact =
|
|
216
|
+
(k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) +
|
|
217
|
+
'k';
|
|
190
218
|
}
|
|
191
219
|
else {
|
|
192
220
|
compact = String(n);
|
|
@@ -214,6 +242,13 @@ export class LineEditor {
|
|
|
214
242
|
_animating;
|
|
215
243
|
_dotPhase;
|
|
216
244
|
_thinkBase;
|
|
245
|
+
// The last rendered status row(s) (status + right-aligned context). Cached so
|
|
246
|
+
// a re-render driven by TYPING (the buffer changed, the status did not) can
|
|
247
|
+
// skip erasing and re-printing the status line entirely. On terminals like
|
|
248
|
+
// Tabby a full erase+rewrite on every keystroke makes the whole screen blink;
|
|
249
|
+
// keeping the status untouched removes that flicker. Cleared whenever the
|
|
250
|
+
// status genuinely changes so the cache can never go stale.
|
|
251
|
+
_lastStatusBlock;
|
|
217
252
|
_wasRaw;
|
|
218
253
|
_onData;
|
|
219
254
|
// History of sent messages (for the up/down arrows).
|
|
@@ -247,10 +282,12 @@ export class LineEditor {
|
|
|
247
282
|
// percentage of the context limit, right-aligned above the input line. Null
|
|
248
283
|
// hides it. Updated by the caller from browser.getLastTokenUsage().
|
|
249
284
|
contextStatus;
|
|
285
|
+
// The raw token count behind contextStatus (for the color level).
|
|
286
|
+
contextTokens;
|
|
250
287
|
// A callback the editor calls to fetch the CURRENT token count before every
|
|
251
288
|
// status render, so the status line stays fresh without the caller polling.
|
|
252
289
|
onContextQuery;
|
|
253
|
-
constructor({ prompt = '> ', commands = [], locale = 'ru' } = {}) {
|
|
290
|
+
constructor({ prompt = '> ', commands = [], locale = 'ru', } = {}) {
|
|
254
291
|
this.locale = locale;
|
|
255
292
|
this.promptStr = prompt;
|
|
256
293
|
this.buf = '';
|
|
@@ -268,6 +305,7 @@ export class LineEditor {
|
|
|
268
305
|
this._animating = false;
|
|
269
306
|
this._dotPhase = 0;
|
|
270
307
|
this._thinkBase = '';
|
|
308
|
+
this._lastStatusBlock = '';
|
|
271
309
|
this._wasRaw = false;
|
|
272
310
|
this._onData = (b) => this._handle(b);
|
|
273
311
|
this.history = [];
|
|
@@ -283,6 +321,7 @@ export class LineEditor {
|
|
|
283
321
|
this.onClipboard = null;
|
|
284
322
|
this.locked = false;
|
|
285
323
|
this.contextStatus = null;
|
|
324
|
+
this.contextTokens = null;
|
|
286
325
|
this.onContextQuery = null;
|
|
287
326
|
}
|
|
288
327
|
// The token status for the CURRENT render: refreshed from onContextQuery
|
|
@@ -291,7 +330,9 @@ export class LineEditor {
|
|
|
291
330
|
if (this.onContextQuery) {
|
|
292
331
|
try {
|
|
293
332
|
const n = this.onContextQuery();
|
|
294
|
-
this.
|
|
333
|
+
this.contextTokens = typeof n === 'number' ? n : null;
|
|
334
|
+
this.contextStatus =
|
|
335
|
+
n === null || n === undefined ? null : formatTokenStatus(n) || null;
|
|
295
336
|
}
|
|
296
337
|
catch {
|
|
297
338
|
// A broken callback must never break the render.
|
|
@@ -299,6 +340,19 @@ export class LineEditor {
|
|
|
299
340
|
}
|
|
300
341
|
return this.contextStatus;
|
|
301
342
|
}
|
|
343
|
+
// The context text colored by fill level: green (ok), yellow (warn),
|
|
344
|
+
// red (high).
|
|
345
|
+
_contextText() {
|
|
346
|
+
const ctx = this._contextForRender();
|
|
347
|
+
if (!ctx)
|
|
348
|
+
return '';
|
|
349
|
+
const level = tokenStatusLevel(this.contextTokens);
|
|
350
|
+
if (level === 'high')
|
|
351
|
+
return theme.error(ctx);
|
|
352
|
+
if (level === 'warn')
|
|
353
|
+
return theme.warn(ctx);
|
|
354
|
+
return theme.success(ctx);
|
|
355
|
+
}
|
|
302
356
|
// Read the OS clipboard for an image and insert its marker. Used when the
|
|
303
357
|
// terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
|
|
304
358
|
async _tryClipboard() {
|
|
@@ -420,6 +474,8 @@ export class LineEditor {
|
|
|
420
474
|
}
|
|
421
475
|
// Update the token-context text shown at the right of the status line.
|
|
422
476
|
setContextStatus(tokens) {
|
|
477
|
+
this.contextTokens =
|
|
478
|
+
typeof tokens === 'number' && Number.isFinite(tokens) ? tokens : null;
|
|
423
479
|
this.contextStatus = formatTokenStatus(tokens) || null;
|
|
424
480
|
this._render();
|
|
425
481
|
}
|
|
@@ -450,35 +506,123 @@ export class LineEditor {
|
|
|
450
506
|
}
|
|
451
507
|
process.stdout.write(CR + ESC + '[J');
|
|
452
508
|
this.rendered = false;
|
|
509
|
+
this._lastStatusBlock = '';
|
|
510
|
+
}
|
|
511
|
+
// Erase ONLY the input rows (leave the status block untouched). Used when a
|
|
512
|
+
// keystroke re-renders the line but the status/context did not change: the
|
|
513
|
+
// status stays on screen, only the input area is redrawn. This removes the
|
|
514
|
+
// full-screen flicker on terminals like Tabby, where erasing the whole block
|
|
515
|
+
// on every character made the display blink.
|
|
516
|
+
_eraseInputOnly(inputRows) {
|
|
517
|
+
if (!this.rendered)
|
|
518
|
+
return;
|
|
519
|
+
const rows = process.stdout.rows || 24;
|
|
520
|
+
const up = Math.min(Math.max(0, inputRows - 1), Math.max(0, rows - 1));
|
|
521
|
+
if (up > 0)
|
|
522
|
+
process.stdout.write(ESC + '[' + up + 'A');
|
|
523
|
+
process.stdout.write(CR + ESC + '[J');
|
|
524
|
+
this.rendered = false;
|
|
453
525
|
}
|
|
454
526
|
_writeBlock() {
|
|
455
527
|
const cols = process.stdout.columns || 80;
|
|
456
|
-
let out = '';
|
|
457
|
-
let top = 0;
|
|
458
528
|
// Status line above the input: the spinner/answer text on the left and the
|
|
459
529
|
// token context right-aligned on the SAME row (10k · 12%). The context is
|
|
460
530
|
// refreshed from onContextQuery() on every render, so it follows the live
|
|
461
531
|
// DeepSeek counter without a polling timer of its own.
|
|
462
|
-
|
|
463
|
-
|
|
532
|
+
//
|
|
533
|
+
// `top` MUST count EXACTLY the rows we print, or the next _eraseBlock()
|
|
534
|
+
// moves the cursor to the wrong row and the block (spinner + input) is
|
|
535
|
+
// re-drawn on top of the previous one instead of replacing it — the
|
|
536
|
+
// operator sees the status line duplicated/stacking, most visibly during
|
|
537
|
+
// the send-pause countdown (which re-renders about once per second).
|
|
538
|
+
// The earlier code computed `top = visRows(status + ' ' + ctxText)` even
|
|
539
|
+
// though it PRINTS `status + ' '.repeat(space) + ctxText` with a DIFFERENT
|
|
540
|
+
// total width, so at certain widths `top` disagreed with the printed rows.
|
|
541
|
+
// Both branches below now derive `top` from the exact string they print.
|
|
542
|
+
const ctxText = this._contextText();
|
|
543
|
+
let statusOut = '';
|
|
544
|
+
let top = 0;
|
|
464
545
|
if (this.statusText) {
|
|
465
546
|
if (ctxText) {
|
|
466
|
-
|
|
467
|
-
|
|
547
|
+
// Right-align the context on the SAME row as the status. When the
|
|
548
|
+
// status is too long to leave room, do NOT cram them together (that
|
|
549
|
+
// pushed the trailing `%` past the right edge and it got truncated):
|
|
550
|
+
// put the context on its own line instead.
|
|
551
|
+
const space = cols - visLen(this.statusText) - visLen(ctxText);
|
|
552
|
+
if (space >= 2) {
|
|
553
|
+
const row = this.statusText + ' '.repeat(space) + ctxText;
|
|
554
|
+
statusOut = row + NL;
|
|
555
|
+
top = visRows(row, cols);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
statusOut = this.statusText + NL;
|
|
559
|
+
top = visRows(this.statusText, cols);
|
|
560
|
+
const ctxRow = ' '.repeat(Math.max(0, cols - visLen(ctxText))) + ctxText;
|
|
561
|
+
statusOut += ctxRow + NL;
|
|
562
|
+
top += visRows(ctxRow, cols);
|
|
563
|
+
}
|
|
468
564
|
}
|
|
469
565
|
else {
|
|
470
|
-
|
|
566
|
+
statusOut = this.statusText + NL;
|
|
567
|
+
// The status may wrap onto several lines — we account for this,
|
|
568
|
+
// otherwise the block erase misses and statuses pile up.
|
|
569
|
+
top = visRows(this.statusText, cols);
|
|
471
570
|
}
|
|
472
|
-
// The status may wrap onto several lines — we account for this,
|
|
473
|
-
// otherwise the block erase misses and statuses pile up.
|
|
474
|
-
top = visRows(this.statusText + (ctxText ? ' '.repeat(2) + ctxText : ''), cols);
|
|
475
571
|
}
|
|
476
572
|
else if (ctxText) {
|
|
477
573
|
// Idle: no spinner, but the context still belongs on its own line just
|
|
478
574
|
// above the input, right-aligned.
|
|
479
|
-
|
|
480
|
-
|
|
575
|
+
const ctxRow = ' '.repeat(Math.max(0, cols - visLen(ctxText))) + ctxText;
|
|
576
|
+
statusOut = ctxRow + NL;
|
|
577
|
+
top = visRows(ctxRow, cols);
|
|
578
|
+
}
|
|
579
|
+
// Incremental re-render: when the status block is byte-identical to what is
|
|
580
|
+
// already on screen (the common case while typing — the buffer changes, the
|
|
581
|
+
// spinner/context does not) we redraw ONLY the input rows. Erasing and
|
|
582
|
+
// re-printing the whole block on every keystroke is what made the display
|
|
583
|
+
// flicker on terminals like Tabby. `top` is recovered from the cache so the
|
|
584
|
+
// cursor math stays correct.
|
|
585
|
+
const statusBlock = statusOut;
|
|
586
|
+
const statusUnchanged = this.rendered && statusBlock === this._lastStatusBlock;
|
|
587
|
+
if (statusUnchanged) {
|
|
588
|
+
// Count the real input rows via layout (wrapping aware).
|
|
589
|
+
const layOnly = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
590
|
+
const inputLineRows = layOnly.rows.length;
|
|
591
|
+
const suggOnly = this._suggestions();
|
|
592
|
+
const shownOnly = suggOnly.slice(0, 8);
|
|
593
|
+
const linesBelowOnly = shownOnly.length
|
|
594
|
+
? shownOnly.length + (suggOnly.length > shownOnly.length ? 1 : 0)
|
|
595
|
+
: 0;
|
|
596
|
+
this._eraseInputOnly(inputLineRows + linesBelowOnly);
|
|
597
|
+
let outOnly = '';
|
|
598
|
+
outOnly += layOnly.rows.map((r) => r.prefix + r.text).join(NL);
|
|
599
|
+
if (shownOnly.length) {
|
|
600
|
+
const maxName = Math.max(...shownOnly.map((c) => c.name.length));
|
|
601
|
+
const lines = shownOnly.map((c) => {
|
|
602
|
+
const name = theme.prompt(c.name.padEnd(maxName));
|
|
603
|
+
const desc = theme.dim(' ' + c.description);
|
|
604
|
+
return ' ' + name + desc;
|
|
605
|
+
});
|
|
606
|
+
outOnly += NL + lines.join(NL);
|
|
607
|
+
const hidden = suggOnly.length - shownOnly.length;
|
|
608
|
+
if (hidden > 0)
|
|
609
|
+
outOnly +=
|
|
610
|
+
NL +
|
|
611
|
+
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
612
|
+
}
|
|
613
|
+
process.stdout.write(outOnly);
|
|
614
|
+
const upOnly = layOnly.rows.length - 1 - layOnly.cursorRow + linesBelowOnly;
|
|
615
|
+
if (upOnly > 0)
|
|
616
|
+
process.stdout.write(ESC + '[' + upOnly + 'A');
|
|
617
|
+
process.stdout.write(CR);
|
|
618
|
+
if (layOnly.cursorCol > 0)
|
|
619
|
+
process.stdout.write(ESC + '[' + layOnly.cursorCol + 'C');
|
|
620
|
+
this.rendered = true;
|
|
621
|
+
this.cursorRowFromTop = top + layOnly.cursorRow;
|
|
622
|
+
return;
|
|
481
623
|
}
|
|
624
|
+
let out = statusOut;
|
|
625
|
+
this._lastStatusBlock = statusBlock;
|
|
482
626
|
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
483
627
|
out += lay.rows.map((r) => r.prefix + r.text).join(NL);
|
|
484
628
|
// We draw the slash-command hints BELOW the input line. We then move the
|
|
@@ -496,13 +640,17 @@ export class LineEditor {
|
|
|
496
640
|
out += NL + lines.join(NL);
|
|
497
641
|
const hidden = sugg.length - shown.length;
|
|
498
642
|
if (hidden > 0)
|
|
499
|
-
out +=
|
|
643
|
+
out +=
|
|
644
|
+
NL +
|
|
645
|
+
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
500
646
|
}
|
|
501
647
|
process.stdout.write(out);
|
|
502
648
|
const lastRow = lay.rows.length - 1;
|
|
503
649
|
// Move the cursor up: first to the input line within lay, then further by
|
|
504
650
|
// the hint lines (if any) — the cursor must sit on the input.
|
|
505
|
-
const linesBelow = shown.length
|
|
651
|
+
const linesBelow = shown.length
|
|
652
|
+
? shown.length + (sugg.length > shown.length ? 1 : 0)
|
|
653
|
+
: 0;
|
|
506
654
|
const up = lastRow - lay.cursorRow + linesBelow;
|
|
507
655
|
if (up > 0)
|
|
508
656
|
process.stdout.write(ESC + '[' + up + 'A');
|
|
@@ -613,7 +761,12 @@ export class LineEditor {
|
|
|
613
761
|
assistant(msg) {
|
|
614
762
|
this.stop();
|
|
615
763
|
const rendered = renderMarkdown(msg);
|
|
616
|
-
this.printAbove(NL +
|
|
764
|
+
this.printAbove(NL +
|
|
765
|
+
theme.assistant(translate(this.locale)('editor.answer')) +
|
|
766
|
+
NL +
|
|
767
|
+
rendered +
|
|
768
|
+
NL +
|
|
769
|
+
theme.dim('─'.repeat(60)));
|
|
617
770
|
}
|
|
618
771
|
// Warning to the operator (e.g. the agent stopped suspiciously).
|
|
619
772
|
// Printed above the input line without overwriting it.
|
|
@@ -657,7 +810,9 @@ export class LineEditor {
|
|
|
657
810
|
_insert(text) {
|
|
658
811
|
const chars = Array.from(this.buf);
|
|
659
812
|
const ins = Array.from(String(text));
|
|
660
|
-
const next = chars
|
|
813
|
+
const next = chars
|
|
814
|
+
.slice(0, this.cursor)
|
|
815
|
+
.concat(ins, chars.slice(this.cursor));
|
|
661
816
|
this.buf = next.join('');
|
|
662
817
|
this.cursor += ins.length;
|
|
663
818
|
}
|
|
@@ -701,7 +856,11 @@ export class LineEditor {
|
|
|
701
856
|
_insertFallback(raw) {
|
|
702
857
|
const rep = pasteReplacement(raw);
|
|
703
858
|
if (!rep) {
|
|
704
|
-
const text = String(raw)
|
|
859
|
+
const text = String(raw)
|
|
860
|
+
.split(CR + NL)
|
|
861
|
+
.join(NL)
|
|
862
|
+
.split(CR)
|
|
863
|
+
.join(NL);
|
|
705
864
|
this._insert(text);
|
|
706
865
|
return;
|
|
707
866
|
}
|
|
@@ -1136,9 +1295,15 @@ export function selftest() {
|
|
|
1136
1295
|
];
|
|
1137
1296
|
for (const t of cases) {
|
|
1138
1297
|
const r = layoutInput(t.p, t.b, t.c, t.w);
|
|
1139
|
-
console.log(JSON.stringify({
|
|
1298
|
+
console.log(JSON.stringify({
|
|
1299
|
+
rows: r.rows.length,
|
|
1300
|
+
row: r.cursorRow,
|
|
1301
|
+
col: r.cursorCol,
|
|
1302
|
+
}));
|
|
1140
1303
|
}
|
|
1141
1304
|
}
|
|
1142
|
-
if (process.argv[1] &&
|
|
1305
|
+
if (process.argv[1] &&
|
|
1306
|
+
process.argv[1].endsWith('input.js') &&
|
|
1307
|
+
process.argv.includes('--selftest')) {
|
|
1143
1308
|
selftest();
|
|
1144
1309
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -15,7 +15,10 @@ export function hardenPlaywrightArgs(command, args) {
|
|
|
15
15
|
const hay = (String(command || '') + ' ' + args.join(' ')).toLowerCase();
|
|
16
16
|
if (!hay.includes('playwright') || !hay.includes('mcp'))
|
|
17
17
|
return args;
|
|
18
|
-
if (args.some((a) => a === '--isolated' ||
|
|
18
|
+
if (args.some((a) => a === '--isolated' ||
|
|
19
|
+
a.startsWith('--user-data-dir') ||
|
|
20
|
+
a === '--extension' ||
|
|
21
|
+
a.startsWith('--cdp-endpoint'))) {
|
|
19
22
|
return args;
|
|
20
23
|
}
|
|
21
24
|
return [...args, '--isolated'];
|
|
@@ -161,7 +164,9 @@ export function renderMcpResult(result) {
|
|
|
161
164
|
parts.push(String(b.text ?? ''));
|
|
162
165
|
}
|
|
163
166
|
else if (type === 'image') {
|
|
164
|
-
parts.push('[image ' +
|
|
167
|
+
parts.push('[image ' +
|
|
168
|
+
String(b.mimeType ?? 'unknown') +
|
|
169
|
+
', written to disk by the MCP server]');
|
|
165
170
|
}
|
|
166
171
|
else if (type === 'resource') {
|
|
167
172
|
const res = (b.resource || {});
|
|
@@ -169,7 +174,11 @@ export function renderMcpResult(result) {
|
|
|
169
174
|
parts.push(String(res.uri ?? '') + String.fromCharCode(10) + res.text);
|
|
170
175
|
}
|
|
171
176
|
else if (typeof res.blob === 'string') {
|
|
172
|
-
parts.push('[binary resource ' +
|
|
177
|
+
parts.push('[binary resource ' +
|
|
178
|
+
String(res.uri ?? '') +
|
|
179
|
+
', ' +
|
|
180
|
+
String(res.mimeType ?? '') +
|
|
181
|
+
']');
|
|
173
182
|
}
|
|
174
183
|
else {
|
|
175
184
|
parts.push(String(res.uri ?? ''));
|
|
@@ -187,7 +196,10 @@ export function renderMcpResult(result) {
|
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
198
|
}
|
|
190
|
-
const text = parts
|
|
199
|
+
const text = parts
|
|
200
|
+
.filter((p) => p !== '')
|
|
201
|
+
.join(String.fromCharCode(10) + String.fromCharCode(10))
|
|
202
|
+
.trim();
|
|
191
203
|
return formatResultString(text || '(MCP tool returned no content)', r.isError);
|
|
192
204
|
}
|
|
193
205
|
function formatResultString(result, isError) {
|
package/dist/path-token.js
CHANGED
|
@@ -13,10 +13,9 @@ export function extractPathToken(text) {
|
|
|
13
13
|
i++;
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
-
let quote = '';
|
|
17
16
|
let start = i;
|
|
18
17
|
if (c === String.fromCharCode(39) || c === String.fromCharCode(34)) {
|
|
19
|
-
quote = c;
|
|
18
|
+
const quote = c;
|
|
20
19
|
start = i + 1;
|
|
21
20
|
i = start;
|
|
22
21
|
while (i < n && chars[i] !== quote)
|
|
@@ -63,7 +62,9 @@ function looksLikeToken(tok) {
|
|
|
63
62
|
if (ext.length > 8)
|
|
64
63
|
return false;
|
|
65
64
|
for (const ch of ext) {
|
|
66
|
-
const ok = (ch >= 'a' && ch <= 'z') ||
|
|
65
|
+
const ok = (ch >= 'a' && ch <= 'z') ||
|
|
66
|
+
(ch >= 'A' && ch <= 'Z') ||
|
|
67
|
+
(ch >= '0' && ch <= '9');
|
|
67
68
|
if (!ok)
|
|
68
69
|
return false;
|
|
69
70
|
}
|
package/dist/self-review.js
CHANGED
|
@@ -54,7 +54,7 @@ async function copyDirJsFiles(from, to) {
|
|
|
54
54
|
return copied;
|
|
55
55
|
}
|
|
56
56
|
// ---------- /self-review ----------
|
|
57
|
-
export async function selfReview({ browser, focus, transcript }) {
|
|
57
|
+
export async function selfReview({ browser, focus, transcript, }) {
|
|
58
58
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
59
59
|
const snapRoot = path.join(SNAP_ROOT);
|
|
60
60
|
const snapDir = path.join(snapRoot, `run-${stamp}`);
|
|
@@ -125,7 +125,7 @@ export async function selfReview({ browser, focus, transcript }) {
|
|
|
125
125
|
return { snapDir, reportPath, changed };
|
|
126
126
|
}
|
|
127
127
|
// ---------- /self-diff ----------
|
|
128
|
-
export async function selfDiff({ name }) {
|
|
128
|
+
export async function selfDiff({ name, }) {
|
|
129
129
|
const snapDir = path.join(SNAP_ROOT, name);
|
|
130
130
|
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
131
131
|
if (!snapStat)
|
|
@@ -154,7 +154,7 @@ export async function selfDiff({ name }) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
// ---------- /self-apply ----------
|
|
157
|
-
export async function selfApply({ name }) {
|
|
157
|
+
export async function selfApply({ name, }) {
|
|
158
158
|
const snapDir = path.join(SNAP_ROOT, name);
|
|
159
159
|
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
160
160
|
if (!snapStat)
|
|
@@ -180,9 +180,9 @@ export async function selfApply({ name }) {
|
|
|
180
180
|
console.log(theme.warn(`\nПерезапусти агента, чтобы изменения вступили в силу.\n`));
|
|
181
181
|
}
|
|
182
182
|
// ---------- /self-list ----------
|
|
183
|
-
export async function selfList({ config } = {}) {
|
|
183
|
+
export async function selfList({ config: _config, } = {}) {
|
|
184
184
|
const snapRoot = path.join(SNAP_ROOT);
|
|
185
|
-
let entries
|
|
185
|
+
let entries;
|
|
186
186
|
try {
|
|
187
187
|
entries = await fs.readdir(snapRoot);
|
|
188
188
|
}
|
|
@@ -230,7 +230,7 @@ async function diffFiles(origDir, newDir) {
|
|
|
230
230
|
}
|
|
231
231
|
return changed;
|
|
232
232
|
}
|
|
233
|
-
function buildReviewPrompt({ focus, snapDir }) {
|
|
233
|
+
function buildReviewPrompt({ focus, snapDir, }) {
|
|
234
234
|
const focusLine = focus
|
|
235
235
|
? `\nThe user asked you to focus especially on: ${focus}\n`
|
|
236
236
|
: '';
|
package/dist/spinner.js
CHANGED
|
@@ -108,7 +108,10 @@ export function createSpinner(locale = 'ru') {
|
|
|
108
108
|
// so the countdown refreshes without resetting the dots.
|
|
109
109
|
if (animating && dotTimer && spinner) {
|
|
110
110
|
animBase = theme.brown(stripEllipsis(label));
|
|
111
|
-
spinner.text =
|
|
111
|
+
spinner.text =
|
|
112
|
+
animBase +
|
|
113
|
+
theme.brown(DOTS[dotPhase] + DOTS_PAD.slice(DOTS[dotPhase].length)) +
|
|
114
|
+
HINT;
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
117
|
startAnimated(label);
|
|
@@ -130,7 +133,10 @@ export function createSpinner(locale = 'ru') {
|
|
|
130
133
|
toolResult: (result) => {
|
|
131
134
|
stop();
|
|
132
135
|
const text = typeof result === 'string' ? result : JSON.stringify(result);
|
|
133
|
-
const preview = text
|
|
136
|
+
const preview = text
|
|
137
|
+
.slice(0, 200)
|
|
138
|
+
.split(String.fromCharCode(10))
|
|
139
|
+
.join(' ↵ ');
|
|
134
140
|
console.log(theme.toolResult(' → ' + preview + String.fromCharCode(10)));
|
|
135
141
|
},
|
|
136
142
|
assistant: (msg) => {
|