zames_pro 2.75.0 → 2.75.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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.75.1] - 2026-10-15
11
+
12
+ ### Added
13
+
14
+ - remove /resume; Ctrl+Delete always deletes the word BEFORE the cursor
15
+ - **tui:** drop the working directory from the prompt
16
+ - **tui:** persistent key hint under the input box
17
+
18
+ ### Fixed
19
+
20
+ - stop advertising the removed /resume and make the i18n guard real
21
+ - **tui:** Ctrl+Delete works in VSCode (it sends ESC d)
22
+ - **tui:** Ctrl+Delete deletes the word BEFORE the cursor
23
+
10
24
  ## [2.75.0] - 2026-10-14
11
25
 
12
26
  ### Added
package/dist/commands.js CHANGED
@@ -981,7 +981,6 @@ export const SLASH_COMMANDS = [
981
981
  { name: '/clear', key: 'help.cmd.new' },
982
982
  { name: '/sessions', key: 'help.cmd.sessions' },
983
983
  { name: '/chats', key: 'help.cmd.chats' },
984
- { name: '/resume', key: 'help.cmd.resume' },
985
984
  { name: '/resume-id', key: 'help.cmd.resume_id' },
986
985
  { name: '/last', key: 'help.cmd.last' },
987
986
  { name: '/retry', key: 'help.cmd.retry' },
@@ -64,10 +64,6 @@ export const commonMessages = {
64
64
  en: 'Working directory: {v}',
65
65
  },
66
66
  'msg.transcript': { ru: 'Транскрипт: {v}', en: 'Transcript: {v}' },
67
- 'msg.mouse_hint': {
68
- ru: 'Мышь: колесо листает диалог (история — только стрелки), выделение текста — Shift+протяжка; PgUp/PgDn листают, Home/End — в начало/конец.',
69
- en: 'Mouse: the wheel scrolls the dialog (history is arrow keys only), select text with Shift+drag; PgUp/PgDn scroll, Home/End jump to top/bottom.',
70
- },
71
67
  'msg.new_chat': { ru: 'Создаю новый чат...', en: 'Creating a new chat...' },
72
68
  'msg.new_chat_ok': { ru: 'Новый чат.', en: 'New chat.' },
73
69
  'msg.resuming': {
@@ -258,14 +254,6 @@ export const commonMessages = {
258
254
  ru: 'Нет блока кода #{n}.',
259
255
  en: 'No code block #{n}.',
260
256
  },
261
- 'copy.code_hint': {
262
- ru: 'блок кода {n}/{m} · Ctrl+Y — копировать',
263
- en: 'code block {n}/{m} · Ctrl+Y to copy',
264
- },
265
- 'copy_code.usage': {
266
- ru: 'Использование: /copy-code [номер] — копировать блок кода последнего ответа (по умолчанию последний). /copy-code list — список.',
267
- en: 'Usage: /copy-code [n] — copy a code block from the last answer (default: the last). /copy-code list — list them.',
268
- },
269
257
  'copy_code.list': {
270
258
  ru: 'Блоки кода последнего ответа:',
271
259
  en: 'Code blocks in the last answer:',
@@ -6,7 +6,6 @@ export const configMessages = {
6
6
  'cfg.group.checkpoint': { ru: 'Чекпойнты', en: 'Checkpoints' },
7
7
  'cfg.group.transcript': { ru: 'Транскрипт', en: 'Transcript' },
8
8
  'cfg.group.browser': { ru: 'Браузер / DeepSeek', en: 'Browser / DeepSeek' },
9
- 'cfg.group.qwen': { ru: 'Qwen', en: 'Qwen' },
10
9
  'cfg.f.provider': { ru: 'Провайдер чата', en: 'Chat provider' },
11
10
  'cfg.f.qwen_model': {
12
11
  ru: 'Модель Qwen (id)',
package/dist/i18n/help.js CHANGED
@@ -133,10 +133,6 @@ export const helpMessages = {
133
133
  ru: '/chats [фильтр] список последних чатов DeepSeek',
134
134
  en: '/chats [filter] recent DeepSeek chats',
135
135
  },
136
- 'help.cmd.resume': {
137
- ru: '/resume <n> открыть чат №n из /chats',
138
- en: '/resume <n> open chat #n from /chats',
139
- },
140
136
  'help.cmd.last': {
141
137
  ru: '/last открыть последний чат этого каталога',
142
138
  en: '/last open the last chat of this directory',
package/dist/i18n/qwen.js CHANGED
@@ -14,10 +14,6 @@ export const qwenMessages = {
14
14
  ru: 'Код из письма Qwen: ',
15
15
  en: 'Qwen email code: ',
16
16
  },
17
- 'qwen.login_pass_prompt': {
18
- ru: 'Пароль Qwen: ',
19
- en: 'Qwen password: ',
20
- },
21
17
  'qwen.login_ok': {
22
18
  ru: 'Qwen: вход выполнен.',
23
19
  en: 'Qwen: signed in.',
@@ -113,8 +113,8 @@ export const selfReviewMessages = {
113
113
  en: 'Pick a chat (↑/↓, Enter to open, Esc to cancel)',
114
114
  },
115
115
  'chats.use_resume': {
116
- ru: '\nИспользуй /resume <n> для продолжения.\n',
117
- en: '\nUse /resume <n> to continue.\n',
116
+ ru: '\nДля продолжения используй /resume-id <id> (полный id в скобках).\n',
117
+ en: '\nTo continue use /resume-id <id> (the full id in brackets).\n',
118
118
  },
119
119
  'chats.none_hint': {
120
120
  ru: 'Чатов не найдено. Возможно, сайдбар свёрнут или селекторы устарели.',
@@ -128,22 +128,6 @@ export const selfReviewMessages = {
128
128
  ru: ' Системный промпт НЕ пересылается (он уже в начале чата). Включить: /config set browser.resendPromptOnResume true.\n',
129
129
  en: ' The system prompt is NOT resent (it is already at the start of the chat). Enable: /config set browser.resendPromptOnResume true.\n',
130
130
  },
131
- 'chats.resume_usage': {
132
- ru: 'Использование: /resume <n> (или /chats для списка)',
133
- en: 'Usage: /resume <n> (or /chats for the list)',
134
- },
135
- 'chats.need_number': {
136
- ru: 'Нужен номер из /chats.',
137
- en: 'A number from /chats is required.',
138
- },
139
- 'chats.run_chats_first': {
140
- ru: 'Сначала выполни /chats.',
141
- en: 'Run /chats first.',
142
- },
143
- 'chats.no_n': {
144
- ru: 'Нет чата №{n}. Всего: {total}.',
145
- en: 'No chat #{n}. Total: {total}.',
146
- },
147
131
  'chats.opening': { ru: 'Открываю: {v}', en: 'Opening: {v}' },
148
132
  'chats.prompt_will_resend': {
149
133
  ru: ' Системный промпт будет переслан на следующей задаче.\n',
@@ -30,6 +30,11 @@ export const spinnerMessages = {
30
30
  ru: 'Напишите задачу и нажмите Enter. /help — команды, Ctrl+J — новая строка.',
31
31
  en: 'Type a task and press Enter. /help for commands, Ctrl+J for a new line.',
32
32
  },
33
+ // One-line key hint UNDER the input box (full-screen mode).
34
+ 'editor.keys_hint': {
35
+ ru: 'выделение — Shift+протяжка · колесо — скролл · PgUp/PgDn листают · Home/End — в начало/конец',
36
+ en: 'select — Shift+drag · wheel — scroll · PgUp/PgDn scroll · Home/End top/bottom',
37
+ },
33
38
  // Reverse search (Ctrl+R) status line. The query may be empty.
34
39
  'editor.search_prompt': {
35
40
  ru: '(обратный поиск) `{q}` — Ctrl+R дальше, Enter принять, Esc отмена',
package/dist/index.js CHANGED
@@ -155,7 +155,7 @@ if (jsonMode) {
155
155
  // ---------- session persistence ----------
156
156
  // We store sessions (DeepSeek chats) in ~/.zames/.sessions so they survive
157
157
  // a restart and are available for explicit restore (--resume-last,
158
- // --chat <id>, /resume <n>). They are NOT brought up automatically on launch.
158
+ // --chat <id>, /resume-id <id>). They are NOT brought up automatically on launch.
159
159
  // Previously there was a single last-chat.json file that got lost when the
160
160
  // project changed and gave no list of sessions to restore.
161
161
  function saveLastChat(id, workdir = '', title = '') {
@@ -163,7 +163,7 @@ function saveLastChat(id, workdir = '', title = '') {
163
163
  return;
164
164
  // Tag the session with the ACTIVE provider: a chat id is only meaningful to
165
165
  // the backend that created it, so a later resume must open it in the same
166
- // one (or refuse). Without this a /provider switch made /resume try to open a
166
+ // one (or refuse). Without this a /provider switch made a resume try to open a
167
167
  // qwen id in DeepSeek (and vice versa).
168
168
  saveSession({ id, workdir, title, provider: config.provider });
169
169
  }
@@ -446,7 +446,6 @@ ${theme.bold(t('help.sec.session'))}
446
446
  ${t('help.cmd.new')}
447
447
  ${t('help.cmd.sessions')}
448
448
  ${t('help.cmd.chats')}
449
- ${t('help.cmd.resume')}
450
449
  ${t('help.cmd.resume_id')}
451
450
  ${t('help.cmd.last')}
452
451
  ${t('help.cmd.retry')}
@@ -1007,7 +1006,10 @@ async function main() {
1007
1006
  // `--resend-prompt` or `browser.resendPromptOnResume` forces it (e.g. when
1008
1007
  // the prompt/tools changed).
1009
1008
  const promptOnResume = () => resendPrompt || config.browser.resendPromptOnResume === true;
1010
- let lastChats = [];
1009
+ // Assigned by the /chats handler before it is read; no initial value needed
1010
+ // (the empty-array default was flagged as a useless assignment once the
1011
+ // /resume <n> handler that read it was removed).
1012
+ let lastChats;
1011
1013
  let currentChatId = null;
1012
1014
  // The last task the operator sent (used by /retry). Plain text only.
1013
1015
  let lastTaskText = '';
@@ -1081,7 +1083,7 @@ async function main() {
1081
1083
  total: String(items.length),
1082
1084
  });
1083
1085
  };
1084
- // Re-inject a persisted checklist after /resume or the startup restore.
1086
+ // Re-inject a persisted checklist after /resume-id or the startup restore.
1085
1087
  // The list is REPLACED (not merged): a resumed chat with no saved checklist
1086
1088
  // must clear the previous chat's tasks, otherwise the status line keeps
1087
1089
  // showing them for a chat that never had them.
@@ -1234,17 +1236,18 @@ async function main() {
1234
1236
  });
1235
1237
  };
1236
1238
  const buildPrompt = () => {
1237
- let tail;
1238
1239
  if (reviewMode) {
1239
- tail =
1240
+ // Review mode is rare and MEANINGFUL, so it stays in the prompt.
1241
+ return (theme.prompt('❯ ') +
1242
+ theme.dim('(') +
1240
1243
  theme.warn(t('prompt.review')) +
1241
- theme.dim(':') +
1242
- theme.dir(reviewMode.snapName);
1244
+ theme.dim(':') +
1245
+ theme.dir(reviewMode.snapName) +
1246
+ theme.dim(') '));
1243
1247
  }
1244
- else {
1245
- tail = theme.dir(dirLabel(currentWorkdir));
1246
- }
1247
- return theme.prompt('❯ ') + tail + theme.dim(' › ');
1248
+ // No working-directory tail: it ate input width and is already in the
1249
+ // header / /pwd. A bare prompt keeps room for the task text.
1250
+ return theme.prompt('❯ ') + theme.dim('› ');
1248
1251
  };
1249
1252
  await refreshDynamicCommands(currentWorkdir);
1250
1253
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -1598,13 +1601,8 @@ async function main() {
1598
1601
  };
1599
1602
  try {
1600
1603
  ed.start();
1601
- // One-time hint so the operator knows how selection works: with wheel
1602
- // scrolling on (the default) the terminal no longer selects natively, so
1603
- // a drag needs Shift. Printed ONCE, above the input, in full-screen TUI
1604
- // only (the legacy editor keeps native selection).
1605
- if (config.ui.tui && config.ui.alternateScreen) {
1606
- ed.printAbove(theme.dim(t('msg.mouse_hint')));
1607
- }
1604
+ // The key/selection hint is now a PERSISTENT row under the input box
1605
+ // (FullScreenApp), so the old one-time printAbove is redundant.
1608
1606
  }
1609
1607
  catch (e) {
1610
1608
  // A TUI mount failure must not kill the agent. We surface it and let the
@@ -1900,7 +1898,7 @@ async function main() {
1900
1898
  });
1901
1899
  }
1902
1900
  // Open a chat by its ChatInfo and print the restored dialogue. Shared by
1903
- // /resume and the interactive /chats list, so both paths behave identically
1901
+ // /resume-id and the interactive /chats list, so both paths behave identically
1904
1902
  // (todos reloaded, prompt-resend decision, transcript event).
1905
1903
  const openResumedChat = async (pick) => {
1906
1904
  if (editor)
@@ -2016,16 +2014,19 @@ async function main() {
2016
2014
  }
2017
2015
  else {
2018
2016
  let tail;
2017
+ // The non-TTY fallback prompt mirrors buildPrompt(): bare, no dir.
2019
2018
  if (reviewMode) {
2020
2019
  tail =
2021
- theme.warn(t('prompt.review')) +
2020
+ theme.dim('(') +
2021
+ theme.warn(t('prompt.review')) +
2022
2022
  theme.dim(':') +
2023
- theme.dir(reviewMode.snapName);
2023
+ theme.dir(reviewMode.snapName) +
2024
+ theme.dim(') ');
2024
2025
  }
2025
2026
  else {
2026
- tail = theme.dir(dirLabel(currentWorkdir));
2027
+ tail = theme.dim('› ');
2027
2028
  }
2028
- input = await promptOnce(theme.prompt('❯ ') + tail + theme.dim(' › '));
2029
+ input = await promptOnce(theme.prompt('❯ ') + tail);
2029
2030
  }
2030
2031
  }
2031
2032
  catch {
@@ -2327,8 +2328,8 @@ async function main() {
2327
2328
  continue;
2328
2329
  }
2329
2330
  console.log(theme.system(t('chats.recent')));
2330
- // The number is the index in the FULL list, so `/resume <n>` keeps
2331
- // pointing at the same chat even when a filter narrows the view.
2331
+ // The number is the index in the FULL list (a filter only narrows
2332
+ // the VIEW), and the full id in brackets is what /resume-id takes.
2332
2333
  for (const c of shown) {
2333
2334
  const idx = lastChats.indexOf(c);
2334
2335
  const n = String(idx + 1).padStart(2, ' ');
@@ -2347,29 +2348,6 @@ async function main() {
2347
2348
  }
2348
2349
  continue;
2349
2350
  }
2350
- if (lower === '/resume' || lower.startsWith('/resume ')) {
2351
- const arg = trimmed.slice(7).trim();
2352
- if (!arg) {
2353
- console.error(theme.error(t('chats.resume_usage')));
2354
- continue;
2355
- }
2356
- const n = Number(arg);
2357
- if (!Number.isFinite(n) || n < 1) {
2358
- console.error(theme.error(t('chats.need_number')));
2359
- continue;
2360
- }
2361
- if (!lastChats.length) {
2362
- console.error(theme.error(t('chats.run_chats_first')));
2363
- continue;
2364
- }
2365
- const pick = lastChats[n - 1];
2366
- if (!pick) {
2367
- console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
2368
- continue;
2369
- }
2370
- await openResumedChat(pick);
2371
- continue;
2372
- }
2373
2351
  if (lower === '/provider' || lower.startsWith('/provider ')) {
2374
2352
  const arg = trimmed.slice('/provider'.length).trim().toLowerCase();
2375
2353
  if (!arg || arg === 'status') {
@@ -63,7 +63,12 @@ export function FullScreenApp({ store, inputProps, }) {
63
63
  // The suggestion popup renders ABOVE the input box; reserve its rows so
64
64
  // opening the list does not push the panel / shift the whole layout.
65
65
  const suggestRows = suggestRowCount(state.edit.buf, state.commands, cols);
66
- const panelRows = HEADER_ROWS + STATUS_ROWS + INPUT_BOX_ROWS + suggestRows;
66
+ // A one-line hint UNDER the input box (selection / scroll keys). It is part
67
+ // of the fixed panel height, so the log viewport shrinks by one row instead
68
+ // of the panel overflowing; `cursorTop` (measured from the top) is NOT
69
+ // affected, so the caret cannot move.
70
+ const HINT_ROWS = 1;
71
+ const panelRows = HEADER_ROWS + STATUS_ROWS + INPUT_BOX_ROWS + suggestRows + HINT_ROWS;
67
72
  const logHeight = Math.max(1, termRows - panelRows);
68
73
  // Window the log BY RENDERED ROW: a long wrapped answer can be taller than
69
74
  // one entry, and an entry-count window either overflowed the panel or left
@@ -108,6 +113,6 @@ export function FullScreenApp({ store, inputProps, }) {
108
113
  }, state.mouseScroll);
109
114
  return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column", height: logHeight, overflow: "hidden", justifyContent: "flex-end", children: all.length === 0 ? (_jsx(Text, { color: "#5b616e", children: translate(state.locale)('editor.empty_hint') })) : (_jsx(LogList, { entries: visible })) }), _jsx(StatusBar, { state: state, divider: false }), _jsx(InputLine, { ...inputProps, store: store,
110
115
  // Cursor rows above the input box: header + log viewport + status.
111
- cursorTop: HEADER_ROWS + logHeight + suggestRows + STATUS_ROWS + 1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked })] }));
116
+ cursorTop: HEADER_ROWS + logHeight + suggestRows + STATUS_ROWS + 1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked }), _jsx(Text, { color: "#5b616e", wrap: "truncate", children: ' ' + translate(state.locale)('editor.keys_hint') })] }));
112
117
  }
