zames_pro 2.50.2 → 2.52.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/README.md +22 -1
- package/dist/commands.js +73 -45
- package/dist/config-menu.js +143 -21
- package/dist/i18n.js +152 -0
- package/dist/index.js +104 -27
- package/dist/input.js +208 -6
- package/dist/sessions.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,23 @@ While the agent is working you can keep typing: press Enter to queue a message
|
|
|
76
76
|
abort the current generation. This mirrors typing during generation on the
|
|
77
77
|
DeepSeek website.
|
|
78
78
|
|
|
79
|
+
### Input line
|
|
80
|
+
|
|
81
|
+
The prompt is a small line editor with persistent history:
|
|
82
|
+
|
|
83
|
+
- `↑` / `↓` — walk the message history (saved in `~/.zames/history.json`, so it
|
|
84
|
+
survives a restart); inside a multiline message the arrows move between lines.
|
|
85
|
+
- `Ctrl+R` — incremental reverse search over the history (bash-style): type to
|
|
86
|
+
filter, `Ctrl+R` for older matches, `Enter` to accept, `Esc` to cancel.
|
|
87
|
+
- `Ctrl+_` — undo the last edit in the input line (a fat-fingered `Ctrl+U` /
|
|
88
|
+
`Ctrl+K` is recoverable).
|
|
89
|
+
- `Ctrl+U` — clear the line, `Ctrl+K` — delete to end of line, `Ctrl+W` —
|
|
90
|
+
delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words.
|
|
91
|
+
- `\` + `Enter`, `Ctrl+J`, `Ctrl+Enter` or `Shift+Enter` — insert a newline.
|
|
92
|
+
- `/` + `Tab` — slash-command hints and completion.
|
|
93
|
+
|
|
94
|
+
Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
|
|
95
|
+
|
|
79
96
|
### Images and files
|
|
80
97
|
|
|
81
98
|
You can paste an image or a file into the input line (Ctrl+Shift+V / Shift+Insert
|
|
@@ -149,7 +166,11 @@ Codex CLI:
|
|
|
149
166
|
startup; relaunch with --dir to write there).
|
|
150
167
|
- /resume <n> (after /chats) and /resume-id <id> — open a chat and PRINT its
|
|
151
168
|
dialogue into the terminal, so the restored context is visible. Only the
|
|
152
|
-
last 20 messages are shown (`RESTORED_HISTORY_LIMIT`).
|
|
169
|
+
last 20 messages are shown (`RESTORED_HISTORY_LIMIT`). /last reopens the last
|
|
170
|
+
chat of the current directory with no lookup step; /sessions shows the saved
|
|
171
|
+
sessions with their relative age.
|
|
172
|
+
- /help <command> — the full description of a single command (e.g. `/help diff`)
|
|
173
|
+
instead of the whole list.
|
|
153
174
|
- /review [focus] [--staged] — ask the agent to review uncommitted changes
|
|
154
175
|
and report findings (no code changes).
|
|
155
176
|
- /compact — ask DeepSeek to compress the current chat into a handover
|
package/dist/commands.js
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { isImageName } from './attachments.js';
|
|
3
|
+
import { translate } from './i18n.js';
|
|
3
4
|
// Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
|
|
4
5
|
// /permissions, /review, /add-dir). Pure functions, unit-tested without a
|
|
5
6
|
// live agent/browser. index.ts only renders their output.
|
|
6
7
|
const NL = String.fromCharCode(10);
|
|
7
8
|
const CR = String.fromCharCode(13);
|
|
8
9
|
// ---------- /diff ----------
|
|
9
|
-
export function formatDiff(diffText, opts = {}) {
|
|
10
|
+
export function formatDiff(diffText, opts = {}, t = translate('en')) {
|
|
10
11
|
const maxLines = opts.maxLines ?? 200;
|
|
11
12
|
const trimmed = String(diffText ?? '')
|
|
12
13
|
.split(CR + NL)
|
|
13
14
|
.join(NL)
|
|
14
15
|
.trimEnd();
|
|
15
16
|
if (!trimmed)
|
|
16
|
-
return '
|
|
17
|
+
return t('diff.no_changes');
|
|
17
18
|
const lines = trimmed.split(NL);
|
|
18
19
|
if (lines.length <= maxLines)
|
|
19
20
|
return trimmed;
|
|
20
21
|
const head = lines.slice(0, maxLines).join(NL);
|
|
21
22
|
const rest = lines.length - maxLines;
|
|
22
|
-
return
|
|
23
|
-
NL +
|
|
24
|
-
'... [' +
|
|
25
|
-
rest +
|
|
26
|
-
' more lines, use Bash git diff for the full output]');
|
|
23
|
+
return head + NL + t('diff.more_lines', { n: String(rest) });
|
|
27
24
|
}
|
|
28
25
|
export function diffGitArgs(staged = false) {
|
|
29
26
|
return staged ? 'git diff --staged' : 'git diff';
|
|
@@ -89,30 +86,51 @@ export function formatDuration(ms, units = EN_DURATION_UNITS) {
|
|
|
89
86
|
return m + units.m + ' ' + pad(s) + units.s;
|
|
90
87
|
return s + units.s;
|
|
91
88
|
}
|
|
92
|
-
|
|
89
|
+
// Short relative age ("just now", "5m ago", "3h ago", "2d ago") for the
|
|
90
|
+
// /sessions and /chats lists. The operator cares about RECENCY, not a raw
|
|
91
|
+
// ISO timestamp, when picking a session to restore.
|
|
92
|
+
export function formatRelativeTime(iso, t = translate('en'), now = Date.now()) {
|
|
93
|
+
if (!iso)
|
|
94
|
+
return '';
|
|
95
|
+
const then = Date.parse(iso);
|
|
96
|
+
if (!Number.isFinite(then))
|
|
97
|
+
return '';
|
|
98
|
+
const diff = Math.max(0, now - then);
|
|
99
|
+
const min = Math.floor(diff / 60_000);
|
|
100
|
+
if (min < 1)
|
|
101
|
+
return t('time.now');
|
|
102
|
+
if (min < 60)
|
|
103
|
+
return t('time.min_ago', { n: String(min) });
|
|
104
|
+
const hours = Math.floor(min / 60);
|
|
105
|
+
if (hours < 24)
|
|
106
|
+
return t('time.hour_ago', { n: String(hours) });
|
|
107
|
+
const days = Math.floor(hours / 24);
|
|
108
|
+
return t('time.day_ago', { n: String(days) });
|
|
109
|
+
}
|
|
110
|
+
export function renderCost(stats, transcriptFile, tokenUsage = null, t = translate('en')) {
|
|
93
111
|
const lines = [];
|
|
94
|
-
lines.push('
|
|
112
|
+
lines.push(t('cost.title'));
|
|
95
113
|
if (typeof tokenUsage === 'number') {
|
|
96
|
-
lines.push('
|
|
114
|
+
lines.push(t('cost.context_known', { n: String(tokenUsage) }));
|
|
97
115
|
}
|
|
98
116
|
else {
|
|
99
|
-
lines.push(
|
|
117
|
+
lines.push(t('cost.context_unknown'));
|
|
100
118
|
}
|
|
101
|
-
lines.push('
|
|
102
|
-
lines.push('
|
|
119
|
+
lines.push(t('cost.tasks', { n: String(stats.turns) }));
|
|
120
|
+
lines.push(t('cost.tool_calls', { n: String(stats.toolCalls) }));
|
|
103
121
|
if (stats.autoCompacts > 0) {
|
|
104
|
-
lines.push('
|
|
122
|
+
lines.push(t('cost.auto_compacts', { n: String(stats.autoCompacts) }));
|
|
105
123
|
}
|
|
106
124
|
const top = Object.entries(stats.toolCounts).sort((a, b) => b[1] - a[1]);
|
|
107
125
|
if (top.length) {
|
|
108
|
-
lines.push('
|
|
126
|
+
lines.push(t('cost.by_tool'));
|
|
109
127
|
for (const [name, n] of top)
|
|
110
128
|
lines.push(' ' + name + ': ' + n);
|
|
111
129
|
}
|
|
112
|
-
lines.push('
|
|
130
|
+
lines.push(t('cost.duration', { dur: formatDuration(stats.durationMs) }));
|
|
113
131
|
if (stats.startedAt)
|
|
114
|
-
lines.push('
|
|
115
|
-
lines.push('
|
|
132
|
+
lines.push(t('cost.started', { v: stats.startedAt }));
|
|
133
|
+
lines.push(t('cost.transcript', { v: transcriptFile || t('cost.off') }));
|
|
116
134
|
return lines.join(NL);
|
|
117
135
|
}
|
|
118
136
|
// ---------- /export ----------
|
|
@@ -172,64 +190,74 @@ export function defaultExportPath(workdir, now = new Date()) {
|
|
|
172
190
|
.replace('Z', '');
|
|
173
191
|
return path.join(workdir, 'zames-export-' + stamp + '.md');
|
|
174
192
|
}
|
|
175
|
-
export function renderDoctor(d) {
|
|
193
|
+
export function renderDoctor(d, t = translate('en')) {
|
|
176
194
|
const rows = [];
|
|
177
195
|
const row = (ok, label, value) => {
|
|
178
196
|
rows.push(' ' + (ok ? '[OK] ' : '[WARN]') + ' ' + label.padEnd(16) + ' ' + value);
|
|
179
197
|
};
|
|
198
|
+
const onoff = (b) => (b ? t('common.on') : t('common.off'));
|
|
180
199
|
row(true, 'node', d.nodeVersion);
|
|
181
200
|
row(true, 'platform', d.platform);
|
|
182
201
|
row(true, 'workdir', d.workdir);
|
|
183
|
-
row(d.gitOk, 'git', d.gitOk
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
row(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
row(
|
|
202
|
+
row(d.gitOk, 'git', d.gitOk
|
|
203
|
+
? t('doctor.repo', { v: d.gitBranch || t('doctor.detached') })
|
|
204
|
+
: t('doctor.not_repo'));
|
|
205
|
+
row(d.configOk, 'config', d.configOk
|
|
206
|
+
? t('doctor.loaded')
|
|
207
|
+
: t('doctor.error', { v: d.configError || t('doctor.unknown') }));
|
|
208
|
+
row(true, 'browser', d.browserChannel ? d.browserChannel : t('doctor.bundled'));
|
|
209
|
+
row(!!d.authSaved, 'auth', d.authSaved ? t('doctor.auth_saved') : t('doctor.auth_none'));
|
|
210
|
+
row(!!d.clipboardTool, 'clipboard', d.clipboardTool || t('doctor.clipboard_none'));
|
|
211
|
+
row(true, 'mcp', t('doctor.mcp', {
|
|
212
|
+
servers: String(d.mcpServers),
|
|
213
|
+
tools: String(d.mcpTools),
|
|
214
|
+
}));
|
|
215
|
+
row(d.transcriptOk, 'transcript', onoff(d.transcriptOk));
|
|
190
216
|
if (typeof d.contextLimit === 'number' && d.contextLimit > 0) {
|
|
191
|
-
row(true, 'context',
|
|
217
|
+
row(true, 'context', t('doctor.tokens', {
|
|
218
|
+
v: d.contextLimit.toLocaleString('en-US'),
|
|
219
|
+
}));
|
|
192
220
|
}
|
|
193
221
|
if (d.gitOk && typeof d.hasOrigin === 'boolean') {
|
|
194
|
-
row(true, 'git remote', d.hasOrigin ? '
|
|
222
|
+
row(true, 'git remote', d.hasOrigin ? t('doctor.origin_ok') : t('doctor.origin_none'));
|
|
195
223
|
}
|
|
196
224
|
if (typeof d.minSendIntervalMs === 'number') {
|
|
197
|
-
row(d.minSendIntervalMs > 0, 'send pause',
|
|
225
|
+
row(d.minSendIntervalMs > 0, 'send pause', t('doctor.send_pause', {
|
|
226
|
+
n: String(Math.round(d.minSendIntervalMs / 1000)),
|
|
227
|
+
}));
|
|
198
228
|
}
|
|
199
229
|
if (typeof d.sshRemote === 'boolean') {
|
|
200
|
-
row(true, 'session', d.sshRemote ? '
|
|
230
|
+
row(true, 'session', d.sshRemote ? t('doctor.ssh') : t('doctor.local'));
|
|
201
231
|
}
|
|
202
232
|
if (d.headless) {
|
|
203
|
-
row(!!d.headlessUaCached, 'headless', d.headlessUaCached
|
|
204
|
-
? 'UA cached (no relaunch)'
|
|
205
|
-
: 'no cached UA yet (first start relaunches once)');
|
|
233
|
+
row(!!d.headlessUaCached, 'headless', d.headlessUaCached ? t('doctor.ua_cached') : t('doctor.ua_not_cached'));
|
|
206
234
|
}
|
|
207
|
-
return '
|
|
235
|
+
return t('doctor.title') + NL + rows.join(NL);
|
|
208
236
|
}
|
|
209
|
-
export function renderPermissions(p) {
|
|
210
|
-
const onoff = (b) => (b ? 'ask' : 'allow');
|
|
237
|
+
export function renderPermissions(p, t = translate('en')) {
|
|
238
|
+
const onoff = (b) => (b ? t('perm.ask') : t('perm.allow'));
|
|
211
239
|
const lines = [];
|
|
212
|
-
lines.push(
|
|
213
|
-
lines.push('
|
|
214
|
-
lines.push('
|
|
215
|
-
lines.push('
|
|
240
|
+
lines.push(t('perm.title'));
|
|
241
|
+
lines.push(t('perm.write', { v: onoff(p.write) }));
|
|
242
|
+
lines.push(t('perm.edit', { v: onoff(p.edit) }));
|
|
243
|
+
lines.push(t('perm.bash', { v: onoff(p.bash) }));
|
|
216
244
|
if (p.alwaysConfirm.length) {
|
|
217
|
-
lines.push(
|
|
245
|
+
lines.push(t('perm.always'));
|
|
218
246
|
for (const re of p.alwaysConfirm)
|
|
219
247
|
lines.push(' ' + re);
|
|
220
248
|
}
|
|
221
249
|
lines.push('');
|
|
222
|
-
lines.push('
|
|
250
|
+
lines.push(t('perm.change_hint'));
|
|
223
251
|
return lines.join(NL);
|
|
224
252
|
}
|
|
225
253
|
// ---------- /add-dir ----------
|
|
226
|
-
export function resolveExtraDir(input, workdir) {
|
|
254
|
+
export function resolveExtraDir(input, workdir, t = translate('en')) {
|
|
227
255
|
const raw = String(input ?? '').trim();
|
|
228
256
|
if (!raw)
|
|
229
|
-
return { error: '
|
|
257
|
+
return { error: t('adddir.usage') };
|
|
230
258
|
const abs = path.resolve(workdir, raw);
|
|
231
259
|
if (abs === path.resolve(workdir)) {
|
|
232
|
-
return { error: '
|
|
260
|
+
return { error: t('adddir.same') };
|
|
233
261
|
}
|
|
234
262
|
return { path: abs };
|
|
235
263
|
}
|
package/dist/config-menu.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
import { theme } from './theme.js';
|
|
12
12
|
import { visLen } from './input.js';
|
|
13
13
|
const ESC = String.fromCharCode(27);
|
|
14
|
-
// Strip
|
|
14
|
+
// Strip ANSH sequences to compute the visible length of a line.
|
|
15
15
|
function stripAnsi(s) {
|
|
16
|
-
return s.replace(/\x1b
|
|
16
|
+
return s.replace(/\x1b\`[0-9;?]*[A-Za-z]/g, '');
|
|
17
17
|
}
|
|
18
18
|
// Visible COLUMNS of a line (ANSI removed, wide chars counted as 2). Using the
|
|
19
19
|
// code-point count made the block height disagree with the real screen on lines
|
|
@@ -35,7 +35,7 @@ function displayValue(field, value, t) {
|
|
|
35
35
|
return String(value);
|
|
36
36
|
}
|
|
37
37
|
function groupLabel(field, t) {
|
|
38
|
-
return t(field.groupKey);
|
|
38
|
+
return theme.system(' ' + t(field.groupKey));
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Show the interactive menu. Returns a promise that resolves when the
|
|
@@ -45,13 +45,20 @@ function groupLabel(field, t) {
|
|
|
45
45
|
export function runConfigMenu(opts) {
|
|
46
46
|
const input = opts.input || process.stdin;
|
|
47
47
|
const output = opts.output || process.stdout;
|
|
48
|
-
const { fields, t, get, set, reset } = opts;
|
|
48
|
+
const { fields, t, get, set, reset, getDefault } = opts;
|
|
49
49
|
if (!input.isTTY || !input.setRawMode) {
|
|
50
50
|
return Promise.reject(new Error(t('cfg.menu.notty')));
|
|
51
51
|
}
|
|
52
|
+
// cursor indexes the VISIBLE (filtered) list, not the full `fields`.
|
|
52
53
|
let cursor = 0;
|
|
53
54
|
let editing = false;
|
|
54
55
|
let editBuf = '';
|
|
56
|
+
// Reverse/forward filter over the list (/ to enter, typing narrows it).
|
|
57
|
+
let filterMode = false;
|
|
58
|
+
let filterQuery = '';
|
|
59
|
+
// Second `d` on the same row confirms the reset, so a stray keypress
|
|
60
|
+
// cannot wipe a setting by accident.
|
|
61
|
+
let pendingReset = null;
|
|
55
62
|
let message = null;
|
|
56
63
|
let exited = false;
|
|
57
64
|
let resolveDone = null;
|
|
@@ -59,18 +66,53 @@ export function runConfigMenu(opts) {
|
|
|
59
66
|
input.setRawMode(true);
|
|
60
67
|
input.resume();
|
|
61
68
|
output.write(ESC + '[?25l'); // hide the cursor while rendering
|
|
69
|
+
// Fields that match the current filter (path, label or group contains the
|
|
70
|
+
// query, case-insensitive). Empty query -> everything.
|
|
71
|
+
const visible = () => {
|
|
72
|
+
const q = filterQuery.trim().toLowerCase();
|
|
73
|
+
if (!q)
|
|
74
|
+
return fields;
|
|
75
|
+
return fields.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.includes(q));
|
|
78
|
+
};
|
|
79
|
+
const currentField = () => {
|
|
80
|
+
const vis = visible();
|
|
81
|
+
if (!vis.length)
|
|
82
|
+
return null;
|
|
83
|
+
if (cursor >= vis.length)
|
|
84
|
+
cursor = vis.length - 1;
|
|
85
|
+
return vis[cursor];
|
|
86
|
+
};
|
|
87
|
+
// True when the value differs from the default (the field is overridden
|
|
88
|
+
// in a config file), so the menu can mark it. Skipped with no getDefault.
|
|
89
|
+
const isModified = (f) => {
|
|
90
|
+
if (!getDefault)
|
|
91
|
+
return false;
|
|
92
|
+
const cur = get(f.path);
|
|
93
|
+
const def = getDefault(f.path);
|
|
94
|
+
try {
|
|
95
|
+
return JSON.stringify(cur) !== JSON.stringify(def);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return String(cur) !== String(def);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
62
101
|
// Menu rendering: lines go top to bottom, and we return the cursor to the
|
|
63
102
|
// start of the block via relative moves so we don't spawn blank lines.
|
|
64
103
|
let lastRows = 0;
|
|
65
104
|
const render = () => {
|
|
66
105
|
const lines = [];
|
|
67
106
|
lines.push(theme.bold(t('cfg.menu.title')));
|
|
107
|
+
const vis = visible();
|
|
108
|
+
if (cursor >= vis.length)
|
|
109
|
+
cursor = Math.max(0, vis.length - 1);
|
|
68
110
|
let lastGroup = '';
|
|
69
|
-
for (let i = 0; i <
|
|
70
|
-
const f =
|
|
111
|
+
for (let i = 0; i < vis.length; i++) {
|
|
112
|
+
const f = vis[i];
|
|
71
113
|
const g = groupLabel(f, t);
|
|
72
114
|
if (g !== lastGroup) {
|
|
73
|
-
lines.push(
|
|
115
|
+
lines.push(g);
|
|
74
116
|
lastGroup = g;
|
|
75
117
|
}
|
|
76
118
|
const selected = i === cursor;
|
|
@@ -80,21 +122,38 @@ export function runConfigMenu(opts) {
|
|
|
80
122
|
: t(f.labelKey || f.path);
|
|
81
123
|
const val = displayValue(f, get(f.path), t);
|
|
82
124
|
const valText = selected ? theme.assistant(val) : theme.dim(val);
|
|
125
|
+
const mod = isModified(f) ? theme.warn('*') : '';
|
|
83
126
|
const pathText = selected ? theme.dim(' ' + f.path) : '';
|
|
84
|
-
lines.push(marker + label + theme.dim(' = ') + valText + pathText);
|
|
127
|
+
lines.push(marker + label + theme.dim(' = ') + valText + mod + pathText);
|
|
128
|
+
}
|
|
129
|
+
if (!vis.length) {
|
|
130
|
+
lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
|
|
85
131
|
}
|
|
86
132
|
if (editing) {
|
|
87
133
|
lines.push('');
|
|
88
134
|
lines.push(theme.user(t('cfg.menu.edit_hint')));
|
|
89
135
|
lines.push(theme.prompt('\u276f ') + editBuf);
|
|
90
136
|
}
|
|
137
|
+
else if (filterMode) {
|
|
138
|
+
lines.push('');
|
|
139
|
+
lines.push(theme.user(t('cfg.menu.filter_hint')));
|
|
140
|
+
lines.push(theme.prompt('\u276f ') + filterQuery);
|
|
141
|
+
}
|
|
91
142
|
else if (message) {
|
|
92
143
|
lines.push('');
|
|
93
144
|
lines.push(theme.assistant(message));
|
|
94
145
|
}
|
|
95
146
|
else {
|
|
96
147
|
lines.push('');
|
|
97
|
-
|
|
148
|
+
if (filterQuery) {
|
|
149
|
+
lines.push(theme.dim(t('cfg.menu.filter_active', { q: filterQuery }) +
|
|
150
|
+
' ' +
|
|
151
|
+
t('cfg.menu.hint')));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
lines.push(' ' + theme.dim(t('cfg.menu.hint')));
|
|
155
|
+
lines.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
|
|
156
|
+
}
|
|
98
157
|
}
|
|
99
158
|
// Erase the previous block and print the new one. We count VISUAL lines
|
|
100
159
|
// (accounting for wrapping by terminal width), otherwise on narrow
|
|
@@ -134,12 +193,12 @@ export function runConfigMenu(opts) {
|
|
|
134
193
|
lastRows = 0;
|
|
135
194
|
}
|
|
136
195
|
};
|
|
137
|
-
const cleanup = (
|
|
196
|
+
const cleanup = (cleanOnExit = false) => {
|
|
138
197
|
if (exited)
|
|
139
198
|
return;
|
|
140
199
|
exited = true;
|
|
141
200
|
input.removeListener('data', onData);
|
|
142
|
-
if (
|
|
201
|
+
if (cleanOnExit)
|
|
143
202
|
clearBlock();
|
|
144
203
|
if (input.setRawMode)
|
|
145
204
|
input.setRawMode(wasRaw || false);
|
|
@@ -148,10 +207,12 @@ export function runConfigMenu(opts) {
|
|
|
148
207
|
resolveDone();
|
|
149
208
|
};
|
|
150
209
|
const commitEdit = () => {
|
|
151
|
-
const f =
|
|
210
|
+
const f = currentField();
|
|
152
211
|
const raw = editBuf.trim();
|
|
153
212
|
editing = false;
|
|
154
213
|
editBuf = '';
|
|
214
|
+
if (!f)
|
|
215
|
+
return;
|
|
155
216
|
if (!raw) {
|
|
156
217
|
message = null;
|
|
157
218
|
return;
|
|
@@ -168,7 +229,9 @@ export function runConfigMenu(opts) {
|
|
|
168
229
|
}
|
|
169
230
|
};
|
|
170
231
|
const toggle = () => {
|
|
171
|
-
const f =
|
|
232
|
+
const f = currentField();
|
|
233
|
+
if (!f)
|
|
234
|
+
return;
|
|
172
235
|
if (f.type === 'boolean') {
|
|
173
236
|
const cur = get(f.path);
|
|
174
237
|
set(f, cur === true ? 'false' : 'true');
|
|
@@ -218,20 +281,49 @@ export function runConfigMenu(opts) {
|
|
|
218
281
|
render();
|
|
219
282
|
return;
|
|
220
283
|
}
|
|
284
|
+
// Filter mode (/): printable keys build the query, Esc/Enter leave it
|
|
285
|
+
// (keeping the query active), Ctrl+C exits the whole menu.
|
|
286
|
+
if (filterMode) {
|
|
287
|
+
for (const ch of s) {
|
|
288
|
+
if (ch === '\r' || ch === '\n' || ch === ESC) {
|
|
289
|
+
filterMode = false;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (ch === '\x03') {
|
|
293
|
+
cleanup(true);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (ch === '\x7f' || ch === '\b') {
|
|
297
|
+
filterQuery = filterQuery.slice(0, -1);
|
|
298
|
+
}
|
|
299
|
+
else if (ch >= ' ') {
|
|
300
|
+
filterQuery += ch;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
cursor = 0;
|
|
304
|
+
render();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
221
307
|
// The terminal may send several keys in one packet (fast typing,
|
|
222
308
|
// auto-repeat, automation). We parse the buffer into tokens: first
|
|
223
309
|
// escape sequences (arrows), then single characters.
|
|
224
310
|
let i = 0;
|
|
225
311
|
while (i < s.length) {
|
|
226
312
|
if (s.startsWith('\x1b[A', i)) {
|
|
227
|
-
|
|
313
|
+
const n = visible().length;
|
|
314
|
+
if (n > 0)
|
|
315
|
+
cursor = (cursor - 1 + n) % n;
|
|
228
316
|
message = null;
|
|
317
|
+
pendingReset = null;
|
|
229
318
|
i += 3;
|
|
230
319
|
continue;
|
|
231
320
|
}
|
|
232
321
|
if (s.startsWith('\x1b[B', i)) {
|
|
233
|
-
|
|
322
|
+
const n = visible().length;
|
|
323
|
+
if (n > 0)
|
|
324
|
+
cursor = (cursor + 1) % n;
|
|
234
325
|
message = null;
|
|
326
|
+
pendingReset = null;
|
|
235
327
|
i += 3;
|
|
236
328
|
continue;
|
|
237
329
|
}
|
|
@@ -242,21 +334,51 @@ export function runConfigMenu(opts) {
|
|
|
242
334
|
cleanup(true);
|
|
243
335
|
return;
|
|
244
336
|
}
|
|
245
|
-
if (ch === '
|
|
246
|
-
|
|
337
|
+
if (ch === '/') {
|
|
338
|
+
filterMode = true;
|
|
339
|
+
message = null;
|
|
340
|
+
// The rest of THIS packet belongs to the filter query (typing " /debug"
|
|
341
|
+
// arrives as one chunk, and without this the trailing characters fell
|
|
342
|
+
// through to the command keys — 'd' armed a reset instead of filtering).
|
|
343
|
+
for (const rest of s.slice(i + 1)) {
|
|
344
|
+
if (rest === '\x7f' || rest === '\b') {
|
|
345
|
+
filterQuery = filterQuery.slice(0, -1);
|
|
346
|
+
}
|
|
347
|
+
else if (rest >= ' ') {
|
|
348
|
+
filterQuery += rest;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
cursor = 0;
|
|
352
|
+
i = s.length;
|
|
353
|
+
}
|
|
354
|
+
else if (ch === 'k') {
|
|
355
|
+
const n = visible().length;
|
|
356
|
+
if (n > 0)
|
|
357
|
+
cursor = (cursor - 1 + n) % n;
|
|
247
358
|
message = null;
|
|
248
359
|
}
|
|
249
360
|
else if (ch === 'j') {
|
|
250
|
-
|
|
361
|
+
const n = visible().length;
|
|
362
|
+
if (n > 0)
|
|
363
|
+
cursor = (cursor + 1) % n;
|
|
251
364
|
message = null;
|
|
252
365
|
}
|
|
253
366
|
else if (ch === '\r' || ch === '\n' || ch === ' ') {
|
|
254
367
|
toggle();
|
|
255
368
|
}
|
|
256
369
|
else if (ch === 'd' || ch === 'D') {
|
|
257
|
-
const f =
|
|
258
|
-
|
|
259
|
-
|
|
370
|
+
const f = currentField();
|
|
371
|
+
if (f) {
|
|
372
|
+
if (pendingReset === f.path) {
|
|
373
|
+
reset(f);
|
|
374
|
+
message = t('cfg.reset', { v: f.path });
|
|
375
|
+
pendingReset = null;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
pendingReset = f.path;
|
|
379
|
+
message = t('cfg.menu.reset_confirm', { v: f.path });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
260
382
|
}
|
|
261
383
|
i++;
|
|
262
384
|
}
|