zames_pro 2.64.0 → 2.65.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/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.65.0] - 2026-10-07
11
+
12
+ ### Changed
13
+
14
+ - README-скриншот `docs/demo.png` перегенерирован на английском языке
15
+ (раньше был на русском). Воспроизводимый генератор
16
+ `npm run render-demo` (`scripts/render-demo.mjs`) рендерит реальную
17
+ раскладку редактора и палитру темы через headless Chromium с
18
+ изолированным профилем.
19
+ - README: ASCII-схема «как это работает» заменена на диаграмму Mermaid
20
+ (стрелки не «плывут») плюс текстовое описание словами.
21
+ - Убраны неиспользуемые файлы: `logo.jpg` (в README используется только
22
+ `logo-small.jpg`) и заготовка `docs/demo.tape` (VHS-рендер так и не был
23
+ сгенерирован). `logo.jpg` удалён и из списка `files` в package.json.
24
+
25
+ ### Fixed
26
+
27
+ - Перерисовка терминала после ресайза окна: относительный стирающий
28
+ `ESC[n A` после изменения размера попадал на устаревшую строку (терминал
29
+ переформатировал переносы), из-за чего статус/инпут рисовались не там.
30
+ Теперь после ресайза блок заново «прибивается» к низу (home + padding),
31
+ история скроллбэка сохраняется. Тест в `test/terminal-drift.test.ts`.
32
+ - Живая смена языка (`/config lang`) не доходила до двух «одноразово
33
+ созданных» объектов: спиннер думающих фраз брал язык по умолчанию
34
+ (`randomThinkingPhrase()` вместо `randomThinkingPhrase(this.locale)`),
35
+ а `DeepSeekBrowser` печатал свои уведомления (`ds.*`) на языке,
36
+ зафиксированном при старте. Из-за этого при английском UI всплывали
37
+ русские «Структурирую мысли…» и «⏳ DeepSeek оборвал ответ…». Добавлен
38
+ `DeepSeekBrowser.setLocale()`, вызываемый из `setConfigRuntime`.
39
+ - Меню `/config` теперь рисуется ОКНОМ по высоте терминала (раньше весь
40
+ список из 49 полей выводился одним блоком). На невысоком терминале блок
41
+ уходил верхними строками за экран, из-за чего восстановление курсора
42
+ `ESC[lastRows A` попадало не на ту строку и навигация стрелками «не
43
+ скроллила» список. Заодно исправлена мёртвая регулярка снятия ANSI
44
+ (лишний обратный апостроф) в `src/config-menu.ts`.
45
+
46
+ ## [2.64.1] - 2026-10-07
47
+
48
+ ### Changed
49
+
50
+ - README: добавлен скриншот терминальной сессии (`docs/demo.png`) под
51
+ шапкой, чтобы интерфейс был виден сразу; скрипт воспроизводимого
52
+ VHS-демо (`docs/demo.tape`) для будущего GIF.
53
+
10
54
  ## [2.64.0] - 2026-10-06
11
55
 
12
56
  ### Added
@@ -348,7 +392,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
348
392
  - Session banner warns when no saved DeepSeek session exists.
349
393
  - `--no-color` flag (explicit `NO_COLOR`).
350
394
 