113
118
  void contentWidth;
@@ -1,4 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ // TuiEditor — a drop-in replacement for the hand-rolled LineEditor backed by an
3
+ // Ink/React render tree.
4
+ //
5
+ // WHY an adapter and not a rewrite of index.ts:
6
+ // * index.ts (4200 lines) and run-task.ts call the UI through a large but
7
+ // STABLE imperative surface (`printAbove`, `lock`, `toolCall`, `busy`, ...);
8
+ // * Ink renders from React state, which can only be driven from inside the
9
+ // tree. The adapter is the seam: it implements the SAME surface and forwards
10
+ // every call into the TuiStore, which the tree subscribes to.
11
+ // So the rest of the agent is unchanged; only the rendering engine differs.
12
+ //
13
+ // Lifecycle: `start()` mounts the Ink app; `dispose()` unmounts it. A `pause()`
14
+ // /`resume()` pair suspends the tree for a foreign full-screen UI (the config
15
+ // menu) — implemented as unmount/remount so the foreign program owns the
16
+ // terminal cleanly.
17
+ import fs from 'node:fs';
2
18
  import { render } from 'ink';
3
19
  import { TuiStore } from './store.js';
4
20
  import { TuiApp } from './TuiApp.js';
@@ -126,6 +142,26 @@ export class TuiEditor {
126
142
  alternateScreen: this.alternateScreen,
127
143
  });
