zames_pro 2.75.0 → 2.75.2
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 +20 -0
- package/dist/agent-loop.js +27 -2
- package/dist/browser.js +27 -9
- package/dist/checkpoint.js +41 -2
- package/dist/commands.js +0 -1
- package/dist/i18n/common.js +0 -12
- package/dist/i18n/config.js +0 -1
- package/dist/i18n/help.js +0 -4
- package/dist/i18n/qwen.js +0 -4
- package/dist/i18n/self-review.js +2 -18
- package/dist/i18n/spinner.js +5 -0
- package/dist/index.js +28 -50
- package/dist/system-prompt.js +27 -0
- package/dist/tui/FullScreenApp.js +7 -2
- package/dist/tui/TuiEditor.js +44 -4
- package/dist/tui/keys.js +14 -3
- package/package.json +1 -1
- package/scripts/ctd-live-driver.mts +51 -0
- package/scripts/ctd-live.tsx +33 -0
- package/scripts/escd-live-driver.mts +37 -0
- package/scripts/ru-shot.mts +20 -0
- package/scripts/tui-repro.tsx +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.75.2] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- batch read-only tool calls and skip redundant checkpoints
|
|
15
|
+
|
|
16
|
+
## [2.75.1] - 2026-10-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- remove /resume; Ctrl+Delete always deletes the word BEFORE the cursor
|
|
21
|
+
- **tui:** drop the working directory from the prompt
|
|
22
|
+
- **tui:** persistent key hint under the input box
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- stop advertising the removed /resume and make the i18n guard real
|
|
27
|
+
- **tui:** Ctrl+Delete works in VSCode (it sends ESC d)
|
|
28
|
+
- **tui:** Ctrl+Delete deletes the word BEFORE the cursor
|
|
29
|
+
|
|
10
30
|
## [2.75.0] - 2026-10-14
|
|
11
31
|
|
|
12
32
|
### Added
|
package/dist/agent-loop.js
CHANGED
|
@@ -180,6 +180,15 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
180
180
|
// block after the tool loop).
|
|
181
181
|
let malformedRetries = 0;
|
|
182
182
|
const MAX_MALFORMED_RETRIES = 3;
|
|
183
|
+
// Tool-result caps for the message fed back to the model. A single result
|
|
184
|
+
// gets a generous cap; in a BATCH each item is capped and the ASSEMBLED
|
|
185
|
+
// message is capped again, so a big batch cannot exceed what the chat input
|
|
186
|
+
// accepts (a failed paste loses the whole turn). The caps are explicit:
|
|
187
|
+
// truncateToolResult() appends `[...truncated N chars]` so the model knows
|
|
188
|
+
// it is looking at a partial output.
|
|
189
|
+
const TOOL_RESULT_LIMIT = 24_000;
|
|
190
|
+
const BATCH_ITEM_LIMIT = 16_000;
|
|
191
|
+
const BATCH_TOTAL_LIMIT = 120_000;
|
|
183
192
|
let stallRetries = 0;
|
|
184
193
|
const MAX_STALL_RETRIES = 5;
|
|
185
194
|
// SINGLE shared budget for "the answer is not a recognized tool call".
|
|
@@ -752,6 +761,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
752
761
|
// and no call matches an `ask` permission rule (a concurrent operator
|
|
753
762
|
// prompt would race). Anything else keeps the strict sequential loop, so
|
|
754
763
|
// mutating side effects never reorder.
|
|
764
|
+
//
|
|
765
|
+
// MUTATING tools (Write/Edit/MultiEdit) are deliberately NOT here even
|
|
766
|
+
// though writes to different files are logically independent: (a)
|
|
767
|
+
// UndoStore.backup() does a read-modify-write of ONE shared index file, so
|
|
768
|
+
// two concurrent backups race and lose a record; (b) PostToolUse hooks and
|
|
769
|
+
// diagnostics must run in a defined order. A batch of mutating calls still
|
|
770
|
+
// costs only ONE round-trip (the sequential loop below runs them all for
|
|
771
|
+
// the same answer), so nothing is lost by keeping them sequential.
|
|
755
772
|
const PARALLEL_SAFE = new Set(['Read', 'Glob', 'Grep', 'LS', 'WebFetch']);
|
|
756
773
|
const readOnlyBatch = callsToRun.length > 1 &&
|
|
757
774
|
!(hookConfig.PreToolUse && hookConfig.PreToolUse.length) &&
|
|
@@ -1064,15 +1081,23 @@ ${diag}`;
|
|
|
1064
1081
|
if (results.length === 1) {
|
|
1065
1082
|
const r = results[0];
|
|
1066
1083
|
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
1067
|
-
message = `Tool result for ${r.tool}:\n${truncateToolResult(resultStr,
|
|
1084
|
+
message = `Tool result for ${r.tool}:\n${truncateToolResult(resultStr, TOOL_RESULT_LIMIT)}`;
|
|
1068
1085
|
}
|
|
1069
1086
|
else {
|
|
1070
1087
|
message = results
|
|
1071
1088
|
.map((r) => {
|
|
1072
1089
|
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
|
1073
|
-
return `Tool result for ${r.tool}:\n${truncateToolResult(resultStr,
|
|
1090
|
+
return `Tool result for ${r.tool}:\n${truncateToolResult(resultStr, BATCH_ITEM_LIMIT)}`;
|
|
1074
1091
|
})
|
|
1075
1092
|
.join('\n\n');
|
|
1093
|
+
// A large batch (e.g. 8 Reads) could still add up past what the chat
|
|
1094
|
+
// input accepts; a too-long message fails to paste (ds.input_partial) and
|
|
1095
|
+
// the whole turn is lost. Cap the ASSEMBLED message as a safety net,
|
|
1096
|
+
// keeping the per-item truncation markers (the model still sees WHAT was
|
|
1097
|
+
// dropped, just a bit more of it).
|
|
1098
|
+
if (message.length > BATCH_TOTAL_LIMIT) {
|
|
1099
|
+
message = truncateToolResult(message, BATCH_TOTAL_LIMIT);
|
|
1100
|
+
}
|
|
1076
1101
|
}
|
|
1077
1102
|
// Append the argument-error nudge (set inside the batch loop) now that
|
|
1078
1103
|
// `message` has been rebuilt from `results`. Cleared so it fires once per
|
package/dist/browser.js
CHANGED
|
@@ -1895,8 +1895,11 @@ export class DeepSeekBrowser {
|
|
|
1895
1895
|
}
|
|
1896
1896
|
// Reasoning-mode pause during the THINK phase: click Continue so the
|
|
1897
1897
|
// generation resumes (the button appears before any RESPONSE text, so
|
|
1898
|
-
// the start-wait would otherwise spin until the deadline).
|
|
1899
|
-
|
|
1898
|
+
// the start-wait would otherwise spin until the deadline). Probing
|
|
1899
|
+
// PROACTIVELY is meaningful only with the reasoning toggle ON.
|
|
1900
|
+
if (this.autoContinue &&
|
|
1901
|
+
this.deepThinking &&
|
|
1902
|
+
(await this._clickContinueIfVisible())) {
|
|
1900
1903
|
this._askDebug('CLICKED Continue (start-loop)');
|
|
1901
1904
|
}
|
|
1902
1905
|
const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
@@ -1935,8 +1938,12 @@ export class DeepSeekBrowser {
|
|
|
1935
1938
|
const notGenerating = !(await this._isGenerating());
|
|
1936
1939
|
const differs = !!cur && normText(cur) !== normText(beforeText);
|
|
1937
1940
|
// Do NOT accept a "settled" answer while Continue is on screen — that
|
|
1938
|
-
// is a paused generation, not a finished one.
|
|
1939
|
-
|
|
1941
|
+
// is a paused generation, not a finished one. The paused check only
|
|
1942
|
+
// applies with the reasoning toggle ON (the THINK-phase pause); without
|
|
1943
|
+
// it a stable answer is a genuinely finished one.
|
|
1944
|
+
const paused = this.autoContinue &&
|
|
1945
|
+
this.deepThinking &&
|
|
1946
|
+
(await this._continueButtonVisible());
|
|
1940
1947
|
if (differs && cur === lastStartCur && notGenerating && !paused) {
|
|
1941
1948
|
settledTicks++;
|
|
1942
1949
|
if (settledTicks >= 2) {
|
|
@@ -2095,8 +2102,15 @@ export class DeepSeekBrowser {
|
|
|
2095
2102
|
}
|
|
2096
2103
|
// Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
|
|
2097
2104
|
// Continue button; the model does not resume by itself. Click it so the
|
|
2098
|
-
// answer keeps flowing instead of waiting for the operator.
|
|
2099
|
-
|
|
2105
|
+
// answer keeps flowing instead of waiting for the operator. Only probe
|
|
2106
|
+
// PROACTIVELY when the reasoning (Deep thinking) toggle is ON: the
|
|
2107
|
+
// THINK-phase pause does not occur without it, so an idle probe on every
|
|
2108
|
+
// tick is a wasted evaluate. The truncated / finished-without-answer
|
|
2109
|
+
// branches above stay unconditional — those are server faults, not a
|
|
2110
|
+
// reasoning pause, and occur with the toggle off too.
|
|
2111
|
+
if (this.autoContinue &&
|
|
2112
|
+
this.deepThinking &&
|
|
2113
|
+
(await this._clickContinueIfVisible())) {
|
|
2100
2114
|
this._askDebug('CLICKED Continue (finish-loop)');
|
|
2101
2115
|
continue;
|
|
2102
2116
|
}
|
|
@@ -2156,8 +2170,11 @@ export class DeepSeekBrowser {
|
|
|
2156
2170
|
// A PAUSED generation also looks "stable" (the text stops changing
|
|
2157
2171
|
// while Continue is on screen), so the old check returned the
|
|
2158
2172
|
// partial answer here — the "agent stopped with a Continue button"
|
|
2159
|
-
// symptom. Click Continue and keep waiting instead.
|
|
2160
|
-
|
|
2173
|
+
// symptom. Click Continue and keep waiting instead. Only relevant
|
|
2174
|
+
// with the reasoning toggle ON (the THINK-phase pause).
|
|
2175
|
+
if (this.autoContinue &&
|
|
2176
|
+
this.deepThinking &&
|
|
2177
|
+
(await this._continueButtonVisible())) {
|
|
2161
2178
|
if (await this._clickContinueIfVisible()) {
|
|
2162
2179
|
this._askDebug('CLICKED Continue (before early return)');
|
|
2163
2180
|
}
|
|
@@ -2205,7 +2222,8 @@ export class DeepSeekBrowser {
|
|
|
2205
2222
|
if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
|
|
2206
2223
|
// If Continue is STILL on screen, the turn is paused, not finished:
|
|
2207
2224
|
// click once more and report what we have rather than silently stopping.
|
|
2208
|
-
|
|
2225
|
+
// Only with the reasoning toggle ON (the THINK-phase pause).
|
|
2226
|
+
if (this.autoContinue && this.deepThinking)
|
|
2209
2227
|
await this._clickContinueIfVisible();
|
|
2210
2228
|
this._askDebug('RETURN last len=' + last.length);
|
|
2211
2229
|
return last;
|
package/dist/checkpoint.js
CHANGED
|
@@ -69,6 +69,19 @@ function runTar(args, cwd, timeout = 60_000) {
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
+
// Git fingerprint of a working tree: HEAD plus the porcelain status. Two
|
|
73
|
+
// snapshots with the same fingerprint hold the same TRACKED state, so a
|
|
74
|
+
// second checkpoint would be a duplicate. Returns null for a non-git dir (or
|
|
75
|
+
// when git is unavailable) — then the caller always snapshots (no debounce).
|
|
76
|
+
function workdirFingerprint(workdir) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
execFile('git', ['-C', workdir, 'status', '--porcelain=v1', '--branch'], { timeout: 10_000, maxBuffer: 1024 * 1024, windowsHide: true }, (err, stdout) => {
|
|
79
|
+
if (err)
|
|
80
|
+
return resolve(null);
|
|
81
|
+
resolve(String(stdout || '').trim() || 'clean');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
72
85
|
async function copyTree(from, to) {
|
|
73
86
|
await fs.mkdir(to, { recursive: true });
|
|
74
87
|
const entries = await fs.readdir(from, { withFileTypes: true });
|
|
@@ -116,10 +129,34 @@ export class CheckpointStore {
|
|
|
116
129
|
* unreadable dir or a timeout yields null, never a thrown error — a failed
|
|
117
130
|
* checkpoint must not abort the task it was meant to protect.
|
|
118
131
|
*/
|
|
119
|
-
async create(workdir, label
|
|
132
|
+
async create(workdir, label,
|
|
133
|
+
/** When false, always create a new archive (skip the fingerprint dedupe). */
|
|
134
|
+
allowDedupe = true) {
|
|
120
135
|
if (!this.enabled)
|
|
121
136
|
return null;
|
|
122
137
|
await this._ensure();
|
|
138
|
+
// Debounce: a checkpoint is taken before EVERY task, and consecutive
|
|
139
|
+
// tasks in one queue often start with an IDENTICAL tracked state (the
|
|
140
|
+
// previous task made no commit and no tracked edit that survived).
|
|
141
|
+
// Re-taring the whole tree then only burns time. If the last record for
|
|
142
|
+
// this workdir carries the SAME git fingerprint, reuse it instead of
|
|
143
|
+
// snapshotting again. Non-git dirs get a null fingerprint and are always
|
|
144
|
+
// snapshotted (we cannot cheaply prove the tree is unchanged).
|
|
145
|
+
const fingerprint = allowDedupe ? await workdirFingerprint(workdir) : null;
|
|
146
|
+
if (fingerprint) {
|
|
147
|
+
const history = await this._readIndex();
|
|
148
|
+
const last = [...history]
|
|
149
|
+
.reverse()
|
|
150
|
+
.find((r) => r.workdir === workdir && r.fingerprint);
|
|
151
|
+
if (last &&
|
|
152
|
+
last.fingerprint === fingerprint &&
|
|
153
|
+
(await fs
|
|
154
|
+
.access(last.file)
|
|
155
|
+
.then(() => true)
|
|
156
|
+
.catch(() => false))) {
|
|
157
|
+
return last;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
123
160
|
const stamp = Date.now();
|
|
124
161
|
// Two checkpoints in the SAME millisecond would share `${stamp}.tgz` and
|
|
125
162
|
// the second would overwrite the first (losing a snapshot). Add a random
|
|
@@ -134,6 +171,8 @@ export class CheckpointStore {
|
|
|
134
171
|
const record = { stamp, file, workdir };
|
|
135
172
|
if (label)
|
|
136
173
|
record.label = label;
|
|
174
|
+
if (fingerprint)
|
|
175
|
+
record.fingerprint = fingerprint;
|
|
137
176
|
const history = await this._readIndex();
|
|
138
177
|
history.push(record);
|
|
139
178
|
const kept = rotateRecords(history, this.maxBackups);
|
|
@@ -171,7 +210,7 @@ export class CheckpointStore {
|
|
|
171
210
|
if (!exists)
|
|
172
211
|
return { ok: false, reason: 'archive_missing' };
|
|
173
212
|
// Backup the CURRENT tree before touching it.
|
|
174
|
-
const backup = await this.create(record.workdir, 'pre-rewind');
|
|
213
|
+
const backup = await this.create(record.workdir, 'pre-rewind', false);
|
|
175
214
|
// Extract to a temp dir FIRST; only a successful extract may touch workdir
|
|
176
215
|
// (otherwise a bad archive would wipe the tree).
|
|
177
216
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'zames-rewind-'));
|
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' },
|
package/dist/i18n/common.js
CHANGED
|
@@ -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:',
|
package/dist/i18n/config.js
CHANGED
|
@@ -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.',
|
package/dist/i18n/self-review.js
CHANGED
|
@@ -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
|
|
117
|
-
en: '\
|
|
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',
|
package/dist/i18n/spinner.js
CHANGED
|
@@ -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 <
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
1244
|
+
theme.dim(':') +
|
|
1245
|
+
theme.dir(reviewMode.snapName) +
|
|
1246
|
+
theme.dim(') '));
|
|
1243
1247
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
//
|
|
1602
|
-
//
|
|
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.
|
|
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.
|
|
2027
|
+
tail = theme.dim('› ');
|
|
2027
2028
|
}
|
|
2028
|
-
input = await promptOnce(theme.prompt('❯ ') + tail
|
|
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
|
|
2331
|
-
//
|
|
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') {
|
package/dist/system-prompt.js
CHANGED
|
@@ -234,6 +234,33 @@ Prefer the most specific tool; a wrong choice wastes a turn:
|
|
|
234
234
|
- Reading a file whose path you already know -> Read, not Bash cat.
|
|
235
235
|
- A broad search/exploration that would flood this chat -> the Task tool
|
|
236
236
|
(a subagent in its own chat) if it is available, then work from its report.
|
|
237
|
+
|
|
238
|
+
## Batch independent calls (IMPORTANT: saves round-trips)
|
|
239
|
+
|
|
240
|
+
Every separate turn costs a full send + throttle pause, so DO NOT dribble
|
|
241
|
+
calls out one at a time when you already know several are needed. When the
|
|
242
|
+
next step requires reading, searching or writing SEVERAL known targets, emit
|
|
243
|
+
ONE JSON array with all of them in a single answer:
|
|
244
|
+
|
|
245
|
+
[{"tool": "Read", "args": {"path": "a.ts"}},
|
|
246
|
+
{"tool": "Read", "args": {"path": "b.ts"}},
|
|
247
|
+
{"tool": "Grep", "args": {"pattern": "foo", "include": "*.ts"}}]
|
|
248
|
+
|
|
249
|
+
Rules for a batch:
|
|
250
|
+
- BATCH only INDEPENDENT calls: no call may depend on another call's result
|
|
251
|
+
(do not batch a Read and an Edit of the same file, and never batch two
|
|
252
|
+
writes to the SAME path).
|
|
253
|
+
- Read-only calls (Read, Glob, Grep, LS, WebFetch) run CONCURRENTLY, so a
|
|
254
|
+
batch of them is several times faster than the same calls one by one.
|
|
255
|
+
- Writing to SEVERAL DIFFERENT files can be batched too (e.g. three Write
|
|
256
|
+
calls to three different new paths, or Edits to different files). Mutating
|
|
257
|
+
calls in a batch run in order, so a batch still saves the extra round-trips
|
|
258
|
+
even though the writes themselves are serialized.
|
|
259
|
+
- If the next step depends on what a call RETURNS, that call must be alone
|
|
260
|
+
(or be the last one in the batch).
|
|
261
|
+
- Typical wins: read all the files a task mentions at once; grep for all the
|
|
262
|
+
symbols you need at once; after planning a multi-file change, write all the
|
|
263
|
+
files you can write independently at once.
|
|
237
264
|
${subagentSection}
|
|
238
265
|
## MCP tools
|
|
239
266
|
|
|
@@ -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
|
-
|
|
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;
|
package/dist/tui/TuiEditor.js
CHANGED
|
@@ -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 }) +
|
|
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 }) +
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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.
|
|
3
|
+
"version": "2.75.2",
|
|
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))
|
package/scripts/tui-repro.tsx
CHANGED
|
@@ -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: '
|
|
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
|
})
|