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,317 @@
1
+ // PURE input-editing logic for the Ink TUI — the state transitions of the
2
+ // input line, extracted from the old LineEditor so they are unit-testable
3
+ // without a terminal.
4
+ //
5
+ // The editor state is a plain object (buffer, cursor, history, pastes, ...).
6
+ // Each action returns a NEW state (or the same object when nothing changed), so
7
+ // the React layer can `setState` cheaply. Terminal I/O, rendering and timers
8
+ // live in the component; everything here is a pure function of (state, action).
9
+ import { layoutInput, pasteReplacement, visLen, charWidth, NL, } from '../input/layout.js';
10
+ export function initialEditState() {
11
+ return {
12
+ buf: '',
13
+ cursor: 0,
14
+ pastes: [],
15
+ history: [],
16
+ histIndex: 0,
17
+ histDraft: '',
18
+ searchMode: false,
19
+ searchQuery: '',
20
+ searchIndex: -1,
21
+ searchSavedBuf: '',
22
+ undo: [],
23
+ typingRun: false,
24
+ };
25
+ }
26
+ /** Chars (code points) of the buffer. */
27
+ function charsOf(s) {
28
+ return Array.from(s.buf);
29
+ }
30
+ /** Insert a string at the cursor. */
31
+ export function insertText(s, text) {
32
+ const chars = charsOf(s);
33
+ const ins = Array.from(String(text));
34
+ const next = chars.slice(0, s.cursor).concat(ins, chars.slice(s.cursor));
35
+ return { ...s, buf: next.join(''), cursor: s.cursor + ins.length };
36
+ }
37
+ /** Push an undo snapshot. `coalesce` keeps one snapshot per typing run. */
38
+ export function pushUndo(s, coalesce = false) {
39
+ if (coalesce && s.typingRun)
40
+ return s;
41
+ const undo = s.undo.concat({ buf: s.buf, cursor: s.cursor });
42
+ if (undo.length > 200)
43
+ undo.shift();
44
+ return { ...s, undo, typingRun: coalesce };
45
+ }
46
+ export function undoEdit(s) {
47
+ if (!s.undo.length)
48
+ return s;
49
+ const undo = s.undo.slice(0, -1);
50
+ const prev = s.undo[s.undo.length - 1];
51
+ return {
52
+ ...s,
53
+ buf: prev.buf,
54
+ cursor: Math.min(prev.cursor, Array.from(prev.buf).length),
55
+ undo,
56
+ typingRun: false,
57
+ };
58
+ }
59
+ export function backspace(s) {
60
+ if (s.cursor <= 0)
61
+ return s;
62
+ const chars = charsOf(s);
63
+ chars.splice(s.cursor - 1, 1);
64
+ return { ...s, buf: chars.join(''), cursor: s.cursor - 1 };
65
+ }
66
+ export function del(s) {
67
+ const chars = charsOf(s);
68
+ if (s.cursor >= chars.length)
69
+ return s;
70
+ chars.splice(s.cursor, 1);
71
+ return { ...s, buf: chars.join('') };
72
+ }
73
+ /** Delete the word on the LEFT of the cursor (Ctrl+W / Alt+Backspace). */
74
+ export function deleteWordLeft(s) {
75
+ const chars = charsOf(s);
76
+ let i = s.cursor;
77
+ while (i > 0 && /\s/.test(chars[i - 1]))
78
+ i--;
79
+ while (i > 0 && !/\s/.test(chars[i - 1]))
80
+ i--;
81
+ chars.splice(i, s.cursor - i);
82
+ return { ...s, buf: chars.join(''), cursor: i };
83
+ }
84
+ /**
85
+ * Delete the word on the RIGHT of the cursor (Ctrl+Delete / Alt+Delete).
86
+ *
87
+ * FALLBACK: at the END of the line there is no word to the right, so the key
88
+ * did NOTHING and the operator (whose terminal sends the standard xterm
89
+ * Ctrl+Delete) read it as "Ctrl+Delete does not delete the word before the
90
+ * cursor". Deleting the word on the LEFT there matches what other terminal
91
+ * editors do and what the operator expects, while keeping the standard
92
+ * right-delete when the cursor is mid-line.
93
+ */
94
+ export function deleteWordRight(s) {
95
+ const chars = charsOf(s);
96
+ // Nothing to the right (end of buffer or only whitespace to the newline):
97
+ // delete the word on the LEFT instead of doing nothing.
98
+ let probe = s.cursor;
99
+ while (probe < chars.length &&
100
+ chars[probe] !== NL &&
101
+ /\s/.test(chars[probe])) {
102
+ probe++;
103
+ }
104
+ if (probe >= chars.length || chars[probe] === NL) {
105
+ return deleteWordLeft(s);
106
+ }
107
+ let i = s.cursor;
108
+ while (i < chars.length && /\s/.test(chars[i]))
109
+ i++;
110
+ while (i < chars.length && !/\s/.test(chars[i]))
111
+ i++;
112
+ chars.splice(s.cursor, i - s.cursor);
113
+ return { ...s, buf: chars.join('') };
114
+ }
115
+ /** Delete from the cursor to the end of the current LOGICAL line (Ctrl+K). */
116
+ export function killToLineEnd(s) {
117
+ const arr = charsOf(s);
118
+ let e = s.cursor;
119
+ while (e < arr.length && arr[e] !== NL)
120
+ e++;
121
+ arr.splice(s.cursor, e - s.cursor);
122
+ return { ...s, buf: arr.join('') };
123
+ }
124
+ export function moveLeft(s) {
125
+ return s.cursor > 0 ? { ...s, cursor: s.cursor - 1 } : s;
126
+ }
127
+ export function moveRight(s) {
128
+ return s.cursor < Array.from(s.buf).length
129
+ ? { ...s, cursor: s.cursor + 1 }
130
+ : s;
131
+ }
132
+ export function moveHome(s) {
133
+ const chars = charsOf(s);
134
+ let i = s.cursor;
135
+ while (i > 0 && chars[i - 1] !== NL)
136
+ i--;
137
+ return i === s.cursor ? s : { ...s, cursor: i };
138
+ }
139
+ export function moveEnd(s) {
140
+ const chars = charsOf(s);
141
+ let i = s.cursor;
142
+ while (i < chars.length && chars[i] !== NL)
143
+ i++;
144
+ return i === s.cursor ? s : { ...s, cursor: i };
145
+ }
146
+ export function wordLeft(s) {
147
+ const chars = charsOf(s);
148
+ let i = s.cursor;
149
+ while (i > 0 && /\s/.test(chars[i - 1]))
150
+ i--;
151
+ while (i > 0 && !/\s/.test(chars[i - 1]))
152
+ i--;
153
+ return i === s.cursor ? s : { ...s, cursor: i };
154
+ }
155
+ export function wordRight(s) {
156
+ const chars = charsOf(s);
157
+ let i = s.cursor;
158
+ while (i < chars.length && /\s/.test(chars[i]))
159
+ i++;
160
+ while (i < chars.length && !/\s/.test(chars[i]))
161
+ i++;
162
+ return i === s.cursor ? s : { ...s, cursor: i };
163
+ }
164
+ /**
165
+ * Move the cursor `delta` VISUAL rows (wrapping-aware) and land in the column
166
+ * closest to the current one. Returns the same object when the target row is
167
+ * off the buffer (so Up/Down fall back to history).
168
+ */
169
+ export function moveVisualRows(s, delta, promptStr, cols) {
170
+ const lay = layoutInput(promptStr, s.buf, s.cursor, cols);
171
+ const target = lay.cursorRow + delta;
172
+ if (target < 0 || target >= lay.rows.length)
173
+ return s;
174
+ const row = lay.rows[target];
175
+ const rowChars = Array.from(row.text);
176
+ const promptW = visLen(row.prefix);
177
+ const want = Math.max(0, lay.cursorCol - promptW);
178
+ let idx = row.start;
179
+ let w = 0;
180
+ while (idx < row.start + rowChars.length && w < want) {
181
+ w += charWidth(rowChars[idx - row.start].codePointAt(0));
182
+ idx++;
183
+ }
184
+ return { ...s, cursor: idx };
185
+ }
186
+ /** Insert a newline; a trailing «\» before the cursor is removed first. */
187
+ export function insertNewline(s) {
188
+ const chars = charsOf(s);
189
+ if (s.cursor > 0 && chars[s.cursor - 1] === '\\') {
190
+ chars.splice(s.cursor - 1, 1);
191
+ const s2 = { ...s, buf: chars.join(''), cursor: s.cursor - 1 };
192
+ return insertText(s2, NL);
193
+ }
194
+ return insertText(s, NL);
195
+ }
196
+ /** Insert a pasted block: small pastes as-is, large ones as a marker. */
197
+ export function insertPaste(s, raw) {
198
+ const rep = pasteReplacement(raw);
199
+ if (!rep) {
200
+ const text = String(raw).split('\r\n').join(NL).split('\r').join(NL);
201
+ return insertText(s, text);
202
+ }
203
+ return { ...insertText(s, rep.marker), pastes: s.pastes.concat(rep) };
204
+ }
205
+ // ---------- history ----------
206
+ export function historyUp(s) {
207
+ if (!s.history.length)
208
+ return s;
209
+ let histIndex = s.histIndex;
210
+ let histDraft = s.histDraft;
211
+ if (histIndex === s.history.length)
212
+ histDraft = s.buf;
213
+ if (histIndex > 0) {
214
+ histIndex--;
215
+ const buf = s.history[histIndex];
216
+ return {
217
+ ...s,
218
+ histIndex,
219
+ histDraft,
220
+ buf,
221
+ cursor: Array.from(buf).length,
222
+ };
223
+ }
224
+ return { ...s, histDraft };
225
+ }
226
+ export function historyDown(s) {
227
+ if (!s.history.length)
228
+ return s;
229
+ let histIndex = s.histIndex;
230
+ let buf;
231
+ if (histIndex < s.history.length - 1) {
232
+ histIndex++;
233
+ buf = s.history[histIndex];
234
+ }
235
+ else {
236
+ histIndex = s.history.length;
237
+ buf = s.histDraft;
238
+ }
239
+ return { ...s, histIndex, buf, cursor: Array.from(buf).length };
240
+ }
241
+ export function searchStart(s) {
242
+ if (!s.history.length)
243
+ return s;
244
+ const st = {
245
+ ...s,
246
+ searchMode: true,
247
+ searchQuery: '',
248
+ searchIndex: s.history.length - 1,
249
+ searchSavedBuf: s.buf,
250
+ };
251
+ return searchRefresh(st, true);
252
+ }
253
+ export function searchRefresh(s, resetToNewest) {
254
+ const q = s.searchQuery;
255
+ const index = resetToNewest ? s.history.length - 1 : s.searchIndex;
256
+ for (let i = index; i >= 0; i--) {
257
+ if (q === '' || s.history[i].toLowerCase().includes(q.toLowerCase())) {
258
+ const buf = s.history[i];
259
+ return {
260
+ ...s,
261
+ searchIndex: i,
262
+ buf,
263
+ cursor: Array.from(buf).length,
264
+ };
265
+ }
266
+ }
267
+ return { ...s, searchIndex: -1 };
268
+ }
269
+ export function searchNext(s) {
270
+ if (s.searchIndex > 0) {
271
+ const prev = s.searchIndex;
272
+ const st = searchRefresh({ ...s, searchIndex: prev - 1 }, false);
273
+ if (st.searchIndex < 0)
274
+ return { ...st, searchIndex: prev };
275
+ return st;
276
+ }
277
+ return s;
278
+ }
279
+ export function searchAccept(s) {
280
+ return { ...s, searchMode: false, searchQuery: '' };
281
+ }
282
+ export function searchCancel(s) {
283
+ return {
284
+ ...s,
285
+ searchMode: false,
286
+ searchQuery: '',
287
+ buf: s.searchSavedBuf,
288
+ cursor: Array.from(s.searchSavedBuf).length,
289
+ };
290
+ }
291
+ export function searchInput(s, ch) {
292
+ return searchRefresh({ ...s, searchQuery: s.searchQuery + ch }, true);
293
+ }
294
+ export function searchBackspace(s) {
295
+ if (!s.searchQuery.length)
296
+ return s;
297
+ return searchRefresh({ ...s, searchQuery: s.searchQuery.slice(0, -1) }, true);
298
+ }
299
+ /** Record a submitted message in the history (dedup the last one). */
300
+ export function recordHistory(s, text) {
301
+ if (!text.trim() || s.history[s.history.length - 1] === text) {
302
+ return { ...s, histIndex: s.history.length, histDraft: '' };
303
+ }
304
+ const history = s.history.concat(text);
305
+ return { ...s, history, histIndex: history.length, histDraft: '' };
306
+ }
307
+ /** Reset the buffer after a submit (keeps history/pastes cleared). */
308
+ export function afterSubmit(s) {
309
+ return {
310
+ ...s,
311
+ buf: '',
312
+ cursor: 0,
313
+ pastes: [],
314
+ histIndex: s.history.length,
315
+ histDraft: '',
316
+ };
317
+ }
@@ -0,0 +1,195 @@
1
+ // PURE key dispatch: turn an Ink (input, key) pair into an editor action.
2
+ //
3
+ // Ink normalizes terminal escape sequences into `input` + a `Key` object
4
+ // (ctrl/meta/shift/arrows/…), so this module does NOT need the raw CSI parsing
5
+ // the old LineEditor had. It maps the normalized keys to the same bindings the
6
+ // operator is used to (Ctrl+W, Ctrl+K, Ctrl+U, Ctrl+_, word movement, history,
7
+ // reverse search, Tab completion, Alt+B/F).
8
+ //
9
+ // Kept free of React/Ink runtime imports beyond the Key TYPE, so it is
10
+ // unit-testable.
11
+ const LF = String.fromCharCode(10);
12
+ /**
13
+ * Ctrl/Alt + named keys under the KITTY keyboard protocol (CSI-u). Ink's
14
+ * `parseKeypress` recognizes the MODIFIED ARROW forms but returns `name=''`
15
+ * for the modified Delete/Backspace codepoints (CSI 3;5u, 127;5u, ...), so
16
+ * `useInput` yields an empty input/name and the binding never fires — the
17
+ * operator's "Ctrl+Delete does nothing". The legacy LineEditor parsed these
18
+ * bytes itself; the TUI reads them from raw stdin (Ink hides `sequence`) and
19
+ * maps the known ones to the SAME actions so both editors behave alike.
20
+ *
21
+ * Returns null when the string is not one of these sequences. Pure + exported
22
+ * for tests.
23
+ */
24
+ export function parseCsiU(raw) {
25
+ // CSI codepoint ; modifier [: eventType] u — e.g. ESC[3;5u (Ctrl+Delete),
26
+ // ESC[127;5u (Ctrl+Backspace), ESC[3u (plain Delete),
27
+ // ESC[127u (plain Backspace), ESC[13;5u (Ctrl+Enter).
28
+ const m = /^\u001b\[(\d+)(?:;(\d+))?(?::\d+)?u$/.exec(raw);
29
+ if (!m)
30
+ return null;
31
+ const cp = Number(m[1]);
32
+ const mod = m[2] ? Number(m[2]) - 1 : 0;
33
+ const ctrl = (mod & 4) !== 0;
34
+ const alt = (mod & 2) !== 0;
35
+ if (cp === 13 && (ctrl || alt))
36
+ return { type: 'newline' };
37
+ if (cp === 3) {
38
+ return ctrl || alt ? { type: 'deleteWordRight' } : { type: 'delete' };
39
+ }
40
+ if (cp === 127) {
41
+ return ctrl || alt ? { type: 'deleteWordLeft' } : { type: 'backspace' };
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * Map a normalized Ink keypress to an editor action.
47
+ *
48
+ * `isVisualEdge` reports whether the cursor is on the first/last VISUAL line so
49
+ * Up/Down act as history only at the edges (inside a multiline input they move
50
+ * by visual rows). The component computes it from the live layout and passes it
51
+ * in, keeping this function pure.
52
+ */
53
+ export function dispatchKey(input, key, ctx, isVisualEdge) {
54
+ // ---- reverse search modal state ----
55
+ if (ctx.searchMode) {
56
+ if (key.ctrl && input === 'r')
57
+ return { type: 'searchNext' };
58
+ if (key.return)
59
+ return { type: 'searchAccept' };
60
+ if (key.escape)
61
+ return { type: 'searchCancel' };
62
+ if (key.ctrl && input === 'c')
63
+ return { type: 'searchCancel' };
64
+ if (key.backspace || key.delete)
65
+ return { type: 'searchBackspace' };
66
+ if (input && input >= ' ' && !key.ctrl && !key.meta) {
67
+ return { type: 'searchInput', text: input };
68
+ }
69
+ return null;
70
+ }
71
+ // ---- control combos ----
72
+ if (key.ctrl) {
73
+ // Ctrl+Enter (kitty protocol only) inserts a newline. Ink reports it as
74
+ // return=true + ctrl=true, so it must be checked BEFORE the plain-return
75
+ // branch below (which would otherwise SUBMIT).
76
+ if (key.return)
77
+ return { type: 'newline' };
78
+ // Ctrl + a NAMED key (arrows, Delete, Backspace) arrives with an EMPTY
79
+ // `input` and the named flag set. These are the word-movement bindings:
80
+ // Ctrl+Left/Right move by word, Ctrl+Delete/Backspace delete a word.
81
+ // They MUST be checked before the `switch (input)` below, whose `default:
82
+ // return null` used to swallow them silently (Ctrl+arrows did nothing).
83
+ if (key.leftArrow)
84
+ return { type: 'wordLeft' };
85
+ if (key.rightArrow)
86
+ return { type: 'wordRight' };
87
+ if (key.delete)
88
+ return { type: 'deleteWordRight' };
89
+ if (key.backspace)
90
+ return { type: 'deleteWordLeft' };
91
+ if (key.home)
92
+ return { type: 'home' };
93
+ if (key.end)
94
+ return { type: 'end' };
95
+ switch (input) {
96
+ case 'c':
97
+ return { type: 'ctrlC' };
98
+ case 'd':
99
+ return ctx.bufferEmpty ? { type: 'ctrlD' } : { type: 'delete' };
100
+ case 'a':
101
+ return { type: 'home' };
102
+ case 'e':
103
+ return { type: 'end' };
104
+ case 'b':
105
+ return { type: 'left' };
106
+ case 'f':
107
+ return { type: 'right' };
108
+ case 'w':
109
+ return { type: 'deleteWordLeft' };
110
+ case 'k':
111
+ return { type: 'killToEnd' };
112
+ case 'u':
113
+ return { type: 'clearLine' };
114
+ case 'l':
115
+ return { type: 'clearScreen' };
116
+ case '_':
117
+ return { type: 'undoEdit' };
118
+ case 'r':
119
+ return { type: 'searchStart' };
120
+ case 'n':
121
+ return { type: 'suggestNext' };
122
+ case 'p':
123
+ return { type: 'suggestPrev' };
124
+ case 'v':
125
+ return { type: 'clipboard' };
126
+ case 'y':
127
+ return { type: 'copyCode' };
128
+ case 'j':
129
+ return { type: 'newline' };
130
+ default:
131
+ return null;
132
+ }
133
+ }
134
+ // ---- Alt / Meta combos (word movement + Alt+Enter newline) ----
135
+ if (key.meta) {
136
+ // Alt+Enter inserts a newline (send without submitting).
137
+ if (key.return)
138
+ return { type: 'newline' };
139
+ if (input === 'b')
140
+ return { type: 'wordLeft' };
141
+ if (input === 'f')
142
+ return { type: 'wordRight' };
143
+ if (key.backspace)
144
+ return { type: 'deleteWordLeft' };
145
+ if (key.delete)
146
+ return { type: 'deleteWordRight' };
147
+ return null;
148
+ }
149
+ // ---- LF as a bare newline (Ctrl+J) ----
150
+ // Ctrl+J sends LF (0x0A). Ink maps LF to `name='enter'` — which the public
151
+ // `Key` type does NOT expose — and leaves `key.ctrl` FALSE, so the ctrl
152
+ // branch above never fires. Without this guard LF fell through every branch
153
+ // (`input >= ' '` is false for 0x0A) and Ctrl+J did NOTHING. Treat a lone
154
+ // LF as a newline; a plain Enter (CR) still submits below.
155
+ if (input === LF)
156
+ return { type: 'newline' };
157
+ // ---- named keys ----
158
+ // Enter inserts a NEWLINE (not submit) when Shift+Enter is pressed, when a
159
+ // trailing backslash precedes the cursor (line continuation), or via
160
+ // Alt+Enter / Ctrl+J above. A PLAIN Enter submits.
161
+ if (key.return) {
162
+ if (key.shift)
163
+ return { type: 'newline' };
164
+ if (ctx.endsWithBackslash)
165
+ return { type: 'newline' };
166
+ return { type: 'submit' };
167
+ }
168
+ if (key.tab)
169
+ return { type: 'complete' };
170
+ if (key.backspace)
171
+ return { type: 'backspace' };
172
+ if (key.delete)
173
+ return { type: 'delete' };
174
+ if (key.leftArrow)
175
+ return { type: 'left' };
176
+ if (key.rightArrow)
177
+ return { type: 'right' };
178
+ if (key.upArrow) {
179
+ return isVisualEdge.atTop ? { type: 'historyPrev' } : { type: 'up' };
180
+ }
181
+ if (key.downArrow) {
182
+ return isVisualEdge.atBottom ? { type: 'historyNext' } : { type: 'down' };
183
+ }
184
+ if (key.home)
185
+ return { type: 'home' };
186
+ if (key.end)
187
+ return { type: 'end' };
188
+ if (key.escape)
189
+ return { type: 'escape' };
190
+ // ---- printable text ----
191
+ if (input && input >= ' ') {
192
+ return { type: 'insert', text: input };
193
+ }
194
+ return null;
195
+ }
@@ -0,0 +1,93 @@
1
+ // PURE windowing of the full-screen log BY RENDERED ROW.
2
+ //
3
+ // The old viewport sliced `log` by ENTRY COUNT: one long wrapped answer could
4
+ // fill (or overflow) the panel while a run of short tool lines left it half
5
+ // empty. Windowing by rows needs each entry's rendered height, which this
6
+ // module estimates from the text and the terminal width. It is an ESTIMATE on
7
+ // purpose: measuring the real Yoga layout per entry would need a ref per row
8
+ // and a re-render, while counting wrapped lines is exact enough for a log and
9
+ // costs nothing.
10
+ /** Visible width of a line without ANSI escapes (wide chars count 2). */
11
+ function visWidth(s) {
12
+ let w = 0;
13
+ let i = 0;
14
+ while (i < s.length) {
15
+ if (s.charCodeAt(i) === 27) {
16
+ i++;
17
+ if (s[i] === '[') {
18
+ i++;
19
+ while (i < s.length && !/[A-Za-z]/.test(s[i]))
20
+ i++;
21
+ i++;
22
+ }
23
+ continue;
24
+ }
25
+ const cp = s.codePointAt(i);
26
+ // Wide ranges (CJK/emoji) count 2; combining marks 0. Approximate — a log
27
+ // windowing error of one cell is invisible.
28
+ if (cp >= 0x1f300 && cp <= 0x1faff)
29
+ w += 2;
30
+ else if (cp >= 0x1100 && cp <= 0x9fff)
31
+ w += 2;
32
+ else if ((cp >= 0x300 && cp <= 0x36f) || cp === 0x200d)
33
+ w += 0;
34
+ else
35
+ w += 1;
36
+ i += cp > 0xffff ? 2 : 1;
37
+ }
38
+ return w;
39
+ }
40
+ /**
41
+ * How many terminal rows one log entry occupies at `cols` columns. A line
42
+ * longer than the width wraps (Ink uses hard wrapping for these rows). An
43
+ * empty line still takes one row.
44
+ */
45
+ export function entryRows(entry, cols) {
46
+ const width = Math.max(1, cols);
47
+ const lines = String(entry.text ?? '').split(String.fromCharCode(10));
48
+ let rows = 0;
49
+ for (const line of lines) {
50
+ rows += Math.max(1, Math.ceil(visWidth(line) / width));
51
+ }
52
+ return Math.max(1, rows);
53
+ }
54
+ /**
55
+ * Slice the log so its RENDERED height fits `height` rows, keeping `scroll`
56
+ * rows hidden from the bottom (0 = pinned to the newest). Returns the visible
57
+ * entries. A single entry taller than the viewport is kept whole (its tail is
58
+ * shown) rather than dropped.
59
+ */
60
+ export function windowByRows(entries, height, scroll, cols) {
61
+ const cap = Math.max(1, Math.floor(height));
62
+ // Walk from the newest entry backwards, accumulating rows, skipping `scroll`
63
+ // rows from the bottom first.
64
+ let skip = Math.max(0, Math.floor(scroll));
65
+ const picked = [];
66
+ let used = 0;
67
+ for (let i = entries.length - 1; i >= 0; i--) {
68
+ const r = entryRows(entries[i], cols);
69
+ if (skip > 0) {
70
+ if (skip >= r) {
71
+ skip -= r;
72
+ continue;
73
+ }
74
+ // Partially scrolled into this entry: keep the hidden part accounted.
75
+ skip = 0;
76
+ }
77
+ if (used + r > cap && picked.length > 0)
78
+ break;
79
+ picked.push(entries[i]);
80
+ used += r;
81
+ if (used >= cap)
82
+ break;
83
+ }
84
+ picked.reverse();
85
+ return picked;
86
+ }
87
+ /** Total rendered rows of a list of entries (for maxScroll). */
88
+ export function totalRows(entries, cols) {
89
+ let n = 0;
90
+ for (const e of entries)
91
+ n += entryRows(e, cols);
92
+ return n;
93
+ }
@@ -0,0 +1,20 @@
1
+ // PURE helpers for the Ink login / text-prompt forms. Kept separate from the
2
+ // components so the masking and validation are unit-tested without rendering.
3
+ /** Mask every character of a secret with `*` (code-point aware). */
4
+ export function maskSecret(s) {
5
+ return '*'.repeat(Array.from(String(s ?? '')).length);
6
+ }
7
+ /**
8
+ * Validate the login form. Username is trimmed; the password is used as-is
9
+ * (leading/trailing spaces are legal in a password). Empty fields are
10
+ * rejected with the field to focus.
11
+ */
12
+ export function validateLogin(username, password) {
13
+ if (!String(username ?? '').trim()) {
14
+ return { ok: false, field: 'user', reason: 'user' };
15
+ }
16
+ if (!password) {
17
+ return { ok: false, field: 'pass', reason: 'pass' };
18
+ }
19
+ return { ok: true, field: 'pass', reason: '' };
20
+ }
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { ConfigMenuApp } from './ConfigMenu.js';
4
+ /**
5
+ * Show the Ink config menu. Returns a promise that resolves when the operator
6
+ * exits (q/Esc/Ctrl+C). Resolves (rather than throwing) when stdin is not a
7
+ * TTY so the caller can fall back to the text list.
8
+ */
9
+ export async function runConfigMenuInk(opts) {
10
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
11
+ throw new Error(opts.t('cfg.menu.notty'));
12
+ }
13
+ await new Promise((resolve) => {
14
+ const app = render(_jsx(ConfigMenuApp, { ...opts, onExit: () => {
15
+ resolve();
16
+ } }), { exitOnCtrlC: false });
17
+ app.waitUntilExit().then(() => resolve());
18
+ });
19
+ }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { ConfirmDialog } from './ConfirmDialog.js';
4
+ /**
5
+ * Show a modal confirm and resolve with the operator's answer. Resolves false
6
+ * when stdin is not a TTY (nothing to prompt on).
7
+ */
8
+ export async function askConfirmInk(opts) {
9
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
10
+ return false;
11
+ return await new Promise((resolve) => {
12
+ let answered = false;
13
+ const app = render(_jsx(ConfirmDialog, { ...opts, onAnswer: (ok) => {
14
+ answered = true;
15
+ resolve(ok);
16
+ } }), { exitOnCtrlC: false });
17
+ app.waitUntilExit().then(() => {
18
+ if (!answered)
19
+ resolve(false);
20
+ });
21
+ });
22
+ }