zames_pro 2.51.0 → 2.53.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/README.md +22 -1
- package/dist/color.js +8 -0
- package/dist/commands.js +21 -0
- package/dist/config-menu.js +23 -1
- package/dist/config.js +10 -0
- package/dist/i18n.js +49 -8
- package/dist/index.js +102 -15
- package/dist/input.js +191 -44
- package/dist/markdown.js +12 -1
- package/dist/sessions.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,23 @@ While the agent is working you can keep typing: press Enter to queue a message
|
|
|
76
76
|
abort the current generation. This mirrors typing during generation on the
|
|
77
77
|
DeepSeek website.
|
|
78
78
|
|
|
79
|
+
### Input line
|
|
80
|
+
|
|
81
|
+
The prompt is a small line editor with persistent history:
|
|
82
|
+
|
|
83
|
+
- `↑` / `↓` — walk the message history (saved in `~/.zames/history.json`, so it
|
|
84
|
+
survives a restart); inside a multiline message the arrows move between lines.
|
|
85
|
+
- `Ctrl+R` — incremental reverse search over the history (bash-style): type to
|
|
86
|
+
filter, `Ctrl+R` for older matches, `Enter` to accept, `Esc` to cancel.
|
|
87
|
+
- `Ctrl+_` — undo the last edit in the input line (a fat-fingered `Ctrl+U` /
|
|
88
|
+
`Ctrl+K` is recoverable).
|
|
89
|
+
- `Ctrl+U` — clear the line, `Ctrl+K` — delete to end of line, `Ctrl+W` —
|
|
90
|
+
delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words.
|
|
91
|
+
- `\` + `Enter`, `Ctrl+J`, `Ctrl+Enter` or `Shift+Enter` — insert a newline.
|
|
92
|
+
- `/` + `Tab` — slash-command hints and completion.
|
|
93
|
+
|
|
94
|
+
Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
|
|
95
|
+
|
|
79
96
|
### Images and files
|
|
80
97
|
|
|
81
98
|
You can paste an image or a file into the input line (Ctrl+Shift+V / Shift+Insert
|
|
@@ -149,7 +166,11 @@ Codex CLI:
|
|
|
149
166
|
startup; relaunch with --dir to write there).
|
|
150
167
|
- /resume <n> (after /chats) and /resume-id <id> — open a chat and PRINT its
|
|
151
168
|
dialogue into the terminal, so the restored context is visible. Only the
|
|
152
|
-
last 20 messages are shown (`RESTORED_HISTORY_LIMIT`).
|
|
169
|
+
last 20 messages are shown (`RESTORED_HISTORY_LIMIT`). /last reopens the last
|
|
170
|
+
chat of the current directory with no lookup step; /sessions shows the saved
|
|
171
|
+
sessions with their relative age.
|
|
172
|
+
- /help <command> — the full description of a single command (e.g. `/help diff`)
|
|
173
|
+
instead of the whole list.
|
|
153
174
|
- /review [focus] [--staged] — ask the agent to review uncommitted changes
|
|
154
175
|
and report findings (no code changes).
|
|
155
176
|
- /compact — ask DeepSeek to compress the current chat into a handover
|
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/commands.js
CHANGED
|
@@ -86,6 +86,27 @@ export function formatDuration(ms, units = EN_DURATION_UNITS) {
|
|
|
86
86
|
return m + units.m + ' ' + pad(s) + units.s;
|
|
87
87
|
return s + units.s;
|
|
88
88
|
}
|
|
89
|
+
// Short relative age ("just now", "5m ago", "3h ago", "2d ago") for the
|
|
90
|
+
// /sessions and /chats lists. The operator cares about RECENCY, not a raw
|
|
91
|
+
// ISO timestamp, when picking a session to restore.
|
|
92
|
+
export function formatRelativeTime(iso, t = translate('en'), now = Date.now()) {
|
|
93
|
+
if (!iso)
|
|
94
|
+
return '';
|
|
95
|
+
const then = Date.parse(iso);
|
|
96
|
+
if (!Number.isFinite(then))
|
|
97
|
+
return '';
|
|
98
|
+
const diff = Math.max(0, now - then);
|
|
99
|
+
const min = Math.floor(diff / 60_000);
|
|
100
|
+
if (min < 1)
|
|
101
|
+
return t('time.now');
|
|
102
|
+
if (min < 60)
|
|
103
|
+
return t('time.min_ago', { n: String(min) });
|
|
104
|
+
const hours = Math.floor(min / 60);
|
|
105
|
+
if (hours < 24)
|
|
106
|
+
return t('time.hour_ago', { n: String(hours) });
|
|
107
|
+
const days = Math.floor(hours / 24);
|
|
108
|
+
return t('time.day_ago', { n: String(days) });
|
|
109
|
+
}
|
|
89
110
|
export function renderCost(stats, transcriptFile, tokenUsage = null, t = translate('en')) {
|
|
90
111
|
const lines = [];
|
|
91
112
|
lines.push(t('cost.title'));
|
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
|
@@ -69,6 +69,8 @@ export const DEFAULTS = {
|
|
|
69
69
|
ui: {
|
|
70
70
|
locale: DEFAULT_LOCALE,
|
|
71
71
|
contextLimit: 1_000_000,
|
|
72
|
+
// 0 = auto: terminal width, capped at 100 columns (the historical width).
|
|
73
|
+
answerWidth: 0,
|
|
72
74
|
},
|
|
73
75
|
};
|
|
74
76
|
export function loadConfig() {
|
|
@@ -116,6 +118,14 @@ export const CONFIG_SCHEMA = [
|
|
|
116
118
|
labelKey: 'cfg.f.maxIterations',
|
|
117
119
|
groupKey: 'cfg.group.agent',
|
|
118
120
|
},
|
|
121
|
+
{
|
|
122
|
+
path: 'ui.answerWidth',
|
|
123
|
+
type: 'number',
|
|
124
|
+
min: 0,
|
|
125
|
+
max: 1000,
|
|
126
|
+
labelKey: 'cfg.f.ui_answerWidth',
|
|
127
|
+
groupKey: 'cfg.group.ui',
|
|
128
|
+
},
|
|
119
129
|
{
|
|
120
130
|
path: 'headless',
|
|
121
131
|
type: 'boolean',
|
package/dist/i18n.js
CHANGED
|
@@ -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: 'Пока агент работает:',
|
|
@@ -103,13 +107,17 @@ const CATALOG = {
|
|
|
103
107
|
ru: 'Ctrl+R поиск по истории (обратный)',
|
|
104
108
|
en: 'Ctrl+R reverse-search the history',
|
|
105
109
|
},
|
|
110
|
+
'help.key.undo': {
|
|
111
|
+
ru: 'Ctrl+_ отменить правку в строке ввода',
|
|
112
|
+
en: 'Ctrl+_ undo the last edit in the input line',
|
|
113
|
+
},
|
|
106
114
|
'help.key.words': {
|
|
107
115
|
ru: 'Ctrl+← / Ctrl+→ перемещение по словам',
|
|
108
116
|
en: 'Ctrl+← / Ctrl+→ move by words',
|
|
109
117
|
},
|
|
110
118
|
'help.key.slash': {
|
|
111
|
-
ru: '/ + Tab подсказка и автодополнение slash-команд',
|
|
112
|
-
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)',
|
|
113
121
|
},
|
|
114
122
|
'help.key.newline': {
|
|
115
123
|
ru: 'Ctrl+J / Ctrl+Enter новая строка (Shift+Enter в терминалах с поддержкой)',
|
|
@@ -133,16 +141,16 @@ const CATALOG = {
|
|
|
133
141
|
en: '/new, /clear new chat (reset context)',
|
|
134
142
|
},
|
|
135
143
|
'help.cmd.sessions': {
|
|
136
|
-
ru: '/sessions
|
|
137
|
-
en: '/sessions
|
|
144
|
+
ru: '/sessions [фильтр] список сохранённых сессий (можно фильтровать)',
|
|
145
|
+
en: '/sessions [filter] list saved sessions (optionally filtered)',
|
|
138
146
|
},
|
|
139
147
|
'help.cmd.resume_id': {
|
|
140
148
|
ru: '/resume-id <id> восстановить сессию по полному id',
|
|
141
149
|
en: '/resume-id <id> resume a session by full id',
|
|
142
150
|
},
|
|
143
151
|
'help.cmd.chats': {
|
|
144
|
-
ru: '/chats
|
|
145
|
-
en: '/chats
|
|
152
|
+
ru: '/chats [фильтр] список последних чатов DeepSeek',
|
|
153
|
+
en: '/chats [filter] recent DeepSeek chats',
|
|
146
154
|
},
|
|
147
155
|
'help.cmd.resume': {
|
|
148
156
|
ru: '/resume <n> открыть чат №n из /chats',
|
|
@@ -363,6 +371,10 @@ const CATALOG = {
|
|
|
363
371
|
ru: '/debug-dom сохранить HTML страницы (для отладки)',
|
|
364
372
|
en: '/debug-dom dump page HTML (debug)',
|
|
365
373
|
},
|
|
374
|
+
'help.no_topic': {
|
|
375
|
+
ru: 'Нет команды по запросу «{v}». Попробуй /help без аргумента.',
|
|
376
|
+
en: 'No command matches "{v}". Try /help without arguments.',
|
|
377
|
+
},
|
|
366
378
|
'help.cmd.help': {
|
|
367
379
|
ru: '/help, help справка',
|
|
368
380
|
en: '/help, help this help',
|
|
@@ -455,7 +467,15 @@ const CATALOG = {
|
|
|
455
467
|
'help.files.profile': { ru: 'Профиль:', en: 'Profile:' },
|
|
456
468
|
'help.files.snapshots': { ru: 'Снапшоты:', en: 'Snapshots:' },
|
|
457
469
|
'help.files.tmp': { ru: 'Временные:', en: 'Temp:' },
|
|
470
|
+
'help.files.tmp_note': {
|
|
471
|
+
ru: '(в .gitignore, очищается при старте)',
|
|
472
|
+
en: '(.gitignore, cleaned on start)',
|
|
473
|
+
},
|
|
458
474
|
'help.files.config': { ru: 'Конфиг:', en: 'Config:' },
|
|
475
|
+
'help.toggles_legend': {
|
|
476
|
+
ru: 'Индикаторы в статус-строке: 🧠 — глубокое размышление, 🌐 — поиск в интернете (видны, когда включены).',
|
|
477
|
+
en: 'Status-line icons: 🧠 — Deep thinking, 🌐 — web search (shown when enabled).',
|
|
478
|
+
},
|
|
459
479
|
// ---------- spinner ----------
|
|
460
480
|
'spinner.phrases': {
|
|
461
481
|
ru: 'Анализирую запрос…|Изучаю контекст…|Планирую решение…|Обрабатываю данные…|Формулирую ответ…|Проверяю детали…|Собираю информацию…|Обдумываю варианты…|Структурирую мысли…|Готовлю решение…|Анализирую код…|Уточняю детали…|Выполняю вычисления…|Систематизирую данные…|Прорабатываю задачу…|Проверяю логику…|Оптимизирую подход…|Формирую выводы…',
|
|
@@ -474,7 +494,12 @@ const CATALOG = {
|
|
|
474
494
|
// animated thinking phrase, it says explicitly that a generation is in
|
|
475
495
|
// flight. 'paused'/'settled' have no label (see LineEditor._stateLabel).
|
|
476
496
|
'state.generating': { ru: '▶ генерация', en: '▶ generating' },
|
|
497
|
+
'state.settled': { ru: '✓ готово', en: '✓ done' },
|
|
477
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
|
+
},
|
|
478
503
|
'editor.answer': { ru: '● Ответ', en: '● Answer' },
|
|
479
504
|
// Reverse search (Ctrl+R) status line. The query may be empty.
|
|
480
505
|
'editor.search_prompt': {
|
|
@@ -565,6 +590,10 @@ const CATALOG = {
|
|
|
565
590
|
ru: 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
|
|
566
591
|
en: 'Interactive mode. Enter a task. Commands — /help. Exit — /exit.',
|
|
567
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
|
+
},
|
|
568
597
|
'msg.first_hint': {
|
|
569
598
|
ru: 'Подсказка: «/» — список команд, Ctrl+R — поиск по истории, картинку можно вставить через Ctrl+Shift+V.',
|
|
570
599
|
en: 'Hint: "/" lists commands, Ctrl+R searches history, paste an image with Ctrl+Shift+V.',
|
|
@@ -1080,6 +1109,10 @@ const CATALOG = {
|
|
|
1080
1109
|
},
|
|
1081
1110
|
'chats.history_you': { ru: 'Вы', en: 'You' },
|
|
1082
1111
|
'chats.history_agent': { ru: 'Агент', en: 'Agent' },
|
|
1112
|
+
'time.now': { ru: 'только что', en: 'just now' },
|
|
1113
|
+
'time.min_ago': { ru: '{n} мин назад', en: '{n}m ago' },
|
|
1114
|
+
'time.hour_ago': { ru: '{n} ч назад', en: '{n}h ago' },
|
|
1115
|
+
'time.day_ago': { ru: '{n} дн назад', en: '{n}d ago' },
|
|
1083
1116
|
'sessions.dir': { ru: 'Папка сессий: {v}', en: 'Sessions dir: {v}' },
|
|
1084
1117
|
'sessions.none': {
|
|
1085
1118
|
ru: 'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
|
|
@@ -1192,6 +1225,10 @@ const CATALOG = {
|
|
|
1192
1225
|
ru: 'Язык интерфейса и ответов агента',
|
|
1193
1226
|
en: 'Interface and agent reply language',
|
|
1194
1227
|
},
|
|
1228
|
+
'cfg.f.ui_answerWidth': {
|
|
1229
|
+
ru: 'Максимальная ширина ответа в колонках (0 — авто, до 100)',
|
|
1230
|
+
en: 'Max answer width in columns (0 = auto, capped at 100)',
|
|
1231
|
+
},
|
|
1195
1232
|
'cfg.f.maxIterations': {
|
|
1196
1233
|
ru: 'Лимит итераций на задачу',
|
|
1197
1234
|
en: 'Max agent loop iterations per task',
|
|
@@ -1380,8 +1417,8 @@ const CATALOG = {
|
|
|
1380
1417
|
en: '⚙ Settings — choose a parameter',
|
|
1381
1418
|
},
|
|
1382
1419
|
'cfg.menu.hint': {
|
|
1383
|
-
ru: '↑/↓ — выбор, Enter — изменить, q/Esc — выйти',
|
|
1384
|
-
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',
|
|
1385
1422
|
},
|
|
1386
1423
|
'cfg.menu.edit_hint': {
|
|
1387
1424
|
ru: 'Введи значение и Enter (пусто — отмена)',
|
|
@@ -1430,6 +1467,10 @@ const CATALOG = {
|
|
|
1430
1467
|
ru: 'Неверное значение для {v}: ожидалось {type}',
|
|
1431
1468
|
en: 'Invalid value for {v}: expected {type}',
|
|
1432
1469
|
},
|
|
1470
|
+
'cfg.range_hint': {
|
|
1471
|
+
ru: 'число от {min} до {max}',
|
|
1472
|
+
en: 'a number from {min} to {max}',
|
|
1473
|
+
},
|
|
1433
1474
|
'cfg.write_error': {
|
|
1434
1475
|
ru: 'Не удалось записать конфиг: {v}',
|
|
1435
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';
|
|
@@ -17,12 +18,12 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
|
|
|
17
18
|
import { Transcript } from './transcript.js';
|
|
18
19
|
import { UndoStore } from './undo.js';
|
|
19
20
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
20
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, formatDuration, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
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
|
-
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, } from './sessions.js';
|
|
26
|
+
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
26
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const NL = String.fromCharCode(10);
|
|
28
29
|
// Floor for a /loop interval. A loop faster than the send throttle (15s)
|
|
@@ -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
|
|
|
@@ -299,6 +303,7 @@ ${theme.bold(t('help.while_working'))}
|
|
|
299
303
|
${t('help.key.history')}
|
|
300
304
|
${t('help.key.search')}
|
|
301
305
|
${t('help.key.words')}
|
|
306
|
+
${t('help.key.undo')}
|
|
302
307
|
${t('help.key.slash')}
|
|
303
308
|
${t('help.key.newline')}
|
|
304
309
|
${t('help.key.backslash')}
|
|
@@ -357,9 +362,11 @@ ${theme.bold(t('help.files'))}
|
|
|
357
362
|
${t('help.files.sessions')} ${sessionsDir()}
|
|
358
363
|
${t('help.files.profile')} ~/.zames/profile
|
|
359
364
|
${t('help.files.snapshots')} ~/.zames/snapshots
|
|
360
|
-
${t('help.files.tmp')} <project>/tmp (.
|
|
365
|
+
${t('help.files.tmp')} <project>/tmp ${t('help.files.tmp_note')}
|
|
361
366
|
${t('help.files.config')} ${CONFIG_PATHS.HOME_CONFIG}
|
|
362
367
|
${CONFIG_PATHS.PROJECT_CONFIG}
|
|
368
|
+
|
|
369
|
+
${theme.dim(t('help.toggles_legend'))}
|
|
363
370
|
`);
|
|
364
371
|
}
|
|
365
372
|
function dirLabel(p) {
|
|
@@ -1339,6 +1346,12 @@ async function main() {
|
|
|
1339
1346
|
console.log(theme.system(t('msg.interactive')));
|
|
1340
1347
|
console.log(theme.system(t('msg.queue_hint')));
|
|
1341
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
|
+
}
|
|
1342
1355
|
let freshChatNext = true;
|
|
1343
1356
|
let sendSystemPromptNext = true;
|
|
1344
1357
|
// Resuming an existing chat already has the system-prompt at its start, so
|
|
@@ -1544,6 +1557,11 @@ async function main() {
|
|
|
1544
1557
|
});
|
|
1545
1558
|
editor = ed;
|
|
1546
1559
|
ed.setTmpDir(TMP_DIR);
|
|
1560
|
+
// Restore the input history from the previous runs and persist every new
|
|
1561
|
+
// entry, so Ctrl+R / arrows can recall older messages after a restart.
|
|
1562
|
+
ed.history = loadHistory();
|
|
1563
|
+
ed._histIndex = ed.history.length;
|
|
1564
|
+
ed.onHistoryChange = (h) => saveHistory(h);
|
|
1547
1565
|
// The token context right-aligned on the status line (above the input).
|
|
1548
1566
|
// The editor pulls the number on every render, so it follows the live
|
|
1549
1567
|
// DeepSeek counter (accumulated_token_usage) without a polling timer.
|
|
@@ -1807,6 +1825,11 @@ async function main() {
|
|
|
1807
1825
|
editor.setPrompt(buildPrompt());
|
|
1808
1826
|
}
|
|
1809
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
|
+
}
|
|
1810
1833
|
// Reflect browser toggles on the LIVE browser object. The browser is
|
|
1811
1834
|
// created once at startup, so without this a /config set browser.*
|
|
1812
1835
|
// change would only apply after a restart. We also update the cached
|
|
@@ -1905,6 +1928,21 @@ async function main() {
|
|
|
1905
1928
|
writeConfigValue('home', field.path, raw);
|
|
1906
1929
|
setConfigRuntime(field.path, value);
|
|
1907
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
|
+
}
|
|
1908
1946
|
function configResetField(field) {
|
|
1909
1947
|
resetConfigValue('home', field.path);
|
|
1910
1948
|
// Reset the runtime value to the default.
|
|
@@ -2050,7 +2088,7 @@ async function main() {
|
|
|
2050
2088
|
catch {
|
|
2051
2089
|
console.error(theme.error(t('cfg.bad_value', {
|
|
2052
2090
|
v: key,
|
|
2053
|
-
type: field
|
|
2091
|
+
type: configValueHint(field),
|
|
2054
2092
|
})));
|
|
2055
2093
|
return;
|
|
2056
2094
|
}
|
|
@@ -2115,8 +2153,29 @@ async function main() {
|
|
|
2115
2153
|
const lower = trimmed.toLowerCase();
|
|
2116
2154
|
if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
|
|
2117
2155
|
break;
|
|
2118
|
-
if (lower === '/help' ||
|
|
2119
|
-
|
|
2156
|
+
if (lower === '/help' ||
|
|
2157
|
+
lower === 'help' ||
|
|
2158
|
+
lower.startsWith('/help ') ||
|
|
2159
|
+
lower.startsWith('help ')) {
|
|
2160
|
+
const topic = trimmed.replace(/^\/?help\s*/i, '').trim();
|
|
2161
|
+
if (!topic) {
|
|
2162
|
+
printHelp();
|
|
2163
|
+
}
|
|
2164
|
+
else {
|
|
2165
|
+
// /help <command> — print the full description of ONE command instead
|
|
2166
|
+
// of making the operator scroll the whole list.
|
|
2167
|
+
const q = '/' + topic.replace(/^\//, '').toLowerCase();
|
|
2168
|
+
const match = buildSlashCommands().filter((c) => c.name.toLowerCase() === q ||
|
|
2169
|
+
c.name.toLowerCase().startsWith(q + ' '));
|
|
2170
|
+
if (!match.length) {
|
|
2171
|
+
console.error(theme.warn(t('help.no_topic', { v: topic })));
|
|
2172
|
+
}
|
|
2173
|
+
else {
|
|
2174
|
+
for (const c of match) {
|
|
2175
|
+
console.log(theme.assistant(c.name) + theme.dim(' ' + c.description));
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2120
2179
|
continue;
|
|
2121
2180
|
}
|
|
2122
2181
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
@@ -2312,7 +2371,8 @@ async function main() {
|
|
|
2312
2371
|
continue;
|
|
2313
2372
|
}
|
|
2314
2373
|
// ---------- Regular commands ----------
|
|
2315
|
-
if (lower === '/chats') {
|
|
2374
|
+
if (lower === '/chats' || lower.startsWith('/chats ')) {
|
|
2375
|
+
const filter = trimmed.slice('/chats'.length).trim().toLowerCase();
|
|
2316
2376
|
const spin = editor || mod.createSpinner(currentLocale);
|
|
2317
2377
|
if (editor)
|
|
2318
2378
|
editor.lock(t('msg.input_locked'));
|
|
@@ -2320,15 +2380,23 @@ async function main() {
|
|
|
2320
2380
|
try {
|
|
2321
2381
|
lastChats = await browser.listChats(30);
|
|
2322
2382
|
spin.stop();
|
|
2323
|
-
|
|
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) {
|
|
2324
2389
|
console.log(theme.system(t('chats.none_hint')));
|
|
2325
2390
|
}
|
|
2326
2391
|
else {
|
|
2327
2392
|
console.log(theme.system(t('chats.recent')));
|
|
2328
|
-
|
|
2329
|
-
|
|
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, ' ');
|
|
2330
2398
|
console.log(` ${theme.user(n)}. ${c.title} ${theme.system('(' + c.id.slice(0, 8) + '…)')}`);
|
|
2331
|
-
}
|
|
2399
|
+
}
|
|
2332
2400
|
console.log(theme.system(t('chats.use_resume')));
|
|
2333
2401
|
}
|
|
2334
2402
|
}
|
|
@@ -2402,8 +2470,19 @@ async function main() {
|
|
|
2402
2470
|
}
|
|
2403
2471
|
continue;
|
|
2404
2472
|
}
|
|
2405
|
-
if (lower === '/sessions' ||
|
|
2406
|
-
|
|
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));
|
|
2407
2486
|
console.log(theme.system(t('sessions.dir', { v: sessionsDir() })));
|
|
2408
2487
|
if (!all.length) {
|
|
2409
2488
|
console.log(theme.system(t('sessions.none')));
|
|
@@ -2414,7 +2493,9 @@ async function main() {
|
|
|
2414
2493
|
const mark = s.id === currentChatId ? theme.user(' *') : '';
|
|
2415
2494
|
const title = s.title ? ` ${s.title}` : '';
|
|
2416
2495
|
const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
|
|
2417
|
-
|
|
2496
|
+
const age = formatRelativeTime(s.updatedAt, t);
|
|
2497
|
+
const ageText = age ? theme.dim(' ' + age) : '';
|
|
2498
|
+
console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${ageText}${mark}`);
|
|
2418
2499
|
});
|
|
2419
2500
|
console.log(theme.system(t('sessions.restore_hint')));
|
|
2420
2501
|
}
|
|
@@ -3133,6 +3214,12 @@ async function main() {
|
|
|
3133
3214
|
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
3134
3215
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3135
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());
|
|
3136
3223
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
3137
3224
|
const tools = mod.createTools(currentWorkdir, { undo, todos: todoStore });
|
|
3138
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)
|
|
@@ -356,9 +370,27 @@ export class LineEditor {
|
|
|
356
370
|
_searchQuery;
|
|
357
371
|
_searchIndex;
|
|
358
372
|
_searchSavedBuf;
|
|
373
|
+
// Called after every submit with the new full history, so the caller can
|
|
374
|
+
// persist it (the history otherwise dies with the process).
|
|
375
|
+
onHistoryChange;
|
|
376
|
+
// Snapshots of {buf, cursor} taken before each edit, so Ctrl+_ can undo a
|
|
377
|
+
// slip (a fat-fingered Ctrl+U / Ctrl+K used to wipe the whole line with no
|
|
378
|
+
// way back). Capped so a long session cannot grow it without bound.
|
|
379
|
+
_undoStack;
|
|
380
|
+
// True while a run of plain typing is in progress, so one undo snapshot
|
|
381
|
+
// covers the whole run instead of every character.
|
|
382
|
+
_typingRun;
|
|
359
383
|
// Slash-command hints (shown when you type «/»).
|
|
360
384
|
slashCommands;
|
|
361
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;
|
|
362
394
|
// Pasted blocks collapsed into markers (expanded back on submit).
|
|
363
395
|
pastes;
|
|
364
396
|
_pasteBuf;
|
|
@@ -462,8 +494,14 @@ export class LineEditor {
|
|
|
462
494
|
this._searchQuery = '';
|
|
463
495
|
this._searchIndex = -1;
|
|
464
496
|
this._searchSavedBuf = '';
|
|
497
|
+
this.onHistoryChange = null;
|
|
498
|
+
this._undoStack = [];
|
|
499
|
+
this._typingRun = false;
|
|
465
500
|
this.slashCommands = commands;
|
|
466
501
|
this._suggestCount = 0;
|
|
502
|
+
this._suggestOffset = 0;
|
|
503
|
+
this._suggestSelected = -1;
|
|
504
|
+
this._suggestQuery = '';
|
|
467
505
|
this.pastes = [];
|
|
468
506
|
this._pasteBuf = '';
|
|
469
507
|
this.attachments = new AttachmentStore();
|
|
@@ -596,15 +634,101 @@ export class LineEditor {
|
|
|
596
634
|
}
|
|
597
635
|
// List of hints for the current input. We show them only when the line
|
|
598
636
|
// starts with «/» and is a single word (no spaces/newlines) — like
|
|
599
|
-
// 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.
|
|
600
640
|
_suggestions() {
|
|
601
641
|
const b = this.buf;
|
|
602
642
|
if (!b.startsWith('/'))
|
|
603
643
|
return [];
|
|
604
644
|
if (b.includes(NL) || b.includes(' '))
|
|
605
645
|
return [];
|
|
646
|
+
if (b.length > 40)
|
|
647
|
+
return [];
|
|
606
648
|
const q = b.toLowerCase();
|
|
607
|
-
|
|
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();
|
|
608
732
|
}
|
|
609
733
|
// Tab: complete the command up to the common prefix; if there is a single
|
|
610
734
|
// match — insert it whole and add a space.
|
|
@@ -612,6 +736,11 @@ export class LineEditor {
|
|
|
612
736
|
const sugg = this._suggestions();
|
|
613
737
|
if (!sugg.length)
|
|
614
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
|
+
}
|
|
615
744
|
if (sugg.length === 1) {
|
|
616
745
|
this.buf = sugg[0].name + ' ';
|
|
617
746
|
this.cursor = Array.from(this.buf).length;
|
|
@@ -880,29 +1009,15 @@ export class LineEditor {
|
|
|
880
1009
|
if (statusUnchanged) {
|
|
881
1010
|
// Count the real input rows via layout (wrapping aware).
|
|
882
1011
|
const layOnly = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
883
|
-
const suggOnly = this.
|
|
884
|
-
const
|
|
885
|
-
const linesBelowOnly = shownOnly.length
|
|
886
|
-
? shownOnly.length + (suggOnly.length > shownOnly.length ? 1 : 0)
|
|
887
|
-
: 0;
|
|
1012
|
+
const suggOnly = this._suggestionLines(this._suggestOffset);
|
|
1013
|
+
const linesBelowOnly = suggOnly.rows;
|
|
888
1014
|
// Move up to the INPUT top using the OLD cursor row (the position that is
|
|
889
1015
|
// on screen right now), never the input's height.
|
|
890
1016
|
this._eraseInputOnly(this.cursorRowFromTop - this._statusTop);
|
|
891
1017
|
let outOnly = '';
|
|
892
1018
|
outOnly += layOnly.rows.map((r) => r.prefix + r.text).join(NL);
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
const lines = shownOnly.map((c) => {
|
|
896
|
-
const name = theme.prompt(c.name.padEnd(maxName));
|
|
897
|
-
const desc = theme.dim(' ' + c.description);
|
|
898
|
-
return ' ' + name + desc;
|
|
899
|
-
});
|
|
900
|
-
outOnly += NL + lines.join(NL);
|
|
901
|
-
const hidden = suggOnly.length - shownOnly.length;
|
|
902
|
-
if (hidden > 0)
|
|
903
|
-
outOnly +=
|
|
904
|
-
NL +
|
|
905
|
-
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
1019
|
+
if (suggOnly.lines.length) {
|
|
1020
|
+
outOnly += NL + suggOnly.lines.join(NL);
|
|
906
1021
|
}
|
|
907
1022
|
// The input may have UNWRAPPED (fewer rows than before) — push the block
|
|
908
1023
|
// back to the bottom BEFORE the new content is printed (the padding is
|
|
@@ -927,26 +1042,10 @@ export class LineEditor {
|
|
|
927
1042
|
out += lay.rows.map((r) => r.prefix + r.text).join(NL);
|
|
928
1043
|
// We draw the slash-command hints BELOW the input line. We then move the
|
|
929
1044
|
// cursor back up to the input line, so cursorRowFromTop doesn't change.
|
|
930
|
-
const sugg = this.
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const maxName = Math.max(...shown.map((c) => c.name.length));
|
|
935
|
-
const lines = shown.map((c) => {
|
|
936
|
-
const name = theme.prompt(c.name.padEnd(maxName));
|
|
937
|
-
const desc = theme.dim(' ' + c.description);
|
|
938
|
-
return ' ' + name + desc;
|
|
939
|
-
});
|
|
940
|
-
out += NL + lines.join(NL);
|
|
941
|
-
const hidden = sugg.length - shown.length;
|
|
942
|
-
if (hidden > 0)
|
|
943
|
-
out +=
|
|
944
|
-
NL +
|
|
945
|
-
theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
|
|
946
|
-
}
|
|
947
|
-
const linesBelow = shown.length
|
|
948
|
-
? shown.length + (sugg.length > shown.length ? 1 : 0)
|
|
949
|
-
: 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;
|
|
950
1049
|
// If the block SHRANK (the pause status went away, the input unwrapped),
|
|
951
1050
|
// the erase left the cursor at the old block top; printing a shorter block
|
|
952
1051
|
// would end above the screen bottom and the footer would float with blank
|
|
@@ -1088,13 +1187,15 @@ export class LineEditor {
|
|
|
1088
1187
|
return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
|
|
1089
1188
|
}
|
|
1090
1189
|
// The lifecycle phase prefix for the status line, colored by meaning:
|
|
1091
|
-
// generating (brown)
|
|
1092
|
-
//
|
|
1093
|
-
// reads "пауза Ns"). Returns '' when no
|
|
1094
|
-
// 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.
|
|
1095
1194
|
_stateLabel() {
|
|
1096
1195
|
if (this._sendState === 'generating')
|
|
1097
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(':'));
|
|
1098
1199
|
return '';
|
|
1099
1200
|
}
|
|
1100
1201
|
// Called by the browser layer at real lifecycle points: right before a
|
|
@@ -1280,6 +1381,9 @@ export class LineEditor {
|
|
|
1280
1381
|
// Add to history only non-empty messages that don't duplicate the previous one.
|
|
1281
1382
|
if (text.trim() && this.history[this.history.length - 1] !== text) {
|
|
1282
1383
|
this.history.push(text);
|
|
1384
|
+
// Persist the new entry: the in-memory history dies with the process.
|
|
1385
|
+
if (this.onHistoryChange)
|
|
1386
|
+
this.onHistoryChange(this.history.slice());
|
|
1283
1387
|
}
|
|
1284
1388
|
this._histIndex = this.history.length;
|
|
1285
1389
|
this._histDraft = '';
|
|
@@ -1304,6 +1408,26 @@ export class LineEditor {
|
|
|
1304
1408
|
if (this.busy)
|
|
1305
1409
|
this._startThinking();
|
|
1306
1410
|
}
|
|
1411
|
+
// Record the current buffer for Ctrl+_ undo. `coalesce` keeps one snapshot
|
|
1412
|
+
// for a whole run of plain typing (so undo removes the run, not one char) —
|
|
1413
|
+
// the run ends as soon as a non-typing key is handled.
|
|
1414
|
+
_pushUndo(coalesce = false) {
|
|
1415
|
+
if (coalesce && this._typingRun)
|
|
1416
|
+
return;
|
|
1417
|
+
this._undoStack.push({ buf: this.buf, cursor: this.cursor });
|
|
1418
|
+
if (this._undoStack.length > 200)
|
|
1419
|
+
this._undoStack.shift();
|
|
1420
|
+
this._typingRun = coalesce;
|
|
1421
|
+
}
|
|
1422
|
+
// Ctrl+_ — restore the buffer to before the last edit.
|
|
1423
|
+
_undoEdit() {
|
|
1424
|
+
const prev = this._undoStack.pop();
|
|
1425
|
+
if (!prev)
|
|
1426
|
+
return;
|
|
1427
|
+
this.buf = prev.buf;
|
|
1428
|
+
this.cursor = Math.min(prev.cursor, Array.from(prev.buf).length);
|
|
1429
|
+
this._typingRun = false;
|
|
1430
|
+
}
|
|
1307
1431
|
_insert(text) {
|
|
1308
1432
|
const chars = Array.from(this.buf);
|
|
1309
1433
|
const ins = Array.from(String(text));
|
|
@@ -1774,12 +1898,19 @@ export class LineEditor {
|
|
|
1774
1898
|
continue;
|
|
1775
1899
|
}
|
|
1776
1900
|
if (code === 21) {
|
|
1901
|
+
this._pushUndo();
|
|
1777
1902
|
this.buf = '';
|
|
1778
1903
|
this.cursor = 0;
|
|
1779
1904
|
this.pastes = [];
|
|
1780
1905
|
this._renderInputOnly();
|
|
1781
1906
|
continue;
|
|
1782
1907
|
}
|
|
1908
|
+
if (code === 31) {
|
|
1909
|
+
// Ctrl+_ — undo the last edit in the input line.
|
|
1910
|
+
this._undoEdit();
|
|
1911
|
+
this._renderInputOnly();
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1783
1914
|
if (code === 22) {
|
|
1784
1915
|
// Ctrl+V: terminals rarely deliver an image as text here, so we try the
|
|
1785
1916
|
// OS clipboard first; if there is no image we fall back to reading the
|
|
@@ -1792,13 +1923,25 @@ export class LineEditor {
|
|
|
1792
1923
|
this._searchStart();
|
|
1793
1924
|
continue;
|
|
1794
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
|
+
}
|
|
1795
1936
|
if (code === 23) {
|
|
1937
|
+
this._pushUndo();
|
|
1796
1938
|
this._deleteWordLeft();
|
|
1797
1939
|
this._renderInputOnly();
|
|
1798
1940
|
continue;
|
|
1799
1941
|
}
|
|
1800
1942
|
if (code === 11) {
|
|
1801
1943
|
// Ctrl+K — delete from the cursor to the end of the line.
|
|
1944
|
+
this._pushUndo();
|
|
1802
1945
|
const arr = Array.from(this.buf);
|
|
1803
1946
|
let e = this.cursor;
|
|
1804
1947
|
while (e < arr.length && arr[e] !== NL)
|
|
@@ -1809,6 +1952,7 @@ export class LineEditor {
|
|
|
1809
1952
|
continue;
|
|
1810
1953
|
}
|
|
1811
1954
|
if (code === 127 || code === 8) {
|
|
1955
|
+
this._pushUndo(true);
|
|
1812
1956
|
this._backspace();
|
|
1813
1957
|
this._renderInputOnly();
|
|
1814
1958
|
continue;
|
|
@@ -1872,6 +2016,7 @@ export class LineEditor {
|
|
|
1872
2016
|
continue;
|
|
1873
2017
|
}
|
|
1874
2018
|
if (s.startsWith('[3~')) {
|
|
2019
|
+
this._pushUndo(true);
|
|
1875
2020
|
this._delete();
|
|
1876
2021
|
s = s.slice(3);
|
|
1877
2022
|
this._renderInputOnly();
|
|
@@ -1898,6 +2043,8 @@ export class LineEditor {
|
|
|
1898
2043
|
}
|
|
1899
2044
|
if (code < 32)
|
|
1900
2045
|
continue;
|
|
2046
|
+
// Coalesce a run of typing into ONE undo snapshot.
|
|
2047
|
+
this._pushUndo(true);
|
|
1901
2048
|
this._insert(ch);
|
|
1902
2049
|
this._renderInputOnly();
|
|
1903
2050
|
}
|
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/sessions.js
CHANGED
|
@@ -132,3 +132,29 @@ export function listSessions() {
|
|
|
132
132
|
export function sessionsDir() {
|
|
133
133
|
return SESSIONS_DIR;
|
|
134
134
|
}
|
|
135
|
+
// ---------- input history ----------
|
|
136
|
+
// The LineEditor history otherwise dies with the process (it was in-memory
|
|
137
|
+
// only), so the arrow keys / Ctrl+R could not recall anything from a previous
|
|
138
|
+
// run. We persist it in ~/.zames/history.json, capped so the file cannot grow
|
|
139
|
+
// without bound.
|
|
140
|
+
const HISTORY_FILE = path.join(os.homedir(), '.zames', 'history.json');
|
|
141
|
+
const HISTORY_LIMIT = 500;
|
|
142
|
+
export function loadHistory() {
|
|
143
|
+
try {
|
|
144
|
+
const raw = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
|
|
145
|
+
if (!Array.isArray(raw))
|
|
146
|
+
return [];
|
|
147
|
+
return raw.filter((x) => typeof x === 'string');
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function saveHistory(history) {
|
|
154
|
+
try {
|
|
155
|
+
ensureDir();
|
|
156
|
+
const trimmed = history.slice(-HISTORY_LIMIT);
|
|
157
|
+
atomicWriteJson(HISTORY_FILE, trimmed);
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|