zames_pro 2.73.11 → 2.75.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 (88) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +37 -7
  3. package/dist/attachments.js +6 -1
  4. package/dist/banner.js +71 -0
  5. package/dist/browser.js +84 -27
  6. package/dist/code-blocks.js +48 -0
  7. package/dist/commands.js +1 -0
  8. package/dist/config-live.js +53 -0
  9. package/dist/config.js +38 -0
  10. package/dist/goal.js +81 -0
  11. package/dist/i18n/common.js +35 -0
  12. package/dist/i18n/config.js +20 -0
  13. package/dist/i18n/deepseek.js +14 -0
  14. package/dist/i18n/help.js +30 -2
  15. package/dist/i18n/qwen.js +4 -0
  16. package/dist/i18n/self-review.js +8 -0
  17. package/dist/i18n/spinner.js +6 -0
  18. package/dist/index.js +487 -65
  19. package/dist/input/layout.js +19 -0
  20. package/dist/input/suggest.js +29 -1
  21. package/dist/input.js +14 -3
  22. package/dist/net-capture.js +18 -0
  23. package/dist/qwen-browser.js +43 -20
  24. package/dist/run-task.js +97 -4
  25. package/dist/theme.js +16 -1
  26. package/dist/tool-summary.js +83 -0
  27. package/dist/tui/ConfigMenu.js +337 -0
  28. package/dist/tui/ConfirmDialog.js +42 -0
  29. package/dist/tui/FullScreenApp.js +113 -0
  30. package/dist/tui/InputLine.js +318 -0
  31. package/dist/tui/LogView.js +48 -0
  32. package/dist/tui/LoginForm.js +83 -0
  33. package/dist/tui/SelectList.js +54 -0
  34. package/dist/tui/StatusBar.js +170 -0
  35. package/dist/tui/TextPrompt.js +41 -0
  36. package/dist/tui/TuiApp.js +29 -0
  37. package/dist/tui/TuiEditor.js +490 -0
  38. package/dist/tui/edit.js +317 -0
  39. package/dist/tui/keys.js +195 -0
  40. package/dist/tui/log-window.js +93 -0
  41. package/dist/tui/login-pure.js +20 -0
  42. package/dist/tui/run-config-menu.js +19 -0
  43. package/dist/tui/run-confirm.js +22 -0
  44. package/dist/tui/run-login.js +43 -0
  45. package/dist/tui/run-select.js +18 -0
  46. package/dist/tui/store.js +104 -0
  47. package/dist/tui/types.js +10 -0
  48. package/dist/tui/useMouseWheel.js +164 -0
  49. package/dist/tui/widgets.js +60 -0
  50. package/package.json +7 -3
  51. package/scripts/coverage-gate.mts +10 -0
  52. package/scripts/inline-status-driver.mts +31 -0
  53. package/scripts/inline-status.tsx +33 -0
  54. package/scripts/mouse-churn.mts +34 -0
  55. package/scripts/qwen-models-smoke.mts +117 -0
  56. package/scripts/render-demo.mts +1 -1
  57. package/scripts/tui-comp-shot.mts +63 -0
  58. package/scripts/tui-components.tsx +62 -0
  59. package/scripts/tui-config-driver.mts +82 -0
  60. package/scripts/tui-config-probe.tsx +74 -0
  61. package/scripts/tui-edit-driver.mts +100 -0
  62. package/scripts/tui-edit-probe.tsx +38 -0
  63. package/scripts/tui-editor-probe.tsx +24 -0
  64. package/scripts/tui-input-driver.mts +54 -0
  65. package/scripts/tui-input-shot.tsx +59 -0
  66. package/scripts/tui-log-driver.mts +35 -0
  67. package/scripts/tui-log-shot.tsx +48 -0
  68. package/scripts/tui-longpaste-driver.mts +56 -0
  69. package/scripts/tui-longpaste.tsx +35 -0
  70. package/scripts/tui-manyselect-shot.mts +34 -0
  71. package/scripts/tui-manyselect.tsx +17 -0
  72. package/scripts/tui-mouse-churn-probe.tsx +28 -0
  73. package/scripts/tui-paste-driver.mts +88 -0
  74. package/scripts/tui-paste-kinds.tsx +76 -0
  75. package/scripts/tui-pastetest.mts +38 -0
  76. package/scripts/tui-probe.tsx +62 -0
  77. package/scripts/tui-repro.tsx +64 -0
  78. package/scripts/tui-resize.mts +70 -0
  79. package/scripts/tui-scroll-driver.mts +53 -0
  80. package/scripts/tui-scroll-probe.tsx +46 -0
  81. package/scripts/tui-shot.mts +106 -0
  82. package/scripts/tui-status-driver.mts +24 -0
  83. package/scripts/tui-status-shot.tsx +27 -0
  84. package/scripts/tui-submit.tsx +57 -0
  85. package/scripts/tui-wheel-driver.mts +61 -0
  86. package/scripts/tui-wheel-probe.tsx +48 -0
  87. package/scripts/tui-widgets-driver.mts +20 -0
  88. package/scripts/tui-widgets.tsx +44 -0
