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/dist/tui/ConfigMenu.js
CHANGED
|
@@ -2,17 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
// The Ink-based CONFIG MENU — a drop-in alternative to the hand-rolled
|
|
3
3
|
// `runConfigMenu` (src/config-menu.ts), used when `ui.tui` is enabled.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// TWO LEVELS. The flat 45-entry list this replaced was unusable: every timing
|
|
6
|
+
// knob of every backend sat in one scroll. `/config` now opens a short list of
|
|
7
|
+
// GROUPS (Agent, Browser & chat, Providers, Interface, Files & history,
|
|
8
|
+
// Diagnostics); Enter opens a group and shows ONLY its settings, Esc goes back.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// Inside a group: ↑/↓ (or j/k) to move, ←/→ or Enter/Space to edit (boolean
|
|
11
|
+
// toggle, enum cycle, number/string prompt), d twice to reset, / to filter,
|
|
12
|
+
// q to quit. All strings come from i18n (cfg.*), so the menu is fully localized.
|
|
13
|
+
// The component never touches config.ts directly: the caller passes get/set/
|
|
14
|
+
// reset callbacks (the SAME ones the legacy menu takes), so the logic is shared.
|
|
13
15
|
import { useMemo, useState } from 'react';
|
|
14
16
|
import { Box, Text, useInput, useApp, useWindowSize } from 'ink';
|
|
15
17
|
import { translate } from '../i18n.js';
|
|
18
|
+
import { CONFIG_TOP_GROUPS } from '../config.js';
|
|
16
19
|
import { Checkbox, Select, UI, HintBar } from './widgets.js';
|
|
17
20
|
import { ThemePreview } from './theme-preview.js';
|
|
18
21
|
/** Strip ANSI to measure a line's visible width. */
|
|
@@ -50,6 +53,9 @@ export function ConfigMenuApp(opts) {
|
|
|
50
53
|
// (a hardcoded Russian bar showed up in an English UI).
|
|
51
54
|
const hint = translate(locale);
|
|
52
55
|
const { columns, rows } = useWindowSize();
|
|
56
|
+
// null = the GROUP list (level 1); a topKey = that group's settings.
|
|
57
|
+
const [openGroup, setOpenGroup] = useState(null);
|
|
58
|
+
const [groupCursor, setGroupCursor] = useState(0);
|
|
53
59
|
const [cursor, setCursor] = useState(0);
|
|
54
60
|
const [filterQuery, setFilterQuery] = useState('');
|
|
55
61
|
const [filterMode, setFilterMode] = useState(false);
|
|
@@ -65,14 +71,20 @@ export function ConfigMenuApp(opts) {
|
|
|
65
71
|
// Bump this counter after every mutation to force a re-render.
|
|
66
72
|
const [version, setVersion] = useState(0);
|
|
67
73
|
const bump = () => setVersion((v) => v + 1);
|
|
74
|
+
const allGroups = opts.topGroups || [...CONFIG_TOP_GROUPS];
|
|
75
|
+
// A group with no visible field is not offered at all.
|
|
76
|
+
const groups = useMemo(() => allGroups.filter((g) => fields.some((f) => f.topKey === g && !f.advanced)), [allGroups, fields]);
|
|
68
77
|
const visible = useMemo(() => {
|
|
78
|
+
if (!openGroup)
|
|
79
|
+
return [];
|
|
80
|
+
const inGroup = fields.filter((f) => f.topKey === openGroup && !f.advanced);
|
|
69
81
|
const q = filterQuery.trim().toLowerCase();
|
|
70
82
|
if (!q)
|
|
71
|
-
return
|
|
72
|
-
return
|
|
83
|
+
return inGroup;
|
|
84
|
+
return inGroup.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
|
|
73
85
|
.toLowerCase()
|
|
74
86
|
.includes(q));
|
|
75
|
-
}, [fields, filterQuery, t]);
|
|
87
|
+
}, [fields, openGroup, filterQuery, t]);
|
|
76
88
|
const isModified = (f) => {
|
|
77
89
|
if (!getDefault)
|
|
78
90
|
return false;
|
|
@@ -85,6 +97,12 @@ export function ConfigMenuApp(opts) {
|
|
|
85
97
|
return String(cur) !== String(def);
|
|
86
98
|
}
|
|
87
99
|
};
|
|
100
|
+
/** How many fields of a top group differ from their defaults (badge). */
|
|
101
|
+
const groupChanged = (topKey) => {
|
|
102
|
+
if (!getDefault)
|
|
103
|
+
return 0;
|
|
104
|
+
return fields.filter((f) => f.topKey === topKey && isModified(f)).length;
|
|
105
|
+
};
|
|
88
106
|
const commitEdit = () => {
|
|
89
107
|
const f = visible[cursor];
|
|
90
108
|
const raw = editBuf.trim();
|
|
@@ -152,6 +170,30 @@ export function ConfigMenuApp(opts) {
|
|
|
152
170
|
setMessage(null);
|
|
153
171
|
}
|
|
154
172
|
};
|
|
173
|
+
/** Enter a group: reset the per-group UI state so nothing leaks across. */
|
|
174
|
+
const enterGroup = (topKey) => {
|
|
175
|
+
setOpenGroup(topKey);
|
|
176
|
+
setCursor(0);
|
|
177
|
+
setFilterQuery('');
|
|
178
|
+
setFilterMode(false);
|
|
179
|
+
setPendingReset(null);
|
|
180
|
+
setMessage(null);
|
|
181
|
+
setPreviewTheme(false);
|
|
182
|
+
};
|
|
183
|
+
const leaveGroup = () => {
|
|
184
|
+
// Keep the group list cursor on the group that was just left, so Esc does
|
|
185
|
+
// not send the operator back to the top of the list.
|
|
186
|
+
const idx = groups.indexOf(openGroup || '');
|
|
187
|
+
setOpenGroup(null);
|
|
188
|
+
setGroupCursor(idx >= 0 ? idx : 0);
|
|
189
|
+
setFilterQuery('');
|
|
190
|
+
setFilterMode(false);
|
|
191
|
+
setPendingReset(null);
|
|
192
|
+
setMessage(null);
|
|
193
|
+
setEditing(false);
|
|
194
|
+
setEditBuf('');
|
|
195
|
+
setPreviewTheme(false);
|
|
196
|
+
};
|
|
155
197
|
const quit = () => {
|
|
156
198
|
opts.onExit();
|
|
157
199
|
exit();
|
|
@@ -199,13 +241,42 @@ export function ConfigMenuApp(opts) {
|
|
|
199
241
|
}
|
|
200
242
|
return;
|
|
201
243
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
244
|
+
// ---------- LEVEL 1: the group list ----------
|
|
245
|
+
if (!openGroup) {
|
|
246
|
+
if (key.escape ||
|
|
247
|
+
input === 'q' ||
|
|
248
|
+
input === 'Q' ||
|
|
249
|
+
(key.ctrl && input === 'c')) {
|
|
250
|
+
quit();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (key.upArrow || input === 'k') {
|
|
254
|
+
setGroupCursor((c) => groups.length ? (c - 1 + groups.length) % groups.length : 0);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (key.downArrow || input === 'j') {
|
|
258
|
+
setGroupCursor((c) => (groups.length ? (c + 1) % groups.length : 0));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (key.return || input === ' ' || key.rightArrow) {
|
|
262
|
+
const g = groups[groupCursor];
|
|
263
|
+
if (g)
|
|
264
|
+
enterGroup(g);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// ---------- LEVEL 2: the settings of one group ----------
|
|
270
|
+
if (input === 'q' || input === 'Q' || (key.ctrl && input === 'c')) {
|
|
206
271
|
quit();
|
|
207
272
|
return;
|
|
208
273
|
}
|
|
274
|
+
// Esc = BACK to the group list (not out of /config): inside a group the
|
|
275
|
+
// operator is one level deep, and quitting from there was a surprise.
|
|
276
|
+
if (key.escape) {
|
|
277
|
+
leaveGroup();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
209
280
|
if (input === '/') {
|
|
210
281
|
setFilterMode(true);
|
|
211
282
|
setMessage(null);
|
|
@@ -234,7 +305,7 @@ export function ConfigMenuApp(opts) {
|
|
|
234
305
|
return;
|
|
235
306
|
}
|
|
236
307
|
if (input === 'g') {
|
|
237
|
-
// Jump to the first field of the NEXT
|
|
308
|
+
// Jump to the first field of the NEXT subsection (and back to the top).
|
|
238
309
|
const curGroup = visible[cursor]?.groupKey;
|
|
239
310
|
let target = -1;
|
|
240
311
|
for (let k = cursor + 1; k < visible.length; k++) {
|
|
@@ -259,18 +330,6 @@ export function ConfigMenuApp(opts) {
|
|
|
259
330
|
return;
|
|
260
331
|
}
|
|
261
332
|
}
|
|
262
|
-
if (input === 'p' || input === 'P') {
|
|
263
|
-
// Inline palette PREVIEW on the theme row: /config is where most
|
|
264
|
-
// operators meet ui.theme, and cycling the enum only shows the id. 'p'
|
|
265
|
-
// toggles a sample session painted with the CURRENT palette, so the
|
|
266
|
-
// choice can be judged without leaving the menu. Other rows: no-op.
|
|
267
|
-
const f = visible[cursor];
|
|
268
|
-
if (f && f.path === 'ui.theme') {
|
|
269
|
-
setPreviewTheme((v) => !v);
|
|
270
|
-
setMessage(null);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
333
|
if (key.return || input === ' ') {
|
|
275
334
|
activate();
|
|
276
335
|
return;
|
|
@@ -302,11 +361,13 @@ export function ConfigMenuApp(opts) {
|
|
|
302
361
|
const maxRows = Math.max(3, termRows - 8);
|
|
303
362
|
const innerCols = Math.max(1, (columns || 80) - 4);
|
|
304
363
|
const shown = useMemo(() => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
364
|
+
const list = openGroup ? visible : groups;
|
|
365
|
+
const groupStart = (i) => !!openGroup &&
|
|
366
|
+
(i === 0 || visible[i].groupKey !== visible[i - 1].groupKey);
|
|
367
|
+
// Rendered height of one item: 1 base row + wrapped extra rows.
|
|
309
368
|
const itemRows = (i) => {
|
|
369
|
+
if (!openGroup)
|
|
370
|
+
return 1;
|
|
310
371
|
const f = visible[i];
|
|
311
372
|
const label = f.labelKey ? t(f.labelKey) : f.path;
|
|
312
373
|
return Math.max(1, Math.ceil((label.length + 12) / innerCols));
|
|
@@ -320,18 +381,19 @@ export function ConfigMenuApp(opts) {
|
|
|
320
381
|
}
|
|
321
382
|
return r;
|
|
322
383
|
};
|
|
323
|
-
|
|
384
|
+
const cur = openGroup ? cursor : groupCursor;
|
|
385
|
+
if (!list.length)
|
|
324
386
|
return { start: 0, end: 0 };
|
|
325
|
-
let start =
|
|
326
|
-
let end =
|
|
387
|
+
let start = cur;
|
|
388
|
+
let end = cur + 1;
|
|
327
389
|
// Fill downward first, then upward, so the cursor stays visible and the
|
|
328
390
|
// window uses the whole budget.
|
|
329
|
-
while (end <
|
|
391
|
+
while (end < list.length && rowsIn(start, end + 1) <= maxRows)
|
|
330
392
|
end++;
|
|
331
393
|
while (start > 0 && rowsIn(start - 1, end) <= maxRows)
|
|
332
394
|
start--;
|
|
333
395
|
return { start, end };
|
|
334
|
-
}, [cursor, visible, maxRows, innerCols, t]);
|
|
396
|
+
}, [cursor, groupCursor, openGroup, visible, groups, maxRows, innerCols, t]);
|
|
335
397
|
const usable = Math.max(1, (columns || 80) - 1);
|
|
336
398
|
void visCols;
|
|
337
399
|
void usable;
|
|
@@ -339,37 +401,59 @@ export function ConfigMenuApp(opts) {
|
|
|
339
401
|
// counter (a pure `get()` call has no reactive dependency of its own).
|
|
340
402
|
void version;
|
|
341
403
|
const items = [];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
404
|
+
if (!openGroup) {
|
|
405
|
+
// ---- level 1: one row per GROUP ----
|
|
406
|
+
for (let i = shown.start; i < shown.end; i++) {
|
|
407
|
+
const g = groups[i];
|
|
408
|
+
const selected = i === groupCursor;
|
|
409
|
+
const count = fields.filter((f) => f.topKey === g && !f.advanced).length;
|
|
410
|
+
const changed = groupChanged(g);
|
|
411
|
+
items.push(_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? '❯ ' : ' ' }), _jsx(Text, { color: selected ? UI.accent : UI.text, bold: selected, children: t(g) }), _jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.group_count', { n: count }) }), changed > 0 ? (_jsx(Text, { color: UI.warn, children: ' ' + t('cfg.menu.group_modified', { n: changed }) })) : null] }, 'grp' + i));
|
|
349
412
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// ---- level 2: the settings of the open group ----
|
|
416
|
+
let lastGroup = '';
|
|
417
|
+
for (let i = shown.start; i < shown.end; i++) {
|
|
418
|
+
const f = visible[i];
|
|
419
|
+
const g = f.groupKey;
|
|
420
|
+
if (i === shown.start || g !== lastGroup) {
|
|
421
|
+
items.push(_jsx(Text, { color: UI.title, children: ' ' + t(f.groupKey).toUpperCase() }, 'g' + i));
|
|
422
|
+
lastGroup = g;
|
|
423
|
+
}
|
|
424
|
+
const selected = i === cursor;
|
|
425
|
+
const label = f.labelKey ? t(f.labelKey) : f.path;
|
|
426
|
+
const raw = get(f.path);
|
|
427
|
+
const mod = isModified(f) ? '*' : '';
|
|
428
|
+
// The VALUE cell depends on the field type: a checkbox for booleans, an
|
|
429
|
+
// inline select for enums, plain text otherwise. The selected row shows
|
|
430
|
+
// the path on the right as a hint.
|
|
431
|
+
let valueCell;
|
|
432
|
+
if (f.type === 'boolean') {
|
|
433
|
+
valueCell = _jsx(Checkbox, { checked: raw === true, focused: selected });
|
|
434
|
+
}
|
|
435
|
+
else if (f.type === 'enum' && f.values && f.values.length) {
|
|
436
|
+
valueCell = (_jsx(Select, { value: String(raw ?? f.values[0]), focused: selected }));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
valueCell = (_jsx(Text, { color: selected ? UI.text : UI.system, children: displayValue(f, raw, t) }));
|
|
440
|
+
}
|
|
441
|
+
items.push(_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? '❯ ' : ' ' }), _jsx(Text, { color: selected ? UI.accent : UI.text, bold: selected, children: label }), _jsx(Text, { color: UI.dim, children: ' ' }), valueCell, mod ? _jsx(Text, { color: UI.warn, children: ' ' + mod }) : null, selected ? _jsx(Text, { color: UI.dim, children: ' ' + f.path }) : null] }, 'f' + i));
|
|
363
442
|
}
|
|
364
|
-
|
|
365
|
-
|
|
443
|
+
if (!visible.length) {
|
|
444
|
+
items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
|
|
366
445
|
}
|
|
367
|
-
items.push(_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? '❯ ' : ' ' }), _jsx(Text, { color: selected ? UI.accent : UI.text, bold: selected, children: label }), _jsx(Text, { color: UI.dim, children: ' ' }), valueCell, mod ? _jsx(Text, { color: UI.warn, children: ' ' + mod }) : null, selected ? _jsx(Text, { color: UI.dim, children: ' ' + f.path }) : null] }, 'f' + i));
|
|
368
|
-
}
|
|
369
|
-
if (!visible.length) {
|
|
370
|
-
items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
|
|
371
446
|
}
|
|
372
|
-
|
|
447
|
+
const count = openGroup ? visible.length : groups.length;
|
|
448
|
+
const pos = openGroup ? cursor : groupCursor;
|
|
449
|
+
const title = openGroup
|
|
450
|
+
? t(openGroup) + ' › ' + t('cfg.menu.title')
|
|
451
|
+
: t('cfg.menu.groups_title');
|
|
452
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: UI.title, children: title }), _jsx(Text, { color: UI.dim, children: (count ? pos + 1 : 0) + '/' + count })] }), _jsx(Text, { color: UI.border, children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), items, _jsx(Text, { color: UI.border, children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), previewTheme && visible[cursor]?.path === 'ui.theme' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('cfg.menu.theme_preview') }), _jsx(ThemePreview, { name: String(get('ui.theme') ?? 'zames'), compact: true })] })) : null, editing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.text, children: t('cfg.menu.edit_hint') }), _jsx(Text, { color: UI.accent, children: '❯ ' + editBuf })] })) : filterMode ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.text, children: t('cfg.menu.filter_hint') }), _jsx(Text, { color: UI.accent, children: '❯ ' + filterQuery })] })) : message ? (_jsx(Text, { color: UI.ok, children: message })) : filterQuery ? (_jsx(Text, { color: UI.dim, children: t('cfg.menu.filter_active', { q: filterQuery }) })) : null, !openGroup ? (_jsx(HintBar, { hints: [
|
|
453
|
+
['↑/↓', hint('tui.hint.select')],
|
|
454
|
+
['Enter', hint('tui.hint.ok')],
|
|
455
|
+
['q', hint('tui.hint.quit')],
|
|
456
|
+
] })) : (_jsx(HintBar, { hints: [
|
|
373
457
|
['↑/↓', hint('tui.hint.select')],
|
|
374
458
|
['←/→', hint('tui.hint.change')],
|
|
375
459
|
['Enter', hint('tui.hint.edit')],
|
|
@@ -378,6 +462,7 @@ export function ConfigMenuApp(opts) {
|
|
|
378
462
|
: []),
|
|
379
463
|
['/', hint('tui.hint.search')],
|
|
380
464
|
['d', hint('tui.hint.reset')],
|
|
465
|
+
['Esc', hint('tui.hint.back')],
|
|
381
466
|
['q', hint('tui.hint.quit')],
|
|
382
|
-
] })] }));
|
|
467
|
+
] }))] }));
|
|
383
468
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.89.0",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {
|