@hackerrank/astra-cli 0.1.32 → 0.1.34

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +2 -2
  3. package/src/repl.js +62 -36
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackerrank/astra-cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Minimal zero-dependency AI coding agent for the HackerRank AI Gateway.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -538,7 +538,7 @@ async function main() {
538
538
  `${Number(args.steps) > 0 ? ` turns<=${args.steps}` : ""}\x1b[0m`
539
539
  );
540
540
  console.error(`\x1b[2m[astra] workspace -> ${benchRun.workspace}\x1b[0m`);
541
- console.error(`\x1b[2m[astra] session -> ${sessionId}\x1b[0m`);
541
+ console.error(`\x1b[2m[astra] To resume session use: astra --resume ${sessionId}\x1b[0m`);
542
542
  } else {
543
543
  console.error(
544
544
  `\x1b[2m[astra] bench · model=${modelId} cwd=${cmdCwd}` +
@@ -618,7 +618,7 @@ async function main() {
618
618
  : "") +
619
619
  `\x1b[0m`
620
620
  );
621
- console.error(`\x1b[2m[astra] session -> ${sessionId}\x1b[0m`);
621
+ console.error(`\x1b[2m[astra] To resume session use: astra --resume ${sessionId}\x1b[0m`);
622
622
  }
623
623
  if (result.submission) console.log(result.submission);
624
624
 
package/src/repl.js CHANGED
@@ -159,11 +159,12 @@ 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("shift+return: newline · 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 · opt+tab: mode · ctrl+y: yolo"),
163
163
  ];
164
164
  }
165
165
 
166
166
  const FOOTER_LINES = 4; // divider + repo/model + usage + shortcuts
167
+ const LOADER_LINES = 1; // permanently reserved row below the divider
167
168
  const PROMPT_PREFIX = C.green("› "); // live input row (minimal)
168
169
  const HISTORY_PREFIX = C.green("you › "); // echoed into transcript/history
169
170
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -300,6 +301,7 @@ export class Screen {
300
301
  this._busyResume = false;
301
302
  this._prevScrollBottom = 0;
302
303
  this._prevFooterTop = 0;
304
+ this._renderedRows = new Map();
303
305
  this._inPaste = false;
304
306
  this.promptLabel = PROMPT_PREFIX;
305
307
  this._onData = this._onData.bind(this);
@@ -310,16 +312,16 @@ export class Screen {
310
312
  get rows() { return this.out.rows || 24; }
311
313
 
312
314
  get promptLines() {
313
- if (this._busy) return 1;
314
315
  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
+ const maxLines = Math.max(1, Math.floor((this.rows - FOOTER_LINES - LOADER_LINES) / 2));
316
317
  return Math.min(visual.length, maxLines);
317
318
  }
318
319
 
319
- get reserved() { return FOOTER_LINES + this.promptLines; }
320
+ get reserved() { return FOOTER_LINES + LOADER_LINES + this.promptLines; }
320
321
  get scrollBottom() { return Math.max(1, this.rows - this.reserved); }
321
322
  get footerTop() { return this.scrollBottom + 1; }
322
- get promptRow() { return this.footerTop + 1; }
323
+ get loaderRow() { return this.footerTop + 1; }
324
+ get promptRow() { return this.loaderRow + LOADER_LINES; }
323
325
  get statusStartRow() { return this.promptRow + this.promptLines; }
324
326
 
325
327
  /** Enter sticky mode: set scroll margin, park cursor, start listening. */
@@ -342,6 +344,7 @@ export class Screen {
342
344
  this.out.off("resize", this._onResize);
343
345
  if (this.inp.isTTY) this.inp.setRawMode(false);
344
346
  this.out.write("\x1b[?2004l"); // disable bracketed paste mode
347
+ this.out.write("\x1b[?25h"); // ensure the cursor is restored on exit
345
348
  this.out.write("\x1b[r"); // reset scroll region
346
349
  this.out.write(`\x1b[${this.rows};1H\n`); // move below footer
347
350
  }
@@ -368,57 +371,63 @@ export class Screen {
368
371
 
369
372
  /** Unified redraw of footer divider, multi-line prompt, and status lines without overwriting. */
370
373
  redraw() {
371
- if (this._prevScrollBottom !== this.scrollBottom) {
374
+ const scrollChanged = this._prevScrollBottom !== this.scrollBottom;
375
+ if (scrollChanged) {
372
376
  this.out.write(`\x1b[1;${this.scrollBottom}r`);
373
377
  this._prevScrollBottom = this.scrollBottom;
378
+ // Prompt wrapping can move the footer. Clear the old pinned region once;
379
+ // ordinary input and spinner frames use the row diff below instead.
380
+ if (this._prevFooterTop && this._prevFooterTop !== this.footerTop) {
381
+ for (let row = this._prevFooterTop; row <= this.rows; row++) {
382
+ this.out.write(`\x1b[${row};1H\x1b[2K`);
383
+ }
384
+ }
385
+ this._prevFooterTop = this.footerTop;
386
+ // A resize or prompt-height change moves pinned rows. Their old
387
+ // coordinates are no longer meaningful, so repaint them once.
388
+ this._renderedRows.clear();
374
389
  }
375
390
 
376
- this.out.write("\x1b[s"); // save cursor
377
-
378
- // Clear only from footerTop down to terminal bottom
379
- for (let r = this.footerTop; r <= this.rows; r++) {
380
- this.out.write(`\x1b[${r};1H\x1b[2K`);
381
- }
382
-
383
- // Draw top divider rule at footerTop
384
- this.out.write(`\x1b[${this.footerTop};1H`);
385
- this.out.write(this.footer[0] || "");
391
+ const rows = new Map();
392
+ const put = (row, text) => {
393
+ if (row >= 1 && row <= this.rows) rows.set(row, text || "");
394
+ };
386
395
 
387
- // Draw prompt (or busy spinner)
396
+ put(this.footerTop, this.footer[0]);
388
397
  if (this._busy) {
389
- this.out.write(`\x1b[${this.promptRow};1H`);
390
398
  const frame = SPINNER_FRAMES[this._busyFrame % SPINNER_FRAMES.length];
391
- this.out.write(C.cyan(frame + " ") + C.dim(this._busyLabel + "…"));
399
+ put(this.loaderRow, C.cyan(frame + " ") + C.dim(this._busyLabel + "…"));
392
400
  } else {
393
401
  const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
394
- const visibleLines = visual.slice(-this.promptLines);
395
- for (let i = 0; i < visibleLines.length; i++) {
396
- const row = this.promptRow + i;
397
- this.out.write(`\x1b[${row};1H`);
398
- this.out.write(visibleLines[i].text);
402
+ for (const [i, line] of visual.slice(-this.promptLines).entries()) {
403
+ put(this.promptRow + i, line.text);
399
404
  }
400
405
  }
401
-
402
- // Draw status lines below the prompt
403
406
  for (let i = 1; i < FOOTER_LINES; i++) {
404
- const row = this.statusStartRow + i - 1;
405
- if (row <= this.rows) {
406
- this.out.write(`\x1b[${row};1H`);
407
- this.out.write(this.footer[i] || "");
407
+ put(this.statusStartRow + i - 1, this.footer[i]);
408
+ }
409
+
410
+ // Update only changed rows. Clearing a row before immediately redrawing it
411
+ // caused a visible blank frame on every keystroke and spinner tick.
412
+ const changed = new Set([...this._renderedRows.keys(), ...rows.keys()]);
413
+ let output = "";
414
+ for (const row of [...changed].sort((a, b) => a - b)) {
415
+ const next = rows.get(row) || "";
416
+ if (this._renderedRows.get(row) !== next) {
417
+ output += `\x1b[${row};1H\x1b[2K${next}`;
408
418
  }
409
419
  }
420
+ this._renderedRows = rows;
410
421
 
411
- // Place cursor at the end of input
412
422
  if (!this._busy) {
413
423
  const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
414
424
  const visibleLines = visual.slice(-this.promptLines);
415
425
  const lastLine = visibleLines[visibleLines.length - 1] || { visibleLength: 0 };
416
- const cursorRow = this.promptRow + (visibleLines.length - 1);
426
+ const cursorRow = this.promptRow + visibleLines.length - 1;
417
427
  const cursorCol = Math.min(this.cols, lastLine.visibleLength + 1);
418
- this.out.write(`\x1b[${cursorRow};${cursorCol}H`);
419
- } else {
420
- this.out.write("\x1b[u"); // restore cursor
428
+ output += `\x1b[${cursorRow};${cursorCol}H`;
421
429
  }
430
+ if (output) this.out.write(output);
422
431
  }
423
432
 
424
433
  drawFooter() {
@@ -432,6 +441,7 @@ export class Screen {
432
441
  /** Start the minimal loading state on the prompt row. */
433
442
  startBusy(label = "working") {
434
443
  this._busy = true;
444
+ this.out.write("\x1b[?25l"); // spinner updates must not move a visible cursor
435
445
  this._busyLabel = label;
436
446
  this._busyFrame = 0;
437
447
  this.redraw();
@@ -448,6 +458,7 @@ export class Screen {
448
458
  if (this._busyTimer) { clearInterval(this._busyTimer); this._busyTimer = null; }
449
459
  this._busy = false;
450
460
  this._busyResume = false;
461
+ this.out.write("\x1b[?25h");
451
462
  this.redraw();
452
463
  }
453
464
 
@@ -529,6 +540,13 @@ export class Screen {
529
540
  this.onToggleMode();
530
541
  return;
531
542
  }
543
+
544
+ // Yolo toggle: Ctrl+Y
545
+ if (this.onToggleYolo && s === "\x19") {
546
+ this._clearEscLatch();
547
+ this.onToggleYolo();
548
+ return;
549
+ }
532
550
  {
533
551
  const opt = matchOptionArrow(s, false);
534
552
  if (opt) {
@@ -649,9 +667,10 @@ Interactive commands:
649
667
 
650
668
  Keyboard shortcuts:
651
669
  shift+return insert a newline for multi-line input (alt/opt+enter also works)
652
- alt/opt+tab switch agent / bench mode (shift+tab also works)
670
+ opt+tab switch agent / bench mode
653
671
  opt+left/right cycle the model
654
672
  opt+up/down cycle reasoning effort (off/low/medium/high)
673
+ ctrl+y toggle auto-run of commands (yolo mode)
655
674
  esc clear the input line
656
675
  `;
657
676
 
@@ -692,6 +711,13 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
692
711
  refresh();
693
712
  };
694
713
 
714
+
715
+ // Ctrl+Y toggles yolo (auto-run) mode.
716
+ screen.onToggleYolo = () => {
717
+ yolo = !yolo;
718
+ log(C.dim(`[astra] auto-run ${yolo ? "ON" : "OFF"}.`));
719
+ refresh();
720
+ };
695
721
  // Option+Left/Right cycles the model; Option+Up/Down cycles reasoning effort.
696
722
  // Changes apply to the live model and are cached as the preferred agent
697
723
  // prefs so the next `astra` launch reuses them.