@@ -0,0 +1,337 @@
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
+ // `get`/`set` mutate the LIVE config object in place, so React has no state
51
+ // change to react to and the row kept the OLD value on screen after a
52
+ // toggle/cycle (the operator saw «ru» while the config already held «en»).
53
+ // Bump this counter after every mutation to force a re-render.
54
+ const [version, setVersion] = useState(0);
55
+ const bump = () => setVersion((v) => v + 1);
56
+ const visible = useMemo(() => {
57
+ const q = filterQuery.trim().toLowerCase();
58
+ if (!q)
59
+ return fields;
60
+ return fields.filter((f) => (f.path + ' ' + t(f.labelKey || f.path) + ' ' + t(f.groupKey))
61
+ .toLowerCase()
62
+ .includes(q));
63
+ }, [fields, filterQuery, t]);
64
+ const isModified = (f) => {
65
+ if (!getDefault)
66
+ return false;
67
+ const cur = get(f.path);
68
+ const def = getDefault(f.path);
69
+ try {
70
+ return JSON.stringify(cur) !== JSON.stringify(def);
71
+ }
72
+ catch {
73
+ return String(cur) !== String(def);
74
+ }
75
+ };
76
+ const commitEdit = () => {
77
+ const f = visible[cursor];
78
+ const raw = editBuf.trim();
79
+ setEditing(false);
80
+ setEditBuf('');
81
+ if (!f || !raw) {
82
+ setMessage(null);
83
+ return;
84
+ }
85
+ try {
86
+ set(f, raw);
87
+ bump();
88
+ setMessage(t('cfg.menu.saved', {
89
+ v: f.path,
90
+ value: displayValue(f, get(f.path), t),
91
+ }));
92
+ }
93
+ catch (e) {
94
+ setMessage(String(e.message || e));
95
+ }
96
+ };
97
+ const activate = () => {
98
+ const f = visible[cursor];
99
+ if (!f)
100
+ return;
101
+ if (f.type === 'boolean') {
102
+ set(f, get(f.path) === true ? 'false' : 'true');
103
+ bump();
104
+ setMessage(null);
105
+ return;
106
+ }
107
+ if (f.type === 'enum' && f.values && f.values.length) {
108
+ cycleEnum(f, 1);
109
+ return;
110
+ }
111
+ setEditing(true);
112
+ setEditBuf(get(f.path) === undefined ? '' : String(get(f.path)));
113
+ };
114
+ /** Cycle an enum value by `dir` (+1 next, -1 prev). */
115
+ const cycleEnum = (f, dir) => {
116
+ if (!f.values || !f.values.length)
117
+ return;
118
+ const cur = String(get(f.path) ?? '');
119
+ const idx = f.values.indexOf(cur);
120
+ const base = idx < 0 ? 0 : idx;
121
+ const next = f.values[(base + dir + f.values.length) % f.values.length];
122
+ set(f, next);
123
+ bump();
124
+ setMessage(null);
125
+ };
126
+ /** ←/→ acts on the focused row: cycle an enum, toggle a boolean. */
127
+ const lateral = (dir) => {
128
+ const f = visible[cursor];
129
+ if (!f)
130
+ return;
131
+ if (f.type === 'enum')
132
+ cycleEnum(f, dir);
133
+ else if (f.type === 'boolean') {
134
+ set(f, get(f.path) === true ? 'false' : 'true');
135
+ bump();
136
+ setMessage(null);
137
+ }
138
+ };
139
+ const quit = () => {
140
+ opts.onExit();
141
+ exit();
142
+ };
143
+ useInput((input, key) => {
144
+ if (editing) {
145
+ if (key.return) {
146
+ commitEdit();
147
+ return;
148
+ }
149
+ if (key.escape) {
150
+ setEditing(false);
151
+ setEditBuf('');
152
+ return;
153
+ }
154
+ if (key.ctrl && input === 'c') {
155
+ quit();
156
+ return;
157
+ }
158
+ if (key.backspace || key.delete) {
159
+ setEditBuf((b) => b.slice(0, -1));
160
+ return;
161
+ }
162
+ if (input && input >= ' ')
163
+ setEditBuf((b) => b + input);
164
+ return;
165
+ }
166
+ if (filterMode) {
167
+ if (key.return || key.escape) {
168
+ setFilterMode(false);
169
+ return;
170
+ }
171
+ if (key.ctrl && input === 'c') {
172
+ quit();
173
+ return;
174
+ }
175
+ if (key.backspace || key.delete) {
176
+ setFilterQuery((q) => q.slice(0, -1));
177
+ setCursor(0);
178
+ return;
179
+ }
180
+ if (input && input >= ' ') {
181
+ setFilterQuery((q) => q + input);
182
+ setCursor(0);
183
+ }
184
+ return;
185
+ }
186
+ if (key.escape ||
187
+ input === 'q' ||
188
+ input === 'Q' ||
189
+ (key.ctrl && input === 'c')) {
190
+ quit();
191
+ return;
192
+ }
193
+ if (input === '/') {
194
+ setFilterMode(true);
195
+ setMessage(null);
196
+ return;
197
+ }
198
+ if (key.leftArrow) {
199
+ lateral(-1);
200
+ return;
201
+ }
202
+ if (key.rightArrow) {
203
+ lateral(1);
204
+ return;
205
+ }
206
+ if (key.upArrow || input === 'k') {
207
+ setCursor((c) => visible.length ? (c - 1 + visible.length) % visible.length : 0);
208
+ setPendingReset(null);
209
+ setMessage(null);
210
+ return;
211
+ }
212
+ if (key.downArrow || input === 'j') {
213
+ setCursor((c) => (visible.length ? (c + 1) % visible.length : 0));
214
+ setPendingReset(null);
215
+ setMessage(null);
216
+ return;
217
+ }
218
+ if (input === 'g') {
219
+ // Jump to the first field of the NEXT group (and back to the top).
220
+ const curGroup = visible[cursor]?.groupKey;
221
+ let target = -1;
222
+ for (let k = cursor + 1; k < visible.length; k++) {
223
+ if (visible[k].groupKey !== curGroup) {
224
+ target = k;
225
+ break;
226
+ }
227
+ }
228
+ setCursor(target >= 0 ? target : 0);
229
+ setMessage(null);
230
+ return;
231
+ }
232
+ if (key.return || input === ' ') {
233
+ activate();
234
+ return;
235
+ }
236
+ if (input === 'd' || input === 'D') {
237
+ const f = visible[cursor];
238
+ if (!f)
239
+ return;
240
+ if (pendingReset === f.path) {
241
+ reset(f);
242
+ bump();
243
+ setMessage(t('cfg.reset', { v: f.path }));
244
+ setPendingReset(null);
245
+ }
246
+ else {
247
+ setPendingReset(f.path);
248
+ setMessage(t('cfg.menu.reset_confirm', { v: f.path }));
249
+ }
250
+ return;
251
+ }
252
+ });
253
+ // Windowed list: show a slice around the cursor. The window is measured in
254
+ // RENDERED ROWS, not items: each new group adds a header row, AND a label
255
+ // long enough to wrap takes more than one row. Counting one row per item
256
+ // let the block grow taller than the terminal and the top border/title
257
+ // scrolled off ("cut off at the top") on a narrow terminal.
258
+ const termRows = rows || 24;
259
+ const maxRows = Math.max(3, termRows - 8);
260
+ const innerCols = Math.max(1, (columns || 80) - 4);
261
+ const shown = useMemo(() => {
262
+ const groupStart = (i) => i === 0 || visible[i].groupKey !== visible[i - 1].groupKey;
263
+ // Rendered height of one item: 1 base row + wrapped extra rows. The label
264
+ // plus the value cell plus the path hint is approximated by the raw label
265
+ // length; over-estimating is safe (it only shortens the window).
266
+ const itemRows = (i) => {
267
+ const f = visible[i];
268
+ const label = f.labelKey ? t(f.labelKey) : f.path;
269
+ return Math.max(1, Math.ceil((label.length + 12) / innerCols));
270
+ };
271
+ const rowsIn = (a, b) => {
272
+ let r = 0;
273
+ for (let i = a; i < b; i++) {
274
+ if (groupStart(i))
275
+ r++;
276
+ r += itemRows(i);
277
+ }
278
+ return r;
279
+ };
280
+ if (!visible.length)
281
+ return { start: 0, end: 0 };
282
+ let start = cursor;
283
+ let end = cursor + 1;
284
+ // Fill downward first, then upward, so the cursor stays visible and the
285
+ // window uses the whole budget.
286
+ while (end < visible.length && rowsIn(start, end + 1) <= maxRows)
287
+ end++;
288
+ while (start > 0 && rowsIn(start - 1, end) <= maxRows)
289
+ start--;
290
+ return { start, end };
291
+ }, [cursor, visible, maxRows, innerCols, t]);
292
+ const usable = Math.max(1, (columns || 80) - 1);
293
+ void visCols;
294
+ void usable;
295
+ // Referenced so the value-cell recompute below depends on the mutation
296
+ // counter (a pure `get()` call has no reactive dependency of its own).
297
+ void version;
298
+ const items = [];
299
+ let lastGroup = '';
300
+ for (let i = shown.start; i < shown.end; i++) {
301
+ const f = visible[i];
302
+ const g = f.groupKey;
303
+ if (i === shown.start || g !== lastGroup) {
304
+ items.push(_jsx(Text, { color: UI.system, children: ' ' + t(f.groupKey).toUpperCase() }, 'g' + i));
305
+ lastGroup = g;
306
+ }
307
+ const selected = i === cursor;
308
+ const label = f.labelKey ? t(f.labelKey) : f.path;
309
+ const raw = get(f.path);
310
+ const mod = isModified(f) ? '*' : '';
311
+ // The VALUE cell depends on the field type: a checkbox for booleans, an
312
+ // inline select for enums, plain text otherwise. The selected row shows
313
+ // the path on the right as a hint.
314
+ let valueCell;
315
+ if (f.type === 'boolean') {
316
+ valueCell = _jsx(Checkbox, { checked: raw === true, focused: selected });
317
+ }
318
+ else if (f.type === 'enum' && f.values && f.values.length) {
319
+ valueCell = (_jsx(Select, { value: String(raw ?? f.values[0]), focused: selected }));
320
+ }
321
+ else {
322
+ valueCell = (_jsx(Text, { color: selected ? UI.text : UI.system, children: displayValue(f, raw, t) }));
323
+ }
324
+ 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));
325
+ }
326
+ if (!visible.length) {
327
+ items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
328
+ }
329
+ 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: [
330
+ ['↑/↓', 'выбор'],
331
+ ['←/→', 'изменить'],
332
+ ['Enter', 'правка'],
333
+ ['/', 'поиск'],
334
+ ['d', 'сброс'],
335
+ ['q', 'выход'],
336
+ ] })] }));
337
+ }
@@ -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,113 @@
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, truncateToWidth, visLen } 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 rawDir = state.workdir ? ' ' + state.workdir : '';
32
+ // The header MUST stay exactly ONE text row: FullScreenApp reserves
33
+ // HEADER_ROWS = 2 (title + divider) and the whole panel height is computed
34
+ // from it. A long working directory used to WRAP the title row at a narrow
35
+ // terminal (or a deep path), adding a third row the layout did not know
36
+ // about — Ink then overflowed the fixed `height={termRows}` column and the
37
+ // TOP input row was clipped (the operator saw the second line overprint the
38
+ // first). Truncate the directory (then the title) so the row always fits.
39
+ const gap = Math.max(1, cols - visLen(left) - visLen(rawDir) - visLen(right) - 2);
40
+ let dir = rawDir;
41
+ const budget = cols - visLen(left) - visLen(right) - 2;
42
+ if (visLen(dir) > budget)
43
+ dir = budget > 0 ? truncateToWidth(dir, budget) : '';
44
+ // If even title + provider exceed the width, drop the directory entirely.
45
+ const finalDir = gap >= 1 ? dir : '';
46
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#c8b06a", bold: true, children: left }), _jsx(Text, { color: "#7fc4f0", children: finalDir }), _jsx(Text, { children: ' '.repeat(Math.max(1, cols - visLen(left) - visLen(finalDir) - visLen(right) - 2)) }), right ? _jsx(Text, { color: "#6fd0b0", children: right }) : null] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(cols) })] }));
47
+ }
48
+ export function FullScreenApp({ store, inputProps, }) {
49
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
50
+ const { rows } = useWindowSize();
51
+ const termRows = rows || 24;
52
+ // Fixed panel height: header (2) + status (1) + the input box (2 borders +
53
+ // its VISUAL rows) + the suggestion popup rows. The log viewport gets the
54
+ // rest.
55
+ const HEADER_ROWS = 2;
56
+ const STATUS_ROWS = 1;
57
+ const cols = Math.max(1, useWindowSize().columns || 80);
58
+ // The box height comes from the SAME layoutInput() the box renders with, so
59
+ // the reserved rows and the drawn rows can never disagree (a logical-line
60
+ // count missed the word-wrap of a long line and the second line overprinted
61
+ // the first).
62
+ const INPUT_BOX_ROWS = inputBoxRows(state.prompt, state.edit.buf, state.edit.cursor, cols);
63
+ // The suggestion popup renders ABOVE the input box; reserve its rows so
64
+ // opening the list does not push the panel / shift the whole layout.
65
+ const suggestRows = suggestRowCount(state.edit.buf, state.commands, cols);
66
+ const panelRows = HEADER_ROWS + STATUS_ROWS + INPUT_BOX_ROWS + suggestRows;
67
+ const logHeight = Math.max(1, termRows - panelRows);
68
+ // Window the log BY RENDERED ROW: a long wrapped answer can be taller than
69
+ // one entry, and an entry-count window either overflowed the panel or left
70
+ // it half empty. `scroll` is measured in ROWS here (0 = newest at the
71
+ // bottom).
72
+ const all = state.log;
73
+ const total = totalRows(all, cols);
74
+ const maxScroll = Math.max(0, total - logHeight);
75
+ const scroll = Math.max(0, Math.min(state.scroll, maxScroll));
76
+ const visible = windowByRows(all, logHeight, scroll, cols);
77
+ // Scroll the log with PgUp/PgDn; Home/End jump to the very top/bottom. The
78
+ // input line OWNS Home/End while its buffer is non-empty (they move the
79
+ // caret), so the jump only applies to an EMPTY buffer — which is exactly
80
+ // when the operator is navigating the log to select a large block.
81
+ //
82
+ // `useInput` binds the callback ONCE, so a `scroll` captured in the closure
83
+ // goes STALE after the first scroll (PgUp/PgDn/Home/End then compute from an
84
+ // old offset and jump wrong). Read the live value from the store instead.
85
+ useInput((_input, key) => {
86
+ const step = Math.max(1, Math.floor(logHeight / 2));
87
+ const cur = store.getSnapshot().scroll;
88
+ if (key.pageUp) {
89
+ store.set({ scroll: Math.min(maxScroll, cur + step) });
90
+ }
91
+ else if (key.pageDown) {
92
+ store.set({ scroll: Math.max(0, cur - step) });
93
+ }
94
+ else if (state.edit.buf.length === 0 && (key.home || key.end)) {
95
+ // Both the bare and the Ctrl-modified forms land here (Ink sets
96
+ // key.end / key.home for either), so Ctrl+End / Ctrl+Home are a
97
+ // reliable fallback on terminals that swallow the bare keys.
98
+ store.set({ scroll: key.home ? maxScroll : 0 });
99
+ }
100
+ });
101
+ // Scroll the log with the MOUSE WHEEL. Ink does not parse mouse events, so
102
+ // useMouseWheel reads the raw SGR sequences (see the hook). One wheel notch
103
+ // scrolls ~3 rows, like most terminals.
104
+ useMouseWheel((delta) => {
105
+ const cur = store.getSnapshot().scroll;
106
+ const next = cur + delta * 3;
107
+ store.set({ scroll: Math.max(0, Math.min(maxScroll, next)) });
108
+ }, state.mouseScroll);
109
+ 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,
110
+ // Cursor rows above the input box: header + log viewport + status.
111
+ cursorTop: HEADER_ROWS + logHeight + suggestRows + STATUS_ROWS + 1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked })] }));
112
+ }
113
+ void contentWidth;