zames_pro 2.75.4 → 2.75.5

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.75.5] - 2026-10-15
11
+
12
+ ### Fixed
13
+
14
+ - **i18n:** clearer TUI key hints; localize the scrolled status; add sched-smoke
15
+
10
16
  ## [2.75.4] - 2026-10-15
11
17
 
12
18
  ### Added
package/README.md CHANGED
@@ -259,8 +259,8 @@ Full-screen mode adds dialog navigation, so a long answer can be read and
259
259
  selected without leaving the terminal:
260
260
 
261
261
  - mouse wheel — scroll the dialog (never the input history; history is arrow
262
- keys only). Selecting text needs `Shift`+drag, the standard TUI convention.
263
- - `PgUp` / `PgDn` — scroll the dialog half a screen.
262
+ keys only). Selecting text needs `Shift`+click, the standard TUI convention.
263
+ - `PgUp` / `PgDn` — move up/down the dialog half a screen.
264
264
  - `Home` / `End` — jump to the very top / bottom of the dialog (only while the
265
265
  input line is empty; with text typed they move the caret as usual).
266
266
  - To copy a LARGE block, use `/export` (writes the whole session as Markdown)
@@ -839,8 +839,8 @@ zames
839
839
  можно было прочитать и выделить не покидая терминал:
840
840
 
841
841
  - колесо мыши — скролл диалога (историю ввода колесо НЕ листает; история —
842
- только стрелки). Выделение текста — `Shift`+протяжка, стандарт TUI.
843
- - `PgUp` / `PgDn` — листать диалог на пол-экрана.
842
+ только стрелки). Выделение текста — `Shift`+клик, стандарт TUI.
843
+ - `PgUp` / `PgDn` — вверх/вниз по диалогу на пол-экрана.
844
844
  - `Home` / `End` — прыжок в самый верх / низ диалога (только когда строка ввода
845
845
  пуста; при набранном тексте они двигают курсор, как обычно).
846
846
  - Чтобы скопировать БОЛЬШОЙ кусок, используйте `/export` (пишет всю сессию в
@@ -44,8 +44,8 @@ export const configMessages = {
44
44
  en: 'Full-screen TUI mode (alternate screen)',
45
45
  },
46
46
  'cfg.f.ui_mouseScroll': {
47
- ru: 'Прокрутка лога колесом мыши (выделение текста — Shift+протяжка)',
48
- en: 'Mouse-wheel log scrolling (select text with Shift+drag)',
47
+ ru: 'Прокрутка лога колесом мыши (выделение текста — Shift+клик)',
48
+ en: 'Mouse-wheel log scrolling (select text with Shift+click)',
49
49
  },
50
50
  'cfg.f.maxIterations': {
51
51
  ru: 'Лимит итераций на задачу',
package/dist/i18n/help.js CHANGED
@@ -78,8 +78,8 @@ export const helpMessages = {
78
78
  en: 'Ctrl+← / Ctrl+→ move by words',
79
79
  },
80
80
  'help.key.slash': {
81
- ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P — листать)',
82
- en: '/ + Tab slash-command hints and completion (Ctrl+N/P to page)',
81
+ ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P — вверх/вниз)',
82
+ en: '/ + Tab slash-command hints and completion (Ctrl+N/P — up/down)',
83
83
  },
