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,285 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The live INPUT LINE — a wrapping, multi-line editor rendered by Ink with a
3
+ // real terminal cursor (useCursor), so IME composition and the caret land on
4
+ // the right cell.
5
+ //
6
+ // The editor state transitions are PURE (src/tui/edit.ts) and the key mapping
7
+ // is PURE (src/tui/keys.ts). This component owns only the imperative parts:
8
+ // reading key events from Ink's useInput, bracketed paste from usePaste,
9
+ // calling the operator callbacks (submit / abort / attach), and painting the
10
+ // visual rows via the shared layoutInput.
11
+ import { useCallback, useMemo, useRef, useState } from 'react';
12
+ import { Box, Text, useInput, usePaste, useCursor, useWindowSize } from 'ink';
13
+ import { translate } from '../i18n.js';
14
+ import { layoutInput, visLen, charWidth, SUGGEST_PAGE, } from '../input/layout.js';
15
+ import { insertText, insertPaste, insertNewline, backspace, del, moveLeft, moveRight, moveHome, moveEnd, wordLeft, wordRight, deleteWordLeft, deleteWordRight, killToLineEnd, moveVisualRows, pushUndo, undoEdit, historyUp, historyDown, searchStart, searchNext, searchAccept, searchCancel, searchInput, searchBackspace, recordHistory, afterSubmit, } from './edit.js';
16
+ import { dispatchKey } from './keys.js';
17
+ import { useSyncExternalStore } from 'react';
18
+ import { matchSlashCommands, suggestWindow, completeSlashCommand, } from '../input/suggest.js';
19
+ /** Render the visual rows of the buffer with the prompt on the first row. */
20
+ function InputRows({ prompt, buf, cols, }) {
21
+ const lay = layoutInput(prompt, buf, 0, cols);
22
+ return (_jsx(Box, { flexDirection: "column", children: lay.rows.map((r, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "#c8b06a", bold: true, children: r.prefix }), r.text] }, i))) }));
23
+ }
24
+ export function InputLine(props) {
25
+ const { columns } = useWindowSize();
26
+ const cols = columns || 80;
27
+ const { setCursorPosition } = useCursor();
28
+ const [suggestSelected, setSuggestSelected] = useState(-1);
29
+ // The visible page of the suggestion list follows the SELECTION: with more
30
+ // matches than one page, Ctrl+N could move the selection past the page and
31
+ // the highlight vanished (the operator "lost" the selected command). Derive
32
+ // the offset from the selection instead of keeping a separate state.
33
+ const suggestOffset = suggestSelected < 0 ? 0 : suggestSelected - (suggestSelected % SUGGEST_PAGE);
34
+ // The editor state lives in the STORE (props.store), read via
35
+ // useSyncExternalStore. This is what makes the typed line survive an Ink
36
+ // remount (a resize / pause): a local useState would be reset to empty on
37
+ // every remount, wiping what the operator had typed.
38
+ const st = useSyncExternalStore(props.store.subscribe, () => props.store.getSnapshot().edit);
39
+ const setSt = useCallback((next) => {
40
+ props.store.update((snap) => ({
41
+ edit: typeof next === 'function' ? next(snap.edit) : next,
42
+ }));
43
+ }, [props.store]);
44
+ // The input lives inside a rounded box: 2 columns of border (left/right) plus
45
+ // 1 column of inner padding on each side. The layout must wrap to the INNER
46
+ // width, or a long line would overflow the frame.
47
+ const innerCols = Math.max(4, cols - 4);
48
+ // Mirror the state into a ref so the useInput callback (a stable closure Ink
49
+ // calls for every key) always sees the latest values.
50
+ const stRef = useRef(st);
51
+ stRef.current = st;
52
+ // Layout for the CURRENT state — cursor positioning + visual-edge check.
53
+ const lay = useMemo(() => layoutInput(props.prompt, st.buf, st.cursor, innerCols), [props.prompt, st.buf, st.cursor, innerCols]);
54
+ const suggestions = useMemo(() => matchSlashCommands(st.buf, props.commands), [st.buf, props.commands]);
55
+ const apply = useCallback((fn) => {
56
+ setSt((prev) => {
57
+ const next = fn(prev);
58
+ if (next.pastes !== prev.pastes)
59
+ props.onPastesChange(next.pastes);
60
+ return next;
61
+ });
62
+ }, [props]);
63
+ const doSubmit = useCallback(() => {
64
+ const s = stRef.current;
65
+ if (props.locked)
66
+ return;
67
+ if (!s.buf.trim())
68
+ return;
69
+ const withHist = recordHistory(s, s.buf);
70
+ const next = afterSubmit(withHist);
71
+ setSt(next);
72
+ props.onPastesChange(next.pastes);
73
+ props.onHistoryChange(withHist.history);
74
+ props.onSubmit(s.buf, props.attachments.slice());
75
+ }, [props]);
76
+ const handlePaste = useCallback(async (text) => {
77
+ const trimmed = String(text).trim();
78
+ const looksPath = !/\s/.test(trimmed) && /[./\\]/.test(trimmed) && trimmed.length < 500;
79
+ if (looksPath || trimmed.startsWith('data:')) {
80
+ try {
81
+ const att = await props.onAttach(text);
82
+ if (att) {
83
+ apply((s) => insertText(s, att.marker));
84
+ return;
85
+ }
86
+ }
87
+ catch {
88
+ // fall through to plain insert
89
+ }
90
+ }
91
+ apply((s) => insertPaste(s, text));
92
+ }, [apply, props]);
93
+ const tryClipboard = useCallback(async () => {
94
+ try {
95
+ const att = await props.onClipboard();
96
+ if (att)
97
+ apply((s) => insertText(s, att.marker));
98
+ }
99
+ catch {
100
+ // ignore
101
+ }
102
+ }, [apply, props]);
103
+ const runAction = useCallback((action) => {
104
+ if (!action)
105
+ return;
106
+ switch (action.type) {
107
+ case 'submit':
108
+ doSubmit();
109
+ return;
110
+ case 'newline':
111
+ apply(insertNewline);
112
+ return;
113
+ case 'insert':
114
+ apply((s) => pushUndo(insertText(s, action.text), true));
115
+ return;
116
+ case 'backspace':
117
+ apply((s) => backspace(pushUndo(s, true)));
118
+ return;
119
+ case 'delete':
120
+ apply((s) => del(pushUndo(s, true)));
121
+ return;
122
+ case 'left':
123
+ apply(moveLeft);
124
+ return;
125
+ case 'right':
126
+ apply(moveRight);
127
+ return;
128
+ case 'up':
129
+ apply((s) => moveVisualRows(s, -1, props.prompt, innerCols));
130
+ return;
131
+ case 'down':
132
+ apply((s) => moveVisualRows(s, 1, props.prompt, innerCols));
133
+ return;
134
+ case 'home':
135
+ apply(moveHome);
136
+ return;
137
+ case 'end':
138
+ apply(moveEnd);
139
+ return;
140
+ case 'wordLeft':
141
+ apply(wordLeft);
142
+ return;
143
+ case 'wordRight':
144
+ apply(wordRight);
145
+ return;
146
+ case 'deleteWordLeft':
147
+ apply((s) => deleteWordLeft(pushUndo(s)));
148
+ return;
149
+ case 'deleteWordRight':
150
+ apply((s) => deleteWordRight(pushUndo(s)));
151
+ return;
152
+ case 'killToEnd':
153
+ apply((s) => killToLineEnd(pushUndo(s)));
154
+ return;
155
+ case 'clearLine':
156
+ apply((s) => ({ ...pushUndo(s), buf: '', cursor: 0, pastes: [] }));
157
+ return;
158
+ case 'undoEdit':
159
+ apply(undoEdit);
160
+ return;
161
+ case 'complete':
162
+ apply((s) => {
163
+ const next = completeSlashCommand(s.buf, matchSlashCommands(s.buf, props.commands), suggestSelected);
164
+ if (next === null)
165
+ return s;
166
+ return { ...s, buf: next, cursor: Array.from(next).length };
167
+ });
168
+ return;
169
+ case 'suggestNext':
170
+ setSuggestSelected((n) => Math.min(suggestions.length - 1, (n < 0 ? -1 : n) + 1));
171
+ return;
172
+ case 'suggestPrev':
173
+ setSuggestSelected((n) => Math.max(-1, n - 1));
174
+ return;
175
+ case 'historyPrev':
176
+ apply(historyUp);
177
+ return;
178
+ case 'historyNext':
179
+ apply(historyDown);
180
+ return;
181
+ case 'searchStart':
182
+ apply(searchStart);
183
+ return;
184
+ case 'searchNext':
185
+ apply(searchNext);
186
+ return;
187
+ case 'searchAccept':
188
+ apply(searchAccept);
189
+ return;
190
+ case 'searchCancel':
191
+ apply(searchCancel);
192
+ return;
193
+ case 'searchInput':
194
+ apply((s) => searchInput(s, action.text));
195
+ return;
196
+ case 'searchBackspace':
197
+ apply(searchBackspace);
198
+ return;
199
+ case 'escape':
200
+ props.onEscape();
201
+ return;
202
+ case 'ctrlC':
203
+ case 'ctrlD':
204
+ props.onCtrlC();
205
+ return;
206
+ case 'clearScreen':
207
+ props.onClearScreen();
208
+ return;
209
+ case 'clipboard':
210
+ void tryClipboard();
211
+ return;
212
+ case 'copyCode':
213
+ props.onCopyCode();
214
+ return;
215
+ default:
216
+ return;
217
+ }
218
+ }, [
219
+ apply,
220
+ props,
221
+ suggestions,
222
+ suggestSelected,
223
+ innerCols,
224
+ doSubmit,
225
+ tryClipboard,
226
+ ]);
227
+ useInput((input, key) => {
228
+ const s = stRef.current;
229
+ if (props.locked) {
230
+ if (key.ctrl && input === 'c')
231
+ props.onCtrlC();
232
+ return;
233
+ }
234
+ const curLay = layoutInput(props.prompt, s.buf, s.cursor, innerCols);
235
+ const atTop = curLay.cursorRow === 0;
236
+ const atBottom = curLay.cursorRow === curLay.rows.length - 1;
237
+ const action = dispatchKey(input, key, {
238
+ searchMode: s.searchMode,
239
+ bufferEmpty: s.buf.length === 0,
240
+ // A trailing backslash before the cursor = line continuation: Enter
241
+ // then inserts a newline (and drops the backslash).
242
+ endsWithBackslash: Array.from(s.buf)[s.cursor - 1] === '\\',
243
+ }, { atTop, atBottom });
244
+ runAction(action);
245
+ }, { isActive: true });
246
+ // Bracketed paste arrives on a SEPARATE channel — Ink enables the paste mode
247
+ // automatically. Deliver the whole string to the same handler.
248
+ usePaste((text) => {
249
+ void handlePaste(text);
250
+ });
251
+ // Position the real terminal cursor on the input cell. `y` is the row within
252
+ // the live Ink output BELOW the log. Panel layout (top to bottom):
253
+ // row 0 = divider
254
+ // row 1 = status bar
255
+ // row 2 = input box TOP border
256
+ // rows 3.. = input rows
257
+ const cursorRow = lay.cursorRow;
258
+ const rowText = lay.rows[cursorRow]?.text ?? '';
259
+ const cursorX =
260
+ // The input rows sit inside the rounded box, indented by 1 column for the
261
+ // left border and 1 for the box's inner padding.
262
+ 2 +
263
+ visLen(lay.rows[cursorRow]?.prefix ?? '') +
264
+ colWidth(rowText, lay.cursorCol - visLen(lay.rows[cursorRow]?.prefix ?? ''));
265
+ setCursorPosition({ x: cursorX, y: (props.cursorTop ?? 3) + cursorRow });
266
+ const { shown, moreCount } = suggestWindow(suggestions, suggestOffset);
267
+ const t = translate(props.locale);
268
+ return (_jsxs(Box, { flexDirection: "column", children: [suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [shown.map((c, i) => {
269
+ const label = c.hint ? c.name + ' ' + c.hint : c.name;
270
+ const selected = suggestOffset + i === suggestSelected;
271
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "#c8b06a", children: selected ? ' ▸ ' : ' ' }), _jsx(Text, { color: "#c8b06a", children: label }), _jsx(Text, { color: "#5b616e", children: ' ' + c.description })] }, c.name));
272
+ }), moreCount > 0 && (_jsx(Text, { color: "#5b616e", children: ' ' +
273
+ t('editor.more', { n: String(moreCount) }) +
274
+ ' ' +
275
+ t('editor.page_hint') }))] })), _jsx(Box, { borderStyle: "round", borderColor: props.locked ? '#5b616e' : '#c8b06a', paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: _jsx(InputRows, { prompt: props.prompt, buf: st.buf, cols: innerCols }) })] }));
276
+ }
277
+ /** Visible width of the first `col` code points of `text` (wide chars = 2). */
278
+ function colWidth(text, col) {
279
+ const chars = Array.from(text);
280
+ let w = 0;
281
+ for (let i = 0; i < col && i < chars.length; i++) {
282
+ w += charWidth(chars[i].codePointAt(0));
283
+ }
284
+ return Math.max(0, w);
285
+ }
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Static, Box, Text } from 'ink';
3
+ import { theme } from '../theme.js';
4
+ /** One log entry. `raw` items already carry ANSI colors and are printed
5
+ * verbatim; structured items get a themed prefix. */
6
+ export function LogRow({ entry }) {
7
+ if (entry.raw) {
8
+ // Pre-rendered (markdown answer, tool preview). Print as-is; Ink passes
9
+ // embedded ANSI escape sequences through to the terminal.
10
+ return _jsx(Text, { children: entry.text });
11
+ }
12
+ switch (entry.kind) {
13
+ case 'user':
14
+ return _jsx(Text, { children: theme.user('❯ ') + entry.text });
15
+ case 'assistant':
16
+ return _jsx(Text, { children: entry.text });
17
+ case 'tool':
18
+ return _jsx(Text, { children: entry.text });
19
+ case 'toolResult':
20
+ return _jsx(Text, { children: entry.text });
21
+ case 'warning':
22
+ return _jsx(Text, { children: theme.warn(entry.text) });
23
+ case 'summary':
24
+ return _jsx(Text, { children: entry.text });
25
+ case 'system':
26
+ default:
27
+ return _jsx(Text, { children: theme.system(entry.text) });
28
+ }
29
+ }
30
+ /**
31
+ * A NON-Static log list — a plain column of rows. Used in FULL-SCREEN mode,
32
+ * where <Static> does NOT work: Static is absolutely positioned and is ignored
33
+ * by a fixed-height flex layout (the rows would not appear inside our
34
+ * viewport). The caller windows the entries, so only the visible slice is
35
+ * rendered each frame.
36
+ */
37
+ export function LogList({ entries, }) {
38
+ return (_jsx(Box, { flexDirection: "column", children: entries.map((entry) => (_jsx(LogRow, { entry: entry }, entry.id))) }));
39
+ }
40
+ /**
41
+ * The INLINE log — rendered through Ink's <Static>, which prints each item
42
+ * ONCE above the live area and never repaints it. This is the right tool for
43
+ * the inline layout (the terminal keeps the scrollback), but it must NOT be
44
+ * used inside a fixed-height flex box (see LogList).
45
+ */
46
+ export function LogView({ entries, }) {
47
+ return (_jsx(Static, { items: entries, children: (entry) => (_jsx(Box, { flexDirection: "column", children: _jsx(LogRow, { entry: entry }) }, entry.id)) }));
48
+ }
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A TUI LOGIN form (username + masked password) rendered by Ink.
3
+ //
4
+ // The browser's own interactive login uses readline / askPassword, which
5
+ // PAUSES the whole TUI (Ink loses the terminal to readline and both fight for
6
+ // stdin). This form keeps the login INSIDE the TUI: the browser calls an
7
+ // injected `onLoginPrompt` callback and never touches stdin itself.
8
+ //
9
+ // It is deliberately simple (two fields, Tab/↑/↓ to switch, Enter to submit,
10
+ // Esc to cancel) — a login is a rare, focused step, not a place for menus.
11
+ import { useState } from 'react';
12
+ import { Box, Text, useInput, useApp } from 'ink';
13
+ import { UI, HintBar } from './widgets.js';
14
+ import { maskSecret, validateLogin } from './login-pure.js';
15
+ /** Mask every character of the password with `*`. */
16
+ function mask(s) {
17
+ return maskSecret(s);
18
+ }
19
+ export function LoginForm({ t, initialUsername = '', initialPassword = '', note, onResult, }) {
20
+ const { exit } = useApp();
21
+ const [field, setField] = useState(initialUsername ? 'pass' : 'user');
22
+ const [user, setUser] = useState(initialUsername);
23
+ const [pass, setPass] = useState(initialPassword);
24
+ const [error, setError] = useState('');
25
+ const finish = (res) => {
26
+ onResult(res);
27
+ exit();
28
+ };
29
+ const submit = () => {
30
+ const v = validateLogin(user, pass);
31
+ if (!v.ok) {
32
+ setError(t(v.reason === 'user' ? 'auth.form.need_user' : 'auth.form.need_pass'));
33
+ setField(v.field);
34
+ return;
35
+ }
36
+ finish({ username: user.trim(), password: pass });
37
+ };
38
+ const append = (ch) => {
39
+ setError('');
40
+ if (field === 'user')
41
+ setUser((v) => v + ch);
42
+ else
43
+ setPass((v) => v + ch);
44
+ };
45
+ const backspace = () => {
46
+ setError('');
47
+ if (field === 'user')
48
+ setUser((v) => Array.from(v).slice(0, -1).join(''));
49
+ else
50
+ setPass((v) => Array.from(v).slice(0, -1).join(''));
51
+ };
52
+ useInput((input, key) => {
53
+ if (key.escape) {
54
+ finish(null);
55
+ return;
56
+ }
57
+ if (key.return) {
58
+ // Enter on the username moves to the password first (so a two-step
59
+ // prompt feels natural); Enter on the password submits.
60
+ if (field === 'user') {
61
+ setField('pass');
62
+ return;
63
+ }
64
+ submit();
65
+ return;
66
+ }
67
+ if (key.tab || key.upArrow || key.downArrow) {
68
+ setField((f) => (f === 'user' ? 'pass' : 'user'));
69
+ return;
70
+ }
71
+ if (key.backspace || key.delete) {
72
+ backspace();
73
+ return;
74
+ }
75
+ if (input && input >= ' ')
76
+ append(input);
77
+ });
78
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.title, bold: true, children: '🔐 ' + t('auth.form.title') }), note ? _jsx(Text, { color: UI.dim, children: note }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: field === 'user' ? UI.accent : UI.dim, children: (field === 'user' ? '❯ ' : ' ') + t('auth.form.login') + ' ' }), _jsx(Text, { color: UI.text, children: user }), field === 'user' ? _jsx(Text, { color: UI.accent, children: "\u258F" }) : null] }), _jsxs(Box, { children: [_jsx(Text, { color: field === 'pass' ? UI.accent : UI.dim, children: (field === 'pass' ? '❯ ' : ' ') + t('auth.form.password') + ' ' }), _jsx(Text, { color: UI.text, children: mask(pass) }), field === 'pass' ? _jsx(Text, { color: UI.accent, children: "\u258F" }) : null] }), error ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: UI.err, children: '⚠ ' + error }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(HintBar, { hints: [
79
+ ['Tab/↑↓', t('auth.form.hint_field')],
80
+ ['Enter', t('auth.form.hint_submit')],
81
+ ['Esc', t('auth.form.hint_cancel')],
82
+ ] }) })] }));
83
+ }
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A reusable Ink SELECT LIST — a scrollable, keyboard-navigable list of items
3
+ // with a title and an optional detail/description per row. Used by the model
4
+ // picker and any future list menu (chats, sessions, ...).
5
+ import { useMemo, useState } from 'react';
6
+ import { Box, Text, useInput, useApp, useWindowSize } from 'ink';
7
+ import { UI, HintBar } from './widgets.js';
8
+ export function SelectList({ title, items, initial, onSelect, }) {
9
+ const { exit } = useApp();
10
+ const { rows } = useWindowSize();
11
+ const start = Math.max(0, items.findIndex((it) => it.value === initial));
12
+ const [cursor, setCursor] = useState(start);
13
+ const maxRows = Math.max(3, (rows || 24) - 7);
14
+ const win = useMemo(() => {
15
+ let top = Math.max(0, cursor - maxRows + 1);
16
+ if (cursor < top)
17
+ top = cursor;
18
+ return { top, end: Math.min(items.length, top + maxRows) };
19
+ }, [cursor, items.length, maxRows]);
20
+ const finish = (value) => {
21
+ onSelect(value);
22
+ exit();
23
+ };
24
+ useInput((input, key) => {
25
+ if (key.escape || (key.ctrl && input === 'c') || input === 'q') {
26
+ finish(null);
27
+ return;
28
+ }
29
+ if (key.upArrow || input === 'k') {
30
+ setCursor((c) => items.length ? (c - 1 + items.length) % items.length : 0);
31
+ return;
32
+ }
33
+ if (key.downArrow || input === 'j') {
34
+ setCursor((c) => (items.length ? (c + 1) % items.length : 0));
35
+ return;
36
+ }
37
+ if (key.return) {
38
+ finish(items[cursor]?.value ?? null);
39
+ }
40
+ });
41
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + title }), _jsx(Text, { color: UI.dim, children: items.length })] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(40) }), items.slice(win.top, win.end).map((it, i) => {
42
+ const idx = win.top + i;
43
+ const sel = idx === cursor;
44
+ return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: it.label }), it.detail ? _jsx(Text, { color: UI.dim, children: ' ' + it.detail }) : null] }, it.value));
45
+ }), _jsx(HintBar, { hints: [
46
+ ['↑/↓', 'выбор'],
47
+ ['Enter', 'ok'],
48
+ ['Esc', 'отмена'],
49
+ ] })] }));
50
+ }
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useAnimation, useWindowSize } from 'ink';
3
+ import { translate } from '../i18n.js';
4
+ import { DOTS, stripEllipsis } from '../spinner.js';
5
+ import { truncateToWidth } from '../input/layout.js';
6
+ /** Elapsed seconds of the current animated phase ("0s", "45s", "2m 05s"). */
7
+ function elapsedLabel(startedAt, animating) {
8
+ if (!animating || !startedAt)
9
+ return '';
10
+ const sec = Math.floor((Date.now() - startedAt) / 1000);
11
+ const m = Math.floor(sec / 60);
12
+ const s = sec % 60;
13
+ const text = m > 0 ? m + 'm ' + String(s).padStart(2, '0') + 's' : s + 's';
14
+ return ' · ' + text;
15
+ }
16
+ /** Lifecycle prefix, colored by meaning (generating / settled). */
17
+ function stateLabel(state, locale) {
18
+ if (state.sendState === 'generating') {
19
+ return translate(locale)('state.generating') + ':';
20
+ }
21
+ if (state.sendState === 'settled') {
22
+ return translate(locale)('state.settled') + ':';
23
+ }
24
+ return '';
25
+ }
26
+ /** Visible width of the badge row (used to reserve space for the left
27
+ * status). An emoji is counted as 2 columns; the rest as 1. Approximation is
28
+ * fine — the goal is only to avoid clipping the badges. */
29
+ function badgeTextWidth(state) {
30
+ let w = 0;
31
+ if (state.statusLine)
32
+ w += state.statusLine.length + 1;
33
+ if (state.provider)
34
+ w += state.provider.length + 3;
35
+ if (state.queueLength > 0)
36
+ w += 2 + String(state.queueLength).length;
37
+ if (state.codeBlocks.length > 0)
38
+ w += 8;
39
+ if (state.tasks)
40
+ w += state.tasks.length + 1;
41
+ if (state.thinkingEnabled)
42
+ w += 3;
43
+ if (state.searchEnabled)
44
+ w += 3;
45
+ if (state.contextText) {
46
+ const marker = state.contextLevel === 'high' ? 2 : state.contextLevel === 'warn' ? 2 : 0;
47
+ w += state.contextText.length + marker;
48
+ }
49
+ return w;
50
+ }
51
+ /** The badge row: statusLine, provider, queue, tasks, toggles, context. */
52
+ function Badges({ state }) {
53
+ const parts = [];
54
+ if (state.statusLine) {
55
+ parts.push(_jsxs(Text, { color: "#808896", children: [state.statusLine, ' '] }, "sl"));
56
+ }
57
+ if (state.provider) {
58
+ parts.push(_jsx(Text, { color: "#808896", children: '[' + state.provider + '] ' }, "prov"));
59
+ }
60
+ if (state.queueLength > 0) {
61
+ parts.push(_jsxs(Text, { color: "#c9a86a", children: ['⧗' + state.queueLength, ' '] }, "q"));
62
+ }
63
+ if (state.codeBlocks.length > 0) {
64
+ // A hint that the last answer has copyable code: Ctrl+Y copies the last
65
+ // block (or the focused one), like a copy button in a GUI chat.
66
+ const idx = state.codeFocus >= 0 ? state.codeFocus + 1 : state.codeBlocks.length;
67
+ parts.push(_jsx(Text, { color: "#7fc4f0", children: '⌘ ' + idx + '/' + state.codeBlocks.length + ' ' }, "code"));
68
+ }
69
+ if (state.tasks) {
70
+ parts.push(_jsxs(Text, { color: "#808896", children: [state.tasks, ' '] }, "t"));
71
+ }
72
+ const toggles = [];
73
+ if (state.thinkingEnabled)
74
+ toggles.push('🧠\uFE0E');
75
+ if (state.searchEnabled)
76
+ toggles.push('🌐\uFE0E');
77
+ if (toggles.length) {
78
+ parts.push(_jsx(Text, { color: "#6fd0b0", children: toggles.join(' ') + ' ' }, "tg"));
79
+ }
80
+ if (state.contextText) {
81
+ const color = state.contextLevel === 'high'
82
+ ? '#c98a80'
83
+ : state.contextLevel === 'warn'
84
+ ? '#c9a86a'
85
+ : '#a9c08c';
86
+ const marker = state.contextLevel === 'high'
87
+ ? '⛔ '
88
+ : state.contextLevel === 'warn'
89
+ ? '▲ '
90
+ : '';
91
+ parts.push(_jsx(Text, { color: color, children: marker + state.contextText }, "ctx"));
92
+ }
93
+ if (!parts.length)
94
+ return null;
95
+ return _jsx(Box, { children: parts });
96
+ }
97
+ export function StatusBar({ state, divider = true, }) {
98
+ // Drive the spinner from Ink's shared animation timer (one timer for the
99
+ // whole tree). The interval matches the old 400ms dot cadence.
100
+ const animating = state.busy || !!state.animBase;
101
+ const { frame } = useAnimation({ interval: 400, isActive: animating });
102
+ const t = translate(state.locale);
103
+ const stateLbl = stateLabel(state, state.locale);
104
+ let left;
105
+ if (state.pendingText) {
106
+ left = '✎ ' + state.pendingText + ' · ' + t('spinner.hint');
107
+ }
108
+ else if (state.animBase) {
109
+ const dots = DOTS[frame % DOTS.length] ?? '';
110
+ const pad = ' '.slice(dots.length);
111
+ left =
112
+ stripEllipsis(state.animBase) +
113
+ dots +
114
+ pad +
115
+ elapsedLabel(state.animStart, animating) +
116
+ ' · ' +
117
+ t('spinner.hint');
118
+ }
119
+ else if (state.statusText) {
120
+ left = state.statusText;
121
+ }
122
+ else {
123
+ left = '';
124
+ }
125
+ const prefix = stateLbl ? stateLbl + ' ' : '';
126
+ // When the log is scrolled up, show a marker in the status line (NOT as a
127
+ // separate row below the input — that pushed the panel up and made the input
128
+ // jump). It replaces the spinner text while scrolled, so it never adds a row.
129
+ const scrollNote = state.scroll > 0 ? `↑ прокручено на ${state.scroll} стр. · PgDn/End` : '';
130
+ const leftText = scrollNote || prefix + left;
131
+ const showLeft = !!leftText;
132
+ // Keep the badge row visible: the left status truncates to whatever space is
133
+ // left after the badges, instead of pushing them off the right edge (they
134
+ // used to be clipped: "[DeepSee⧗1 задачи: 3/...").
135
+ const cols = useWindowSize().columns || 80;
136
+ const badgeWidth = badgeTextWidth(state);
137
+ const maxLeft = Math.max(0, cols - badgeWidth - 2);
138
+ const leftOut = showLeft ? truncateToWidth(leftText, maxLeft) : '';
139
+ return (_jsxs(Box, { flexDirection: "column", children: [divider ? _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(cols) }) : null, _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { children: leftOut ? _jsx(Text, { color: "#a1723f", children: leftOut }) : _jsx(Text, { children: " " }) }), _jsx(Badges, { state: state })] })] }));
140
+ }
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A one-line Ink TEXT PROMPT (label + single input + Enter/Esc).
3
+ //
4
+ // Used for steps the browser asks for interactively (currently the DeepSeek
5
+ // email one-time code) so they stay INSIDE the TUI instead of falling back to
6
+ // promptOnce/readline, which pauses Ink and fights it for stdin.
7
+ import { useState } from 'react';
8
+ import { Box, Text, useInput, useApp } from 'ink';
9
+ import { UI, HintBar } from './widgets.js';
10
+ import { maskSecret } from './login-pure.js';
11
+ function mask(s) {
12
+ return maskSecret(s);
13
+ }
14
+ export function TextPrompt({ label, secret, t, onResult, }) {
15
+ const { exit } = useApp();
16
+ const [value, setValue] = useState('');
17
+ const finish = (v) => {
18
+ onResult(v);
19
+ exit();
20
+ };
21
+ useInput((input, key) => {
22
+ if (key.escape) {
23
+ finish(null);
24
+ return;
25
+ }
26
+ if (key.return) {
27
+ finish(value.trim());
28
+ return;
29
+ }
30
+ if (key.backspace || key.delete) {
31
+ setValue((v) => Array.from(v).slice(0, -1).join(''));
32
+ return;
33
+ }
34
+ if (input && input >= ' ')
35
+ setValue((v) => v + input);
36
+ });
37
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.text, children: label }), _jsxs(Text, { color: UI.accent, children: ['❯ ', secret ? mask(value) : value, '▏'] }), _jsx(HintBar, { hints: [
38
+ ['Enter', t('auth.form.hint_submit')],
39
+ ['Esc', t('auth.form.hint_cancel')],
40
+ ] })] }));
41
+ }