zames_pro 2.88.1 → 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 +13 -0
- package/README.md +10 -8
- 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/llms-full.txt +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ 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
|
+
|
|
17
|
+
## [2.88.2] - 2026-10-27
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- docs: no manual step — the agent owns every file and every release action
|
|
22
|
+
|
|
10
23
|
## [2.88.1] - 2026-10-27
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -485,14 +485,15 @@ hidden until the first answer delivers it.
|
|
|
485
485
|
Like Codex / Claude Code, zames reads project instructions and reusable
|
|
486
486
|
workflows from your repository and from `~/.zames`.
|
|
487
487
|
|
|
488
|
-
- **AGENTS.md** — project instructions
|
|
489
|
-
|
|
488
|
+
- **AGENTS.md** — project instructions, in the repo root (or any parent folder
|
|
489
|
+
of the working directory); the agent writes it with `/init`. Global
|
|
490
|
+
instructions live in
|
|
490
491
|
`~/.zames/AGENTS.md` (and `~/.claude/CLAUDE.md`). Run `/init` and the agent
|
|
491
492
|
will explore the project and write an AGENTS.md based on the real build/test
|
|
492
493
|
commands and conventions (use `/init --force` to overwrite an existing file).
|
|
493
494
|
- **MEMORY.md** — durable notes that persist between sessions. The agent
|
|
494
|
-
appends useful facts here
|
|
495
|
-
prompt
|
|
495
|
+
appends useful facts here as it works; `/remember <text>` adds one from the
|
|
496
|
+
prompt, and any `/task`-style edit through the agent keeps the file in sync.
|
|
496
497
|
- **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
|
|
497
498
|
`description`, optional `allowed-tools`, `user-invokable`) plus any helper
|
|
498
499
|
files. Discovered under `.zames/skills/`, `.claude/skills/`,
|
|
@@ -1230,14 +1231,15 @@ zames --task "..." --output-format jsonl \
|
|
|
1230
1231
|
Как в Codex / Claude Code, zames читает инструкции проекта и переиспользуемые
|
|
1231
1232
|
процессы из вашего репозитория и из `~/.zames`.
|
|
1232
1233
|
|
|
1233
|
-
- **AGENTS.md** — инструкции
|
|
1234
|
-
|
|
1234
|
+
- **AGENTS.md** — инструкции проекта, лежит в корне репозитория (или в любой
|
|
1235
|
+
родительской папке рабочей директории); агент создаёт его командой `/init`.
|
|
1236
|
+
Глобальные инструкции живут в
|
|
1235
1237
|
`~/.zames/AGENTS.md` (и `~/.claude/CLAUDE.md`). Запустите `/init`, и агент
|
|
1236
1238
|
изучит проект и напишет AGENTS.md на основе реальных команд сборки/тестов и
|
|
1237
1239
|
соглашений (`/init --force` перезапишет существующий файл).
|
|
1238
1240
|
- **MEMORY.md** — долговечные заметки, переживающие сессии. Агент дописывает
|
|
1239
|
-
сюда полезные
|
|
1240
|
-
|
|
1241
|
+
сюда полезные факты по ходу работы; `/remember <text>` добавляет заметку из
|
|
1242
|
+
промпта — файл остаётся согласованным без ручного редактирования.
|
|
1241
1243
|
- **Навыки** — папка с файлом `SKILL.md` (YAML-фронтматтер: `name`,
|
|
1242
1244
|
`description`, опционально `allowed-tools`, `user-invokable`) плюс любые
|
|
1243
1245
|
вспомогательные файлы. Ищутся в `.zames/skills/`, `.claude/skills/`,
|
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;
|