zames_pro 2.88.2 → 2.89.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/CHANGELOG.md +7 -0
- package/dist/config-commands.js +12 -4
- package/dist/config-menu.js +179 -81
- package/dist/config-schema.js +554 -0
- package/dist/config.js +5 -405
- package/dist/i18n/config.js +62 -3
- package/dist/i18n/deepseek.js +1 -0
- package/dist/index.js +10 -5
- package/dist/tui/ConfigMenu.js +149 -64
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.89.0] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **config:** two-level /config menu with groups and sections
|
|
15
|
+
- **config:** diagnostics settings (post-edit check command, timeout, output cap) are now editable from `/config`
|
|
16
|
+
|
|
10
17
|
## [2.88.2] - 2026-10-27
|
|
11
18
|
|
|
12
19
|
### Changed
|
package/dist/config-commands.js
CHANGED
|
@@ -53,17 +53,25 @@ export function configValueHint(field, t) {
|
|
|
53
53
|
return field.type;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Render the full settings LIST as an array of lines
|
|
57
|
-
* `get` reads the current value of
|
|
58
|
-
* lines so index.ts only has to
|
|
56
|
+
* Render the full settings LIST as an array of lines, TWO levels deep: a
|
|
57
|
+
* top-level group header, then its sections. `get` reads the current value of
|
|
58
|
+
* a path (undefined = default). Returned as lines so index.ts only has to
|
|
59
|
+
* print them and the formatting is testable.
|
|
59
60
|
*/
|
|
60
61
|
export function formatConfigList(fields, get, t) {
|
|
61
62
|
const out = [theme.system(t('cfg.title'))];
|
|
63
|
+
let lastTop = '';
|
|
62
64
|
let lastGroup = '';
|
|
63
65
|
for (const f of fields) {
|
|
66
|
+
const top = t(f.topKey);
|
|
67
|
+
if (top !== lastTop) {
|
|
68
|
+
out.push(theme.system(theme.bold('\n' + top.toUpperCase())));
|
|
69
|
+
lastTop = top;
|
|
70
|
+
lastGroup = '';
|
|
71
|
+
}
|
|
64
72
|
const g = t(f.groupKey);
|
|
65
73
|
if (g !== lastGroup) {
|
|
66
|
-
out.push(theme.system('
|
|
74
|
+
out.push(theme.system(' ' + g));
|
|
67
75
|
lastGroup = g;
|
|
68
76
|
}
|
|
69
77
|
const shown = configListValue(f, get(f.path), t);
|
package/dist/config-menu.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
// Interactive settings menu for the /config command
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// prompt for input. All strings come from i18n (cfg.*), so the menu is
|
|
6
|
-
// fully localized.
|
|
1
|
+
// Interactive settings menu for the /config command — the NON-Ink fallback
|
|
2
|
+
// (used when ui.tui is off and in tests). The Ink menu in src/tui/ConfigMenu.tsx
|
|
3
|
+
// is the primary one; this one mirrors its two-level navigation: `/config`
|
|
4
|
+
// shows the GROUPS, Enter opens one, Esc goes back to the group list.
|
|
7
5
|
//
|
|
8
6
|
// The module doesn't know about config.ts directly (except for the schema
|
|
9
7
|
// types): the caller reads and writes values through callbacks. This makes the
|
|
10
8
|
// menu easy to test and reuse.
|
|
11
9
|
import { theme } from './theme.js';
|
|
10
|
+
import { CONFIG_TOP_GROUPS } from './config.js';
|
|
12
11
|
import { visLen } from './input.js';
|
|
13
12
|
const ESC = String.fromCharCode(27);
|
|
14
13
|
// Strip ANSI sequences to compute the visible length of a line. A local copy
|
|
@@ -41,8 +40,8 @@ function groupLabel(field, t) {
|
|
|
41
40
|
}
|
|
42
41
|
/**
|
|
43
42
|
* Show the interactive menu. Returns a promise that resolves when the
|
|
44
|
-
* user exits (q/Esc
|
|
45
|
-
* must show a text list.
|
|
43
|
+
* user exits (q/Esc on the group list, Ctrl+C). In a non-TTY it throws — the
|
|
44
|
+
* caller must show a text list.
|
|
46
45
|
*/
|
|
47
46
|
export function runConfigMenu(opts) {
|
|
48
47
|
const input = opts.input || process.stdin;
|
|
@@ -51,8 +50,14 @@ export function runConfigMenu(opts) {
|
|
|
51
50
|
if (!input.isTTY || !input.setRawMode) {
|
|
52
51
|
return Promise.reject(new Error(t('cfg.menu.notty')));
|
|
53
52
|
}
|
|
53
|
+
const allGroups = opts.topGroups || [...CONFIG_TOP_GROUPS];
|
|
54
|
+
// A group with no visible field is not offered at all.
|
|
55
|
+
const groups = allGroups.filter((g) => fields.some((f) => f.topKey === g && !f.advanced));
|
|
54
56
|
// cursor indexes the VISIBLE (filtered) list, not the full `fields`.
|
|
55
57
|
let cursor = 0;
|
|
58
|
+
// null = the group list (level 1); a topKey = that group's settings.
|
|
59
|
+
let openGroup = null;
|
|
60
|
+
let groupCursor = 0;
|
|
56
61
|
let editing = false;
|
|
57
62
|
let editBuf = '';
|
|
58
63
|
// Reverse/forward filter over the list (/ to enter, typing narrows it).
|
|
@@ -68,13 +73,16 @@ export function runConfigMenu(opts) {
|
|
|
68
73
|
input.setRawMode(true);
|
|
69
74
|
input.resume();
|
|
70
75
|
output.write(ESC + '[?25l'); // hide the cursor while rendering
|
|
71
|
-
// Fields that match the current filter (path, label or
|
|
72
|
-
// query, case-insensitive). Empty query -> everything.
|
|
76
|
+
// Fields that match the current filter (path, label or section contains the
|
|
77
|
+
// query, case-insensitive) INSIDE the open group. Empty query -> everything.
|
|
73
78
|
const visible = () => {
|
|
79
|
+
if (!openGroup)
|
|
80
|
+
return [];
|
|
81
|
+
const inGroup = fields.filter((f) => f.topKey === openGroup && !f.advanced);
|
|
74
82
|
const q = filterQuery.trim().toLowerCase();
|
|
75
83
|
if (!q)
|
|
76
|
-
return
|
|
77
|
-
return
|
|
84
|
+
return inGroup;
|
|
85
|
+
return inGroup.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
|
|
78
86
|
.toLowerCase()
|
|
79
87
|
.includes(q));
|
|
80
88
|
};
|
|
@@ -100,6 +108,27 @@ export function runConfigMenu(opts) {
|
|
|
100
108
|
return String(cur) !== String(def);
|
|
101
109
|
}
|
|
102
110
|
};
|
|
111
|
+
const enterGroup = (topKey) => {
|
|
112
|
+
openGroup = topKey;
|
|
113
|
+
cursor = 0;
|
|
114
|
+
filterQuery = '';
|
|
115
|
+
filterMode = false;
|
|
116
|
+
pendingReset = null;
|
|
117
|
+
message = null;
|
|
118
|
+
editing = false;
|
|
119
|
+
editBuf = '';
|
|
120
|
+
};
|
|
121
|
+
const leaveGroup = () => {
|
|
122
|
+
const idx = groups.indexOf(openGroup || '');
|
|
123
|
+
openGroup = null;
|
|
124
|
+
groupCursor = idx >= 0 ? idx : 0;
|
|
125
|
+
filterQuery = '';
|
|
126
|
+
filterMode = false;
|
|
127
|
+
pendingReset = null;
|
|
128
|
+
message = null;
|
|
129
|
+
editing = false;
|
|
130
|
+
editBuf = '';
|
|
131
|
+
};
|
|
103
132
|
// Menu rendering: lines go top to bottom, and we return the cursor to the
|
|
104
133
|
// start of the block via relative moves so we don't spawn blank lines.
|
|
105
134
|
let lastRows = 0;
|
|
@@ -125,6 +154,10 @@ export function runConfigMenu(opts) {
|
|
|
125
154
|
' ' +
|
|
126
155
|
t('cfg.menu.hint')));
|
|
127
156
|
}
|
|
157
|
+
else if (!openGroup) {
|
|
158
|
+
tail.push(' ' +
|
|
159
|
+
theme.dim(t('cfg.menu.group_hint') + ' · ' + t('cfg.menu.back_hint')));
|
|
160
|
+
}
|
|
128
161
|
else {
|
|
129
162
|
tail.push(' ' + theme.dim(t('cfg.menu.hint')));
|
|
130
163
|
tail.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
|
|
@@ -136,63 +169,90 @@ export function runConfigMenu(opts) {
|
|
|
136
169
|
// Bias one row to the SAFE side (a spare blank row is harmless, a block
|
|
137
170
|
// that overflows scrolls the top away and breaks the cursor restore).
|
|
138
171
|
const maxItemRows = Math.max(3, termRows - 2 - tail.length);
|
|
139
|
-
// WINDOW over the fields. We walk BACK from the cursor, counting the rows
|
|
140
|
-
// the window would take (a group header consumes a row too), until adding
|
|
141
|
-
// one more field would overflow. This guarantees the SELECTED item is always
|
|
142
|
-
// on screen — the old index-based scrollTop could push it out when many
|
|
143
|
-
// group headers sat between the window top and the cursor.
|
|
144
|
-
const groupStart = (i) => i === 0 || vis[i].groupKey !== vis[i - 1].groupKey;
|
|
145
|
-
// Rows needed to draw the window [from, cursor] (inclusive).
|
|
146
|
-
const rowsFor = (from) => {
|
|
147
|
-
let rows = 0;
|
|
148
|
-
for (let i = from; i <= cursor; i++) {
|
|
149
|
-
if (i === from || groupStart(i))
|
|
150
|
-
rows++;
|
|
151
|
-
rows++;
|
|
152
|
-
}
|
|
153
|
-
return rows;
|
|
154
|
-
};
|
|
155
|
-
let start = vis.length ? Math.max(0, Math.min(cursor, vis.length - 1)) : 0;
|
|
156
|
-
while (start > 0 && rowsFor(start - 1) <= maxItemRows)
|
|
157
|
-
start--;
|
|
158
172
|
const lines = [];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
if (!openGroup) {
|
|
174
|
+
// ---- level 1: the group list ----
|
|
175
|
+
const pos = groups.length
|
|
176
|
+
? ' ' + (groupCursor + 1) + '/' + groups.length
|
|
177
|
+
: '';
|
|
178
|
+
lines.push(theme.bold(t('cfg.menu.groups_title')) + theme.dim(pos));
|
|
179
|
+
// Window the groups around the cursor.
|
|
180
|
+
let start = Math.max(0, Math.min(groupCursor, groups.length - 1));
|
|
181
|
+
while (start > 0 && groupCursor - start + 1 < maxItemRows)
|
|
182
|
+
start--;
|
|
183
|
+
const end = Math.min(groups.length, start + maxItemRows);
|
|
184
|
+
for (let i = start; i < end; i++) {
|
|
185
|
+
const g = groups[i];
|
|
186
|
+
const selected = i === groupCursor;
|
|
187
|
+
const marker = selected ? theme.prompt('\u276f ') : ' ';
|
|
188
|
+
const label = selected ? theme.prompt(t(g)) : t(g);
|
|
189
|
+
const count = fields.filter((f) => f.topKey === g && !f.advanced).length;
|
|
190
|
+
const changed = fields.filter((f) => f.topKey === g && isModified(f)).length;
|
|
191
|
+
let extra = theme.dim(' ' + t('cfg.menu.group_count', { n: count }));
|
|
192
|
+
if (changed > 0) {
|
|
193
|
+
extra += theme.warn(' ' + t('cfg.menu.group_modified', { n: changed }));
|
|
194
|
+
}
|
|
195
|
+
lines.push(marker + label + extra);
|
|
196
|
+
}
|
|
197
|
+
if (!groups.length) {
|
|
198
|
+
lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// ---- level 2: the settings of the open group ----
|
|
203
|
+
const groupStart = (i) => i === 0 || vis[i].groupKey !== vis[i - 1].groupKey;
|
|
204
|
+
// Rows needed to draw the window [from, cursor] (inclusive).
|
|
205
|
+
const rowsFor = (from) => {
|
|
206
|
+
let rows = 0;
|
|
207
|
+
for (let i = from; i <= cursor; i++) {
|
|
208
|
+
if (i === from || groupStart(i))
|
|
209
|
+
rows++;
|
|
210
|
+
rows++;
|
|
211
|
+
}
|
|
212
|
+
return rows;
|
|
213
|
+
};
|
|
214
|
+
let start = vis.length ? Math.max(0, Math.min(cursor, vis.length - 1)) : 0;
|
|
215
|
+
while (start > 0 && rowsFor(start - 1) <= maxItemRows)
|
|
216
|
+
start--;
|
|
217
|
+
// Title with a position counter ("Settings — 12/49") so the operator
|
|
218
|
+
// knows how far down the list they are; a filter narrows both numbers.
|
|
219
|
+
const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
|
|
220
|
+
lines.push(theme.bold(t(openGroup) + ' \u203a ' + t('cfg.menu.title')) +
|
|
221
|
+
theme.dim(pos));
|
|
222
|
+
// Group headers also consume a row, so we stop as soon as the item-row
|
|
223
|
+
// budget is used up (the window is advisory; this is the hard cap that
|
|
224
|
+
// guarantees the block fits the terminal).
|
|
225
|
+
let itemRows = 0;
|
|
226
|
+
let lastGroup = '';
|
|
227
|
+
for (let i = start; i < vis.length; i++) {
|
|
228
|
+
const f = vis[i];
|
|
229
|
+
const g = groupLabel(f, t);
|
|
230
|
+
// Always print the header for the FIRST shown row, so a windowed list
|
|
231
|
+
// still tells the operator which section it starts in.
|
|
232
|
+
if (i === start || g !== lastGroup) {
|
|
233
|
+
if (itemRows >= maxItemRows)
|
|
234
|
+
break;
|
|
235
|
+
lines.push(g);
|
|
236
|
+
lastGroup = g;
|
|
237
|
+
itemRows++;
|
|
238
|
+
}
|
|
174
239
|
if (itemRows >= maxItemRows)
|
|
175
240
|
break;
|
|
176
|
-
lines.push(g);
|
|
177
|
-
lastGroup = g;
|
|
178
241
|
itemRows++;
|
|
242
|
+
const selected = i === cursor;
|
|
243
|
+
const marker = selected ? theme.prompt('\u276f ') : ' ';
|
|
244
|
+
const label = selected
|
|
245
|
+
? theme.prompt(f.labelKey ? t(f.labelKey) : f.path)
|
|
246
|
+
: t(f.labelKey || f.path);
|
|
247
|
+
const val = displayValue(f, get(f.path), t);
|
|
248
|
+
const valText = selected ? theme.assistant(val) : theme.dim(val);
|
|
249
|
+
const mod = isModified(f) ? theme.warn('*') : '';
|
|
250
|
+
const pathText = selected ? theme.dim(' ' + f.path) : '';
|
|
251
|
+
lines.push(marker + label + theme.dim(' = ') + valText + mod + pathText);
|
|
252
|
+
}
|
|
253
|
+
if (!vis.length) {
|
|
254
|
+
lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
|
|
179
255
|
}
|
|
180
|
-
if (itemRows >= maxItemRows)
|
|
181
|
-
break;
|
|
182
|
-
itemRows++;
|
|
183
|
-
const selected = i === cursor;
|
|
184
|
-
const marker = selected ? theme.prompt('\u276f ') : ' ';
|
|
185
|
-
const label = selected
|
|
186
|
-
? theme.prompt(f.labelKey ? t(f.labelKey) : f.path)
|
|
187
|
-
: t(f.labelKey || f.path);
|
|
188
|
-
const val = displayValue(f, get(f.path), t);
|
|
189
|
-
const valText = selected ? theme.assistant(val) : theme.dim(val);
|
|
190
|
-
const mod = isModified(f) ? theme.warn('*') : '';
|
|
191
|
-
const pathText = selected ? theme.dim(' ' + f.path) : '';
|
|
192
|
-
lines.push(marker + label + theme.dim(' = ') + valText + mod + pathText);
|
|
193
|
-
}
|
|
194
|
-
if (!vis.length) {
|
|
195
|
-
lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
|
|
196
256
|
}
|
|
197
257
|
lines.push(...tail);
|
|
198
258
|
// Erase the previous block and print the new one. We count VISUAL lines
|
|
@@ -207,9 +267,9 @@ export function runConfigMenu(opts) {
|
|
|
207
267
|
// reported bug). Overcounting by one is a harmless blank row; undercounting
|
|
208
268
|
// is not, so we bias to the safe side here.
|
|
209
269
|
const cols = output.columns || 80;
|
|
210
|
-
const
|
|
270
|
+
const usableCols = Math.max(1, cols - 1);
|
|
211
271
|
const rowsOf = (s) => {
|
|
212
|
-
return Math.max(1, Math.ceil(visCols(s) /
|
|
272
|
+
return Math.max(1, Math.ceil(visCols(s) / usableCols));
|
|
213
273
|
};
|
|
214
274
|
if (lastRows > 0) {
|
|
215
275
|
output.write(ESC + '[' + lastRows + 'A');
|
|
@@ -351,30 +411,68 @@ export function runConfigMenu(opts) {
|
|
|
351
411
|
let i = 0;
|
|
352
412
|
while (i < s.length) {
|
|
353
413
|
if (s.startsWith('\x1b[A', i)) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
414
|
+
if (openGroup) {
|
|
415
|
+
const n = visible().length;
|
|
416
|
+
if (n > 0)
|
|
417
|
+
cursor = (cursor - 1 + n) % n;
|
|
418
|
+
message = null;
|
|
419
|
+
pendingReset = null;
|
|
420
|
+
}
|
|
421
|
+
else if (groups.length) {
|
|
422
|
+
groupCursor = (groupCursor - 1 + groups.length) % groups.length;
|
|
423
|
+
}
|
|
359
424
|
i += 3;
|
|
360
425
|
continue;
|
|
361
426
|
}
|
|
362
427
|
if (s.startsWith('\x1b[B', i)) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
428
|
+
if (openGroup) {
|
|
429
|
+
const n = visible().length;
|
|
430
|
+
if (n > 0)
|
|
431
|
+
cursor = (cursor + 1) % n;
|
|
432
|
+
message = null;
|
|
433
|
+
pendingReset = null;
|
|
434
|
+
}
|
|
435
|
+
else if (groups.length) {
|
|
436
|
+
groupCursor = (groupCursor + 1) % groups.length;
|
|
437
|
+
}
|
|
368
438
|
i += 3;
|
|
369
439
|
continue;
|
|
370
440
|
}
|
|
371
441
|
const ch = s[i];
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
442
|
+
// ---------- LEVEL 1: the group list ----------
|
|
443
|
+
if (!openGroup) {
|
|
444
|
+
if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
|
|
445
|
+
// Erase the menu block from the screen so the config list does not
|
|
446
|
+
// linger in the terminal history after Esc.
|
|
447
|
+
cleanup(true);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (ch === 'k' && groups.length) {
|
|
451
|
+
groupCursor = (groupCursor - 1 + groups.length) % groups.length;
|
|
452
|
+
}
|
|
453
|
+
else if (ch === 'j' && groups.length) {
|
|
454
|
+
groupCursor = (groupCursor + 1) % groups.length;
|
|
455
|
+
}
|
|
456
|
+
else if (ch === '\r' || ch === '\n' || ch === ' ') {
|
|
457
|
+
const g = groups[groupCursor];
|
|
458
|
+
if (g)
|
|
459
|
+
enterGroup(g);
|
|
460
|
+
}
|
|
461
|
+
i++;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
// ---------- LEVEL 2: the settings of one group ----------
|
|
465
|
+
if (ch === 'q' || ch === 'Q' || ch === '\x03') {
|
|
375
466
|
cleanup(true);
|
|
376
467
|
return;
|
|
377
468
|
}
|
|
469
|
+
// Esc = BACK to the group list (not out of /config): inside a group the
|
|
470
|
+
// operator is one level deep, and quitting from there was a surprise.
|
|
471
|
+
if (ch === ESC) {
|
|
472
|
+
leaveGroup();
|
|
473
|
+
i++;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
378
476
|
if (ch === '/') {
|
|
379
477
|
filterMode = true;
|
|
380
478
|
message = null;
|
|
@@ -407,8 +505,8 @@ export function runConfigMenu(opts) {
|
|
|
407
505
|
pendingReset = null;
|
|
408
506
|
}
|
|
409
507
|
else if (ch === 'g') {
|
|
410
|
-
// Jump to the first field of the NEXT
|
|
411
|
-
// the last), so a long grouped list can be navigated quickly.
|
|
508
|
+
// Jump to the first field of the NEXT subsection (and back to the top
|
|
509
|
+
// after the last), so a long grouped list can be navigated quickly.
|
|
412
510
|
const vis = visible();
|
|
413
511
|
if (vis.length) {
|
|
414
512
|
let target = -1;
|