@semalt-ai/code 1.5.0 → 1.6.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/lib/ui.js +237 -91
- package/package.json +1 -1
package/lib/ui.js
CHANGED
|
@@ -111,133 +111,279 @@ function readInteractiveInput(promptText, options = {}) {
|
|
|
111
111
|
let done = false;
|
|
112
112
|
let historyIndex = Array.isArray(history) ? history.length : -1;
|
|
113
113
|
let historyActive = false;
|
|
114
|
+
let pasting = false;
|
|
115
|
+
let lastLineCount = 1;
|
|
116
|
+
let lastCursorRow = 0;
|
|
117
|
+
let lastWasCR = false;
|
|
114
118
|
|
|
115
|
-
readline.emitKeypressEvents(process.stdin);
|
|
116
119
|
process.stdin.setRawMode(true);
|
|
117
120
|
process.stdin.resume();
|
|
121
|
+
process.stdout.write('\x1b[?2004h'); // enable bracketed paste mode
|
|
122
|
+
|
|
123
|
+
const promptWidth = stripAnsi(promptText).length;
|
|
118
124
|
|
|
119
125
|
const render = () => {
|
|
126
|
+
const lines = buffer.split('\n');
|
|
127
|
+
const newLineCount = lines.length;
|
|
128
|
+
|
|
129
|
+
if (lastLineCount > 1) {
|
|
130
|
+
readline.moveCursor(process.stdout, 0, -(lastLineCount - 1));
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
readline.cursorTo(process.stdout, 0);
|
|
121
134
|
readline.clearLine(process.stdout, 0);
|
|
122
|
-
process.stdout.write(`${promptText}${
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
process.stdout.write(`${promptText}${lines[0]}`);
|
|
136
|
+
|
|
137
|
+
for (let i = 1; i < newLineCount; i++) {
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
readline.cursorTo(process.stdout, 0);
|
|
140
|
+
readline.clearLine(process.stdout, 0);
|
|
141
|
+
process.stdout.write(lines[i]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Clear any extra lines from a previous longer render
|
|
145
|
+
for (let i = newLineCount; i < lastLineCount; i++) {
|
|
146
|
+
process.stdout.write('\n');
|
|
147
|
+
readline.cursorTo(process.stdout, 0);
|
|
148
|
+
readline.clearLine(process.stdout, 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lastLineCount = newLineCount;
|
|
152
|
+
|
|
153
|
+
const beforeCursor = Array.from(buffer).slice(0, cursor).join('');
|
|
154
|
+
const cursorLines = beforeCursor.split('\n');
|
|
155
|
+
lastCursorRow = cursorLines.length - 1;
|
|
156
|
+
const cursorCol = (lastCursorRow === 0 ? promptWidth : 0) + cursorLines[lastCursorRow].length;
|
|
157
|
+
|
|
158
|
+
const rowsFromBottom = newLineCount - 1 - lastCursorRow;
|
|
159
|
+
if (rowsFromBottom > 0) {
|
|
160
|
+
readline.moveCursor(process.stdout, 0, -rowsFromBottom);
|
|
161
|
+
}
|
|
162
|
+
readline.cursorTo(process.stdout, cursorCol);
|
|
125
163
|
};
|
|
126
164
|
|
|
127
165
|
const finish = (result, addNewline = true) => {
|
|
128
166
|
if (done) return;
|
|
129
167
|
done = true;
|
|
168
|
+
process.stdout.write('\x1b[?2004l'); // disable bracketed paste mode
|
|
130
169
|
process.stdin.setRawMode(wasRaw);
|
|
131
|
-
process.stdin.removeListener('
|
|
132
|
-
if (addNewline)
|
|
170
|
+
process.stdin.removeListener('data', onData);
|
|
171
|
+
if (addNewline) {
|
|
172
|
+
const rowsToBottom = lastLineCount - 1 - lastCursorRow;
|
|
173
|
+
if (rowsToBottom > 0) {
|
|
174
|
+
readline.moveCursor(process.stdout, 0, rowsToBottom);
|
|
175
|
+
}
|
|
176
|
+
process.stdout.write('\n');
|
|
177
|
+
}
|
|
133
178
|
resolve(result);
|
|
134
179
|
};
|
|
135
180
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
// Parse raw stdin data directly so we can intercept bracketed paste markers
|
|
182
|
+
// (\x1b[200~ ... \x1b[201~) before any higher-level key parser sees them.
|
|
183
|
+
const onData = (chunk) => {
|
|
184
|
+
if (done) return;
|
|
185
|
+
const data = chunk.toString('utf8');
|
|
186
|
+
let i = 0;
|
|
187
|
+
|
|
188
|
+
while (i < data.length) {
|
|
189
|
+
// --- Bracketed paste markers ---
|
|
190
|
+
if (data.startsWith('\x1b[200~', i)) {
|
|
191
|
+
pasting = true;
|
|
192
|
+
lastWasCR = false;
|
|
193
|
+
i += 6;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (data.startsWith('\x1b[201~', i)) {
|
|
197
|
+
pasting = false;
|
|
198
|
+
lastWasCR = false;
|
|
199
|
+
i += 6;
|
|
200
|
+
continue;
|
|
143
201
|
}
|
|
144
|
-
finish({ type: 'sigint' }, false);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
202
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
203
|
+
const ch = data[i];
|
|
204
|
+
|
|
205
|
+
// --- Escape sequences ---
|
|
206
|
+
if (ch === '\x1b') {
|
|
207
|
+
lastWasCR = false;
|
|
208
|
+
const next = data[i + 1];
|
|
209
|
+
|
|
210
|
+
// CSI sequences: \x1b[...
|
|
211
|
+
if (next === '[') {
|
|
212
|
+
const seq3 = data.slice(i, i + 3);
|
|
213
|
+
const seq4 = data.slice(i, i + 4);
|
|
214
|
+
|
|
215
|
+
if (seq3 === '\x1b[A') { // up
|
|
216
|
+
if (Array.isArray(history) && history.length) {
|
|
217
|
+
if (historyActive || buffer.length === 0) {
|
|
218
|
+
historyActive = true;
|
|
219
|
+
historyIndex = Math.max(0, historyIndex - 1);
|
|
220
|
+
buffer = historyIndex >= history.length ? '' : history[historyIndex];
|
|
221
|
+
cursor = Array.from(buffer).length;
|
|
222
|
+
render();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
i += 3; continue;
|
|
226
|
+
}
|
|
227
|
+
if (seq3 === '\x1b[B') { // down
|
|
228
|
+
if (Array.isArray(history) && history.length) {
|
|
229
|
+
if (historyActive || buffer.length === 0) {
|
|
230
|
+
historyActive = true;
|
|
231
|
+
historyIndex = Math.min(history.length, historyIndex + 1);
|
|
232
|
+
buffer = historyIndex >= history.length ? '' : history[historyIndex];
|
|
233
|
+
cursor = Array.from(buffer).length;
|
|
234
|
+
render();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
i += 3; continue;
|
|
238
|
+
}
|
|
239
|
+
if (seq3 === '\x1b[C') { // right
|
|
240
|
+
if (allowCursorNavigation && cursor < Array.from(buffer).length) { cursor++; render(); }
|
|
241
|
+
i += 3; continue;
|
|
242
|
+
}
|
|
243
|
+
if (seq3 === '\x1b[D') { // left
|
|
244
|
+
if (allowCursorNavigation && cursor > 0) { cursor--; render(); }
|
|
245
|
+
i += 3; continue;
|
|
246
|
+
}
|
|
247
|
+
if (seq3 === '\x1b[H' || seq4 === '\x1b[1~') { // home
|
|
248
|
+
if (allowCursorNavigation) { cursor = 0; render(); }
|
|
249
|
+
i += (seq3 === '\x1b[H' ? 3 : 4); continue;
|
|
250
|
+
}
|
|
251
|
+
if (seq3 === '\x1b[F' || seq4 === '\x1b[4~') { // end
|
|
252
|
+
if (allowCursorNavigation) { cursor = Array.from(buffer).length; render(); }
|
|
253
|
+
i += (seq3 === '\x1b[F' ? 3 : 4); continue;
|
|
254
|
+
}
|
|
255
|
+
if (seq4 === '\x1b[3~') { // delete
|
|
256
|
+
if (cursor < Array.from(buffer).length) {
|
|
257
|
+
buffer = removeCharAt(buffer, cursor);
|
|
258
|
+
historyActive = false;
|
|
259
|
+
render();
|
|
260
|
+
}
|
|
261
|
+
i += 4; continue;
|
|
262
|
+
}
|
|
263
|
+
// Skip unknown CSI sequence (read until terminating byte)
|
|
264
|
+
let j = i + 2;
|
|
265
|
+
while (j < data.length && (data.charCodeAt(j) < 0x40 || data.charCodeAt(j) > 0x7e)) j++;
|
|
266
|
+
i = j + 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// SS3 sequences: \x1bO... (application cursor keys)
|
|
271
|
+
if (next === 'O') {
|
|
272
|
+
const c = data[i + 2];
|
|
273
|
+
if (c === 'A') { // up
|
|
274
|
+
if (Array.isArray(history) && history.length && (historyActive || buffer.length === 0)) {
|
|
275
|
+
historyActive = true;
|
|
276
|
+
historyIndex = Math.max(0, historyIndex - 1);
|
|
277
|
+
buffer = historyIndex >= history.length ? '' : history[historyIndex];
|
|
278
|
+
cursor = Array.from(buffer).length;
|
|
279
|
+
render();
|
|
280
|
+
}
|
|
281
|
+
i += 3; continue;
|
|
282
|
+
}
|
|
283
|
+
if (c === 'B') { // down
|
|
284
|
+
if (Array.isArray(history) && history.length && (historyActive || buffer.length === 0)) {
|
|
285
|
+
historyActive = true;
|
|
286
|
+
historyIndex = Math.min(history.length, historyIndex + 1);
|
|
287
|
+
buffer = historyIndex >= history.length ? '' : history[historyIndex];
|
|
288
|
+
cursor = Array.from(buffer).length;
|
|
289
|
+
render();
|
|
290
|
+
}
|
|
291
|
+
i += 3; continue;
|
|
292
|
+
}
|
|
293
|
+
if (c === 'C' && allowCursorNavigation) { if (cursor < Array.from(buffer).length) { cursor++; render(); } i += 3; continue; }
|
|
294
|
+
if (c === 'D' && allowCursorNavigation) { if (cursor > 0) { cursor--; render(); } i += 3; continue; }
|
|
295
|
+
if (c === 'H' && allowCursorNavigation) { cursor = 0; render(); i += 3; continue; }
|
|
296
|
+
if (c === 'F' && allowCursorNavigation) { cursor = Array.from(buffer).length; render(); i += 3; continue; }
|
|
297
|
+
i += 3; continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Unknown escape - skip
|
|
301
|
+
i += 2;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
152
304
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
305
|
+
// --- Ctrl+C ---
|
|
306
|
+
if (ch === '\x03') {
|
|
307
|
+
lastWasCR = false;
|
|
308
|
+
if (buffer) { buffer = ''; cursor = 0; pasting = false; render(); }
|
|
309
|
+
else { finish({ type: 'sigint' }, false); return; }
|
|
310
|
+
i++; continue;
|
|
311
|
+
}
|
|
157
312
|
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
render();
|
|
164
|
-
} else if (key.name === 'delete' && cursor < Array.from(buffer).length) {
|
|
165
|
-
buffer = removeCharAt(buffer, cursor);
|
|
166
|
-
historyActive = false;
|
|
167
|
-
render();
|
|
313
|
+
// --- Ctrl+D ---
|
|
314
|
+
if (ch === '\x04') {
|
|
315
|
+
lastWasCR = false;
|
|
316
|
+
if (!buffer) { finish({ type: 'eof' }, false); return; }
|
|
317
|
+
i++; continue;
|
|
168
318
|
}
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
319
|
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
320
|
+
// --- Enter / newline ---
|
|
321
|
+
if (ch === '\r' || ch === '\n') {
|
|
322
|
+
// Collapse CRLF into a single newline
|
|
323
|
+
if (ch === '\n' && lastWasCR) { lastWasCR = false; i++; continue; }
|
|
324
|
+
lastWasCR = (ch === '\r');
|
|
325
|
+
|
|
326
|
+
if (pasting) {
|
|
327
|
+
buffer = insertCharAt(buffer, cursor, '\n');
|
|
328
|
+
cursor++;
|
|
329
|
+
historyActive = false;
|
|
330
|
+
render();
|
|
331
|
+
} else {
|
|
332
|
+
finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
i++; continue;
|
|
336
|
+
}
|
|
337
|
+
lastWasCR = false;
|
|
338
|
+
|
|
339
|
+
// --- Backspace ---
|
|
340
|
+
if (ch === '\x7f' || ch === '\x08') {
|
|
341
|
+
if (cursor > 0) {
|
|
342
|
+
buffer = removeCharAt(buffer, cursor - 1);
|
|
343
|
+
cursor--;
|
|
344
|
+
historyActive = false;
|
|
345
|
+
render();
|
|
346
|
+
}
|
|
347
|
+
i++; continue;
|
|
176
348
|
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
349
|
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
350
|
+
// --- Tab ---
|
|
351
|
+
if (ch === '\t') {
|
|
352
|
+
if (pasting) {
|
|
353
|
+
buffer = insertCharAt(buffer, cursor, '\t');
|
|
354
|
+
cursor++;
|
|
355
|
+
render();
|
|
356
|
+
}
|
|
357
|
+
i++; continue;
|
|
184
358
|
}
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
359
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
render();
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
360
|
+
// --- Other control characters: skip ---
|
|
361
|
+
if (ch.charCodeAt(0) < 0x20) { i++; continue; }
|
|
193
362
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
363
|
+
// --- Printable character (handles multi-byte Unicode via code points) ---
|
|
364
|
+
const cp = data.codePointAt(i);
|
|
365
|
+
const char = String.fromCodePoint(cp);
|
|
199
366
|
|
|
200
|
-
|
|
201
|
-
const canEnterHistory = buffer.length === 0;
|
|
202
|
-
if (!history.length) return;
|
|
203
|
-
if (!historyActive && !canEnterHistory) return;
|
|
367
|
+
if (allowed && !allowed.includes(char)) { i += char.length; continue; }
|
|
204
368
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
369
|
+
if (immediate) {
|
|
370
|
+
buffer = char;
|
|
371
|
+
cursor = 1;
|
|
372
|
+
historyActive = false;
|
|
373
|
+
render();
|
|
374
|
+
finish({ type: 'submit', value: char });
|
|
375
|
+
return;
|
|
210
376
|
}
|
|
211
377
|
|
|
212
|
-
buffer =
|
|
213
|
-
cursor
|
|
214
|
-
render();
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'tab'].includes(key.name)) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (!isPrintableKey(str, key)) return;
|
|
223
|
-
if (allowed && !allowed.includes(str)) return;
|
|
224
|
-
|
|
225
|
-
if (immediate) {
|
|
226
|
-
buffer = str;
|
|
227
|
-
cursor = Array.from(buffer).length;
|
|
378
|
+
buffer = insertCharAt(buffer, cursor, char);
|
|
379
|
+
cursor++;
|
|
228
380
|
historyActive = false;
|
|
229
381
|
render();
|
|
230
|
-
|
|
231
|
-
return;
|
|
382
|
+
i += char.length;
|
|
232
383
|
}
|
|
233
|
-
|
|
234
|
-
buffer = insertCharAt(buffer, cursor, str);
|
|
235
|
-
cursor++;
|
|
236
|
-
historyActive = false;
|
|
237
|
-
render();
|
|
238
384
|
};
|
|
239
385
|
|
|
240
|
-
process.stdin.on('
|
|
386
|
+
process.stdin.on('data', onData);
|
|
241
387
|
render();
|
|
242
388
|
});
|
|
243
389
|
}
|