128
144
  this.rendered = true;
145
+ // DEV DIAGNOSTIC: dump every raw stdin chunk to a file so a terminal's
146
+ // key encoding (e.g. Ctrl+Delete = ESC d in VSCode) can be captured from
147
+ // the LIVE agent, not guessed. Off unless ZAMES_TUI_KEY_DEBUG names a
148
+ // file.
149
+ const keyDbg = process.env.ZAMES_TUI_KEY_DEBUG;
150
+ if (keyDbg) {
151
+ process.stdin.on('data', (d) => {
152
+ try {
153
+ fs.appendFileSync(keyDbg, new Date().toISOString() +
154
+ ' ' +
155
+ d.toString('hex') +
156
+ ' ' +
157
+ JSON.stringify(d.toString('utf-8')) +
158
+ String.fromCharCode(10));
159
+ }
160
+ catch {
161
+ /* ignore */
162
+ }
163
+ });
164
+ }
129
165
  this.installResizeHandler();
130
166
  // Entries shown so far are now on screen; a later remount must not reprint
131
167
  // them.
@@ -453,9 +489,9 @@ export class TuiEditor {
453
489
  return false;
454
490
  }
455
491
  const pick = typeof index === 'number'
456
- ? blocks.find((b) => b.index === index) ?? null
492
+ ? (blocks.find((b) => b.index === index) ?? null)
457
493
  : snap.codeFocus >= 0
