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.
- package/CHANGELOG.md +48 -0
- package/README.md +37 -7
- package/dist/attachments.js +6 -1
- package/dist/banner.js +71 -0
- package/dist/browser.js +84 -27
- package/dist/code-blocks.js +48 -0
- package/dist/commands.js +1 -0
- package/dist/config-live.js +53 -0
- package/dist/config.js +38 -0
- package/dist/goal.js +81 -0
- package/dist/i18n/common.js +35 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +30 -2
- package/dist/i18n/qwen.js +4 -0
- package/dist/i18n/self-review.js +8 -0
- package/dist/i18n/spinner.js +6 -0
- package/dist/index.js +487 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +29 -1
- package/dist/input.js +14 -3
- package/dist/net-capture.js +18 -0
- package/dist/qwen-browser.js +43 -20
- package/dist/run-task.js +97 -4
- package/dist/theme.js +16 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +337 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +113 -0
- package/dist/tui/InputLine.js +318 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +54 -0
- package/dist/tui/StatusBar.js +170 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +29 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +317 -0
- package/dist/tui/keys.js +195 -0
- package/dist/tui/log-window.js +93 -0
- package/dist/tui/login-pure.js +20 -0
- package/dist/tui/run-config-menu.js +19 -0
- package/dist/tui/run-confirm.js +22 -0
- package/dist/tui/run-login.js +43 -0
- package/dist/tui/run-select.js +18 -0
- package/dist/tui/store.js +104 -0
- package/dist/tui/types.js +10 -0
- package/dist/tui/useMouseWheel.js +164 -0
- package/dist/tui/widgets.js +60 -0
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +10 -0
- package/scripts/inline-status-driver.mts +31 -0
- package/scripts/inline-status.tsx +33 -0
- package/scripts/mouse-churn.mts +34 -0
- package/scripts/qwen-models-smoke.mts +117 -0
- package/scripts/render-demo.mts +1 -1
- package/scripts/tui-comp-shot.mts +63 -0
- package/scripts/tui-components.tsx +62 -0
- package/scripts/tui-config-driver.mts +82 -0
- package/scripts/tui-config-probe.tsx +74 -0
- package/scripts/tui-edit-driver.mts +100 -0
- package/scripts/tui-edit-probe.tsx +38 -0
- package/scripts/tui-editor-probe.tsx +24 -0
- package/scripts/tui-input-driver.mts +54 -0
- package/scripts/tui-input-shot.tsx +59 -0
- package/scripts/tui-log-driver.mts +35 -0
- package/scripts/tui-log-shot.tsx +48 -0
- package/scripts/tui-longpaste-driver.mts +56 -0
- package/scripts/tui-longpaste.tsx +35 -0
- package/scripts/tui-manyselect-shot.mts +34 -0
- package/scripts/tui-manyselect.tsx +17 -0
- package/scripts/tui-mouse-churn-probe.tsx +28 -0
- package/scripts/tui-paste-driver.mts +88 -0
- package/scripts/tui-paste-kinds.tsx +76 -0
- package/scripts/tui-pastetest.mts +38 -0
- package/scripts/tui-probe.tsx +62 -0
- package/scripts/tui-repro.tsx +64 -0
- package/scripts/tui-resize.mts +70 -0
- package/scripts/tui-scroll-driver.mts +53 -0
- package/scripts/tui-scroll-probe.tsx +46 -0
- package/scripts/tui-shot.mts +106 -0
- package/scripts/tui-status-driver.mts +24 -0
- package/scripts/tui-status-shot.tsx +27 -0
- package/scripts/tui-submit.tsx +57 -0
- package/scripts/tui-wheel-driver.mts +61 -0
- package/scripts/tui-wheel-probe.tsx +48 -0
- package/scripts/tui-widgets-driver.mts +20 -0
- package/scripts/tui-widgets.tsx +44 -0
package/dist/tui/edit.js
ADDED
|
@@ -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
|
+
}
|
package/dist/tui/keys.js
ADDED
|
@@ -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
|
+
}
|