zames_pro 2.64.1 → 2.65.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/CHANGELOG.md CHANGED
@@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.65.1] - 2026-10-07
11
+
12
+ ### Fixed
13
+
14
+ - Длинные строки preview больше не создают горизонтальный скролл в
15
+ терминале: `toolCall`/`toolResult` (LineEditor и спиннер) теперь режут
16
+ превью по ВИДИМОЙ ШИРИНЕ терминала (`truncateToWidth`), а не по числу
17
+ символов. Раньше длинная команда/результат печатались шире экрана.
18
+ - Ctrl+Delete / Ctrl+Backspace (и Alt+Delete / Alt+Backspace) теперь удаляют
19
+ слово в строке ввода. Раньше эти escape-последовательности попадали в
20
+ общий пропуск клавиш и ничего не делали. Поддержаны варианты: `ESC[3;5~`
21
+ (xterm Ctrl+Delete), `ESC[3;3~` (Alt+Delete), `ESC DEL`/`ESC 8`
22
+ (Alt/Ctrl+Backspace) и kitty-протокол `ESC[127;5u` / `ESC[127;3u`.
23
+ Новый метод `_deleteWordRight()` + тест `test/editor-word-delete.test.ts`.
24
+ - Меню `/config` теперь держит ВЫБРАННЫЙ пункт на экране: окно строится
25
+ «назад» от курсора с учётом строк, которые занимают заголовки групп
26
+ (раньше индексный `scrollTop` мог вытолкнуть выбранную строку вниз за
27
+ пределы окна).
28
+ - `/new` сбрасывает счётчик контекста: `_lastTokenUsage` копится НА ЧАТ,
29
+ и без сброса в пустом чате оставался старый `ctx: 302k · 30%` до первой
30
+ отправки. Тест в `test/newchat-verify.test.ts`.
31
+
32
+ ## [2.65.0] - 2026-10-07
33
+
34
+ ### Changed
35
+
36
+ - README-скриншот `docs/demo.png` перегенерирован на английском языке
37
+ (раньше был на русском). Воспроизводимый генератор
38
+ `npm run render-demo` (`scripts/render-demo.mjs`) рендерит реальную
39
+ раскладку редактора и палитру темы через headless Chromium с
40
+ изолированным профилем.
41
+ - README: ASCII-схема «как это работает» заменена на диаграмму Mermaid
42
+ (стрелки не «плывут») плюс текстовое описание словами.
43
+ - Убраны неиспользуемые файлы: `logo.jpg` (в README используется только
44
+ `logo-small.jpg`) и заготовка `docs/demo.tape` (VHS-рендер так и не был
45
+ сгенерирован). `logo.jpg` удалён и из списка `files` в package.json.
46
+
47
+ ### Fixed
48
+
49
+ - Перерисовка терминала после ресайза окна: относительный стирающий
50
+ `ESC[n A` после изменения размера попадал на устаревшую строку (терминал
51
+ переформатировал переносы), из-за чего статус/инпут рисовались не там.
52
+ Теперь после ресайза блок заново «прибивается» к низу (home + padding),
53
+ история скроллбэка сохраняется. Тест в `test/terminal-drift.test.ts`.
54
+ - Живая смена языка (`/config lang`) не доходила до двух «одноразово
55
+ созданных» объектов: спиннер думающих фраз брал язык по умолчанию
56
+ (`randomThinkingPhrase()` вместо `randomThinkingPhrase(this.locale)`),
57
+ а `DeepSeekBrowser` печатал свои уведомления (`ds.*`) на языке,
58
+ зафиксированном при старте. Из-за этого при английском UI всплывали
59
+ русские «Структурирую мысли…» и «⏳ DeepSeek оборвал ответ…». Добавлен
60
+ `DeepSeekBrowser.setLocale()`, вызываемый из `setConfigRuntime`.
61
+ - Меню `/config` теперь рисуется ОКНОМ по высоте терминала (раньше весь
62
+ список из 49 полей выводился одним блоком). На невысоком терминале блок
63
+ уходил верхними строками за экран, из-за чего восстановление курсора
64
+ `ESC[lastRows A` попадало не на ту строку и навигация стрелками «не
65
+ скроллила» список. Заодно исправлена мёртвая регулярка снятия ANSI
66
+ (лишний обратный апостроф) в `src/config-menu.ts`.
67
+
10
68
  ## [2.64.1] - 2026-10-07