351
- [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.64.0...HEAD
395
+ [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.64.1...HEAD
396
+ [2.64.1]: https://github.com/Viqto0r/zames_pro/compare/v2.64.0...v2.64.1
352
397
  [2.64.0]: https://github.com/Viqto0r/zames_pro/compare/v2.63.1...v2.64.0
353
398
  [2.63.1]: https://github.com/Viqto0r/zames_pro/compare/v2.63.0...v2.63.1
354
399
  [2.63.0]: https://github.com/Viqto0r/zames_pro/compare/v2.61.0...v2.63.0
package/README.md CHANGED
@@ -23,24 +23,29 @@ directory, reads and edits files, runs commands, and commits to git.
23
23
  > No API key required — it drives the DeepSeek web chat like a regular user
24
24
  > through a real (headless) browser.
25
25
 
26
+ <p align="center">
27
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames session in the terminal" width="860">
28
+ </p>
29
+
26
30
  ## How it works
27
31
 
28
32
  zames does not call the model API. It launches a headless Chromium with a
29
33
  persistent profile, signs in to `chat.deepseek.com` like a human, types the task
30
34
  into the chat box, and reads the answer back.
31
35
 
36
+ ```mermaid
37
+ flowchart LR
38
+ U["terminal (you)"] -->|"task"| Z["zames"]
39
+ Z -->|"system prompt + task"| D["chat.deepseek.com"]
40
+ D -->|"model answers (raw SSE)"| Z
41
+ Z -->|"parse tool-call"| Z
42
+ Z -->|"run tool: Read/Edit/Bash/…"| Z
43
+ Z -->|"answer"| U
32
44
  ```
33
- terminal (you) zames chat.deepseek.com
34
- ─────────────── ───── ─────────────────
35
- task ──────────────────────▶ system prompt + task ────────▶ browser types it
36
- ▲ │
37
- │ model answers
38
- │ │
39
- answer ◀──── render ◀───── parse tool-call ◀──── raw answer ◀────┘
40
- │
41
- ▼
42
- run tool (Read/Edit/Bash/…) → feed result back
43
- ```
45
+
46
+ In text: you type a task → zames sends the system prompt plus the task into
47
+ the DeepSeek web chat → the model answers → zames parses the tool call, runs
48
+ the tool locally, feeds the result back, and prints the final answer for you.
44
49
 
45
50
  Key pieces:
46
51
 
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.
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)
@@ -51,6 +53,11 @@ export function runConfigMenu(opts) {
51
53
  }
52
54
  // cursor indexes the VISIBLE (filtered) list, not the full `fields`.
53
55
  let cursor = 0;
56
+ // Index of the first VISIBLE field drawn. The menu is WINDOWED so the whole
57
+ // block always fits the terminal height; otherwise a block taller than the
58
+ // screen scrolled the top rows off and the `ESC[lastRows A` cursor restore
59
+ // landed on the wrong line, so arrow-key navigation did not scroll the list.
60
+ let scrollTop = 0;
54
61
  let editing = false;
55
62
  let editBuf = '';
56
63
  // Reverse/forward filter over the list (/ to enter, typing narrows it).
@@ -102,22 +109,78 @@ export function runConfigMenu(opts) {
102
109
  // start of the block via relative moves so we don't spawn blank lines.
103
110
  let lastRows = 0;
104
111
  const render = () => {
105
- const lines = [];
106
112
  const vis = visible();
107
113
  if (cursor >= vis.length)
108
114
  cursor = Math.max(0, vis.length - 1);
115
+ // Trailing block (below the list): the edit/filter prompt or the hints.
116
+ const tail = [''];
117
+ if (editing) {
118
+ tail.push(theme.user(t('cfg.menu.edit_hint')));
119
+ tail.push(theme.prompt('\u276f ') + editBuf);
120
+ }
121
+ else if (filterMode) {
122
+ tail.push(theme.user(t('cfg.menu.filter_hint')));
123
+ tail.push(theme.prompt('\u276f ') + filterQuery);
124
+ }
125
+ else if (message) {
126
+ tail.push(theme.assistant(message));
127
+ }
128
+ else if (filterQuery) {
129
+ tail.push(theme.dim(t('cfg.menu.filter_active', { q: filterQuery }) +
130
+ ' ' +
131
+ t('cfg.menu.hint')));
132
+ }
133
+ else {
134
+ tail.push(' ' + theme.dim(t('cfg.menu.hint')));
135
+ tail.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
136
+ }
137
+ // How many ITEM rows (fields + group headers) fit: terminal height minus
138
+ // the title and the trailing block, with one spare row. This is what keeps
139
+ // the block from scrolling off the top on a long list.
140
+ const termRows = output.rows || 24;
141
+ // Bias one row to the SAFE side (a spare blank row is harmless, a block
142
+ // that overflows scrolls the top away and breaks the cursor restore).
143
+ const maxItemRows = Math.max(3, termRows - 2 - tail.length);
144
+ // WINDOW over the fields: keep the cursor inside [scrollTop, +maxItemRows).
145
+ // Because a group header also consumes a row we use the cursor index as the
146
+ // primary bound and let the header be counted when the first shown field
147
+ // starts a new group (a rare one-row overshoot is harmless — a spare row is
148
+ // reserved above).
149
+ if (cursor < scrollTop)
150
+ scrollTop = cursor;
151
+ if (cursor >= scrollTop + maxItemRows)
152
+ scrollTop = cursor - maxItemRows + 1;
153
+ const maxTop = Math.max(0, vis.length - maxItemRows);
154
+ if (scrollTop > maxTop)
155
+ scrollTop = maxTop;
156
+ if (scrollTop < 0)
157
+ scrollTop = 0;
158
+ const start = scrollTop;
159
+ const lines = [];
109
160
  // Title with a position counter ("Settings — 12/49") so the operator knows
110
161
  // how far down a long list they are; a filter narrows both numbers.
111
162
  const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
112
163
  lines.push(theme.bold(t('cfg.menu.title')) + theme.dim(pos));
164
+ // Group headers also consume a row, so we stop as soon as the item-row
165
+ // budget is used up (the window is advisory; this is the hard cap that
166
+ // guarantees the block fits the terminal).
167
+ let itemRows = 0;
113
168
  let lastGroup = '';
114
- for (let i = 0; i < vis.length; i++) {
169
+ for (let i = start; i < vis.length; i++) {
115
170
  const f = vis[i];
116
171
  const g = groupLabel(f, t);
117
- if (g !== lastGroup) {
172
+ // Always print the header for the FIRST shown row, so a windowed list
173
+ // still tells the operator which section it starts in.
174
+ if (i === start || g !== lastGroup) {
175
+ if (itemRows >= maxItemRows)
176
+ break;
118
177
  lines.push(g);
119
178
  lastGroup = g;
179
+ itemRows++;
120
180
  }
181
+ if (itemRows >= maxItemRows)
182
+ break;
183
+ itemRows++;
121
184
  const selected = i === cursor;
122
185
  const marker = selected ? theme.prompt('\u276f ') : ' ';
123
186
  const label = selected
@@ -132,32 +195,7 @@ export function runConfigMenu(opts) {
132
195
  if (!vis.length) {
133
196
  lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
134
197
  }
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
- }
198
+ lines.push(...tail);
161
199
  // Erase the previous block and print the new one. We count VISUAL lines
162
200
  // (accounting for wrapping by terminal width), otherwise on narrow
163
201
  // terminals you get "garbage" from unerased tails.