@hackerrank/astra-cli 0.1.25 → 0.1.27
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/README.md +2 -0
- package/package.json +1 -1
- package/src/repl.js +198 -88
package/README.md
CHANGED
|
@@ -82,6 +82,8 @@ astra -m claude-sonnet-5 -y # auto-run commands (no prompts)
|
|
|
82
82
|
|
|
83
83
|
In-REPL commands: `/help /plan <task> /exit /clear /history /tokens /yolo`.
|
|
84
84
|
|
|
85
|
+
Keyboard shortcuts: `Shift+Return` (or `Opt+Return`) for multi-line input, `Alt/Opt+Tab` to toggle bench mode, `Opt+Left/Right` to cycle models, `Opt+Up/Down` to cycle reasoning effort, `Esc` to clear input.
|
|
86
|
+
|
|
85
87
|
### Autonomous task run
|
|
86
88
|
|
|
87
89
|
When you pass a task with `-t`/`-f`, astra appends the autonomous rules and runs
|
package/package.json
CHANGED
package/src/repl.js
CHANGED
|
@@ -159,7 +159,7 @@ function footerLines(agent, model, yolo, mode = "agent") {
|
|
|
159
159
|
C.dim(rule),
|
|
160
160
|
modeTag + " " + C.cyan(repoLabel()) + " " + C.dim(modelBits.join(" ")),
|
|
161
161
|
C.dim(usage),
|
|
162
|
-
C.dim("opt+left/right: model · opt+up/down: reasoning · shift+tab: mode"),
|
|
162
|
+
C.dim("shift+return: newline · opt+left/right: model · opt+up/down: reasoning · shift+tab: mode"),
|
|
163
163
|
];
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -219,19 +219,76 @@ function setReasoning(model, level) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/** Strip ANSI color/style escape codes to calculate visual string lengths. */
|
|
223
|
+
export function stripAnsi(str) {
|
|
224
|
+
return String(str || "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Break an input buffer (with potential newlines and line wraps) into
|
|
229
|
+
* visual lines suitable for terminal display.
|
|
230
|
+
*/
|
|
231
|
+
export function computeVisualLines(buf, promptPrefix, cols = 80) {
|
|
232
|
+
const logicalLines = String(buf || "").split("\n");
|
|
233
|
+
const visualLines = [];
|
|
234
|
+
const prefixVisible = stripAnsi(promptPrefix);
|
|
235
|
+
const contPrefix = " ";
|
|
236
|
+
const contPrefixVisible = " ";
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < logicalLines.length; i++) {
|
|
239
|
+
const p = i === 0 ? promptPrefix : contPrefix;
|
|
240
|
+
const pVis = i === 0 ? prefixVisible : contPrefixVisible;
|
|
241
|
+
const line = logicalLines[i];
|
|
242
|
+
const full = p + line;
|
|
243
|
+
const fullVis = pVis + line;
|
|
244
|
+
|
|
245
|
+
if (fullVis.length <= cols || cols <= 0) {
|
|
246
|
+
visualLines.push({ text: full, visibleLength: fullVis.length });
|
|
247
|
+
} else {
|
|
248
|
+
let remaining = line;
|
|
249
|
+
let first = true;
|
|
250
|
+
while (remaining.length > 0 || first) {
|
|
251
|
+
const currentP = first ? p : contPrefix;
|
|
252
|
+
const currentPVis = first ? pVis : contPrefixVisible;
|
|
253
|
+
const availableWidth = Math.max(1, cols - currentPVis.length);
|
|
254
|
+
const take = remaining.slice(0, availableWidth);
|
|
255
|
+
remaining = remaining.slice(availableWidth);
|
|
256
|
+
visualLines.push({ text: currentP + take, visibleLength: currentPVis.length + take.length });
|
|
257
|
+
first = false;
|
|
258
|
+
if (remaining.length === 0) break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return visualLines.length > 0 ? visualLines : [{ text: promptPrefix, visibleLength: prefixVisible.length }];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Check if an escape sequence or key code is a newline-insertion shortcut (Shift+Enter, Option+Enter, etc.). */
|
|
266
|
+
export function isNewlineKey(s, pendingEsc = false) {
|
|
267
|
+
if (!s || typeof s !== "string") return false;
|
|
268
|
+
if (/^\x1b\[(?:13|10);[2-8]u$/.test(s)) return true;
|
|
269
|
+
if (/^\x1b\[27;[2-8];(?:13|10)~$/.test(s)) return true;
|
|
270
|
+
if (/^\x1b\[(?:13|10);[2-8]~$/.test(s)) return true;
|
|
271
|
+
if (s === "\x1bOM") return true;
|
|
272
|
+
if (s === "\x1b\r" || s === "\x1b\n" || s === "\x1b\x0d") return true;
|
|
273
|
+
if (pendingEsc && (s === "\r" || s === "\n" || s === "\x0d" || s === "\x0a")) return true;
|
|
274
|
+
if (s === "\x0a" || s === "\n") return true;
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
222
278
|
/**
|
|
223
279
|
* A bottom-anchored screen: conversation output scrolls in the top region
|
|
224
280
|
* (bounded by a DECSTBM scroll margin) while the footer + input prompt stay
|
|
225
281
|
* glued to the terminal's bottom rows. Input is read via raw keypresses (not
|
|
226
|
-
* readline) so nothing fights the scroll margin.
|
|
227
|
-
* the
|
|
282
|
+
* readline) so nothing fights the scroll margin. Multi-line input and wrapped
|
|
283
|
+
* typing adjust the scroll margin dynamically to prevent overwriting status
|
|
284
|
+
* lines or history. Requires a TTY; callers use the readline fallback otherwise.
|
|
228
285
|
*/
|
|
229
|
-
class Screen {
|
|
286
|
+
export class Screen {
|
|
230
287
|
constructor(out = process.stdout, inp = process.stdin) {
|
|
231
288
|
this.out = out;
|
|
232
289
|
this.inp = inp;
|
|
233
290
|
this.footer = [];
|
|
234
|
-
this.buf = ""; // current input
|
|
291
|
+
this.buf = ""; // current input buffer (can contain newlines)
|
|
235
292
|
this.resolve = null; // pending readLine() resolver
|
|
236
293
|
this._lastWasCR = false;
|
|
237
294
|
this._guardEnterUntil = 0; // ignore stray Enter until this timestamp
|
|
@@ -241,21 +298,39 @@ class Screen {
|
|
|
241
298
|
this._busyFrame = 0;
|
|
242
299
|
this._busyLabel = "working";
|
|
243
300
|
this._busyResume = false;
|
|
301
|
+
this._prevScrollBottom = 0;
|
|
302
|
+
this._prevFooterTop = 0;
|
|
303
|
+
this._inPaste = false;
|
|
244
304
|
this.promptLabel = PROMPT_PREFIX;
|
|
245
305
|
this._onData = this._onData.bind(this);
|
|
246
306
|
this._onResize = this._onResize.bind(this);
|
|
247
307
|
}
|
|
248
308
|
|
|
249
|
-
get
|
|
250
|
-
get
|
|
251
|
-
|
|
252
|
-
get
|
|
253
|
-
|
|
309
|
+
get cols() { return this.out.columns || 80; }
|
|
310
|
+
get rows() { return this.out.rows || 24; }
|
|
311
|
+
|
|
312
|
+
get promptLines() {
|
|
313
|
+
if (this._busy) return 1;
|
|
314
|
+
const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
|
|
315
|
+
const maxLines = Math.max(1, Math.floor((this.rows - FOOTER_LINES) / 2));
|
|
316
|
+
return Math.min(visual.length, maxLines);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
get reserved() { return FOOTER_LINES + this.promptLines; }
|
|
320
|
+
get scrollBottom() { return Math.max(1, this.rows - this.reserved); }
|
|
321
|
+
get footerTop() { return this.scrollBottom + 1; }
|
|
322
|
+
get promptRow() { return this.footerTop + 1; }
|
|
323
|
+
get statusStartRow() { return this.promptRow + this.promptLines; }
|
|
254
324
|
|
|
255
325
|
/** Enter sticky mode: set scroll margin, park cursor, start listening. */
|
|
256
326
|
enter() {
|
|
327
|
+
this._prevScrollBottom = this.scrollBottom;
|
|
328
|
+
this._prevFooterTop = this.footerTop;
|
|
257
329
|
this.out.write(`\x1b[1;${this.scrollBottom}r`); // scroll region = top area
|
|
258
330
|
this.out.write(`\x1b[${this.scrollBottom};1H`); // cursor at bottom of scroll area
|
|
331
|
+
this.out.write("\x1b[?2004h"); // enable bracketed paste mode
|
|
332
|
+
this.out.write("\x1b[>4;2m"); // enable modifyOtherKeys
|
|
333
|
+
this.out.write("\x1b[=1u"); // enable kitty keyboard protocol
|
|
259
334
|
if (this.inp.isTTY) this.inp.setRawMode(true);
|
|
260
335
|
this.inp.resume();
|
|
261
336
|
this.inp.setEncoding("utf8");
|
|
@@ -268,6 +343,9 @@ class Screen {
|
|
|
268
343
|
this.inp.off("data", this._onData);
|
|
269
344
|
this.out.off("resize", this._onResize);
|
|
270
345
|
if (this.inp.isTTY) this.inp.setRawMode(false);
|
|
346
|
+
this.out.write("\x1b[>4;0m"); // disable modifyOtherKeys
|
|
347
|
+
this.out.write("\x1b[=0u"); // disable kitty keyboard protocol
|
|
348
|
+
this.out.write("\x1b[?2004l"); // disable bracketed paste mode
|
|
271
349
|
this.out.write("\x1b[r"); // reset scroll region
|
|
272
350
|
this.out.write(`\x1b[${this.rows};1H\n`); // move below footer
|
|
273
351
|
}
|
|
@@ -281,41 +359,78 @@ class Screen {
|
|
|
281
359
|
/** Write a block of text into the scrolling region (may contain newlines). */
|
|
282
360
|
log(text) {
|
|
283
361
|
this.out.write(`\x1b[${this.scrollBottom};1H`); // park at bottom of scroll area
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
this.
|
|
362
|
+
const formatted = String(text).replace(/\r?\n/g, "\r\n") + "\r\n";
|
|
363
|
+
this.out.write(formatted);
|
|
364
|
+
this.redraw();
|
|
287
365
|
}
|
|
288
366
|
|
|
289
367
|
/** Redraw the pinned footer + prompt (e.g. when usage changes). */
|
|
290
368
|
refresh(footer) {
|
|
291
369
|
if (footer) this.footer = footer;
|
|
292
|
-
this.
|
|
293
|
-
this.drawPrompt();
|
|
370
|
+
this.redraw();
|
|
294
371
|
}
|
|
295
372
|
|
|
296
|
-
|
|
373
|
+
/** Unified redraw of footer divider, multi-line prompt, and status lines without overwriting. */
|
|
374
|
+
redraw() {
|
|
375
|
+
if (this._prevScrollBottom !== this.scrollBottom) {
|
|
376
|
+
this.out.write(`\x1b[1;${this.scrollBottom}r`);
|
|
377
|
+
this._prevScrollBottom = this.scrollBottom;
|
|
378
|
+
}
|
|
379
|
+
|
|
297
380
|
this.out.write("\x1b[s"); // save cursor
|
|
298
|
-
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.out.write(this.footer[0] || ""); // divider
|
|
303
|
-
for (let i = 1; i < FOOTER_LINES; i++) {
|
|
304
|
-
const row = this.promptRow + i; // status lines under the prompt
|
|
305
|
-
this.out.write(`\x1b[${row};1H\x1b[2K`);
|
|
306
|
-
this.out.write(this.footer[i] || "");
|
|
381
|
+
|
|
382
|
+
// Clear only from footerTop down to terminal bottom
|
|
383
|
+
for (let r = this.footerTop; r <= this.rows; r++) {
|
|
384
|
+
this.out.write(`\x1b[${r};1H\x1b[2K`);
|
|
307
385
|
}
|
|
308
|
-
this.out.write("\x1b[u"); // restore cursor
|
|
309
|
-
}
|
|
310
386
|
|
|
311
|
-
|
|
312
|
-
this.out.write(`\x1b[${this.
|
|
387
|
+
// Draw top divider rule at footerTop
|
|
388
|
+
this.out.write(`\x1b[${this.footerTop};1H`);
|
|
389
|
+
this.out.write(this.footer[0] || "");
|
|
390
|
+
|
|
391
|
+
// Draw prompt (or busy spinner)
|
|
313
392
|
if (this._busy) {
|
|
393
|
+
this.out.write(`\x1b[${this.promptRow};1H`);
|
|
314
394
|
const frame = SPINNER_FRAMES[this._busyFrame % SPINNER_FRAMES.length];
|
|
315
395
|
this.out.write(C.cyan(frame + " ") + C.dim(this._busyLabel + "…"));
|
|
316
|
-
|
|
396
|
+
} else {
|
|
397
|
+
const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
|
|
398
|
+
const visibleLines = visual.slice(-this.promptLines);
|
|
399
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
400
|
+
const row = this.promptRow + i;
|
|
401
|
+
this.out.write(`\x1b[${row};1H`);
|
|
402
|
+
this.out.write(visibleLines[i].text);
|
|
403
|
+
}
|
|
317
404
|
}
|
|
318
|
-
|
|
405
|
+
|
|
406
|
+
// Draw status lines below the prompt
|
|
407
|
+
for (let i = 1; i < FOOTER_LINES; i++) {
|
|
408
|
+
const row = this.statusStartRow + i - 1;
|
|
409
|
+
if (row <= this.rows) {
|
|
410
|
+
this.out.write(`\x1b[${row};1H`);
|
|
411
|
+
this.out.write(this.footer[i] || "");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Place cursor at the end of input
|
|
416
|
+
if (!this._busy) {
|
|
417
|
+
const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
|
|
418
|
+
const visibleLines = visual.slice(-this.promptLines);
|
|
419
|
+
const lastLine = visibleLines[visibleLines.length - 1] || { visibleLength: 0 };
|
|
420
|
+
const cursorRow = this.promptRow + (visibleLines.length - 1);
|
|
421
|
+
const cursorCol = Math.min(this.cols, lastLine.visibleLength + 1);
|
|
422
|
+
this.out.write(`\x1b[${cursorRow};${cursorCol}H`);
|
|
423
|
+
} else {
|
|
424
|
+
this.out.write("\x1b[u"); // restore cursor
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
drawFooter() {
|
|
429
|
+
this.redraw();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
drawPrompt() {
|
|
433
|
+
this.redraw();
|
|
319
434
|
}
|
|
320
435
|
|
|
321
436
|
/** Start the minimal loading state on the prompt row. */
|
|
@@ -323,11 +438,11 @@ class Screen {
|
|
|
323
438
|
this._busy = true;
|
|
324
439
|
this._busyLabel = label;
|
|
325
440
|
this._busyFrame = 0;
|
|
326
|
-
this.
|
|
441
|
+
this.redraw();
|
|
327
442
|
if (this._busyTimer) clearInterval(this._busyTimer);
|
|
328
443
|
this._busyTimer = setInterval(() => {
|
|
329
444
|
this._busyFrame++;
|
|
330
|
-
this.
|
|
445
|
+
this.redraw();
|
|
331
446
|
}, 90);
|
|
332
447
|
if (this._busyTimer.unref) this._busyTimer.unref();
|
|
333
448
|
}
|
|
@@ -337,7 +452,7 @@ class Screen {
|
|
|
337
452
|
if (this._busyTimer) { clearInterval(this._busyTimer); this._busyTimer = null; }
|
|
338
453
|
this._busy = false;
|
|
339
454
|
this._busyResume = false;
|
|
340
|
-
this.
|
|
455
|
+
this.redraw();
|
|
341
456
|
}
|
|
342
457
|
|
|
343
458
|
/** True while the agent is working (a turn is in progress). */
|
|
@@ -349,16 +464,9 @@ class Screen {
|
|
|
349
464
|
* Resolve with the next full line the user types. An optional prompt label
|
|
350
465
|
* replaces the default "you › " (used by the approval gate so the question
|
|
351
466
|
* is shown right where the user is typing).
|
|
352
|
-
*
|
|
353
|
-
* `opts.guardEnter` ignores a bare Enter that lands within a short window of
|
|
354
|
-
* the prompt appearing. This prevents a stray newline still in the input
|
|
355
|
-
* buffer (from the previous line, a CRLF pair, or a paste) from instantly
|
|
356
|
-
* "answering" a confirmation prompt the user never actually saw.
|
|
357
467
|
*/
|
|
358
468
|
readLine(promptLabel, opts = {}) {
|
|
359
469
|
return new Promise((res) => {
|
|
360
|
-
// A prompt (main input or approval) takes over the row; pause the spinner
|
|
361
|
-
// timer but remember whether we were busy so we can resume after.
|
|
362
470
|
this._busyResume = this._busy;
|
|
363
471
|
if (this._busyTimer) { clearInterval(this._busyTimer); this._busyTimer = null; }
|
|
364
472
|
this._busy = false;
|
|
@@ -367,37 +475,50 @@ class Screen {
|
|
|
367
475
|
this.resolve = res;
|
|
368
476
|
this._guardEnterUntil = opts.guardEnter ? Date.now() + 250 : 0;
|
|
369
477
|
this._echoOnCommit = opts.echo !== false;
|
|
370
|
-
this.
|
|
478
|
+
this.redraw();
|
|
371
479
|
});
|
|
372
480
|
}
|
|
373
481
|
|
|
374
482
|
_onResize() {
|
|
375
|
-
this.
|
|
376
|
-
this.footer = this.footer; // caller refreshes content separately
|
|
377
|
-
this.drawFooter();
|
|
378
|
-
this.drawPrompt();
|
|
483
|
+
this.redraw();
|
|
379
484
|
}
|
|
380
485
|
|
|
381
486
|
_onData(chunk) {
|
|
382
487
|
const s = chunk.toString();
|
|
383
|
-
// Optional key debug: set ASTRA_KEYDEBUG=1 to print the raw bytes of every
|
|
384
|
-
// keypress into the transcript. Use it to discover what your terminal
|
|
385
|
-
// actually sends for Option/Alt+Tab, then report it.
|
|
386
488
|
if (process.env.ASTRA_KEYDEBUG && this.log) {
|
|
387
489
|
const bytes = Array.from(chunk).map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ");
|
|
388
490
|
this.log(`\x1b[35m[keydebug] ${bytes}\x1b[0m`);
|
|
389
491
|
}
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
492
|
+
|
|
493
|
+
// Bracketed paste handling (pasting multi-line blocks into the input)
|
|
494
|
+
let str = s;
|
|
495
|
+
if (str.includes("\x1b[200~") || this._inPaste) {
|
|
496
|
+
if (str.includes("\x1b[200~")) {
|
|
497
|
+
this._inPaste = true;
|
|
498
|
+
str = str.slice(str.indexOf("\x1b[200~") + 6);
|
|
499
|
+
}
|
|
500
|
+
if (str.includes("\x1b[201~")) {
|
|
501
|
+
const end = str.indexOf("\x1b[201~");
|
|
502
|
+
const pasted = str.slice(0, end);
|
|
503
|
+
this.buf += pasted.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
504
|
+
this._inPaste = false;
|
|
505
|
+
this.redraw();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.buf += str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
509
|
+
this.redraw();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Newline insertion shortcut (Shift+Enter, Option+Enter, Alt+Enter, Ctrl+Enter, Esc+Enter)
|
|
514
|
+
if (isNewlineKey(s, this._pendingEsc)) {
|
|
515
|
+
this._clearEscLatch();
|
|
516
|
+
this.buf += "\n";
|
|
517
|
+
this.redraw();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Mode toggle: Alt/Option+Tab or Shift+Tab
|
|
401
522
|
if (this.onToggleMode && (s === "\x1b\t" || s === "\x1b[Z")) {
|
|
402
523
|
this._clearEscLatch();
|
|
403
524
|
this.onToggleMode();
|
|
@@ -415,17 +536,17 @@ class Screen {
|
|
|
415
536
|
this.onCycleReasoning(opt === "up" ? 1 : -1);
|
|
416
537
|
return;
|
|
417
538
|
}
|
|
418
|
-
return;
|
|
539
|
+
return;
|
|
419
540
|
}
|
|
420
541
|
}
|
|
421
542
|
|
|
422
|
-
// A Tab following a latched ESC -> mode toggle
|
|
543
|
+
// A Tab following a latched ESC -> mode toggle
|
|
423
544
|
if (this._pendingEsc && s === "\t" && this.onToggleMode) {
|
|
424
545
|
this._clearEscLatch();
|
|
425
546
|
this.onToggleMode();
|
|
426
547
|
return;
|
|
427
548
|
}
|
|
428
|
-
// A latched ESC followed by an arrow tail
|
|
549
|
+
// A latched ESC followed by an arrow tail -> option-arrow
|
|
429
550
|
if (this._pendingEsc) {
|
|
430
551
|
const opt = matchOptionArrow("\x1b" + s, true);
|
|
431
552
|
if (opt) {
|
|
@@ -440,13 +561,10 @@ class Screen {
|
|
|
440
561
|
}
|
|
441
562
|
return;
|
|
442
563
|
}
|
|
443
|
-
// Any other input right after a latched ESC cancels the latch and is
|
|
444
|
-
// processed normally below.
|
|
445
564
|
this._clearEscLatch();
|
|
446
565
|
}
|
|
447
566
|
|
|
448
|
-
// A lone ESC: latch briefly to disambiguate from split escape sequences
|
|
449
|
-
// If nothing arrives, the timer treats it as a bare Esc and clears input.
|
|
567
|
+
// A lone ESC: latch briefly to disambiguate from split escape sequences
|
|
450
568
|
if (s === "\x1b") {
|
|
451
569
|
this._pendingEsc = true;
|
|
452
570
|
clearTimeout(this._escTimer);
|
|
@@ -454,19 +572,15 @@ class Screen {
|
|
|
454
572
|
this._pendingEsc = false;
|
|
455
573
|
if (this.onClearInput) this.onClearInput();
|
|
456
574
|
this.buf = "";
|
|
457
|
-
this.
|
|
575
|
+
this.redraw();
|
|
458
576
|
}, 60);
|
|
459
577
|
return;
|
|
460
578
|
}
|
|
461
579
|
|
|
462
|
-
for (const ch of
|
|
580
|
+
for (const ch of s) {
|
|
463
581
|
if (ch === "\r" || ch === "\n") {
|
|
464
|
-
// Collapse a CRLF pair into one Enter: ignore a \n right after a \r.
|
|
465
582
|
if (ch === "\n" && this._lastWasCR) { this._lastWasCR = false; continue; }
|
|
466
583
|
this._lastWasCR = ch === "\r";
|
|
467
|
-
// Guard: ignore a bare Enter (empty buffer) that arrives immediately
|
|
468
|
-
// after a guarded prompt was shown. This stops a leftover newline from
|
|
469
|
-
// auto-resolving a confirmation the user never got to answer.
|
|
470
584
|
if (
|
|
471
585
|
this.buf === "" &&
|
|
472
586
|
this._guardEnterUntil &&
|
|
@@ -474,30 +588,25 @@ class Screen {
|
|
|
474
588
|
) {
|
|
475
589
|
continue;
|
|
476
590
|
}
|
|
477
|
-
const
|
|
591
|
+
const text = this.buf;
|
|
478
592
|
this.buf = "";
|
|
479
|
-
|
|
480
|
-
// User input is echoed with the "you › " history prefix; approval
|
|
481
|
-
// prompts pass echo:false so their Q&A stays out of the transcript.
|
|
482
|
-
// Empty input is skipped so a bare Enter doesn't stack blank lines.
|
|
483
|
-
if (this._echoOnCommit && line.trim() !== "") {
|
|
593
|
+
if (this._echoOnCommit && text.trim() !== "") {
|
|
484
594
|
const isUserPrompt = (this.promptLabel || PROMPT_PREFIX) === PROMPT_PREFIX;
|
|
485
|
-
|
|
595
|
+
const prefix = isUserPrompt ? HISTORY_PREFIX : this.promptLabel;
|
|
596
|
+
const lines = text.split("\n");
|
|
597
|
+
for (let i = 0; i < lines.length; i++) {
|
|
598
|
+
this.log((i === 0 ? prefix : " ") + lines[i]);
|
|
599
|
+
}
|
|
486
600
|
}
|
|
487
601
|
this.promptLabel = PROMPT_PREFIX;
|
|
488
602
|
this._echoOnCommit = true;
|
|
489
|
-
// Resume the loading state if a turn is still in progress (e.g. after
|
|
490
|
-
// an approval prompt hands control back to the running agent).
|
|
491
603
|
if (this._busyResume) { this._busyResume = false; this.startBusy(this._busyLabel); }
|
|
492
604
|
const r = this.resolve; this.resolve = null;
|
|
493
|
-
if (r) r(
|
|
605
|
+
if (r) r(text);
|
|
494
606
|
} else if (ch === "\x7f" || ch === "\b") { // backspace
|
|
495
607
|
this.buf = this.buf.slice(0, -1);
|
|
496
|
-
this.
|
|
608
|
+
this.redraw();
|
|
497
609
|
} else if (ch === "\x03") { // Ctrl+C
|
|
498
|
-
// If a prompt is pending (idle), resolve it so the loop can react.
|
|
499
|
-
// Also always notify the interrupt hook so a *running* agent turn can
|
|
500
|
-
// be aborted even when no readLine is pending.
|
|
501
610
|
const r = this.resolve; this.resolve = null;
|
|
502
611
|
if (r) r("__SIGINT__");
|
|
503
612
|
if (this.onInterrupt) this.onInterrupt();
|
|
@@ -507,7 +616,7 @@ class Screen {
|
|
|
507
616
|
} else if (ch >= " ") { // printable
|
|
508
617
|
this._lastWasCR = false;
|
|
509
618
|
this.buf += ch;
|
|
510
|
-
this.
|
|
619
|
+
this.redraw();
|
|
511
620
|
}
|
|
512
621
|
}
|
|
513
622
|
}
|
|
@@ -524,6 +633,7 @@ Interactive commands:
|
|
|
524
633
|
/yolo toggle auto-run of commands (no confirmation)
|
|
525
634
|
|
|
526
635
|
Keyboard shortcuts:
|
|
636
|
+
shift+return insert a newline for multi-line input (alt/opt+enter also works)
|
|
527
637
|
alt/opt+tab switch agent / bench mode (shift+tab also works)
|
|
528
638
|
opt+left/right cycle the model
|
|
529
639
|
opt+up/down cycle reasoning effort (off/low/medium/high)
|