11
69
 
12
70
  ### Changed
package/README.md CHANGED
@@ -33,18 +33,13 @@ zames does not call the model API. It launches a headless Chromium with a
33
33
  persistent profile, signs in to `chat.deepseek.com` like a human, types the task
34
34
  into the chat box, and reads the answer back.
35
35
 
36
- ```
37
- terminal (you) zames chat.deepseek.com
38
- ─────────────── ───── ─────────────────
39
- task ──────────────────────▶ system prompt + task ────────▶ browser types it
40
- ▲ │
41
- │ model answers
42
- │ │
43
- answer ◀──── render ◀───── parse tool-call ◀──── raw answer ◀────┘
44
- │
45
- ▼
46
- run tool (Read/Edit/Bash/…) → feed result back
47
- ```
36
+ <p align="center">
37
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/how-it-works.png" alt="how zames works" width="860">
38
+ </p>
39
+
40
+ In text: you type a task → zames sends the system prompt plus the task into
41
+ the DeepSeek web chat → the model answers → zames parses the tool call, runs
42
+ the tool locally, feeds the result back, and prints the final answer for you.
48
43
 
49
44
  Key pieces:
50
45
 
package/dist/backlog.js CHANGED
@@ -15,6 +15,26 @@ const NEW_ITEM_PREFIX = 'N';
15
15
  // worth, so the operator is nudged to collapse it.
16
16
  export const BACKLOG_WARN_LINES = 500;
17
17
  export const BACKLOG_WARN_CHARS = 60_000;