458
- ? blocks[snap.codeFocus] ?? null
494
+ ? (blocks[snap.codeFocus] ?? null)
459
495
  : blocks[blocks.length - 1];
460
496
  if (!pick) {
461
497
  this.printAbove(theme.warn(translate(this.locale)('copy.bad_index', { n: String(index) })));
@@ -464,10 +500,14 @@ export class TuiEditor {
464
500
  const res = this.onCopyText?.(pick.text) ?? { ok: false, via: 'none' };
465
501
  const label = 'code #' + pick.index + (pick.lang ? ' (' + pick.lang + ')' : '');
466
502
  if (res.ok) {
467
- this.printAbove(theme.assistant(translate(this.locale)('copy.done', { tool: res.via }) + ' — ' + label));
503
+ this.printAbove(theme.assistant(translate(this.locale)('copy.done', { tool: res.via }) +
504
+ ' — ' +
505
+ label));
468
506
  }
469
507
  else {
470
- this.printAbove(theme.warn(translate(this.locale)('copy.failed', { tool: res.via }) + ' — ' + label));
508
+ this.printAbove(theme.warn(translate(this.locale)('copy.failed', { tool: res.via }) +
509
+ ' — ' +
510
+ label));
471
511
  }
472
512
  return res.ok;
473
513
  }
package/dist/tui/keys.js CHANGED
@@ -35,7 +35,8 @@ export function parseCsiU(raw) {
35
35
  if (cp === 13 && (ctrl || alt))
36
36
  return { type: 'newline' };
37
37
  if (cp === 3) {
38
- return ctrl || alt ? { type: 'deleteWordRight' } : { type: 'delete' };
38
+ // Ctrl/Alt+Delete deletes the word on the LEFT (see dispatchKey).
39
+ return ctrl || alt ? { type: 'deleteWordLeft' } : { type: 'delete' };
39
40
  }
40
41
  if (cp === 127) {
41
42
  return ctrl || alt ? { type: 'deleteWordLeft' } : { type: 'backspace' };
@@ -84,8 +85,11 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
84
85
  return { type: 'wordLeft' };
85
86
  if (key.rightArrow)
86
87
  return { type: 'wordRight' };
88
+ // Ctrl+Delete deletes the word BEFORE the cursor (the operator's binding:
89
+ // "Ctrl+Del must delete the word before the cursor"). Ctrl+Backspace does
90
+ // the same. Plain Delete still deletes one char to the RIGHT.
87
91
  if (key.delete)
88
- return { type: 'deleteWordRight' };
92
+ return { type: 'deleteWordLeft' };
89
93
  if (key.backspace)
90
94
  return { type: 'deleteWordLeft' };
91
95
  if (key.home)
@@ -140,10 +144,17 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
140
144
  return { type: 'wordLeft' };
141
145
  if (input === 'f')
142
146
  return { type: 'wordRight' };
147
+ // VSCode sends Ctrl+Delete as `ESC d` (which Ink reports as Alt+d).
148
+ // The operator expects it to delete the word BEFORE the cursor.
149
+ if (input === 'd')
150
+ return { type: 'deleteWordLeft' };
143
151
  if (key.backspace)
144
152
  return { type: 'deleteWordLeft' };
153
+ // Alt/Ctrl+Delete also deletes the word BEFORE the cursor (the operator's
154
+ // binding, consistent with ESC d): some terminals encode Ctrl+Delete as
155
+ // meta+Delete rather than ESC d.
145
156
  if (key.delete)
146
- return { type: 'deleteWordRight' };
157
+ return { type: 'deleteWordLeft' };
147
158
  return null;
148
159
  }
149
160
  // ---- LF as a bare newline (Ctrl+J) ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.0",
3
+ "version": "2.75.1",
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": {
@@ -0,0 +1,51 @@
1
+ // Drive the live-like probe: type a seed, then send Ctrl+Delete in EVERY
2
+ // encoding a terminal might use, and print the raw bytes + the buffer.
3
+ import { spawn } from 'node:child_process'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+
7
+ const ESC = String.fromCharCode(27)
8
+ const LOG = '/tmp/ctd-live.log'
9
+ try {
10
+ fs.unlinkSync(LOG)
11
+ } catch {
12
+ /* ignore */
13
+ }
14
+
15
+ const child = spawn(
16
+ 'script',
17
+ ['-qec', 'npx tsx scripts/ctd-live.tsx', '/dev/null'],
18
+ {
19
+ cwd: path.resolve('.'),
20
+ env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_CTD_LOG: LOG },
21
+ },
22
+ )
23
+ child.stdout.on('data', () => {})
24
+ child.stderr.on('data', () => {})
25
+
26
+ const seq = process.argv[2] || 'xterm'
27
+ const enc: Record<string, string> = {
28
+ xterm: ESC + '[3;5~',
29
+ kitty: ESC + '[3;5u',
30
+ vt: ESC + '[3;5:1~',
31
+ }
32
+ const bytes = enc[seq] ?? seq
33
+
34
+ setTimeout(() => {
35
+ child.stdin.write('foo bar baz') // seed
36
+ setTimeout(() => {
37
+ child.stdin.write(bytes) // the Ctrl+Delete encoding
38
+ setTimeout(() => {
39
+ const log = fs.readFileSync(LOG, 'utf-8')
40
+ process.stdout.write('=== ' + seq + ' ===' + String.fromCharCode(10))
41
+ process.stdout.write(
42
+ log
43
+ .split(String.fromCharCode(10))
44
+ .filter((l) => l.startsWith('RAW') || l.startsWith('BUF'))
45
+ .join(String.fromCharCode(10)) + String.fromCharCode(10),
46
+ )
47
+ child.kill('SIGTERM')
48
+ }, 500)
49
+ }, 400)
50
+ }, 5000)
51
+ child.on('close', () => process.exit(0))
@@ -0,0 +1,33 @@
1
+ // Minimal LIVE-like probe: the REAL TuiEditor exactly as index.ts builds it
2
+ // (default options), plus a raw-stdin TAP that logs every byte the terminal
3
+ // sends, so we can see EXACTLY what Ctrl+Delete produces on this terminal.
4
+ import fs from 'node:fs'
5
+ import { TuiEditor } from '../src/tui/TuiEditor.js'
6
+
7
+ const LOG = process.env.PROBE_CTD_LOG || '/tmp/ctd-live.log'
8
+ fs.writeFileSync(LOG, '')
9
+ const w = (s: string): void => {
10
+ fs.appendFileSync(LOG, s + String.fromCharCode(10))
11
+ }
12
+
13
+ const ed = new TuiEditor({
14
+ prompt: '❯ › ',
15
+ commands: [],
16
+ locale: 'ru',
17
+ alternateScreen: true,
18
+ })
19
+ ed.onSubmit = () => {}
20
+ ed.onAttach = () => null
21
+ ed.onClipboard = () => Promise.resolve(null)
22
+ ed.start()
23
+
24
+ // Tap raw stdin: hex-dump every chunk so we see the exact bytes.
25
+ process.stdin.on('data', (d: Buffer) => {
26
+ w('RAW ' + d.toString('hex') + ' ' + JSON.stringify(d.toString('utf-8')))
27
+ })
28
+ // Log the buffer after every change.
29
+ ed.store.subscribe(() => {
30
+ const s = ed.store.getSnapshot().edit
31
+ w('BUF ' + JSON.stringify(s.buf) + ' cur=' + s.cursor)
32
+ })
33
+ setInterval(() => {}, 1 << 30)
@@ -0,0 +1,37 @@
1
+ // Send `ESC d` (what VSCode sends for Ctrl+Delete) to the REAL TuiEditor and
2
+ // print the resulting buffer.
3
+ import { spawn } from 'node:child_process'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ const ESC = String.fromCharCode(27)
7
+ const LOG = '/tmp/ctd-live.log'
8
+ try {
9
+ fs.unlinkSync(LOG)
10
+ } catch {}
11
+ const child = spawn(
12
+ 'script',
13
+ ['-qec', 'npx tsx scripts/ctd-live.tsx', '/dev/null'],
14
+ {
15
+ cwd: path.resolve('.'),
16
+ env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_CTD_LOG: LOG },
17
+ },
18
+ )
19
+ child.stdout.on('data', () => {})
20
+ child.stderr.on('data', () => {})
21
+ setTimeout(() => {
22
+ child.stdin.write('foo bar baz')
23
+ setTimeout(() => {
24
+ child.stdin.write(ESC + 'd')
25
+ setTimeout(() => {
26
+ const log = fs.readFileSync(LOG, 'utf-8')
27
+ process.stdout.write(
28
+ log
29
+ .split(String.fromCharCode(10))
30
+ .filter((l) => l.startsWith('RAW') || l.startsWith('BUF'))
31
+ .join(String.fromCharCode(10)) + String.fromCharCode(10),
32
+ )
33
+ child.kill('SIGTERM')
34
+ }, 500)
35
+ }, 400)
36
+ }, 5000)
37
+ child.on('close', () => process.exit(0))
@@ -0,0 +1,20 @@
1
+ import { spawn } from 'node:child_process'
2
+ import path from 'node:path'
3
+ import { Term } from '../test/vt100.mts'
4
+ const NL = String.fromCharCode(10)
5
+ const term = new Term(20, 80)
6
+ const child = spawn(
7
+ 'script',
8
+ ['-qec', 'npx tsx scripts/tui-repro.tsx', '/dev/null'],
9
+ {
10
+ cwd: path.resolve('.'),
11
+ env: { ...process.env, COLUMNS: '80', LINES: '20', PROBE_LOCALE: 'ru' },
12
+ },
13
+ )
14
+ child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
15
+ child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
16
+ setTimeout(() => {
17
+ process.stdout.write(term.render() + NL)
18
+ child.kill('SIGTERM')
19
+ }, 5000)
20
+ child.on('close', () => process.exit(0))
@@ -28,8 +28,8 @@ store.set({
28
28
  title: 'zames_pro',
29
29
  version: '2.34.0',
30
30
  workdir: '/workspaces/zames/zames_pro',
31
- prompt: '➔ zames_pro › ',
32
- locale: 'en',
31
+ prompt: '❯ › ',
32
+ locale: process.env.PROBE_LOCALE === 'ru' ? 'ru' : 'en',
33
33
  commands,
34
34
  edit: { ...initialEditState(), buf: process.argv[2] ?? '' },
35
35
  })