zames_pro 2.64.0 → 2.65.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 +46 -1
- package/README.md +16 -11
- package/dist/backlog.js +20 -0
- package/dist/browser.js +11 -0
- package/dist/commands.js +81 -0
- package/dist/config-menu.js +71 -33
- package/dist/i18n/common.js +362 -0
- package/dist/i18n/config.js +199 -0
- package/dist/i18n/deepseek.js +194 -0
- package/dist/i18n/doctor.js +127 -0
- package/dist/i18n/help.js +552 -0
- package/dist/i18n/mcp.js +41 -0
- package/dist/i18n/prompt.js +20 -0
- package/dist/i18n/self-review.js +198 -0
- package/dist/i18n/spinner.js +36 -0
- package/dist/i18n.js +21 -1724
- package/dist/index.js +39 -503
- package/dist/input/layout.js +292 -0
- package/dist/input.js +37 -290
- package/dist/run-task.js +451 -0
- package/package.json +2 -2
- package/scripts/render-demo.mjs +153 -0
- package/logo.jpg +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.65.0] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- README-скриншот `docs/demo.png` перегенерирован на английском языке
|
|
15
|
+
(раньше был на русском). Воспроизводимый генератор
|
|
16
|
+
`npm run render-demo` (`scripts/render-demo.mjs`) рендерит реальную
|
|
17
|
+
раскладку редактора и палитру темы через headless Chromium с
|
|
18
|
+
изолированным профилем.
|
|
19
|
+
- README: ASCII-схема «как это работает» заменена на диаграмму Mermaid
|
|
20
|
+
(стрелки не «плывут») плюс текстовое описание словами.
|
|
21
|
+
- Убраны неиспользуемые файлы: `logo.jpg` (в README используется только
|
|
22
|
+
`logo-small.jpg`) и заготовка `docs/demo.tape` (VHS-рендер так и не был
|
|
23
|
+
сгенерирован). `logo.jpg` удалён и из списка `files` в package.json.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Перерисовка терминала после ресайза окна: относительный стирающий
|
|
28
|
+
`ESC[n A` после изменения размера попадал на устаревшую строку (терминал
|
|
29
|
+
переформатировал переносы), из-за чего статус/инпут рисовались не там.
|
|
30
|
+
Теперь после ресайза блок заново «прибивается» к низу (home + padding),
|
|
31
|
+
история скроллбэка сохраняется. Тест в `test/terminal-drift.test.ts`.
|
|
32
|
+
- Живая смена языка (`/config lang`) не доходила до двух «одноразово
|
|
33
|
+
созданных» объектов: спиннер думающих фраз брал язык по умолчанию
|
|
34
|
+
(`randomThinkingPhrase()` вместо `randomThinkingPhrase(this.locale)`),
|
|
35
|
+
а `DeepSeekBrowser` печатал свои уведомления (`ds.*`) на языке,
|
|
36
|
+
зафиксированном при старте. Из-за этого при английском UI всплывали
|
|
37
|
+
русские «Структурирую мысли…» и «⏳ DeepSeek оборвал ответ…». Добавлен
|
|
38
|
+
`DeepSeekBrowser.setLocale()`, вызываемый из `setConfigRuntime`.
|
|
39
|
+
- Меню `/config` теперь рисуется ОКНОМ по высоте терминала (раньше весь
|
|
40
|
+
список из 49 полей выводился одним блоком). На невысоком терминале блок
|
|
41
|
+
уходил верхними строками за экран, из-за чего восстановление курсора
|
|
42
|
+
`ESC[lastRows A` попадало не на ту строку и навигация стрелками «не
|
|
43
|
+
скроллила» список. Заодно исправлена мёртвая регулярка снятия ANSI
|
|
44
|
+
(лишний обратный апостроф) в `src/config-menu.ts`.
|
|
45
|
+
|
|
46
|
+
## [2.64.1] - 2026-10-07
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- README: добавлен скриншот терминальной сессии (`docs/demo.png`) под
|
|
51
|
+
шапкой, чтобы интерфейс был виден сразу; скрипт воспроизводимого
|
|
52
|
+
VHS-демо (`docs/demo.tape`) для будущего GIF.
|
|
53
|
+
|
|
10
54
|
## [2.64.0] - 2026-10-06
|
|
11
55
|
|
|
12
56
|
### Added
|
|
@@ -348,7 +392,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
348
392
|
- Session banner warns when no saved DeepSeek session exists.
|
|
349
393
|
- `--no-color` flag (explicit `NO_COLOR`).
|
|
350
394
|
|
|
351
|
-
[Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.64.
|
|
395
|
+
[Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.64.1...HEAD
|
|
396
|
+
[2.64.1]: https://github.com/Viqto0r/zames_pro/compare/v2.64.0...v2.64.1
|
|
352
397
|
[2.64.0]: https://github.com/Viqto0r/zames_pro/compare/v2.63.1...v2.64.0
|
|
353
398
|
[2.63.1]: https://github.com/Viqto0r/zames_pro/compare/v2.63.0...v2.63.1
|
|
354
399
|
[2.63.0]: https://github.com/Viqto0r/zames_pro/compare/v2.61.0...v2.63.0
|
package/README.md
CHANGED
|
@@ -23,24 +23,29 @@ directory, reads and edits files, runs commands, and commits to git.
|
|
|
23
23
|
> No API key required — it drives the DeepSeek web chat like a regular user
|
|
24
24
|
> through a real (headless) browser.
|
|
25
25
|
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames session in the terminal" width="860">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
26
30
|
## How it works
|
|
27
31
|
|
|
28
32
|
zames does not call the model API. It launches a headless Chromium with a
|
|
29
33
|
persistent profile, signs in to `chat.deepseek.com` like a human, types the task
|
|
30
34
|
into the chat box, and reads the answer back.
|
|
31
35
|
|
|
36
|
+
```mermaid
|
|
37
|
+
flowchart LR
|
|
38
|
+
U["terminal (you)"] -->|"task"| Z["zames"]
|
|
39
|
+
Z -->|"system prompt + task"| D["chat.deepseek.com"]
|
|
40
|
+
D -->|"model answers (raw SSE)"| Z
|
|
41
|
+
Z -->|"parse tool-call"| Z
|
|
42
|
+
Z -->|"run tool: Read/Edit/Bash/…"| Z
|
|
43
|
+
Z -->|"answer"| U
|
|
32
44
|
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
│ model answers
|
|
38
|
-
│ │
|
|
39
|
-
answer ◀──── render ◀───── parse tool-call ◀──── raw answer ◀────┘
|
|
40
|
-
│
|
|
41
|
-
▼
|
|
42
|
-
run tool (Read/Edit/Bash/…) → feed result back
|
|
43
|
-
```
|
|
45
|
+
|
|
46
|
+
In text: you type a task → zames sends the system prompt plus the task into
|
|
47
|
+
the DeepSeek web chat → the model answers → zames parses the tool call, runs
|
|
48
|
+
the tool locally, feeds the result back, and prints the final answer for you.
|
|
44
49
|
|
|
45
50
|
Key pieces:
|
|
46
51
|
|
package/dist/backlog.js
CHANGED
|
@@ -15,6 +15,26 @@ const NEW_ITEM_PREFIX = 'N';
|
|
|
15
15
|
// worth, so the operator is nudged to collapse it.
|
|
16
16
|
export const BACKLOG_WARN_LINES = 500;
|
|
17
17
|
export const BACKLOG_WARN_CHARS = 60_000;
|
|
18
|
+
// Parse a `/backlog <text>` note into the shape appendBacklogItem() wants. An
|
|
19
|
+
// optional leading `P0..P3` token sets the priority; the first line becomes the
|
|
20
|
+
// title and the rest the body. PURE so the operator's note is placed exactly as
|
|
21
|
+
// shown — no model round-trip, no chance of a different id than the one printed.
|
|
22
|
+
export function parseBacklogNote(note) {
|
|
23
|
+
const raw = String(note ?? '');
|
|
24
|
+
let priority = 'P2';
|
|
25
|
+
let body = raw;
|
|
26
|
+
for (const p of ['P0', 'P1', 'P2', 'P3']) {
|
|
27
|
+
if (raw.toUpperCase().startsWith(p + ' ')) {
|
|
28
|
+
priority = p;
|
|
29
|
+
body = raw.slice(p.length + 1);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const bodyLines = body.split(NL);
|
|
34
|
+
const title = bodyLines[0];
|
|
35
|
+
const rest = bodyLines.slice(1).join(NL).trim();
|
|
36
|
+
return { priority, title, note: rest || undefined };
|
|
37
|
+
}
|
|
18
38
|
function squashBlankLines(lines) {
|
|
19
39
|
const out = [];
|
|
20
40
|
let blanks = 0;
|
package/dist/browser.js
CHANGED
|
@@ -550,6 +550,17 @@ export class DeepSeekBrowser {
|
|
|
550
550
|
_t(key, params) {
|
|
551
551
|
return translate(this.locale)(key, params);
|
|
552
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Switch the language of the messages the browser itself prints (login
|
|
555
|
+
* prompts, DeepSeek-side notices from `_notice()`). The browser is created
|
|
556
|
+
* ONCE at startup, so without this a `/config lang` change would leave every
|
|
557
|
+
* later notice in the OLD language until a restart — the operator saw
|
|
558
|
+
* Russian "⏳ DeepSeek оборвал ответ…" while the UI was set to English.
|
|
559
|
+
*/
|
|
560
|
+
setLocale(locale) {
|
|
561
|
+
if (locale)
|
|
562
|
+
this.locale = locale;
|
|
563
|
+
}
|
|
553
564
|
// A service notice for the operator. When a UI hook is wired (LineEditor is
|
|
554
565
|
// active during a task) the notice goes ABOVE the input line so it is not
|
|
555
566
|
// overwritten by the editor's repaint; otherwise it falls back to stderr.
|
package/dist/commands.js
CHANGED
|
@@ -951,3 +951,84 @@ export function buildReviewPrompt(focus, hasStaged = false) {
|
|
|
951
951
|
s += NL + 'Extra focus: ' + f;
|
|
952
952
|
return s;
|
|
953
953
|
}
|
|
954
|
+
// The canonical list. Dev-only entries are filtered by buildSlashCommandHints()
|
|
955
|
+
// unless dev mode is on (DEV_ONLY_COMMANDS above), so a regular install never
|
|
956
|
+
// advertises them.
|
|
957
|
+
export const SLASH_COMMANDS = [
|
|
958
|
+
{ name: '/help', key: 'help.cmd.help' },
|
|
959
|
+
{ name: '/new', key: 'help.cmd.new' },
|
|
960
|
+
{ name: '/clear', key: 'help.cmd.new' },
|
|
961
|
+
{ name: '/sessions', key: 'help.cmd.sessions' },
|
|
962
|
+
{ name: '/chats', key: 'help.cmd.chats' },
|
|
963
|
+
{ name: '/resume', key: 'help.cmd.resume' },
|
|
964
|
+
{ name: '/resume-id', key: 'help.cmd.resume_id' },
|
|
965
|
+
{ name: '/last', key: 'help.cmd.last' },
|
|
966
|
+
{ name: '/retry', key: 'help.cmd.retry' },
|
|
967
|
+
{ name: '/rename', key: 'help.cmd.rename' },
|
|
968
|
+
{ name: '/context', key: 'help.cmd.context' },
|
|
969
|
+
{ name: '/copy', key: 'help.cmd.copy' },
|
|
970
|
+
{ name: '/chat', key: 'help.cmd.chat' },
|
|
971
|
+
{ name: '/cd', key: 'help.cmd.cd' },
|
|
972
|
+
{ name: '/pwd', key: 'help.cmd.pwd' },
|
|
973
|
+
{ name: '/status', key: 'help.cmd.status' },
|
|
974
|
+
{ name: '/reload', key: 'help.cmd.reload' },
|
|
975
|
+
{ name: '/undo', key: 'help.cmd.undo' },
|
|
976
|
+
{ name: '/undo-list', key: 'help.cmd.undo_list' },
|
|
977
|
+
{ name: '/rewind', key: 'help.cmd.rewind' },
|
|
978
|
+
{ name: '/rewind-list', key: 'help.cmd.rewind_list' },
|
|
979
|
+
{ name: '/transcript', key: 'help.cmd.transcript' },
|
|
980
|
+
{ name: '/diff', key: 'help.cmd.diff' },
|
|
981
|
+
{ name: '/diffstat', key: 'help.cmd.diffstat' },
|
|
982
|
+
{ name: '/cost', key: 'help.cmd.cost' },
|
|
983
|
+
{ name: '/export', key: 'help.cmd.export' },
|
|
984
|
+
{ name: '/doctor', key: 'help.cmd.doctor' },
|
|
985
|
+
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
986
|
+
{ name: '/review', key: 'help.cmd.review' },
|
|
987
|
+
{ name: '/improve', key: 'help.cmd.improve' },
|
|
988
|
+
{ name: '/backlog', key: 'help.cmd.backlog' },
|
|
989
|
+
{ name: '/goal', key: 'help.cmd.goal' },
|
|
990
|
+
{ name: '/loop', key: 'help.cmd.loop' },
|
|
991
|
+
{ name: '/cron', key: 'help.cmd.cron' },
|
|
992
|
+
{ name: '/jobs', key: 'help.cmd.jobs' },
|
|
993
|
+
{ name: '/thinking', key: 'help.cmd.thinking' },
|
|
994
|
+
{ name: '/web', key: 'help.cmd.web' },
|
|
995
|
+
{ name: '/plan', key: 'help.cmd.plan' },
|
|
996
|
+
{ name: '/compact', key: 'help.cmd.compact' },
|
|
997
|
+
{ name: '/queue', key: 'help.cmd.queue' },
|
|
998
|
+
{ name: '/tasks', key: 'help.cmd.tasks' },
|
|
999
|
+
{ name: '/config', key: 'help.cmd.config' },
|
|
1000
|
+
{ name: '/skills', key: 'help.cmd.skills' },
|
|
1001
|
+
{ name: '/memory', key: 'help.cmd.memory' },
|
|
1002
|
+
{ name: '/remember', key: 'help.cmd.remember' },
|
|
1003
|
+
{ name: '/init', key: 'help.cmd.init' },
|
|
1004
|
+
{ name: '/mcp', key: 'help.cmd.mcp' },
|
|
1005
|
+
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
1006
|
+
{ name: '/self-review', key: 'help.self.review' },
|
|
1007
|
+
{ name: '/self-fix', key: 'help.self.fix' },
|
|
1008
|
+
{ name: '/self-done', key: 'help.self.done' },
|
|
1009
|
+
{ name: '/self-list', key: 'help.self.list' },
|
|
1010
|
+
{ name: '/self-diff', key: 'help.self.diff' },
|
|
1011
|
+
{ name: '/self-apply', key: 'help.self.apply' },
|
|
1012
|
+
{ name: '/exit', key: 'help.cmd.exit' },
|
|
1013
|
+
{ name: '/quit', key: 'help.cmd.exit' },
|
|
1014
|
+
];
|
|
1015
|
+
// Build the localized hint list from the built-in table plus the dynamic
|
|
1016
|
+
// skills/custom commands discovered for the current workdir. PURE: the caller
|
|
1017
|
+
// passes `t` and the data; nothing here touches the terminal or the editor.
|
|
1018
|
+
// Dev-only commands are dropped unless `devMode`; a dynamic entry whose name
|
|
1019
|
+
// clashes with a built-in (case-insensitive) is skipped, so a skill cannot
|
|
1020
|
+
// shadow /help.
|
|
1021
|
+
export function buildSlashCommandHints(opts) {
|
|
1022
|
+
const { devMode, t } = opts;
|
|
1023
|
+
const table = opts.commands ?? SLASH_COMMANDS;
|
|
1024
|
+
const base = table
|
|
1025
|
+
.filter((c) => devMode || !isDevOnlyCommand(c.name))
|
|
1026
|
+
.map((c) => ({ name: c.name, description: t(c.key) }));
|
|
1027
|
+
const known = new Set(base.map((c) => c.name.toLowerCase()));
|
|
1028
|
+
for (const d of opts.dynamic ?? []) {
|
|
1029
|
+
if (known.has(d.name.toLowerCase()))
|
|
1030
|
+
continue;
|
|
1031
|
+
base.push(d);
|
|
1032
|
+
}
|
|
1033
|
+
return base;
|
|
1034
|
+
}
|
package/dist/config-menu.js
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
import { theme } from './theme.js';
|
|
12
12
|
import { visLen } from './input.js';
|
|
13
13
|
const ESC = String.fromCharCode(27);
|
|
14
|
-
// Strip
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Strip ANSI sequences to compute the visible length of a line. (A module-
|
|
15
|
+
// level name distinct from input.ts's internal helper; the old regex had a
|
|
16
|
+
// stray backtick and never matched a real CSI, so visCols was over-counted.)
|
|
17
|
+
function stripAnsiLocal(s) {
|
|
18
|
+
return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
17
19
|
}
|
|
18
20
|
// Visible COLUMNS of a line (ANSI removed, wide chars counted as 2). Using the
|
|
19
21
|
// code-point count made the block height disagree with the real screen on lines
|
|
@@ -21,7 +23,7 @@ function stripAnsi(s) {
|
|
|
21
23
|
// duplicated. A row that fills the terminal exactly also triggers autowrap on
|
|
22
24
|
// some terminals, so we keep one column free here as the editor does.
|
|
23
25
|
function visCols(s) {
|
|
24
|
-
return visLen(
|
|
26
|
+
return visLen(stripAnsiLocal(s));
|
|
25
27
|
}
|
|
26
28
|
function displayValue(field, value, t) {
|
|
27
29
|
if (value === undefined)
|
|
@@ -51,6 +53,11 @@ export function runConfigMenu(opts) {
|
|
|
51
53
|
}
|
|
52
54
|
// cursor indexes the VISIBLE (filtered) list, not the full `fields`.
|
|
53
55
|
let cursor = 0;
|
|
56
|
+
// Index of the first VISIBLE field drawn. The menu is WINDOWED so the whole
|
|
57
|
+
// block always fits the terminal height; otherwise a block taller than the
|
|
58
|
+
// screen scrolled the top rows off and the `ESC[lastRows A` cursor restore
|
|
59
|
+
// landed on the wrong line, so arrow-key navigation did not scroll the list.
|
|
60
|
+
let scrollTop = 0;
|
|
54
61
|
let editing = false;
|
|
55
62
|
let editBuf = '';
|
|
56
63
|
// Reverse/forward filter over the list (/ to enter, typing narrows it).
|
|
@@ -102,22 +109,78 @@ export function runConfigMenu(opts) {
|
|
|
102
109
|
// start of the block via relative moves so we don't spawn blank lines.
|
|
103
110
|
let lastRows = 0;
|
|
104
111
|
const render = () => {
|
|
105
|
-
const lines = [];
|
|
106
112
|
const vis = visible();
|
|
107
113
|
if (cursor >= vis.length)
|
|
108
114
|
cursor = Math.max(0, vis.length - 1);
|
|
115
|
+
// Trailing block (below the list): the edit/filter prompt or the hints.
|
|
116
|
+
const tail = [''];
|
|
117
|
+
if (editing) {
|
|
118
|
+
tail.push(theme.user(t('cfg.menu.edit_hint')));
|
|
119
|
+
tail.push(theme.prompt('\u276f ') + editBuf);
|
|
120
|
+
}
|
|
121
|
+
else if (filterMode) {
|
|
122
|
+
tail.push(theme.user(t('cfg.menu.filter_hint')));
|
|
123
|
+
tail.push(theme.prompt('\u276f ') + filterQuery);
|
|
124
|
+
}
|
|
125
|
+
else if (message) {
|
|
126
|
+
tail.push(theme.assistant(message));
|
|
127
|
+
}
|
|
128
|
+
else if (filterQuery) {
|
|
129
|
+
tail.push(theme.dim(t('cfg.menu.filter_active', { q: filterQuery }) +
|
|
130
|
+
' ' +
|
|
131
|
+
t('cfg.menu.hint')));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
tail.push(' ' + theme.dim(t('cfg.menu.hint')));
|
|
135
|
+
tail.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
|
|
136
|
+
}
|
|
137
|
+
// How many ITEM rows (fields + group headers) fit: terminal height minus
|
|
138
|
+
// the title and the trailing block, with one spare row. This is what keeps
|
|
139
|
+
// the block from scrolling off the top on a long list.
|
|
140
|
+
const termRows = output.rows || 24;
|
|
141
|
+
// Bias one row to the SAFE side (a spare blank row is harmless, a block
|
|
142
|
+
// that overflows scrolls the top away and breaks the cursor restore).
|
|
143
|
+
const maxItemRows = Math.max(3, termRows - 2 - tail.length);
|
|
144
|
+
// WINDOW over the fields: keep the cursor inside [scrollTop, +maxItemRows).
|
|
145
|
+
// Because a group header also consumes a row we use the cursor index as the
|
|
146
|
+
// primary bound and let the header be counted when the first shown field
|
|
147
|
+
// starts a new group (a rare one-row overshoot is harmless — a spare row is
|
|
148
|
+
// reserved above).
|
|
149
|
+
if (cursor < scrollTop)
|
|
150
|
+
scrollTop = cursor;
|
|
151
|
+
if (cursor >= scrollTop + maxItemRows)
|
|
152
|
+
scrollTop = cursor - maxItemRows + 1;
|
|
153
|
+
const maxTop = Math.max(0, vis.length - maxItemRows);
|
|
154
|
+
if (scrollTop > maxTop)
|
|
155
|
+
scrollTop = maxTop;
|
|
156
|
+
if (scrollTop < 0)
|
|
157
|
+
scrollTop = 0;
|
|
158
|
+
const start = scrollTop;
|
|
159
|
+
const lines = [];
|
|
109
160
|
// Title with a position counter ("Settings — 12/49") so the operator knows
|
|
110
161
|
// how far down a long list they are; a filter narrows both numbers.
|
|
111
162
|
const pos = vis.length ? ' ' + (cursor + 1) + '/' + vis.length : '';
|
|
112
163
|
lines.push(theme.bold(t('cfg.menu.title')) + theme.dim(pos));
|
|
164
|
+
// Group headers also consume a row, so we stop as soon as the item-row
|
|
165
|
+
// budget is used up (the window is advisory; this is the hard cap that
|
|
166
|
+
// guarantees the block fits the terminal).
|
|
167
|
+
let itemRows = 0;
|
|
113
168
|
let lastGroup = '';
|
|
114
|
-
for (let i =
|
|
169
|
+
for (let i = start; i < vis.length; i++) {
|
|
115
170
|
const f = vis[i];
|
|
116
171
|
const g = groupLabel(f, t);
|
|
117
|
-
|
|
172
|
+
// Always print the header for the FIRST shown row, so a windowed list
|
|
173
|
+
// still tells the operator which section it starts in.
|
|
174
|
+
if (i === start || g !== lastGroup) {
|
|
175
|
+
if (itemRows >= maxItemRows)
|
|
176
|
+
break;
|
|
118
177
|
lines.push(g);
|
|
119
178
|
lastGroup = g;
|
|
179
|
+
itemRows++;
|
|
120
180
|
}
|
|
181
|
+
if (itemRows >= maxItemRows)
|
|
182
|
+
break;
|
|
183
|
+
itemRows++;
|
|
121
184
|
const selected = i === cursor;
|
|
122
185
|
const marker = selected ? theme.prompt('\u276f ') : ' ';
|
|
123
186
|
const label = selected
|
|
@@ -132,32 +195,7 @@ export function runConfigMenu(opts) {
|
|
|
132
195
|
if (!vis.length) {
|
|
133
196
|
lines.push(theme.dim(' ' + t('cfg.menu.no_match')));
|
|
134
197
|
}
|
|
135
|
-
|
|
136
|
-
lines.push('');
|
|
137
|
-
lines.push(theme.user(t('cfg.menu.edit_hint')));
|
|
138
|
-
lines.push(theme.prompt('\u276f ') + editBuf);
|
|
139
|
-
}
|
|
140
|
-
else if (filterMode) {
|
|
141
|
-
lines.push('');
|
|
142
|
-
lines.push(theme.user(t('cfg.menu.filter_hint')));
|
|
143
|
-
lines.push(theme.prompt('\u276f ') + filterQuery);
|
|
144
|
-
}
|
|
145
|
-
else if (message) {
|
|
146
|
-
lines.push('');
|
|
147
|
-
lines.push(theme.assistant(message));
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
lines.push('');
|
|
151
|
-
if (filterQuery) {
|
|
152
|
-
lines.push(theme.dim(t('cfg.menu.filter_active', { q: filterQuery }) +
|
|
153
|
-
' ' +
|
|
154
|
-
t('cfg.menu.hint')));
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
lines.push(' ' + theme.dim(t('cfg.menu.hint')));
|
|
158
|
-
lines.push(' ' + theme.dim(t('cfg.menu.filter_hint')));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
198
|
+
lines.push(...tail);
|
|
161
199
|
// Erase the previous block and print the new one. We count VISUAL lines
|
|
162
200
|
// (accounting for wrapping by terminal width), otherwise on narrow
|
|
163
201
|
// terminals you get "garbage" from unerased tails.
|