18
+ // Parse a `/backlog <text>` note into the shape appendBacklogItem() wants. An
19
+ // optional leading `P0..P3` token sets the priority; the first line becomes the
20
+ // title and the rest the body. PURE so the operator's note is placed exactly as
21
+ // shown — no model round-trip, no chance of a different id than the one printed.
22
+ export function parseBacklogNote(note) {
23
+ const raw = String(note ?? '');
24
+ let priority = 'P2';
25
+ let body = raw;
26
+ for (const p of ['P0', 'P1', 'P2', 'P3']) {
27
+ if (raw.toUpperCase().startsWith(p + ' ')) {
28
+ priority = p;
29
+ body = raw.slice(p.length + 1);
30
+ break;
31
+ }
32
+ }
33
+ const bodyLines = body.split(NL);
34
+ const title = bodyLines[0];
35
+ const rest = bodyLines.slice(1).join(NL).trim();
36
+ return { priority, title, note: rest || undefined };
37
+ }
18
38
  function squashBlankLines(lines) {
19
39
  const out = [];
20
40
  let blanks = 0;
package/dist/browser.js CHANGED
@@ -550,6 +550,17 @@ export class DeepSeekBrowser {
550
550
  _t(key, params) {
551
551
  return translate(this.locale)(key, params);
552
552
  }
553
+ /**
554
+ * Switch the language of the messages the browser itself prints (login
555
+ * prompts, DeepSeek-side notices from `_notice()`). The browser is created
556
+ * ONCE at startup, so without this a `/config lang` change would leave every
557
+ * later notice in the OLD language until a restart — the operator saw
558
+ * Russian "⏳ DeepSeek оборвал ответ…" while the UI was set to English.
559
+ */
560
+ setLocale(locale) {
561
+ if (locale)
562
+ this.locale = locale;
563
+ }
553
564
  // A service notice for the operator. When a UI hook is wired (LineEditor is
554
565
  // active during a task) the notice goes ABOVE the input line so it is not
555
566
  // overwritten by the editor's repaint; otherwise it falls back to stderr.
@@ -1219,6 +1230,10 @@ export class DeepSeekBrowser {
1219
1230
  // STALE old id for the fresh chat. Clear it — a fresh chat has no id until
1220
1231
  // the first message creates one.
1221
1232
  this._netChatId = null;
1233
+ // The token counter is cumulative FOR A CHAT. A fresh chat has no messages,
1234
+ // so carrying the old value over left a stale "ctx: 302k · 30%" on the
1235
+ // status line until the first send. Reset it (and the cached status).
1236
+ this._lastTokenUsage = null;
1222
1237
  }
1223
1238
  async _findVisible(selectors, timeout = 1000) {
1224
1239
  for (const sel of selectors) {
package/dist/commands.js CHANGED
@@ -951,3 +951,84 @@ export function buildReviewPrompt(focus, hasStaged = false) {
951
951
  s += NL + 'Extra focus: ' + f;
952
952
  return s;
953
953
  }
954
+ // The canonical list. Dev-only entries are filtered by buildSlashCommandHints()
955
+ // unless dev mode is on (DEV_ONLY_COMMANDS above), so a regular install never
956
+ // advertises them.
957
+ export const SLASH_COMMANDS = [
958
+ { name: '/help', key: 'help.cmd.help' },
959
+ { name: '/new', key: 'help.cmd.new' },
960
+ { name: '/clear', key: 'help.cmd.new' },
961
+ { name: '/sessions', key: 'help.cmd.sessions' },
962
+ { name: '/chats', key: 'help.cmd.chats' },
963
+ { name: '/resume', key: 'help.cmd.resume' },
964
+ { name: '/resume-id', key: 'help.cmd.resume_id' },
965
+ { name: '/last', key: 'help.cmd.last' },
966
+ { name: '/retry', key: 'help.cmd.retry' },
967
+ { name: '/rename', key: 'help.cmd.rename' },
968
+ { name: '/context', key: 'help.cmd.context' },
969
+ { name: '/copy', key: 'help.cmd.copy' },
970
+ { name: '/chat', key: 'help.cmd.chat' },
971
+ { name: '/cd', key: 'help.cmd.cd' },
972
+ { name: '/pwd', key: 'help.cmd.pwd' },
973
+ { name: '/status', key: 'help.cmd.status' },
974
+ { name: '/reload', key: 'help.cmd.reload' },
975
+ { name: '/undo', key: 'help.cmd.undo' },
976
+ { name: '/undo-list', key: 'help.cmd.undo_list' },
977
+ { name: '/rewind', key: 'help.cmd.rewind' },
978
+ { name: '/rewind-list', key: 'help.cmd.rewind_list' },
979
+ { name: '/transcript', key: 'help.cmd.transcript' },
980
+ { name: '/diff', key: 'help.cmd.diff' },
981
+ { name: '/diffstat', key: 'help.cmd.diffstat' },
982
+ { name: '/cost', key: 'help.cmd.cost' },
983
+ { name: '/export', key: 'help.cmd.export' },
984
+ { name: '/doctor', key: 'help.cmd.doctor' },
985
+ { name: '/add-dir', key: 'help.cmd.add_dir' },
986
+ { name: '/review', key: 'help.cmd.review' },
987
+ { name: '/improve', key: 'help.cmd.improve' },
988
+ { name: '/backlog', key: 'help.cmd.backlog' },
989
+ { name: '/goal', key: 'help.cmd.goal' },
990
+ { name: '/loop', key: 'help.cmd.loop' },
991
+ { name: '/cron', key: 'help.cmd.cron' },
992
+ { name: '/jobs', key: 'help.cmd.jobs' },
993
+ { name: '/thinking', key: 'help.cmd.thinking' },
994
+ { name: '/web', key: 'help.cmd.web' },
995
+ { name: '/plan', key: 'help.cmd.plan' },
996
+ { name: '/compact', key: 'help.cmd.compact' },
997
+ { name: '/queue', key: 'help.cmd.queue' },
998
+ { name: '/tasks', key: 'help.cmd.tasks' },
999
+ { name: '/config', key: 'help.cmd.config' },
1000
+ { name: '/skills', key: 'help.cmd.skills' },
1001
+ { name: '/memory', key: 'help.cmd.memory' },
1002
+ { name: '/remember', key: 'help.cmd.remember' },
1003
+ { name: '/init', key: 'help.cmd.init' },
1004
+ { name: '/mcp', key: 'help.cmd.mcp' },
1005
+ { name: '/debug-dom', key: 'help.cmd.debug_dom' },
1006
+ { name: '/self-review', key: 'help.self.review' },
1007
+ { name: '/self-fix', key: 'help.self.fix' },
1008
+ { name: '/self-done', key: 'help.self.done' },
1009
+ { name: '/self-list', key: 'help.self.list' },
1010
+ { name: '/self-diff', key: 'help.self.diff' },
1011
+ { name: '/self-apply', key: 'help.self.apply' },
1012
+ { name: '/exit', key: 'help.cmd.exit' },
1013
+ { name: '/quit', key: 'help.cmd.exit' },
1014
+ ];
1015
+ // Build the localized hint list from the built-in table plus the dynamic
1016
+ // skills/custom commands discovered for the current workdir. PURE: the caller
1017
+ // passes `t` and the data; nothing here touches the terminal or the editor.
1018
+ // Dev-only commands are dropped unless `devMode`; a dynamic entry whose name
1019
+ // clashes with a built-in (case-insensitive) is skipped, so a skill cannot
1020
+ // shadow /help.
1021
+ export function buildSlashCommandHints(opts) {
1022
+ const { devMode, t } = opts;
1023
+ const table = opts.commands ?? SLASH_COMMANDS;
1024
+ const base = table
1025
+ .filter((c) => devMode || !isDevOnlyCommand(c.name))
1026
+ .map((c) => ({ name: c.name, description: t(c.key) }));
1027
+ const known = new Set(base.map((c) => c.name.toLowerCase()));
1028
+ for (const d of opts.dynamic ?? []) {
1029
+ if (known.has(d.name.toLowerCase()))
1030
+ continue;
1031
+ base.push(d);
1032
+ }
1033
+ return base;
1034
+ }
@@ -11,9 +11,11 @@
11
11
  import { theme } from './theme.js';
12
12
  import { visLen } from './input.js';
13
13
  const ESC = String.fromCharCode(27);
14
- // Strip ANSH sequences to compute the visible length of a line.
15
- function stripAnsi(s) {
16
- return s.replace(/\x1b\`[0-9;?]*[A-Za-z]/g, '');
14
+ // Strip ANSI sequences to compute the visible length of a line. (A module-
15
+ // level name distinct from input.ts's internal helper; the old regex had a
16
+ // stray backtick and never matched a real CSI, so visCols was over-counted.)
17
+ function stripAnsiLocal(s) {
18
+ return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
17
19
  }
18
20
  // Visible COLUMNS of a line (ANSI removed, wide chars counted as 2). Using the
19
21
  // code-point count made the block height disagree with the real screen on lines
@@ -21,7 +23,7 @@ function stripAnsi(s) {
21
23
  // duplicated. A row that fills the terminal exactly also triggers autowrap on
22
24
  // some terminals, so we keep one column free here as the editor does.
23
25
  function visCols(s) {
24
- return visLen(stripAnsi(s));
26
+ return visLen(stripAnsiLocal(s));
25
27
  }
26
28
  function displayValue(field, value, t) {
27
29
  if (value === undefined)
@@ -102,22 +104,82 @@ export function runConfigMenu(opts) {
102
104
  // start of the block via relative moves so we don't spawn blank lines.
103
105
  let lastRows = 0;
104
106
  const render = () => {
105
- const lines = [];
106
107
  const vis = visible();
107
108
  if (cursor >= vis.length)
108
109
  cursor = Math.max(0, vis.length - 1);
110
+ // Trailing block (below the list): the edit/filter prompt or the hints.
111
+ const tail = [''];
112
+ if (editing) {
113
+ tail.push(theme.user(t('cfg.menu.edit_hint')));
114
+ tail.push(theme.prompt('\u276f ') + editBuf);
115
+ }
116
+ else if (filterMode) {
117
+ tail.push(theme.user(t('cfg.menu.filter_hint')));
118
+ tail.push(theme.prompt('\u276f ') + filterQuery);
119
+ }
120
+ else if (message) {
121
+ tail.push(theme.assistant(message));
122
+ }
123
+ else if (filterQuery) {
124
+ tail.push(theme.dim(t('cfg.menu.filter_active', { q: filterQuery }) +
125
+ ' ' +
126
+ t('cfg.menu.hint')));
127
+ }
128
+ else {
129
+ tail.push(' ' + theme.dim(t('cfg.menu.hint')));
130
+ tail.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
131
+ }
132
+ // How many ITEM rows (fields + group headers) fit: terminal height minus
133
+ // the title and the trailing block, with one spare row. This is what keeps
134
+ // the block from scrolling off the top on a long list.
135
+ const termRows = output.rows || 24;
136
+ // Bias one row to the SAFE side (a spare blank row is harmless, a block
137
+ // that overflows scrolls the top away and breaks the cursor restore).
138
+ const maxItemRows = Math.max(3, termRows - 2 - tail.length);
139
+ // WINDOW over the fields. We walk BACK from the cursor, counting the rows
140
+ // the window would take (a group header consumes a row too), until adding
141
+ // one more field would overflow. This guarantees the SELECTED item is always
142
+ // on screen — the old index-based scrollTop could push it out when many
143
+ // group headers sat between the window top and the cursor.
144
+ const groupStart = (i) => i === 0 || vis[i].groupKey !== vis[i - 1].groupKey;
145
+ // Rows needed to draw the window [from, cursor] (inclusive).
146
+ const rowsFor = (from) => {
147
+ let rows = 0;
148
+ for (let i = from; i <= cursor; i++) {
149
+ if (i === from || groupStart(i))
150
+ rows++;
151
+ rows++;
152
+ }
153
+ return rows;
154
+ };
155
+ let start = vis.length ? Math.max(0, Math.min(cursor, vis.length - 1)) : 0;
156
+ while (start > 0 && rowsFor(start - 1) <= maxItemRows)
157
+ start--;
158
+ const lines = [];
109
159
  // Title with a position counter ("Settings — 12/49") so the operator knows
110
160
  // how far down a long list they are; a filter narrows both numbers.
111
161
  const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
112
162
  lines.push(theme.bold(t('cfg.menu.title')) + theme.dim(pos));
163
+ // Group headers also consume a row, so we stop as soon as the item-row
164
+ // budget is used up (the window is advisory; this is the hard cap that
165
+ // guarantees the block fits the terminal).
166
+ let itemRows = 0;
113
167
  let lastGroup = '';
114
- for (let i = 0; i < vis.length; i++) {
168
+ for (let i = start; i < vis.length; i++) {
115
169
  const f = vis[i];
116
170
  const g = groupLabel(f, t);
117
- if (g !== lastGroup) {
171
+ // Always print the header for the FIRST shown row, so a windowed list
172
+ // still tells the operator which section it starts in.
173
+ if (i === start || g !== lastGroup) {
174
+ if (itemRows >= maxItemRows)
175
+ break;
118
176
  lines.push(g);
119
177
  lastGroup = g;
178
+ itemRows++;
120
179
  }
180
+ if (itemRows >= maxItemRows)
181
+ break;
182
+ itemRows++;
121
183
  const selected = i === cursor;
122
184
  const marker = selected ? theme.prompt('\u276f ') : ' ';
123
185
  const label = selected
@@ -132,32 +194,7 @@ export function runConfigMenu(opts) {
132
194
  if (!vis.length) {
133
195
  lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
134
196
  }
135
- if (editing) {
136
- lines.push('');
137
- lines.push(theme.user(t('cfg.menu.edit_hint')));
138
- lines.push(theme.prompt('\u276f ') + editBuf);
139
- }
140
- else if (filterMode) {
141
- lines.push('');
142
- lines.push(theme.user(t('cfg.menu.filter_hint')));
143
- lines.push(theme.prompt('\u276f ') + filterQuery);
144
- }
145
- else if (message) {
146
- lines.push('');
147
- lines.push(theme.assistant(message));
148
- }
149
- else {
150
- lines.push('');
151
- if (filterQuery) {
152
- lines.push(theme.dim(t('cfg.menu.filter_active', { q: filterQuery }) +
153
- ' ' +
154
- t('cfg.menu.hint')));
155
- }
156
- else {
157
- lines.push(' ' + theme.dim(t('cfg.menu.hint')));
158
- lines.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
159
- }
160
- }
197
+ lines.push(...tail);
161
198
  // Erase the previous block and print the new one. We count VISUAL lines
162
199
  // (accounting for wrapping by terminal width), otherwise on narrow
163
200
  // terminals you get "garbage" from unerased tails.