84
84
  'help.key.newline': {
85
85
  ru: 'Ctrl+J / Ctrl+Enter новая строка (Shift+Enter в терминалах с поддержкой)',
@@ -98,12 +98,12 @@ export const helpMessages = {
98
98
  en: 'Esc, Ctrl+C abort current generation',
99
99
  },
100
100
  'help.key.scroll': {
101
- ru: 'PgUp / PgDn листать диалог (Home/End — в начало/конец) [full-screen]',
102
- en: 'PgUp / PgDn scroll the dialog (Home/End — top/bottom) [full-screen]',
101
+ ru: 'PgUp / PgDn вверх/вниз по диалогу (Home/End — в начало/конец) [full-screen]',
102
+ en: 'PgUp / PgDn up/down the dialog (Home/End — top/bottom) [full-screen]',
103
103
  },
104
104
  'help.key.wheel': {
105
- ru: 'колесо мыши скролл диалога; выделение — Shift+протяжка',
106
- en: 'mouse wheel scroll the dialog; select text with Shift+drag',
105
+ ru: 'колесо мыши скролл диалога; выделение — Shift+клик',
106
+ en: 'mouse wheel scroll the dialog; select text with Shift+click',
107
107
  },
108
108
  'help.key.bang': {
109
109
  ru: '!команда выполнить shell-команду напрямую (мимо модели)',
@@ -18,10 +18,15 @@ export const spinnerMessages = {
18
18
  // flight. 'paused'/'settled' have no label (see LineEditor._stateLabel).
19
19
  'state.generating': { ru: '▶ генерация', en: '▶ generating' },
20
20
  'state.settled': { ru: '✓ готово', en: '✓ done' },
21
+ // Shown in the status line while the full-screen log is scrolled up.
22
+ 'status.scrolled': {
23
+ ru: '↑ прокручено на {n} стр. · PgDn/End',
24
+ en: '↑ scrolled {n} lines · PgDn/End',
25
+ },
21
26
  'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
22
27
  'editor.page_hint': {
23
- ru: '(Ctrl+N/Ctrl+P — листать, Tab — вставить)',
24
- en: '(Ctrl+N/Ctrl+P to page, Tab to insert)',
28
+ ru: '(Ctrl+N/Ctrl+P — вверх/вниз, Tab — вставить)',
29
+ en: '(Ctrl+N/Ctrl+P — up/down, Tab — insert)',
25
30
  },
26
31
  'editor.answer': { ru: '● Ответ', en: '● Answer' },
27
32
  // Shown in the full-screen log viewport before the first turn, so a fresh
@@ -32,8 +37,8 @@ export const spinnerMessages = {
32
37
  },
33
38
  // One-line key hint UNDER the input box (full-screen mode).
34
39
  'editor.keys_hint': {
35
- ru: 'выделение — Shift+протяжка · колесо — скролл · PgUp/PgDn листают · Home/End — в начало/конец',
36
- en: 'select — Shift+drag · wheel — scroll · PgUp/PgDn scroll · Home/End top/bottom',
40
+ ru: 'выделение — Shift+клик · колесо — скролл · PgUp/PgDn — вверх/вниз · Home/End — в начало/конец',
41
+ en: 'select — Shift+click · wheel — scroll · PgUp/PgDn — up/down · Home/End — top/bottom',
37
42
  },
38
43
  // Reverse search (Ctrl+R) status line. The query may be empty.
39
44
  'editor.search_prompt': {
@@ -155,7 +155,11 @@ export function StatusBar({ state, divider = true, }) {
155
155
  // When the log is scrolled up, show a marker in the status line (NOT as a
156
156
  // separate row below the input — that pushed the panel up and made the input
157
157
  // jump). It replaces the spinner text while scrolled, so it never adds a row.
158
- const scrollNote = state.scroll > 0 ? `↑ прокручено на ${state.scroll} стр. · PgDn/End` : '';
158
+ const scrollNote = state.scroll > 0
159
+ ? translate(state.locale)('status.scrolled', {
160
+ n: String(state.scroll),
161
+ })
162
+ : '';
159
163
  const leftText = scrollNote || prefix + left;
160
164
  const showLeft = !!leftText;
161
165
  // Keep the row on ONE line. The left status gets at most a third of the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.4",
3
+ "version": "2.75.5",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "self-smoke": "tsx scripts/self-smoke.mts",
36
36
  "qwen-smoke": "tsx scripts/qwen-smoke.mts",
37
37
  "qwen-models-smoke": "tsx scripts/qwen-models-smoke.mts",
38
+ "sched-smoke": "tsx scripts/sched-smoke.mts",
38
39
  "render-demo": "tsx scripts/render-demo.mts",
39
40
  "render-how": "tsx scripts/render-how-it-works.mts",
40
41
  "build:readme": "tsx scripts/build-readme.mts",
@@ -0,0 +1,150 @@
1
+ // sched-smoke: ЖИВАЯ проверка планировщика (/loop, /cron, /jobs) в РЕАЛЬНОМ
2
+ // zames.
3
+ //
4
+ // Ключевая идея: TUI-ввод в песочнице проверить нельзя (Ink требует настоящий
5
+ // интерактивный терминал, а pty.fork/script в CI его не дают — панель ввода
6
+ // просто не рендерится). Поэтому проверяем ДВЕ вещи независимо и по-настоящему:
7
+ //
8
+ // 1) ЧИСТЫЕ модули планировщика (src/scheduler.ts) — экспортируемый код
9
+ // ядра, который вызывает main loop: parseInterval, parseCron, nextCronTime,
10
+ // Scheduler.addLoop/addCron/due/remove/clear, formatJobLine.
11
+ // 2) ФАКТИЧЕСКУЮ РЕАКЦИЮ main loop: планировщик реально enqueue'ит задачу в
12
+ // очередь, и она попадает в тот же pendingQueue, что и ввод оператора.
13
+ // Это воспроизводим без браузера: Scheduler + due() + push в очередь —
14
+ // ровно то, что делает 1-секундный tick в index.ts.
15
+ //
16
+ // Плюс: /loop, /cron, /jobs присутствуют в реальном списке команд (SLASH_COMMANDS)
17
+ // и их описания локализованы — чтобы оператор их видел в подсказке и /help.
18
+ //
19
+ // Изоляция: HOME -> временная папка ДО импорта модулей (как в self-smoke),
20
+ // живой ~/.zames не трогается. Флагами можно попросить --headed, но браузер
21
+ // здесь не поднимается вообще: сценарии не требуют сети.
22
+ import fs from 'fs'
23
+ import path from 'path'
24
+ import os from 'os'
25
+
26
+ const REAL_HOME = os.homedir()
27
+ if (!process.env.ZAMES_SCHED_SMOKE_HOME) {
28
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-sched-'))
29
+ process.env.HOME = tmp
30
+ process.env.ZAMES_SCHED_SMOKE_HOME = tmp
31
+ fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
32
+ const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
33
+ if (fs.existsSync(realCfg)) {
34
+ fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
35
+ }
36
+ }
37
+ const SMOKE_HOME = process.env.ZAMES_SCHED_SMOKE_HOME as string
38
+ console.log('sched-smoke: HOME=' + SMOKE_HOME)
39
+
40
+ const { Scheduler, parseInterval, parseCron, nextCronTime, formatJobLine } =
41
+ await import('../src/scheduler.ts')
42
+ const { SLASH_COMMANDS } = await import('../src/commands.ts')
43
+ const { translate } = await import('../src/i18n.ts')
44
+
45
+ let failures = 0
46
+ const check = (name: string, ok: boolean, note?: string): void => {
47
+ console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
48
+ if (!ok) failures++
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // 1. /loop: интервал -> задача
53
+ // ---------------------------------------------------------------------------
54
+ console.log('\n--- /loop ---')
55
+ check('parseInterval 10s = 10000ms', parseInterval('10s') === 10_000)
56
+ check('parseInterval 5m = 300000ms', parseInterval('5m') === 300_000)
57
+ check('parseInterval 2h = 7200000ms', parseInterval('2h') === 7_200_000)
58
+ check('parseInterval bare 30 = 30000ms', parseInterval('30') === 30_000)
59
+ check('parseInterval мусор = null', parseInterval('abc') === null)
60
+
61
+ const s = new Scheduler()
62
+ const loop = s.addLoop(parseInterval('20s')!, 'проверь состояние', 1_000_000)
63
+ check('addLoop создал job #' + loop.id, loop.kind === 'loop' && loop.id === 1)
64
+ check('nextAt = now + interval', loop.nextAt === 1_000_000 + 20_000)
65
+ console.log(' ' + formatJobLine(loop, 1_000_000))
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // 2. /cron: выражение -> задача
69
+ // ---------------------------------------------------------------------------
70
+ console.log('\n--- /cron ---')
71
+ check('parseCron */5 * * * *', parseCron('*/5 * * * *') !== null)
72
+ check('parseCron 0 9 * * 1-5', parseCron('0 9 * * 1-5') !== null)
73
+ check('parseCron отверг 4 поля', parseCron('* * * *') === null)
74
+ check('parseCron отверг имя дня MON', parseCron('0 9 * * MON') === null)
75
+ const nct = nextCronTime('*/2 * * * *', 1_000_000_000_000)
76
+ check(
77
+ 'nextCronTime вернул будущее время',
78
+ typeof nct === 'number' && nct > 1_000_000_000_000,
79
+ )
80
+ const cron = s.addCron('*/2 * * * *', 'проверь сборку', 1_000_000)
81
+ check('addCron создал job #' + cron!.id, cron !== null && cron.kind === 'cron')
82
+ console.log(' ' + formatJobLine(cron!, 1_000_000))
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // 3. Главное: tick реально enqueue'ит задачу (то, что делает 1-сек таймер)
86
+ // ---------------------------------------------------------------------------
87
+ console.log('\n--- tick -> очередь (как в main loop) ---')
88
+ const pendingQueue: Array<{ text: string; jobId?: number }> = []
89
+ // Эмулируем 1-секундный tick из index.ts: due(now) -> push в очередь.
90
+ const tick = (now: number): number => {
91
+ const due = s.due(now)
92
+ for (const job of due) {
93
+ if (pendingQueue.some((m) => m.jobId === job.id)) continue
94
+ pendingQueue.push({ text: job.task, jobId: job.id })
95
+ }
96
+ return due.length
97
+ }
98
+ check('до срока tick ничего не даёт', tick(1_000_000 + 1_000) === 0)
99
+ check('cron */2 сработал по сроку', tick(cron!.nextAt) >= 1)
100
+ check(
101
+ 'задача попала в очередь',
102
+ pendingQueue.some((m) => m.jobId === cron!.id),
103
+ )
104
+ check(
105
+ 'в очереди текст задачи',
106
+ pendingQueue.some((m) => m.text === 'проверь сборку'),
107
+ )
108
+ // Даже если tick вызовут повторно, дубликата нет (та же логика дедупа).
109
+ const before = pendingQueue.length
110
+ tick(cron!.nextAt)
111
+ check('повторный tick не дублирует задачу', pendingQueue.length === before)
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // 4. /jobs: удаление и очистка
115
+ // ---------------------------------------------------------------------------
116
+ console.log('\n--- /jobs ---')
117
+ check('в списке 2 задачи', s.count() === 2)
118
+ check('remove(1) удалил loop', s.remove(1) === true)
119
+ check('count после rm = 1', s.count() === 1)
120
+ check('remove(999) несуществующей = false', s.remove(999) === false)
121
+ check('clear() вернул 1', s.clear() === 1)
122
+ check('count после clear = 0', s.count() === 0)
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // 5. /loop, /cron, /jobs есть в реальном списке команд и локализованы
126
+ // ---------------------------------------------------------------------------
127
+ console.log('\n--- список команд и локализация ---')
128
+ for (const name of ['/loop', '/cron', '/jobs']) {
129
+ const entry = SLASH_COMMANDS.find((c) => c.name === name)
130
+ check(name + ' в SLASH_COMMANDS', !!entry)
131
+ if (entry) {
132
+ const ru = translate('ru')(entry.key)
133
+ const en = translate('en')(entry.key)
134
+ check(
135
+ name + ' локализован (ru/en)',
136
+ !!ru && !!en && ru !== entry.key && en !== entry.key,
137
+ ru,
138
+ )
139
+ }
140
+ }
141
+
142
+ console.log(
143
+ '\nsched-smoke: ' + (failures === 0 ? 'ALL PASS' : failures + ' FAILED'),
144
+ )
145
+ if (!process.env.ZAMES_SCHED_SMOKE_KEEP) {
146
+ try {
147
+ fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
148
+ } catch {}
149
+ }
150
+ process.exit(failures === 0 ? 0 : 1)