zames_pro 2.52.0 → 2.53.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/dist/attachments.js +2 -1
- package/dist/color.js +8 -0
- package/dist/config-menu.js +23 -1
- package/dist/config.js +20 -3
- package/dist/i18n.js +30 -9
- package/dist/index.js +65 -9
- package/dist/input.js +142 -44
- package/dist/markdown.js +12 -1
- package/dist/self-review.js +2 -1
- package/dist/undo.js +4 -1
- package/package.json +1 -2
package/dist/attachments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
4
5
|
import { createHash } from 'crypto';
|
|
5
6
|
import { execFileSync } from 'child_process';
|
|
6
7
|
import { extractPathToken } from './path-token.js';
|
|
@@ -233,7 +234,7 @@ export function windowsReachable() {
|
|
|
233
234
|
if (process.platform !== 'linux')
|
|
234
235
|
return false;
|
|
235
236
|
try {
|
|
236
|
-
return
|
|
237
|
+
return existsSync('/mnt/c/Windows');
|
|
237
238
|
}
|
|
238
239
|
catch {
|
|
239
240
|
return false;
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// CLI color opt-out. This module MUST be imported before anything that pulls
|
|
2
|
+
// in chalk (see the import order in src/index.ts): chalk reads NO_COLOR at
|
|
3
|
+
// module-load time, so the env var has to be set first. A `--no-color` flag
|
|
4
|
+
// is the explicit equivalent of NO_COLOR=1 (which chalk already honors).
|
|
5
|
+
if (process.argv.includes('--no-color')) {
|
|
6
|
+
process.env.NO_COLOR = '1';
|
|
7
|
+
}
|
|
8
|
+
export {};
|
package/dist/config-menu.js
CHANGED
|
@@ -103,10 +103,13 @@ export function runConfigMenu(opts) {
|
|
|
103
103
|
let lastRows = 0;
|
|
104
104
|
const render = () => {
|
|
105
105
|
const lines = [];
|
|
106
|
-
lines.push(theme.bold(t('cfg.menu.title')));
|
|
107
106
|
const vis = visible();
|
|
108
107
|
if (cursor >= vis.length)
|
|
109
108
|
cursor = Math.max(0, vis.length - 1);
|
|
109
|
+
// Title with a position counter ("Settings — 12/49") so the operator knows
|
|
110
|
+
// how far down a long list they are; a filter narrows both numbers.
|
|
111
|
+
const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
|
|
112
|
+
lines.push(theme.bold(t('cfg.menu.title')) + theme.dim(pos));
|
|
110
113
|
let lastGroup = '';
|
|
111
114
|
for (let i = 0; i < vis.length; i++) {
|
|
112
115
|
const f = vis[i];
|
|
@@ -356,12 +359,31 @@ export function runConfigMenu(opts) {
|
|
|
356
359
|
if (n > 0)
|
|
357
360
|
cursor = (cursor - 1 + n) % n;
|
|
358
361
|
message = null;
|
|
362
|
+
pendingReset = null;
|
|
359
363
|
}
|
|
360
364
|
else if (ch === 'j') {
|
|
361
365
|
const n = visible().length;
|
|
362
366
|
if (n > 0)
|
|
363
367
|
cursor = (cursor + 1) % n;
|
|
364
368
|
message = null;
|
|
369
|
+
pendingReset = null;
|
|
370
|
+
}
|
|
371
|
+
else if (ch === 'g') {
|
|
372
|
+
// Jump to the first field of the NEXT group (and back to the top after
|
|
373
|
+
// the last), so a long grouped list can be navigated quickly.
|
|
374
|
+
const vis = visible();
|
|
375
|
+
if (vis.length) {
|
|
376
|
+
let target = -1;
|
|
377
|
+
const curGroup = vis[cursor] ? vis[cursor].groupKey : '';
|
|
378
|
+
for (let k = cursor + 1; k < vis.length; k++) {
|
|
379
|
+
if (vis[k].groupKey !== curGroup) {
|
|
380
|
+
target = k;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
cursor = target >= 0 ? target : 0;
|
|
385
|
+
}
|
|
386
|
+
message = null;
|
|
365
387
|
}
|
|
366
388
|
else if (ch === '\r' || ch === '\n' || ch === ' ') {
|
|
367
389
|
toggle();
|
package/dist/config.js
CHANGED
|
@@ -10,6 +10,13 @@ const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
|
|
|
10
10
|
export function projectConfigPath() {
|
|
11
11
|
return path.join(process.cwd(), '.zamesrc.json');
|
|
12
12
|
}
|
|
13
|
+
// Write via a temp file + rename so a crash or Ctrl+C mid-write can never
|
|
14
|
+
// leave a truncated/broken config (it is read at every startup).
|
|
15
|
+
function atomicWriteJson(file, value) {
|
|
16
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
17
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
18
|
+
fs.renameSync(tmp, file);
|
|
19
|
+
}
|
|
13
20
|
export const DEFAULTS = {
|
|
14
21
|
maxIterations: 0,
|
|
15
22
|
// Headless by default: the browser runs without a visible window. Set
|
|
@@ -69,6 +76,8 @@ export const DEFAULTS = {
|
|
|
69
76
|
ui: {
|
|
70
77
|
locale: DEFAULT_LOCALE,
|
|
71
78
|
contextLimit: 1_000_000,
|
|
79
|
+
// 0 = auto: terminal width, capped at 100 columns (the historical width).
|
|
80
|
+
answerWidth: 0,
|
|
72
81
|
},
|
|
73
82
|
};
|
|
74
83
|
export function loadConfig() {
|
|
@@ -116,6 +125,14 @@ export const CONFIG_SCHEMA = [
|
|
|
116
125
|
labelKey: 'cfg.f.maxIterations',
|
|
117
126
|
groupKey: 'cfg.group.agent',
|
|
118
127
|
},
|
|
128
|
+
{
|
|
129
|
+
path: 'ui.answerWidth',
|
|
130
|
+
type: 'number',
|
|
131
|
+
min: 0,
|
|
132
|
+
max: 1000,
|
|
133
|
+
labelKey: 'cfg.f.ui_answerWidth',
|
|
134
|
+
groupKey: 'cfg.group.ui',
|
|
135
|
+
},
|
|
119
136
|
{
|
|
120
137
|
path: 'headless',
|
|
121
138
|
type: 'boolean',
|
|
@@ -416,7 +433,7 @@ export function readConfigFile(file) {
|
|
|
416
433
|
catch (e) {
|
|
417
434
|
const err = e;
|
|
418
435
|
if (err.code !== 'ENOENT') {
|
|
419
|
-
throw new Error(
|
|
436
|
+
throw new Error(`failed to read ${file}: ${err.message}`);
|
|
420
437
|
}
|
|
421
438
|
}
|
|
422
439
|
return {};
|
|
@@ -434,7 +451,7 @@ export function writeConfigValue(scope, key, raw) {
|
|
|
434
451
|
const data = readConfigFile(file);
|
|
435
452
|
setByPath(data, key, value);
|
|
436
453
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
437
|
-
|
|
454
|
+
atomicWriteJson(file, data);
|
|
438
455
|
return { value, file };
|
|
439
456
|
}
|
|
440
457
|
/** Reset a setting to the default (remove the key from the file). */
|
|
@@ -457,7 +474,7 @@ export function resetConfigValue(scope, key) {
|
|
|
457
474
|
if (cur)
|
|
458
475
|
delete cur[parts[parts.length - 1]];
|
|
459
476
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
460
|
-
|
|
477
|
+
atomicWriteJson(file, data);
|
|
461
478
|
return file;
|
|
462
479
|
}
|
|
463
480
|
// 'PROJECT_CONFIG' stays a getter so existing callers that only DISPLAY the
|
package/dist/i18n.js
CHANGED
|
@@ -26,7 +26,7 @@ export function normalizeLocale(v) {
|
|
|
26
26
|
return DEFAULT_LOCALE;
|
|
27
27
|
}
|
|
28
28
|
// String catalog. Key -> { ru, en }. If a key is missing — we return the key itself.
|
|
29
|
-
const CATALOG = {
|
|
29
|
+
export const CATALOG = {
|
|
30
30
|
'app.tagline': {
|
|
31
31
|
ru: 'агент поверх chat.deepseek.com через Playwright',
|
|
32
32
|
en: 'coding agent over chat.deepseek.com via Playwright',
|
|
@@ -86,6 +86,10 @@ const CATALOG = {
|
|
|
86
86
|
en: 'dev mode: auto-reload modules',
|
|
87
87
|
},
|
|
88
88
|
'help.opt.version': { ru: 'показать версию', en: 'show version' },
|
|
89
|
+
'help.opt.no_color': {
|
|
90
|
+
ru: 'отключить цвет (то же, что NO_COLOR=1)',
|
|
91
|
+
en: 'disable color (same as NO_COLOR=1)',
|
|
92
|
+
},
|
|
89
93
|
'help.opt.help': { ru: 'эта справка', en: 'this help' },
|
|
90
94
|
'help.while_working': {
|
|
91
95
|
ru: 'Пока агент работает:',
|
|
@@ -112,8 +116,8 @@ const CATALOG = {
|
|
|
112
116
|
en: 'Ctrl+← / Ctrl+→ move by words',
|
|
113
117
|
},
|
|
114
118
|
'help.key.slash': {
|
|
115
|
-
ru: '/ + Tab подсказка и автодополнение slash-команд',
|
|
116
|
-
en: '/ + Tab slash-command hints and completion',
|
|
119
|
+
ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P — листать)',
|
|
120
|
+
en: '/ + Tab slash-command hints and completion (Ctrl+N/P to page)',
|
|
117
121
|
},
|
|
118
122
|
'help.key.newline': {
|
|
119
123
|
ru: 'Ctrl+J / Ctrl+Enter новая строка (Shift+Enter в терминалах с поддержкой)',
|
|
@@ -137,16 +141,16 @@ const CATALOG = {
|
|
|
137
141
|
en: '/new, /clear new chat (reset context)',
|
|
138
142
|
},
|
|
139
143
|
'help.cmd.sessions': {
|
|
140
|
-
ru: '/sessions
|
|
141
|
-
en: '/sessions
|
|
144
|
+
ru: '/sessions [фильтр] список сохранённых сессий (можно фильтровать)',
|
|
145
|
+
en: '/sessions [filter] list saved sessions (optionally filtered)',
|
|
142
146
|
},
|
|
143
147
|
'help.cmd.resume_id': {
|
|
144
148
|
ru: '/resume-id <id> восстановить сессию по полному id',
|
|
145
149
|
en: '/resume-id <id> resume a session by full id',
|
|
146
150
|
},
|
|
147
151
|
'help.cmd.chats': {
|
|
148
|
-
ru: '/chats
|
|
149
|
-
en: '/chats
|
|
152
|
+
ru: '/chats [фильтр] список последних чатов DeepSeek',
|
|
153
|
+
en: '/chats [filter] recent DeepSeek chats',
|
|
150
154
|
},
|
|
151
155
|
'help.cmd.resume': {
|
|
152
156
|
ru: '/resume <n> открыть чат №n из /chats',
|
|
@@ -490,7 +494,12 @@ const CATALOG = {
|
|
|
490
494
|
// animated thinking phrase, it says explicitly that a generation is in
|
|
491
495
|
// flight. 'paused'/'settled' have no label (see LineEditor._stateLabel).
|
|
492
496
|
'state.generating': { ru: '▶ генерация', en: '▶ generating' },
|
|
497
|
+
'state.settled': { ru: '✓ готово', en: '✓ done' },
|
|
493
498
|
'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
|
|
499
|
+
'editor.page_hint': {
|
|
500
|
+
ru: '(Ctrl+N/Ctrl+P — листать, Tab — вставить)',
|
|
501
|
+
en: '(Ctrl+N/Ctrl+P to page, Tab to insert)',
|
|
502
|
+
},
|
|
494
503
|
'editor.answer': { ru: '● Ответ', en: '● Answer' },
|
|
495
504
|
// Reverse search (Ctrl+R) status line. The query may be empty.
|
|
496
505
|
'editor.search_prompt': {
|
|
@@ -581,6 +590,10 @@ const CATALOG = {
|
|
|
581
590
|
ru: 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
|
|
582
591
|
en: 'Interactive mode. Enter a task. Commands — /help. Exit — /exit.',
|
|
583
592
|
},
|
|
593
|
+
'msg.first_login_hint': {
|
|
594
|
+
ru: 'Сохранённой сессии DeepSeek нет — при первом запросе потребуется вход (логин/пароль спросят в терминале или открой --headed).',
|
|
595
|
+
en: 'No saved DeepSeek session — the first task will need a sign-in (login/password are asked in the terminal, or use --headed).',
|
|
596
|
+
},
|
|
584
597
|
'msg.first_hint': {
|
|
585
598
|
ru: 'Подсказка: «/» — список команд, Ctrl+R — поиск по истории, картинку можно вставить через Ctrl+Shift+V.',
|
|
586
599
|
en: 'Hint: "/" lists commands, Ctrl+R searches history, paste an image with Ctrl+Shift+V.',
|
|
@@ -1212,6 +1225,10 @@ const CATALOG = {
|
|
|
1212
1225
|
ru: 'Язык интерфейса и ответов агента',
|
|
1213
1226
|
en: 'Interface and agent reply language',
|
|
1214
1227
|
},
|
|
1228
|
+
'cfg.f.ui_answerWidth': {
|
|
1229
|
+
ru: 'Максимальная ширина ответа в колонках (0 — авто, до 100)',
|
|
1230
|
+
en: 'Max answer width in columns (0 = auto, capped at 100)',
|
|
1231
|
+
},
|
|
1215
1232
|
'cfg.f.maxIterations': {
|
|
1216
1233
|
ru: 'Лимит итераций на задачу',
|
|
1217
1234
|
en: 'Max agent loop iterations per task',
|
|
@@ -1400,8 +1417,8 @@ const CATALOG = {
|
|
|
1400
1417
|
en: '⚙ Settings — choose a parameter',
|
|
1401
1418
|
},
|
|
1402
1419
|
'cfg.menu.hint': {
|
|
1403
|
-
ru: '↑/↓ — выбор, Enter — изменить, q/Esc — выйти',
|
|
1404
|
-
en: '↑/↓ select, Enter edit, q/Esc quit',
|
|
1420
|
+
ru: '↑/↓ — выбор, Enter — изменить, g — след. группа, / — поиск, q/Esc — выйти',
|
|
1421
|
+
en: '↑/↓ select, Enter edit, g next group, / search, q/Esc quit',
|
|
1405
1422
|
},
|
|
1406
1423
|
'cfg.menu.edit_hint': {
|
|
1407
1424
|
ru: 'Введи значение и Enter (пусто — отмена)',
|
|
@@ -1450,6 +1467,10 @@ const CATALOG = {
|
|
|
1450
1467
|
ru: 'Неверное значение для {v}: ожидалось {type}',
|
|
1451
1468
|
en: 'Invalid value for {v}: expected {type}',
|
|
1452
1469
|
},
|
|
1470
|
+
'cfg.range_hint': {
|
|
1471
|
+
ru: 'число от {min} до {max}',
|
|
1472
|
+
en: 'a number from {min} to {max}',
|
|
1473
|
+
},
|
|
1453
1474
|
'cfg.write_error': {
|
|
1454
1475
|
ru: 'Не удалось записать конфиг: {v}',
|
|
1455
1476
|
en: 'Failed to write config: {v}',
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import './color.js';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import fs from 'fs/promises';
|
|
4
5
|
import { existsSync, statSync } from 'fs';
|
|
@@ -20,7 +21,7 @@ import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
|
20
21
|
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
21
22
|
import { performCompact } from './compact.js';
|
|
22
23
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
23
|
-
import { renderMarkdown } from './markdown.js';
|
|
24
|
+
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
24
25
|
import { closeWeb } from './web.js';
|
|
25
26
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
26
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -71,6 +72,8 @@ function getPositional() {
|
|
|
71
72
|
// compaction).
|
|
72
73
|
const COMPACT_FALLBACK_LIMIT = 40;
|
|
73
74
|
const config = loadConfig();
|
|
75
|
+
// Apply the configured answer width to the markdown renderer (0 = auto).
|
|
76
|
+
setAnswerWidth(config.ui?.answerWidth ?? 0);
|
|
74
77
|
// Current interface/agent language. Changed by the /config lang <ru|en> command.
|
|
75
78
|
let currentLocale = isLocale(config.ui?.locale)
|
|
76
79
|
? config.ui.locale
|
|
@@ -289,6 +292,7 @@ ${theme.bold(t('help.options'))}
|
|
|
289
292
|
--debug ${t('help.opt.debug')}
|
|
290
293
|
--calibrate ${t('help.opt.calibrate')}
|
|
291
294
|
--dev ${t('help.opt.dev')}
|
|
295
|
+
--no-color ${t('help.opt.no_color')}
|
|
292
296
|
--version, -v ${t('help.opt.version')}
|
|
293
297
|
--help, -h ${t('help.opt.help')}
|
|
294
298
|
|
|
@@ -1342,6 +1346,12 @@ async function main() {
|
|
|
1342
1346
|
console.log(theme.system(t('msg.interactive')));
|
|
1343
1347
|
console.log(theme.system(t('msg.queue_hint')));
|
|
1344
1348
|
console.log(theme.dim(t('msg.first_hint')));
|
|
1349
|
+
// No stored session yet (auth.json absent): DeepSeek will ask for a sign-in.
|
|
1350
|
+
// The auto-login handles it, but a heads-up in the banner saves the operator
|
|
1351
|
+
// from wondering why the first task pauses on a login form.
|
|
1352
|
+
if (!authMarkerExists()) {
|
|
1353
|
+
console.log(theme.warn(t('msg.first_login_hint')));
|
|
1354
|
+
}
|
|
1345
1355
|
let freshChatNext = true;
|
|
1346
1356
|
let sendSystemPromptNext = true;
|
|
1347
1357
|
// Resuming an existing chat already has the system-prompt at its start, so
|
|
@@ -1815,6 +1825,11 @@ async function main() {
|
|
|
1815
1825
|
editor.setPrompt(buildPrompt());
|
|
1816
1826
|
}
|
|
1817
1827
|
}
|
|
1828
|
+
// Apply a live change to the answer width immediately, so the next
|
|
1829
|
+
// rendered answer uses the new width without a restart.
|
|
1830
|
+
if (path === 'ui.answerWidth') {
|
|
1831
|
+
setAnswerWidth(typeof value === 'number' ? value : Number(value) || 0);
|
|
1832
|
+
}
|
|
1818
1833
|
// Reflect browser toggles on the LIVE browser object. The browser is
|
|
1819
1834
|
// created once at startup, so without this a /config set browser.*
|
|
1820
1835
|
// change would only apply after a restart. We also update the cached
|
|
@@ -1913,6 +1928,21 @@ async function main() {
|
|
|
1913
1928
|
writeConfigValue('home', field.path, raw);
|
|
1914
1929
|
setConfigRuntime(field.path, value);
|
|
1915
1930
|
}
|
|
1931
|
+
// Human-readable hint of the accepted values for a field, used in the error
|
|
1932
|
+
// message when /config set gets a bad value. Enum -> the allowed list,
|
|
1933
|
+
// number -> a range, boolean -> "true|false".
|
|
1934
|
+
function configValueHint(field) {
|
|
1935
|
+
if (field.values && field.values.length)
|
|
1936
|
+
return field.values.join('|');
|
|
1937
|
+
if (field.type === 'boolean')
|
|
1938
|
+
return 'true|false';
|
|
1939
|
+
if (field.type === 'number') {
|
|
1940
|
+
const lo = field.min !== undefined ? String(field.min) : '-';
|
|
1941
|
+
const hi = field.max !== undefined ? String(field.max) : '-';
|
|
1942
|
+
return t('cfg.range_hint', { min: lo, max: hi });
|
|
1943
|
+
}
|
|
1944
|
+
return field.type;
|
|
1945
|
+
}
|
|
1916
1946
|
function configResetField(field) {
|
|
1917
1947
|
resetConfigValue('home', field.path);
|
|
1918
1948
|
// Reset the runtime value to the default.
|
|
@@ -2058,7 +2088,7 @@ async function main() {
|
|
|
2058
2088
|
catch {
|
|
2059
2089
|
console.error(theme.error(t('cfg.bad_value', {
|
|
2060
2090
|
v: key,
|
|
2061
|
-
type: field
|
|
2091
|
+
type: configValueHint(field),
|
|
2062
2092
|
})));
|
|
2063
2093
|
return;
|
|
2064
2094
|
}
|
|
@@ -2341,7 +2371,8 @@ async function main() {
|
|
|
2341
2371
|
continue;
|
|
2342
2372
|
}
|
|
2343
2373
|
// ---------- Regular commands ----------
|
|
2344
|
-
if (lower === '/chats') {
|
|
2374
|
+
if (lower === '/chats' || lower.startsWith('/chats ')) {
|
|
2375
|
+
const filter = trimmed.slice('/chats'.length).trim().toLowerCase();
|
|
2345
2376
|
const spin = editor || mod.createSpinner(currentLocale);
|
|
2346
2377
|
if (editor)
|
|
2347
2378
|
editor.lock(t('msg.input_locked'));
|
|
@@ -2349,15 +2380,23 @@ async function main() {
|
|
|
2349
2380
|
try {
|
|
2350
2381
|
lastChats = await browser.listChats(30);
|
|
2351
2382
|
spin.stop();
|
|
2352
|
-
|
|
2383
|
+
// An optional query filters by title (substring, case-insensitive),
|
|
2384
|
+
// so a long chat list does not have to be scanned by eye.
|
|
2385
|
+
const shown = filter
|
|
2386
|
+
? lastChats.filter((c) => c.title.toLowerCase().includes(filter))
|
|
2387
|
+
: lastChats;
|
|
2388
|
+
if (!shown.length) {
|
|
2353
2389
|
console.log(theme.system(t('chats.none_hint')));
|
|
2354
2390
|
}
|
|
2355
2391
|
else {
|
|
2356
2392
|
console.log(theme.system(t('chats.recent')));
|
|
2357
|
-
|
|
2358
|
-
|
|
2393
|
+
// The number is the index in the FULL list, so `/resume <n>` keeps
|
|
2394
|
+
// pointing at the same chat even when a filter narrows the view.
|
|
2395
|
+
for (const c of shown) {
|
|
2396
|
+
const idx = lastChats.indexOf(c);
|
|
2397
|
+
const n = String(idx + 1).padStart(2, ' ');
|
|
2359
2398
|
console.log(` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`);
|
|
2360
|
-
}
|
|
2399
|
+
}
|
|
2361
2400
|
console.log(theme.system(t('chats.use_resume')));
|
|
2362
2401
|
}
|
|
2363
2402
|
}
|
|
@@ -2431,8 +2470,19 @@ async function main() {
|
|
|
2431
2470
|
}
|
|
2432
2471
|
continue;
|
|
2433
2472
|
}
|
|
2434
|
-
if (lower === '/sessions' ||
|
|
2435
|
-
|
|
2473
|
+
if (lower === '/sessions' ||
|
|
2474
|
+
lower === '/session' ||
|
|
2475
|
+
lower.startsWith('/sessions ') ||
|
|
2476
|
+
lower.startsWith('/session ')) {
|
|
2477
|
+
const filter = trimmed
|
|
2478
|
+
.replace(/^\/sessions?/i, '')
|
|
2479
|
+
.trim()
|
|
2480
|
+
.toLowerCase();
|
|
2481
|
+
// /sessions <query> filters by title or workdir (substring). Sorted by
|
|
2482
|
+
// recency already (listSessions).
|
|
2483
|
+
const all = listSessions().filter((s) => !filter ||
|
|
2484
|
+
(s.title || '').toLowerCase().includes(filter) ||
|
|
2485
|
+
(s.workdir || '').toLowerCase().includes(filter));
|
|
2436
2486
|
console.log(theme.system(t('sessions.dir', { v: sessionsDir() })));
|
|
2437
2487
|
if (!all.length) {
|
|
2438
2488
|
console.log(theme.system(t('sessions.none')));
|
|
@@ -3164,6 +3214,12 @@ async function main() {
|
|
|
3164
3214
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3165
3215
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3166
3216
|
await autoReload();
|
|
3217
|
+
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
3218
|
+
// command .md created during this session would otherwise not appear in
|
|
3219
|
+
// the «/» hints until a /reload.
|
|
3220
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
3221
|
+
if (editor)
|
|
3222
|
+
editor.setCommands(buildSlashCommands());
|
|
3167
3223
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
3168
3224
|
const tools = mod.createTools(currentWorkdir, { undo, todos: todoStore });
|
|
3169
3225
|
if (mcpPool)
|
package/dist/input.js
CHANGED
|
@@ -34,6 +34,20 @@ const PASTE_END = ESC + '[201~';
|
|
|
34
34
|
// kept aside and expanded back on submit. Pastes of 1–2 lines are inserted
|
|
35
35
|
// as-is (small pastes are usually short and don't clutter the line).
|
|
36
36
|
export const PASTE_MIN_LINES = 3;
|
|
37
|
+
// How many slash-command hints are shown at once (the rest are paged with
|
|
38
|
+
// Ctrl+N/P). The list used to be hard-capped at 8 with a "...more" note and no
|
|
39
|
+
// way to reach the rest.
|
|
40
|
+
export const SUGGEST_PAGE = 8;
|
|
41
|
+
// True when `q` is a subsequence of `s` (e.g. "hst" in "/history"). Used for
|
|
42
|
+
// the fuzzy fallback in _suggestions().
|
|
43
|
+
export function isSubsequence(q, s) {
|
|
44
|
+
let i = 0;
|
|
45
|
+
for (let j = 0; j < s.length && i < q.length; j++) {
|
|
46
|
+
if (s[j] === q[i])
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
return i === q.length;
|
|
50
|
+
}
|
|
37
51
|
// Number of lines in a pasted block (the trailing newline doesn't add a line).
|
|
38
52
|
export function countPasteLines(raw) {
|
|
39
53
|
const normalized = String(raw)
|
|
@@ -369,6 +383,14 @@ export class LineEditor {
|
|
|
369
383
|
// Slash-command hints (shown when you type «/»).
|
|
370
384
|
slashCommands;
|
|
371
385
|
_suggestCount;
|
|
386
|
+
// Scroll window of the suggestion list and the currently highlighted entry
|
|
387
|
+
// (Ctrl+N/P move the highlight; Enter inserts it). Without these the list
|
|
388
|
+
// was capped at 8 and the rest were unreachable.
|
|
389
|
+
_suggestOffset;
|
|
390
|
+
_suggestSelected;
|
|
391
|
+
// The buffer the suggestion highlight/scroll was computed for; when the
|
|
392
|
+
// buffer changes the selection resets to the top.
|
|
393
|
+
_suggestQuery;
|
|
372
394
|
// Pasted blocks collapsed into markers (expanded back on submit).
|
|
373
395
|
pastes;
|
|
374
396
|
_pasteBuf;
|
|
@@ -477,6 +499,9 @@ export class LineEditor {
|
|
|
477
499
|
this._typingRun = false;
|
|
478
500
|
this.slashCommands = commands;
|
|
479
501
|
this._suggestCount = 0;
|
|
502
|
+
this._suggestOffset = 0;
|
|
503
|
+
this._suggestSelected = -1;
|
|
504
|
+
this._suggestQuery = '';
|
|
480
505
|
this.pastes = [];
|
|
481
506
|
this._pasteBuf = '';
|
|
482
507
|
this.attachments = new AttachmentStore();
|
|
@@ -609,15 +634,101 @@ export class LineEditor {
|
|
|
609
634
|
}
|
|
610
635
|
// List of hints for the current input. We show them only when the line
|
|
611
636
|
// starts with «/» and is a single word (no spaces/newlines) — like
|
|
612
|
-
// command completion in Claude Code.
|
|
637
|
+
// command completion in Claude Code. The match is fuzzy: an exact prefix
|
|
638
|
+
// first, then a substring, then a subsequence (e.g. "hst" -> "/history"),
|
|
639
|
+
// so a half-remembered command name still surfaces. Order is preserved.
|
|
613
640
|
_suggestions() {
|
|
614
641
|
const b = this.buf;
|
|
615
642
|
if (!b.startsWith('/'))
|
|
616
643
|
return [];
|
|
617
644
|
if (b.includes(NL) || b.includes(' '))
|
|
618
645
|
return [];
|
|
646
|
+
if (b.length > 40)
|
|
647
|
+
return [];
|
|
619
648
|
const q = b.toLowerCase();
|
|
620
|
-
|
|
649
|
+
if (q === '/')
|
|
650
|
+
return this.slashCommands;
|
|
651
|
+
// Match on the part AFTER the leading slash: comparing the raw query
|
|
652
|
+
// (which itself starts with '/') against the full name made the fuzzy
|
|
653
|
+
// subsequence match fire on the slash alone (e.g. "/x" matched
|
|
654
|
+
// "/self-fix"), so a stray letter pulled in unrelated commands.
|
|
655
|
+
const body = q.slice(1);
|
|
656
|
+
const prefix = [];
|
|
657
|
+
const substring = [];
|
|
658
|
+
const fuzzy = [];
|
|
659
|
+
// A single character only matches by PREFIX: a one-letter substring /
|
|
660
|
+
// subsequence match is far too loose ("x" would pull in "/self-fix").
|
|
661
|
+
const loose = body.length >= 2;
|
|
662
|
+
for (const c of this.slashCommands) {
|
|
663
|
+
const name = c.name.toLowerCase().slice(1);
|
|
664
|
+
if (name.startsWith(body))
|
|
665
|
+
prefix.push(c);
|
|
666
|
+
else if (loose && name.includes(body))
|
|
667
|
+
substring.push(c);
|
|
668
|
+
else if (loose && isSubsequence(body, name))
|
|
669
|
+
fuzzy.push(c);
|
|
670
|
+
}
|
|
671
|
+
return prefix.concat(substring, fuzzy);
|
|
672
|
+
}
|
|
673
|
+
// Render the suggestion block for the current query. `offset` scrolls the
|
|
674
|
+
// window so Ctrl+N/P can page through more than the visible size (the list
|
|
675
|
+
// used to be silently capped at 8). Returns the lines to print BELOW the
|
|
676
|
+
// input and how many rows they occupy.
|
|
677
|
+
_suggestionLines(offset) {
|
|
678
|
+
const sugg = this._suggestions();
|
|
679
|
+
// The buffer changed since the last highlight -> drop the selection and
|
|
680
|
+
// reset the window to the top, so a stale highlight never sticks to an
|
|
681
|
+
// unrelated entry.
|
|
682
|
+
if (this.buf !== this._suggestQuery) {
|
|
683
|
+
this._suggestQuery = this.buf;
|
|
684
|
+
this._suggestSelected = 0;
|
|
685
|
+
this._suggestOffset = 0;
|
|
686
|
+
}
|
|
687
|
+
if (!sugg.length)
|
|
688
|
+
return { lines: [], rows: 0 };
|
|
689
|
+
const pageSize = SUGGEST_PAGE;
|
|
690
|
+
const maxOffset = Math.max(0, sugg.length - pageSize);
|
|
691
|
+
const off = Math.max(0, Math.min(offset, maxOffset));
|
|
692
|
+
const shown = sugg.slice(off, off + pageSize);
|
|
693
|
+
this._suggestOffset = off;
|
|
694
|
+
this._suggestCount = sugg.length;
|
|
695
|
+
const maxName = Math.max(...shown.map((c) => c.name.length));
|
|
696
|
+
const lines = shown.map((c, i) => {
|
|
697
|
+
const selected = off + i === this._suggestSelected && this._suggestSelected > 0;
|
|
698
|
+
const name = theme.prompt(c.name.padEnd(maxName));
|
|
699
|
+
const desc = theme.dim(' ' + c.description);
|
|
700
|
+
return (selected ? theme.prompt(' ▸ ') : ' ') + name + desc;
|
|
701
|
+
});
|
|
702
|
+
if (sugg.length > pageSize) {
|
|
703
|
+
const t = translate(this.locale);
|
|
704
|
+
lines.push(theme.dim(' ' +
|
|
705
|
+
t('editor.more', {
|
|
706
|
+
n: String(sugg.length - shown.length),
|
|
707
|
+
}) +
|
|
708
|
+
' ' +
|
|
709
|
+
t('editor.page_hint')));
|
|
710
|
+
}
|
|
711
|
+
return { lines, rows: lines.length };
|
|
712
|
+
}
|
|
713
|
+
// Move the highlight in the suggestion list (Ctrl+N/Ctrl+P). The window
|
|
714
|
+
// scrolls so the highlighted entry stays visible. -1 means "no explicit
|
|
715
|
+
// selection" (Tab then falls back to the common-prefix completion).
|
|
716
|
+
_suggestMove(delta) {
|
|
717
|
+
const sugg = this._suggestions();
|
|
718
|
+
if (!sugg.length)
|
|
719
|
+
return;
|
|
720
|
+
const next = this._suggestSelected + delta;
|
|
721
|
+
this._suggestSelected = Math.max(-1, Math.min(sugg.length - 1, next));
|
|
722
|
+
if (this._suggestSelected < 0) {
|
|
723
|
+
this._suggestOffset = 0;
|
|
724
|
+
}
|
|
725
|
+
else if (this._suggestSelected < this._suggestOffset) {
|
|
726
|
+
this._suggestOffset = this._suggestSelected;
|
|
727
|
+
}
|
|
728
|
+
else if (this._suggestSelected >= this._suggestOffset + SUGGEST_PAGE) {
|
|
729
|
+
this._suggestOffset = this._suggestSelected - SUGGEST_PAGE + 1;
|
|
730
|
+
}
|
|
731
|
+
this._renderInputOnly();
|
|
621
732
|
}
|
|
622
733
|
// Tab: complete the command up to the common prefix; if there is a single
|
|
623
734
|
// match — insert it whole and add a space.
|
|
@@ -625,6 +736,11 @@ export class LineEditor {
|
|
|
625
736
|
const sugg = this._suggestions();
|
|
626
737
|
if (!sugg.length)
|
|
627
738
|
return;
|
|
739
|
+
if (this._suggestSelected >= 0 && this._suggestSelected < sugg.length) {
|
|
740
|
+
this.buf = sugg[this._suggestSelected].name + ' ';
|
|
741
|
+
this.cursor = Array.from(this.buf).length;
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
628
744
|
if (sugg.length === 1) {
|
|
629
745
|
this.buf = sugg[0].name + ' ';
|
|
630
746
|
this.cursor = Array.from(this.buf).length;
|
|
@@ -893,29 +1009,15 @@ export class LineEditor {
|
|
|
893
1009
|
if (statusUnchanged) {
|
|
894
1010
|
// Count the real input rows via layout (wrapping aware).
|
|
895
1011
|
const layOnly = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
896
|
-
const suggOnly = this.
|
|
897
|
-
const
|
|
898
|
-
const linesBelowOnly = shownOnly.length
|
|
899
|
-
? shownOnly.length + (suggOnly.length > shownOnly.length ? 1 : 0)
|
|
900
|
-
: 0;
|
|
1012
|
+
const suggOnly = this._suggestionLines(this._suggestOffset);
|
|
1013
|
+
const linesBelowOnly = suggOnly.rows;
|
|
901
1014
|
// Move up to the INPUT top using the OLD cursor row (the position that is
|
|
902
1015
|
// on screen right now), never the input's height.
|
|
903
1016
|
this._eraseInputOnly(this.cursorRowFromTop - this._statusTop);
|
|
904
1017
|
let outOnly = '';
|
|
905
1018
|
outOnly += layOnly.rows.map((r) => r.prefix + r.text).join(NL);
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
const lines = shownOnly.map((c) => {
|
|
909
|
-
const name = theme.prompt(c.name.padEnd(maxName));
|
|
910
|
-
const desc = theme.dim(' ' + c.description);
|
|
911
|
-
return ' ' + name + desc;
|
|
912
|
-
});
|
|
913
|
-
outOnly += NL + lines.join(NL);
|
|
914
|
-
const hidden = suggOnly.length - shownOnly.length;
|
|
915
|
-
if (hidden > 0)
|
|
916
|
-
outOnly +=
|
|
917
|
-
NL +
|
|
918
|
-
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
1019
|
+
if (suggOnly.lines.length) {
|
|
1020
|
+
outOnly += NL + suggOnly.lines.join(NL);
|
|
919
1021
|
}
|
|
920
1022
|
// The input may have UNWRAPPED (fewer rows than before) — push the block
|
|
921
1023
|
// back to the bottom BEFORE the new content is printed (the padding is
|
|
@@ -940,26 +1042,10 @@ export class LineEditor {
|
|
|
940
1042
|
out += lay.rows.map((r) => r.prefix + r.text).join(NL);
|
|
941
1043
|
// We draw the slash-command hints BELOW the input line. We then move the
|
|
942
1044
|
// cursor back up to the input line, so cursorRowFromTop doesn't change.
|
|
943
|
-
const sugg = this.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const maxName = Math.max(...shown.map((c) => c.name.length));
|
|
948
|
-
const lines = shown.map((c) => {
|
|
949
|
-
const name = theme.prompt(c.name.padEnd(maxName));
|
|
950
|
-
const desc = theme.dim(' ' + c.description);
|
|
951
|
-
return ' ' + name + desc;
|
|
952
|
-
});
|
|
953
|
-
out += NL + lines.join(NL);
|
|
954
|
-
const hidden = sugg.length - shown.length;
|
|
955
|
-
if (hidden > 0)
|
|
956
|
-
out +=
|
|
957
|
-
NL +
|
|
958
|
-
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
959
|
-
}
|
|
960
|
-
const linesBelow = shown.length
|
|
961
|
-
? shown.length + (sugg.length > shown.length ? 1 : 0)
|
|
962
|
-
: 0;
|
|
1045
|
+
const sugg = this._suggestionLines(this._suggestOffset);
|
|
1046
|
+
if (sugg.lines.length)
|
|
1047
|
+
out += NL + sugg.lines.join(NL);
|
|
1048
|
+
const linesBelow = sugg.rows;
|
|
963
1049
|
// If the block SHRANK (the pause status went away, the input unwrapped),
|
|
964
1050
|
// the erase left the cursor at the old block top; printing a shorter block
|
|
965
1051
|
// would end above the screen bottom and the footer would float with blank
|
|
@@ -1101,13 +1187,15 @@ export class LineEditor {
|
|
|
1101
1187
|
return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
|
|
1102
1188
|
}
|
|
1103
1189
|
// The lifecycle phase prefix for the status line, colored by meaning:
|
|
1104
|
-
// generating (brown)
|
|
1105
|
-
//
|
|
1106
|
-
// reads "пауза Ns"). Returns '' when no
|
|
1107
|
-
// is not prefixed.
|
|
1190
|
+
// generating (brown) and settled (green check) both get a prefix, so the
|
|
1191
|
+
// transition from "still generating" to "answer done" is visible; 'paused'
|
|
1192
|
+
// has none (the base status itself reads "пауза Ns"). Returns '' when no
|
|
1193
|
+
// phase is active, so an idle status is not prefixed.
|
|
1108
1194
|
_stateLabel() {
|
|
1109
1195
|
if (this._sendState === 'generating')
|
|
1110
1196
|
return (theme.brown(translate(this.locale)('state.generating')) + theme.dim(':'));
|
|
1197
|
+
if (this._sendState === 'settled')
|
|
1198
|
+
return (theme.success(translate(this.locale)('state.settled')) + theme.dim(':'));
|
|
1111
1199
|
return '';
|
|
1112
1200
|
}
|
|
1113
1201
|
// Called by the browser layer at real lifecycle points: right before a
|
|
@@ -1835,6 +1923,16 @@ export class LineEditor {
|
|
|
1835
1923
|
this._searchStart();
|
|
1836
1924
|
continue;
|
|
1837
1925
|
}
|
|
1926
|
+
if (code === 14) {
|
|
1927
|
+
// Ctrl+N — move the suggestion highlight down (like a menu).
|
|
1928
|
+
this._suggestMove(1);
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
if (code === 16) {
|
|
1932
|
+
// Ctrl+P — move the suggestion highlight up.
|
|
1933
|
+
this._suggestMove(-1);
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1838
1936
|
if (code === 23) {
|
|
1839
1937
|
this._pushUndo();
|
|
1840
1938
|
this._deleteWordLeft();
|
package/dist/markdown.js
CHANGED
|
@@ -26,12 +26,23 @@ function highlighter(code, lang) {
|
|
|
26
26
|
return code;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
// Configured maximum answer width (columns). 0 = auto: terminal width, capped
|
|
30
|
+
// at 100. Set once from config (ui.answerWidth) so every render path honors it
|
|
31
|
+
// without threading the option through every call site.
|
|
32
|
+
let configuredWidth = 0;
|
|
33
|
+
export function setAnswerWidth(width) {
|
|
34
|
+
configuredWidth = typeof width === 'number' && width > 0 ? width : 0;
|
|
35
|
+
}
|
|
29
36
|
export function renderMarkdown(text) {
|
|
30
37
|
if (!text)
|
|
31
38
|
return '';
|
|
32
39
|
try {
|
|
40
|
+
const cols = process.stdout.columns || 80;
|
|
41
|
+
const width = configuredWidth > 0
|
|
42
|
+
? Math.min(configuredWidth, cols)
|
|
43
|
+
: Math.min(cols, 100);
|
|
33
44
|
return render(String(text), {
|
|
34
|
-
width
|
|
45
|
+
width,
|
|
35
46
|
theme: theme,
|
|
36
47
|
highlighter,
|
|
37
48
|
codeBox: true,
|
package/dist/self-review.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
+
import { readdirSync } from 'fs';
|
|
3
4
|
import { theme, divider } from './theme.js';
|
|
4
5
|
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -13,7 +14,7 @@ function resolveSrcDir() {
|
|
|
13
14
|
const candidates = [__dirname, path.join(__dirname, '..', 'src')];
|
|
14
15
|
for (const dir of candidates) {
|
|
15
16
|
try {
|
|
16
|
-
const entries =
|
|
17
|
+
const entries = readdirSync(dir);
|
|
17
18
|
if (entries.some((e) => e.endsWith('.ts')))
|
|
18
19
|
return dir;
|
|
19
20
|
}
|
package/dist/undo.js
CHANGED
|
@@ -22,7 +22,10 @@ export class UndoStore {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
async _writeIndex(history) {
|
|
25
|
-
|
|
25
|
+
// Atomic write: a crash mid-write must not corrupt the undo history.
|
|
26
|
+
const tmp = `${INDEX}.tmp-${process.pid}`;
|
|
27
|
+
await fs.writeFile(tmp, JSON.stringify(history, null, 2));
|
|
28
|
+
await fs.rename(tmp, INDEX);
|
|
26
29
|
}
|
|
27
30
|
async backup(filePath) {
|
|
28
31
|
if (!this.enabled)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.53.1",
|
|
4
4
|
"description": "Terminal coding agent over chat.deepseek.com via Playwright",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"chalk": "^5.3.0",
|
|
46
46
|
"cli-highlight": "^2.1.11",
|
|
47
47
|
"markdansi": "^0.3.4",
|
|
48
|
-
"marked": "^15.0.12",
|
|
49
48
|
"ora": "^8.0.1",
|
|
50
49
|
"playwright": "^1.48.0"
|
|
51
50
|
},
|