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 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 '(no changes)';
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 (head +
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
- export function renderCost(stats, transcriptFile, tokenUsage = null) {
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('Session stats:');
112
+ lines.push(t('cost.title'));
95
113
  if (typeof tokenUsage === 'number') {
96
- lines.push(' context: ~' + tokenUsage + ' tokens (DeepSeek accumulated_token_usage)');
114
+ lines.push(t('cost.context_known', { n: String(tokenUsage) }));
97
115
  }
98
116
  else {
99
- lines.push(' context: unknown (DeepSeek reports it after the first answer in a chat)');
117
+ lines.push(t('cost.context_unknown'));
100
118
  }
101
- lines.push(' tasks: ' + stats.turns);
102
- lines.push(' tool calls: ' + stats.toolCalls);
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(' auto-compacts: ' + stats.autoCompacts);
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(' by tool:');
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(' duration: ' + formatDuration(stats.durationMs));
130
+ lines.push(t('cost.duration', { dur: formatDuration(stats.durationMs) }));
113
131
  if (stats.startedAt)
114
- lines.push(' started: ' + stats.startedAt);
115
- lines.push(' transcript: ' + (transcriptFile || '(off)'));
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 ? 'repo (' + (d.gitBranch || 'detached') + ')' : 'not a repository');
184
- row(d.configOk, 'config', d.configOk ? 'loaded' : 'error: ' + (d.configError || 'unknown'));
185
- row(true, 'browser', d.browserChannel ? d.browserChannel : 'bundled chromium');
186
- row(!!d.authSaved, 'auth', d.authSaved ? 'session saved (auto re-login ready)' : 'no saved session');
187
- row(!!d.clipboardTool, 'clipboard', d.clipboardTool || 'no tool found (image paste disabled)');
188
- row(true, 'mcp', d.mcpServers + ' server(s), ' + d.mcpTools + ' tool(s)');
189
- row(d.transcriptOk, 'transcript', d.transcriptOk ? 'on' : 'off');
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', d.contextLimit.toLocaleString('en-US') + ' tokens');
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 ? 'origin configured' : 'no origin');
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', Math.round(d.minSendIntervalMs / 1000) + 's between agent sends');
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 ? 'SSH remote' : 'local terminal');
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 'Doctor:' + NL + rows.join(NL);
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('Tool permissions (confirmation settings):');
213
- lines.push(' Write: ' + onoff(p.write));
214
- lines.push(' Edit: ' + onoff(p.edit));
215
- lines.push(' Bash: ' + onoff(p.bash));
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(' Always confirm (regex):');
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('Change with: /config set confirmation.write false (and .edit / .bash)');
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: 'Usage: /add-dir <path>' };
257
+ return { error: t('adddir.usage') };
230
258
  const abs = path.resolve(workdir, raw);
231
259
  if (abs === path.resolve(workdir)) {
232
- return { error: 'This is already the working directory.' };
260
+ return { error: t('adddir.same') };
233
261
  }
234
262
  return { path: abs };
235
263
  }
@@ -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 ANSI sequences to compute the visible length of a line.
14
+ // Strip ANSH sequences to compute the visible length of a line.
15
15
  function stripAnsi(s) {
16
- return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
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 < fields.length; i++) {
70
- const f = fields[i];
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(theme.system(' ' + g));
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
- lines.push(theme.dim(t('cfg.menu.hint')));
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 = (clearOnExit = false) => {
196
+ const cleanup = (cleanOnExit = false) => {
138
197
  if (exited)
139
198
  return;
140
199
  exited = true;
141
200
  input.removeListener('data', onData);
142
- if (clearOnExit)
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 = fields[cursor];
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 = fields[cursor];
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
- cursor = (cursor - 1 + fields.length) % fields.length;
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
- cursor = (cursor + 1) % fields.length;
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 === 'k') {
246
- cursor = (cursor - 1 + fields.length) % fields.length;
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
- cursor = (cursor + 1) % fields.length;
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 = fields[cursor];
258
- reset(f);
259
- message = t('cfg.reset', { v: f.path });
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
  }