zames_pro 2.51.0 → 2.52.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 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/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/i18n.js CHANGED
@@ -103,6 +103,10 @@ const CATALOG = {
103
103
  ru: 'Ctrl+R поиск по истории (обратный)',
104
104
  en: 'Ctrl+R reverse-search the history',
105
105
  },
106
+ 'help.key.undo': {
107
+ ru: 'Ctrl+_ отменить правку в строке ввода',
108
+ en: 'Ctrl+_ undo the last edit in the input line',
109
+ },
106
110
  'help.key.words': {
107
111
  ru: 'Ctrl+← / Ctrl+→ перемещение по словам',
108
112
  en: 'Ctrl+← / Ctrl+→ move by words',
@@ -363,6 +367,10 @@ const CATALOG = {
363
367
  ru: '/debug-dom сохранить HTML страницы (для отладки)',
364
368
  en: '/debug-dom dump page HTML (debug)',
365
369
  },
370
+ 'help.no_topic': {
371
+ ru: 'Нет команды по запросу «{v}». Попробуй /help без аргумента.',
372
+ en: 'No command matches "{v}". Try /help without arguments.',
373
+ },
366
374
  'help.cmd.help': {
367
375
  ru: '/help, help справка',
368
376
  en: '/help, help this help',
@@ -455,7 +463,15 @@ const CATALOG = {
455
463
  'help.files.profile': { ru: 'Профиль:', en: 'Profile:' },
456
464
  'help.files.snapshots': { ru: 'Снапшоты:', en: 'Snapshots:' },
457
465
  'help.files.tmp': { ru: 'Временные:', en: 'Temp:' },
466
+ 'help.files.tmp_note': {
467
+ ru: '(в .gitignore, очищается при старте)',
468
+ en: '(.gitignore, cleaned on start)',
469
+ },
458
470
  'help.files.config': { ru: 'Конфиг:', en: 'Config:' },
471
+ 'help.toggles_legend': {
472
+ ru: 'Индикаторы в статус-строке: 🧠 — глубокое размышление, 🌐 — поиск в интернете (видны, когда включены).',
473
+ en: 'Status-line icons: 🧠 — Deep thinking, 🌐 — web search (shown when enabled).',
474
+ },
459
475
  // ---------- spinner ----------
460
476
  'spinner.phrases': {
461
477
  ru: 'Анализирую запрос…|Изучаю контекст…|Планирую решение…|Обрабатываю данные…|Формулирую ответ…|Проверяю детали…|Собираю информацию…|Обдумываю варианты…|Структурирую мысли…|Готовлю решение…|Анализирую код…|Уточняю детали…|Выполняю вычисления…|Систематизирую данные…|Прорабатываю задачу…|Проверяю логику…|Оптимизирую подход…|Формирую выводы…',
@@ -1080,6 +1096,10 @@ const CATALOG = {
1080
1096
  },
1081
1097
  'chats.history_you': { ru: 'Вы', en: 'You' },
1082
1098
  'chats.history_agent': { ru: 'Агент', en: 'Agent' },
1099
+ 'time.now': { ru: 'только что', en: 'just now' },
1100
+ 'time.min_ago': { ru: '{n} мин назад', en: '{n}m ago' },
1101
+ 'time.hour_ago': { ru: '{n} ч назад', en: '{n}h ago' },
1102
+ 'time.day_ago': { ru: '{n} дн назад', en: '{n}d ago' },
1083
1103
  'sessions.dir': { ru: 'Папка сессий: {v}', en: 'Sessions dir: {v}' },
1084
1104
  'sessions.none': {
1085
1105
  ru: 'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
package/dist/index.js CHANGED
@@ -17,12 +17,12 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
17
17
  import { Transcript } from './transcript.js';
18
18
  import { UndoStore } from './undo.js';
19
19
  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';
20
+ 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
21
  import { performCompact } from './compact.js';
22
22
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
23
23
  import { renderMarkdown } from './markdown.js';
24
24
  import { closeWeb } from './web.js';
25
- import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, } from './sessions.js';
25
+ import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
26
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
27
  const NL = String.fromCharCode(10);
28
28
  // Floor for a /loop interval. A loop faster than the send throttle (15s)
@@ -299,6 +299,7 @@ ${theme.bold(t('help.while_working'))}
299
299
  ${t('help.key.history')}
300
300
  ${t('help.key.search')}
301
301
  ${t('help.key.words')}
302
+ ${t('help.key.undo')}
302
303
  ${t('help.key.slash')}
303
304
  ${t('help.key.newline')}
304
305
  ${t('help.key.backslash')}
@@ -357,9 +358,11 @@ ${theme.bold(t('help.files'))}
357
358
  ${t('help.files.sessions')} ${sessionsDir()}
358
359
  ${t('help.files.profile')} ~/.zames/profile
359
360
  ${t('help.files.snapshots')} ~/.zames/snapshots
360
- ${t('help.files.tmp')} <project>/tmp (.gitignore, cleaned on start)
361
+ ${t('help.files.tmp')} <project>/tmp ${t('help.files.tmp_note')}
361
362
  ${t('help.files.config')} ${CONFIG_PATHS.HOME_CONFIG}
362
363
  ${CONFIG_PATHS.PROJECT_CONFIG}
364
+
365
+ ${theme.dim(t('help.toggles_legend'))}
363
366
  `);
364
367
  }
365
368
  function dirLabel(p) {
@@ -1544,6 +1547,11 @@ async function main() {
1544
1547
  });
1545
1548
  editor = ed;
1546
1549
  ed.setTmpDir(TMP_DIR);
1550
+ // Restore the input history from the previous runs and persist every new
1551
+ // entry, so Ctrl+R / arrows can recall older messages after a restart.
1552
+ ed.history = loadHistory();
1553
+ ed._histIndex = ed.history.length;
1554
+ ed.onHistoryChange = (h) => saveHistory(h);
1547
1555
  // The token context right-aligned on the status line (above the input).
1548
1556
  // The editor pulls the number on every render, so it follows the live
1549
1557
  // DeepSeek counter (accumulated_token_usage) without a polling timer.
@@ -2115,8 +2123,29 @@ async function main() {
2115
2123
  const lower = trimmed.toLowerCase();
2116
2124
  if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
2117
2125
  break;
2118
- if (lower === '/help' || lower === 'help') {
2119
- printHelp();
2126
+ if (lower === '/help' ||
2127
+ lower === 'help' ||
2128
+ lower.startsWith('/help ') ||
2129
+ lower.startsWith('help ')) {
2130
+ const topic = trimmed.replace(/^\/?help\s*/i, '').trim();
2131
+ if (!topic) {
2132
+ printHelp();
2133
+ }
2134
+ else {
2135
+ // /help <command> — print the full description of ONE command instead
2136
+ // of making the operator scroll the whole list.
2137
+ const q = '/' + topic.replace(/^\//, '').toLowerCase();
2138
+ const match = buildSlashCommands().filter((c) => c.name.toLowerCase() === q ||
2139
+ c.name.toLowerCase().startsWith(q + ' '));
2140
+ if (!match.length) {
2141
+ console.error(theme.warn(t('help.no_topic', { v: topic })));
2142
+ }
2143
+ else {
2144
+ for (const c of match) {
2145
+ console.log(theme.assistant(c.name) + theme.dim(' ' + c.description));
2146
+ }
2147
+ }
2148
+ }
2120
2149
  continue;
2121
2150
  }
2122
2151
  if (['/new', '/clear', 'new'].includes(lower)) {
@@ -2414,7 +2443,9 @@ async function main() {
2414
2443
  const mark = s.id === currentChatId ? theme.user(' *') : '';
2415
2444
  const title = s.title ? ` ${s.title}` : '';
2416
2445
  const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
2417
- console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`);
2446
+ const age = formatRelativeTime(s.updatedAt, t);
2447
+ const ageText = age ? theme.dim(' ' + age) : '';
2448
+ console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${ageText}${mark}`);
2418
2449
  });
2419
2450
  console.log(theme.system(t('sessions.restore_hint')));
2420
2451
  }
package/dist/input.js CHANGED
@@ -356,6 +356,16 @@ export class LineEditor {
356
356
  _searchQuery;
357
357
  _searchIndex;
358
358
  _searchSavedBuf;
359
+ // Called after every submit with the new full history, so the caller can
360
+ // persist it (the history otherwise dies with the process).
361
+ onHistoryChange;
362
+ // Snapshots of {buf, cursor} taken before each edit, so Ctrl+_ can undo a
363
+ // slip (a fat-fingered Ctrl+U / Ctrl+K used to wipe the whole line with no
364
+ // way back). Capped so a long session cannot grow it without bound.
365
+ _undoStack;
366
+ // True while a run of plain typing is in progress, so one undo snapshot
367
+ // covers the whole run instead of every character.
368
+ _typingRun;
359
369
  // Slash-command hints (shown when you type «/»).
360
370
  slashCommands;
361
371
  _suggestCount;
@@ -462,6 +472,9 @@ export class LineEditor {
462
472
  this._searchQuery = '';
463
473
  this._searchIndex = -1;
464
474
  this._searchSavedBuf = '';
475
+ this.onHistoryChange = null;
476
+ this._undoStack = [];
477
+ this._typingRun = false;
465
478
  this.slashCommands = commands;
466
479
  this._suggestCount = 0;
467
480
  this.pastes = [];
@@ -1280,6 +1293,9 @@ export class LineEditor {
1280
1293
  // Add to history only non-empty messages that don't duplicate the previous one.
1281
1294
  if (text.trim() && this.history[this.history.length - 1] !== text) {
1282
1295
  this.history.push(text);
1296
+ // Persist the new entry: the in-memory history dies with the process.
1297
+ if (this.onHistoryChange)
1298
+ this.onHistoryChange(this.history.slice());
1283
1299
  }
1284
1300
  this._histIndex = this.history.length;
1285
1301
  this._histDraft = '';
@@ -1304,6 +1320,26 @@ export class LineEditor {
1304
1320
  if (this.busy)
1305
1321
  this._startThinking();
1306
1322
  }
1323
+ // Record the current buffer for Ctrl+_ undo. `coalesce` keeps one snapshot
1324
+ // for a whole run of plain typing (so undo removes the run, not one char) —
1325
+ // the run ends as soon as a non-typing key is handled.
1326
+ _pushUndo(coalesce = false) {
1327
+ if (coalesce && this._typingRun)
1328
+ return;
1329
+ this._undoStack.push({ buf: this.buf, cursor: this.cursor });
1330
+ if (this._undoStack.length > 200)
1331
+ this._undoStack.shift();
1332
+ this._typingRun = coalesce;
1333
+ }
1334
+ // Ctrl+_ — restore the buffer to before the last edit.
1335
+ _undoEdit() {
1336
+ const prev = this._undoStack.pop();
1337
+ if (!prev)
1338
+ return;
1339
+ this.buf = prev.buf;
1340
+ this.cursor = Math.min(prev.cursor, Array.from(prev.buf).length);
1341
+ this._typingRun = false;
1342
+ }
1307
1343
  _insert(text) {
1308
1344
  const chars = Array.from(this.buf);
1309
1345
  const ins = Array.from(String(text));
@@ -1774,12 +1810,19 @@ export class LineEditor {
1774
1810
  continue;
1775
1811
  }
1776
1812
  if (code === 21) {
1813
+ this._pushUndo();
1777
1814
  this.buf = '';
1778
1815
  this.cursor = 0;
1779
1816
  this.pastes = [];
1780
1817
  this._renderInputOnly();
1781
1818
  continue;
1782
1819
  }
1820
+ if (code === 31) {
1821
+ // Ctrl+_ — undo the last edit in the input line.
1822
+ this._undoEdit();
1823
+ this._renderInputOnly();
1824
+ continue;
1825
+ }
1783
1826
  if (code === 22) {
1784
1827
  // Ctrl+V: terminals rarely deliver an image as text here, so we try the
1785
1828
  // OS clipboard first; if there is no image we fall back to reading the
@@ -1793,12 +1836,14 @@ export class LineEditor {
1793
1836
  continue;
1794
1837
  }
1795
1838
  if (code === 23) {
1839
+ this._pushUndo();
1796
1840
  this._deleteWordLeft();
1797
1841
  this._renderInputOnly();
1798
1842
  continue;
1799
1843
  }
1800
1844
  if (code === 11) {
1801
1845
  // Ctrl+K — delete from the cursor to the end of the line.
1846
+ this._pushUndo();
1802
1847
  const arr = Array.from(this.buf);
1803
1848
  let e = this.cursor;
1804
1849
  while (e < arr.length && arr[e] !== NL)
@@ -1809,6 +1854,7 @@ export class LineEditor {
1809
1854
  continue;
1810
1855
  }
1811
1856
  if (code === 127 || code === 8) {
1857
+ this._pushUndo(true);
1812
1858
  this._backspace();
1813
1859
  this._renderInputOnly();
1814
1860
  continue;
@@ -1872,6 +1918,7 @@ export class LineEditor {
1872
1918
  continue;
1873
1919
  }
1874
1920
  if (s.startsWith('[3~')) {
1921
+ this._pushUndo(true);
1875
1922
  this._delete();
1876
1923
  s = s.slice(3);
1877
1924
  this._renderInputOnly();
@@ -1898,6 +1945,8 @@ export class LineEditor {
1898
1945
  }
1899
1946
  if (code < 32)
1900
1947
  continue;
1948
+ // Coalesce a run of typing into ONE undo snapshot.
1949
+ this._pushUndo(true);
1901
1950
  this._insert(ch);
1902
1951
  this._renderInputOnly();
1903
1952
  }
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.51.0",
3
+ "version": "2.52.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {