zames_pro 2.73.10 → 2.74.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +13 -7
  3. package/dist/banner.js +71 -0
  4. package/dist/browser.js +84 -27
  5. package/dist/code-blocks.js +48 -0
  6. package/dist/commands.js +1 -0
  7. package/dist/config-live.js +53 -0
  8. package/dist/config.js +38 -0
  9. package/dist/goal.js +81 -0
  10. package/dist/i18n/common.js +31 -0
  11. package/dist/i18n/config.js +20 -0
  12. package/dist/i18n/deepseek.js +14 -0
  13. package/dist/i18n/help.js +22 -2
  14. package/dist/i18n/qwen.js +4 -0
  15. package/dist/i18n/self-review.js +8 -0
  16. package/dist/i18n/spinner.js +6 -0
  17. package/dist/index.js +478 -65
  18. package/dist/input/layout.js +19 -0
  19. package/dist/input/suggest.js +13 -0
  20. package/dist/input.js +14 -3
  21. package/dist/net-capture.js +18 -0
  22. package/dist/qwen-browser.js +43 -20
  23. package/dist/run-task.js +97 -4
  24. package/dist/theme.js +7 -1
  25. package/dist/tool-summary.js +83 -0
  26. package/dist/tui/ConfigMenu.js +304 -0
  27. package/dist/tui/ConfirmDialog.js +42 -0
  28. package/dist/tui/FullScreenApp.js +85 -0
  29. package/dist/tui/InputLine.js +285 -0
  30. package/dist/tui/LogView.js +48 -0
  31. package/dist/tui/LoginForm.js +83 -0
  32. package/dist/tui/SelectList.js +50 -0
  33. package/dist/tui/StatusBar.js +140 -0
  34. package/dist/tui/TextPrompt.js +41 -0
  35. package/dist/tui/TuiApp.js +28 -0
  36. package/dist/tui/TuiEditor.js +490 -0
  37. package/dist/tui/edit.js +297 -0
  38. package/dist/tui/keys.js +162 -0
  39. package/dist/tui/log-window.js +93 -0
  40. package/dist/tui/login-pure.js +20 -0
  41. package/dist/tui/run-config-menu.js +19 -0
  42. package/dist/tui/run-confirm.js +22 -0
  43. package/dist/tui/run-login.js +43 -0
  44. package/dist/tui/run-select.js +18 -0
  45. package/dist/tui/store.js +104 -0
  46. package/dist/tui/types.js +10 -0
  47. package/dist/tui/useMouseWheel.js +113 -0
  48. package/dist/tui/widgets.js +55 -0
  49. package/package.json +6 -4
  50. package/scripts/coverage-gate.mts +10 -0
  51. package/scripts/render-demo.mts +1 -1
  52. package/scripts/tui-probe.tsx +62 -0
@@ -0,0 +1,304 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The Ink-based CONFIG MENU — a drop-in alternative to the hand-rolled
3
+ // `runConfigMenu` (src/config-menu.ts), used when `ui.tui` is enabled.
4
+ //
5
+ // It offers the same operations with a cleaner layout: a scrolling list of
6
+ // settings grouped by section, ↑/↓ (or j/k) to move, g to jump to the next
7
+ // group, Enter/Space to edit (boolean/enum toggle in place, number/string
8
+ // prompt), d twice to reset, / to filter, q/Esc to quit.
9
+ //
10
+ // All strings come from i18n (cfg.*), so the menu is fully localized. The
11
+ // component never touches config.ts directly: the caller passes get/set/reset
12
+ // callbacks (the SAME ones the legacy menu takes), so the logic is shared.
13
+ import { useMemo, useState } from 'react';
14
+ import { Box, Text, useInput, useApp, useWindowSize } from 'ink';
15
+ import { Checkbox, Select, UI, HintBar } from './widgets.js';
16
+ /** Strip ANSI to measure a line's visible width. */
17
+ function stripAnsi(s) {
18
+ return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
19
+ }
20
+ /** Visible width in columns (wide chars count 2). */
21
+ function visCols(s) {
22
+ const str = stripAnsi(s);
23
+ let w = 0;
24
+ for (const ch of str) {
25
+ const cp = ch.codePointAt(0);
26
+ w += cp >= 0x1100 ? 2 : 1;
27
+ }
28
+ return w;
29
+ }
30
+ function displayValue(field, value, t) {
31
+ if (value === undefined)
32
+ return t('cfg.menu.default');
33
+ if (typeof value === 'boolean')
34
+ return value ? t('common.on') : t('common.off');
35
+ if (/password/i.test(field.path) && String(value).length > 0)
36
+ return '********';
37
+ return String(value);
38
+ }
39
+ export function ConfigMenuApp(opts) {
40
+ const { fields, t, get, set, reset, getDefault } = opts;
41
+ const { exit } = useApp();
42
+ const { columns, rows } = useWindowSize();
43
+ const [cursor, setCursor] = useState(0);
44
+ const [filterQuery, setFilterQuery] = useState('');
45
+ const [filterMode, setFilterMode] = useState(false);
46
+ const [editing, setEditing] = useState(false);
47
+ const [editBuf, setEditBuf] = useState('');
48
+ const [pendingReset, setPendingReset] = useState(null);
49
+ const [message, setMessage] = useState(null);
50
+ const visible = useMemo(() => {
51
+ const q = filterQuery.trim().toLowerCase();
52
+ if (!q)
53
+ return fields;
54
+ return fields.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
55
+ .toLowerCase()
56
+ .includes(q));
57
+ }, [fields, filterQuery, t]);
58
+ const isModified = (f) => {
59
+ if (!getDefault)
60
+ return false;
61
+ const cur = get(f.path);
62
+ const def = getDefault(f.path);
63
+ try {
64
+ return JSON.stringify(cur) !== JSON.stringify(def);
65
+ }
66
+ catch {
67
+ return String(cur) !== String(def);
68
+ }
69
+ };
70
+ const commitEdit = () => {
71
+ const f = visible[cursor];
72
+ const raw = editBuf.trim();
73
+ setEditing(false);
74
+ setEditBuf('');
75
+ if (!f || !raw) {
76
+ setMessage(null);
77
+ return;
78
+ }
79
+ try {
80
+ set(f, raw);
81
+ setMessage(t('cfg.menu.saved', {
82
+ v: f.path,
83
+ value: displayValue(f, get(f.path), t),
84
+ }));
85
+ }
86
+ catch (e) {
87
+ setMessage(String(e.message || e));
88
+ }
89
+ };
90
+ const activate = () => {
91
+ const f = visible[cursor];
92
+ if (!f)
93
+ return;
94
+ if (f.type === 'boolean') {
95
+ set(f, get(f.path) === true ? 'false' : 'true');
96
+ setMessage(null);
97
+ return;
98
+ }
99
+ if (f.type === 'enum' && f.values && f.values.length) {
100
+ cycleEnum(f, 1);
101
+ return;
102
+ }
103
+ setEditing(true);
104
+ setEditBuf(get(f.path) === undefined ? '' : String(get(f.path)));
105
+ };
106
+ /** Cycle an enum value by `dir` (+1 next, -1 prev). */
107
+ const cycleEnum = (f, dir) => {
108
+ if (!f.values || !f.values.length)
109
+ return;
110
+ const cur = String(get(f.path) ?? '');
111
+ const idx = f.values.indexOf(cur);
112
+ const base = idx < 0 ? 0 : idx;
113
+ const next = f.values[(base + dir + f.values.length) % f.values.length];
114
+ set(f, next);
115
+ setMessage(null);
116
+ };
117
+ /** ←/→ acts on the focused row: cycle an enum, toggle a boolean. */
118
+ const lateral = (dir) => {
119
+ const f = visible[cursor];
120
+ if (!f)
121
+ return;
122
+ if (f.type === 'enum')
123
+ cycleEnum(f, dir);
124
+ else if (f.type === 'boolean') {
125
+ set(f, get(f.path) === true ? 'false' : 'true');
126
+ setMessage(null);
127
+ }
128
+ };
129
+ const quit = () => {
130
+ opts.onExit();
131
+ exit();
132
+ };
133
+ useInput((input, key) => {
134
+ if (editing) {
135
+ if (key.return) {
136
+ commitEdit();
137
+ return;
138
+ }
139
+ if (key.escape) {
140
+ setEditing(false);
141
+ setEditBuf('');
142
+ return;
143
+ }
144
+ if (key.ctrl && input === 'c') {
145
+ quit();
146
+ return;
147
+ }
148
+ if (key.backspace || key.delete) {
149
+ setEditBuf((b) => b.slice(0, -1));
150
+ return;
151
+ }
152
+ if (input && input >= ' ')
153
+ setEditBuf((b) => b + input);
154
+ return;
155
+ }
156
+ if (filterMode) {
157
+ if (key.return || key.escape) {
158
+ setFilterMode(false);
159
+ return;
160
+ }
161
+ if (key.ctrl && input === 'c') {
162
+ quit();
163
+ return;
164
+ }
165
+ if (key.backspace || key.delete) {
166
+ setFilterQuery((q) => q.slice(0, -1));
167
+ setCursor(0);
168
+ return;
169
+ }
170
+ if (input && input >= ' ') {
171
+ setFilterQuery((q) => q + input);
172
+ setCursor(0);
173
+ }
174
+ return;
175
+ }
176
+ if (key.escape ||
177
+ input === 'q' ||
178
+ input === 'Q' ||
179
+ (key.ctrl && input === 'c')) {
180
+ quit();
181
+ return;
182
+ }
183
+ if (input === '/') {
184
+ setFilterMode(true);
185
+ setMessage(null);
186
+ return;
187
+ }
188
+ if (key.leftArrow) {
189
+ lateral(-1);
190
+ return;
191
+ }
192
+ if (key.rightArrow) {
193
+ lateral(1);
194
+ return;
195
+ }
196
+ if (key.upArrow || input === 'k') {
197
+ setCursor((c) => visible.length ? (c - 1 + visible.length) % visible.length : 0);
198
+ setPendingReset(null);
199
+ setMessage(null);
200
+ return;
201
+ }
202
+ if (key.downArrow || input === 'j') {
203
+ setCursor((c) => (visible.length ? (c + 1) % visible.length : 0));
204
+ setPendingReset(null);
205
+ setMessage(null);
206
+ return;
207
+ }
208
+ if (input === 'g') {
209
+ // Jump to the first field of the NEXT group (and back to the top).
210
+ const curGroup = visible[cursor]?.groupKey;
211
+ let target = -1;
212
+ for (let k = cursor + 1; k < visible.length; k++) {
213
+ if (visible[k].groupKey !== curGroup) {
214
+ target = k;
215
+ break;
216
+ }
217
+ }
218
+ setCursor(target >= 0 ? target : 0);
219
+ setMessage(null);
220
+ return;
221
+ }
222
+ if (key.return || input === ' ') {
223
+ activate();
224
+ return;
225
+ }
226
+ if (input === 'd' || input === 'D') {
227
+ const f = visible[cursor];
228
+ if (!f)
229
+ return;
230
+ if (pendingReset === f.path) {
231
+ reset(f);
232
+ setMessage(t('cfg.reset', { v: f.path }));
233
+ setPendingReset(null);
234
+ }
235
+ else {
236
+ setPendingReset(f.path);
237
+ setMessage(t('cfg.menu.reset_confirm', { v: f.path }));
238
+ }
239
+ return;
240
+ }
241
+ });
242
+ // Windowed list: show a slice around the cursor, accounting for group headers.
243
+ const termRows = rows || 24;
244
+ const maxItemRows = Math.max(3, termRows - 6);
245
+ const shown = useMemo(() => {
246
+ // Walk back from the cursor until the window would overflow.
247
+ let start = Math.min(cursor, Math.max(0, visible.length - 1));
248
+ const groupStart = (i) => i === 0 || visible[i].groupKey !== visible[i - 1].groupKey;
249
+ const rowsFor = (from) => {
250
+ let r = 0;
251
+ for (let i = from; i <= cursor; i++) {
252
+ if (i === from || groupStart(i))
253
+ r++;
254
+ r++;
255
+ }
256
+ return r;
257
+ };
258
+ while (start > 0 && rowsFor(start - 1) <= maxItemRows)
259
+ start--;
260
+ return { start, end: Math.min(visible.length, start + maxItemRows) };
261
+ }, [cursor, visible, maxItemRows]);
262
+ const usable = Math.max(1, (columns || 80) - 1);
263
+ void visCols;
264
+ void usable;
265
+ const items = [];
266
+ let lastGroup = '';
267
+ for (let i = shown.start; i < shown.end; i++) {
268
+ const f = visible[i];
269
+ const g = f.groupKey;
270
+ if (i === shown.start || g !== lastGroup) {
271
+ items.push(_jsx(Text, { color: UI.system, children: ' ' + t(f.groupKey).toUpperCase() }, 'g' + i));
272
+ lastGroup = g;
273
+ }
274
+ const selected = i === cursor;
275
+ const label = f.labelKey ? t(f.labelKey) : f.path;
276
+ const raw = get(f.path);
277
+ const mod = isModified(f) ? '*' : '';
278
+ // The VALUE cell depends on the field type: a checkbox for booleans, an
279
+ // inline select for enums, plain text otherwise. The selected row shows
280
+ // the path on the right as a hint.
281
+ let valueCell;
282
+ if (f.type === 'boolean') {
283
+ valueCell = _jsx(Checkbox, { checked: raw === true, focused: selected });
284
+ }
285
+ else if (f.type === 'enum' && f.values && f.values.length) {
286
+ valueCell = (_jsx(Select, { value: String(raw ?? f.values[0]), focused: selected }));
287
+ }
288
+ else {
289
+ valueCell = (_jsx(Text, { color: selected ? UI.text : UI.system, children: displayValue(f, raw, t) }));
290
+ }
291
+ items.push(_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? '❯ ' : ' ' }), _jsx(Text, { color: selected ? UI.accent : undefined, 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));
292
+ }
293
+ if (!visible.length) {
294
+ items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
295
+ }
296
+ 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: t('cfg.menu.title') }), _jsx(Text, { color: UI.dim, children: (visible.length ? cursor + 1 : 0) + '/' + visible.length })] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), items, _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), 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, _jsx(HintBar, { hints: [
297
+ ['↑/↓', 'выбор'],
298
+ ['←/→', 'изменить'],
299
+ ['Enter', 'правка'],
300
+ ['/', 'поиск'],
301
+ ['d', 'сброс'],
302
+ ['q', 'выход'],
303
+ ] })] }));
304
+ }
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A reusable Ink CONFIRM dialog (y/N) — a small modal box with two buttons
3
+ // navigated by ←/→ (or y/n) and confirmed with Enter. Replaces the raw
4
+ // `promptOnce('... [y/N] ')` that paused the whole TUI.
5
+ import { useState } from 'react';
6
+ import { Box, Text, useInput, useApp } from 'ink';
7
+ import { UI, HintBar } from './widgets.js';
8
+ function Button({ label, focused, danger, }) {
9
+ const color = danger ? UI.err : UI.on;
10
+ return (_jsxs(Text, { children: [focused ? _jsx(Text, { color: color, children: '▸ ' }) : _jsx(Text, { children: ' ' }), _jsx(Text, { color: focused ? color : UI.dim, bold: focused, children: label })] }));
11
+ }
12
+ export function ConfirmDialog({ title, message, yesLabel, noLabel, onAnswer, }) {
13
+ // Focus starts on "no" — a stray Enter must not confirm a destructive action.
14
+ const [yes, setYes] = useState(false);
15
+ const { exit } = useApp();
16
+ const answer = (ok) => {
17
+ onAnswer(ok);
18
+ exit();
19
+ };
20
+ useInput((input, key) => {
21
+ if (key.escape || input === 'n' || input === 'N' || input === 'т') {
22
+ answer(false);
23
+ return;
24
+ }
25
+ if (input === 'y' || input === 'Y' || input === 'д' || input === 'Д') {
26
+ answer(true);
27
+ return;
28
+ }
29
+ if (key.leftArrow || key.rightArrow || key.tab) {
30
+ setYes((v) => !v);
31
+ return;
32
+ }
33
+ if (key.return) {
34
+ answer(yes);
35
+ }
36
+ });
37
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.warn, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: UI.warn, bold: true, children: '⚠ ' + title }), message ? _jsx(Text, { color: UI.text, children: message }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Button, { label: yesLabel, focused: yes, danger: true }), _jsx(Text, { children: ' ' }), _jsx(Button, { label: noLabel, focused: !yes })] }), _jsx(HintBar, { hints: [
38
+ ['←/→', 'выбор'],
39
+ ['Enter', 'ok'],
40
+ ['y/n', ''],
41
+ ] })] }));
42
+ }
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The FULL-SCREEN app layout (used when `ui.alternateScreen` is on).
3
+ //
4
+ // Unlike the inline layout (where Ink prints the log as <Static> scrollback),
5
+ // here the WHOLE terminal is ours: a header at the top, a SCROLLABLE log
6
+ // viewport in the middle and the live panel pinned at the bottom. There is no
7
+ // terminal reflow to fight, so a resize never leaves ghost lines — this is the
8
+ // layout Codex / opencode / Claude Code use.
9
+ //
10
+ // Scrolling: the viewport shows the newest content by default (scroll = 0);
11
+ // PgUp/PgDn (and the wheel) move a `scroll` offset in the store.
12
+ import { useSyncExternalStore } from 'react';
13
+ import { Box, Text, useWindowSize, useInput } from 'ink';
14
+ import { LogList } from './LogView.js';
15
+ import { windowByRows, totalRows } from './log-window.js';
16
+ import { StatusBar } from './StatusBar.js';
17
+ import { InputLine } from './InputLine.js';
18
+ import { contentWidth } from '../width.js';
19
+ import { translate } from '../i18n.js';
20
+ import { suggestRowCount } from '../input/suggest.js';
21
+ import { inputBoxRows } from '../input/layout.js';
22
+ import { useMouseWheel } from './useMouseWheel.js';
23
+ /** The header bar: title, version, working directory. */
24
+ function Header({ store }) {
25
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
26
+ const cols = Math.max(1, useWindowSize().columns || 80);
27
+ const right = [state.provider && '[' + state.provider + ']']
28
+ .filter(Boolean)
29
+ .join(' ');
30
+ const left = '❯ ' + state.title + (state.version ? ' v' + state.version : '');
31
+ const dir = state.workdir ? ' ' + state.workdir : '';
32
+ const gap = Math.max(1, cols - left.length - dir.length - right.length - 2);
33
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#c8b06a", bold: true, children: left }), _jsx(Text, { color: "#7fc4f0", children: dir }), _jsx(Text, { children: ' '.repeat(gap) }), right ? _jsx(Text, { color: "#6fd0b0", children: right }) : null] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(cols) })] }));
34
+ }
35
+ export function FullScreenApp({ store, inputProps, }) {
36
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
37
+ const { rows } = useWindowSize();
38
+ const termRows = rows || 24;
39
+ // Fixed panel height: header (2) + status (1) + the input box (2 borders +
40
+ // its VISUAL rows) + the suggestion popup rows. The log viewport gets the
41
+ // rest.
42
+ const HEADER_ROWS = 2;
43
+ const STATUS_ROWS = 1;
44
+ const cols = Math.max(1, useWindowSize().columns || 80);
45
+ // The box height comes from the SAME layoutInput() the box renders with, so
46
+ // the reserved rows and the drawn rows can never disagree (a logical-line
47
+ // count missed the word-wrap of a long line and the second line overprinted
48
+ // the first).
49
+ const INPUT_BOX_ROWS = inputBoxRows(state.prompt, state.edit.buf, state.edit.cursor, cols);
50
+ // The suggestion popup renders ABOVE the input box; reserve its rows so
51
+ // opening the list does not push the panel / shift the whole layout.
52
+ const suggestRows = suggestRowCount(state.edit.buf, state.commands);
53
+ const panelRows = HEADER_ROWS + STATUS_ROWS + INPUT_BOX_ROWS + suggestRows;
54
+ const logHeight = Math.max(1, termRows - panelRows);
55
+ // Window the log BY RENDERED ROW: a long wrapped answer can be taller than
56
+ // one entry, and an entry-count window either overflowed the panel or left
57
+ // it half empty. `scroll` is measured in ROWS here (0 = newest at the
58
+ // bottom).
59
+ const all = state.log;
60
+ const total = totalRows(all, cols);
61
+ const maxScroll = Math.max(0, total - logHeight);
62
+ const scroll = Math.max(0, Math.min(state.scroll, maxScroll));
63
+ const visible = windowByRows(all, logHeight, scroll, cols);
64
+ // Scroll the log with PgUp/PgDn.
65
+ useInput((_input, key) => {
66
+ const step = Math.max(1, Math.floor(logHeight / 2));
67
+ if (key.pageUp) {
68
+ store.set({ scroll: Math.min(maxScroll, scroll + step) });
69
+ }
70
+ else if (key.pageDown) {
71
+ store.set({ scroll: Math.max(0, scroll - step) });
72
+ }
73
+ });
74
+ // Scroll the log with the MOUSE WHEEL. Ink does not parse mouse events, so
75
+ // useMouseWheel reads the raw SGR sequences (see the hook). One wheel notch
76
+ // scrolls ~3 rows, like most terminals.
77
+ useMouseWheel((delta) => {
78
+ const next = scroll + delta * 3;
79
+ store.set({ scroll: Math.max(0, Math.min(maxScroll, next)) });
80
+ }, state.mouseScroll);
81
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column", height: logHeight, overflow: "hidden", justifyContent: "flex-end", children: all.length === 0 ? (_jsx(Text, { color: "#5b616e", children: translate(state.locale)('editor.empty_hint') })) : (_jsx(LogList, { entries: visible })) }), _jsx(StatusBar, { state: state, divider: false }), _jsx(InputLine, { ...inputProps, store: store,
82
+ // Cursor rows above the input box: header + log viewport + status.
83
+ cursorTop: HEADER_ROWS + logHeight + suggestRows + STATUS_ROWS + 1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked })] }));
84
+ }
85
+ void contentWidth;