@hackerrank/astra-cli 0.1.33 → 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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/repl.js +41 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackerrank/astra-cli",
3
- "version": "0.1.33",
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/repl.js CHANGED
@@ -301,6 +301,7 @@ export class Screen {
301
301
  this._busyResume = false;
302
302
  this._prevScrollBottom = 0;
303
303
  this._prevFooterTop = 0;
304
+ this._renderedRows = new Map();
304
305
  this._inPaste = false;
305
306
  this.promptLabel = PROMPT_PREFIX;
306
307
  this._onData = this._onData.bind(this);
@@ -343,6 +344,7 @@ export class Screen {
343
344
  this.out.off("resize", this._onResize);
344
345
  if (this.inp.isTTY) this.inp.setRawMode(false);
345
346
  this.out.write("\x1b[?2004l"); // disable bracketed paste mode
347
+ this.out.write("\x1b[?25h"); // ensure the cursor is restored on exit
346
348
  this.out.write("\x1b[r"); // reset scroll region
347
349
  this.out.write(`\x1b[${this.rows};1H\n`); // move below footer
348
350
  }
@@ -369,61 +371,63 @@ export class Screen {
369
371
 
370
372
  /** Unified redraw of footer divider, multi-line prompt, and status lines without overwriting. */
371
373
  redraw() {
372
- if (this._prevScrollBottom !== this.scrollBottom) {
374
+ const scrollChanged = this._prevScrollBottom !== this.scrollBottom;
375
+ if (scrollChanged) {
373
376
  this.out.write(`\x1b[1;${this.scrollBottom}r`);
374
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();
375
389
  }
376
390
 
377
- this.out.write("\x1b[s"); // save cursor
378
-
379
- // Clear only from footerTop down to terminal bottom
380
- for (let r = this.footerTop; r <= this.rows; r++) {
381
- this.out.write(`\x1b[${r};1H\x1b[2K`);
382
- }
383
-
384
- // Draw top divider rule at footerTop
385
- this.out.write(`\x1b[${this.footerTop};1H`);
386
- 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
+ };
387
395
 
388
- // The loader has its own permanently reserved row. This prevents a busy
389
- // transition from moving the input or status rows and causing terminal flicker.
396
+ put(this.footerTop, this.footer[0]);
390
397
  if (this._busy) {
391
- this.out.write(`\x1b[${this.loaderRow};1H`);
392
398
  const frame = SPINNER_FRAMES[this._busyFrame % SPINNER_FRAMES.length];
393
- this.out.write(C.cyan(frame + " ") + C.dim(this._busyLabel + "…"));
394
- }
395
-
396
- // Draw the input below the reserved loader row.
397
- if (!this._busy) {
399
+ put(this.loaderRow, C.cyan(frame + " ") + C.dim(this._busyLabel + "…"));
400
+ } else {
398
401
  const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
399
- const visibleLines = visual.slice(-this.promptLines);
400
- for (let i = 0; i < visibleLines.length; i++) {
401
- const row = this.promptRow + i;
402
- this.out.write(`\x1b[${row};1H`);
403
- this.out.write(visibleLines[i].text);
402
+ for (const [i, line] of visual.slice(-this.promptLines).entries()) {
403
+ put(this.promptRow + i, line.text);
404
404
  }
405
405
  }
406
-
407
- // Draw status lines below the prompt
408
406
  for (let i = 1; i < FOOTER_LINES; i++) {
409
- const row = this.statusStartRow + i - 1;
410
- if (row <= this.rows) {
411
- this.out.write(`\x1b[${row};1H`);
412
- 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}`;
413
418
  }
414
419
  }
420
+ this._renderedRows = rows;
415
421
 
416
- // Place cursor at the end of input
417
422
  if (!this._busy) {
418
423
  const visual = computeVisualLines(this.buf, this.promptLabel || PROMPT_PREFIX, this.cols);
419
424
  const visibleLines = visual.slice(-this.promptLines);
420
425
  const lastLine = visibleLines[visibleLines.length - 1] || { visibleLength: 0 };
421
- const cursorRow = this.promptRow + (visibleLines.length - 1);
426
+ const cursorRow = this.promptRow + visibleLines.length - 1;
422
427
  const cursorCol = Math.min(this.cols, lastLine.visibleLength + 1);
423
- this.out.write(`\x1b[${cursorRow};${cursorCol}H`);
424
- } else {
425
- this.out.write("\x1b[u"); // restore cursor
428
+ output += `\x1b[${cursorRow};${cursorCol}H`;
426
429
  }
430
+ if (output) this.out.write(output);
427
431
  }
428
432
 
429
433
  drawFooter() {
@@ -437,6 +441,7 @@ export class Screen {
437
441
  /** Start the minimal loading state on the prompt row. */
438
442
  startBusy(label = "working") {
439
443
  this._busy = true;
444
+ this.out.write("\x1b[?25l"); // spinner updates must not move a visible cursor
440
445
  this._busyLabel = label;
441
446
  this._busyFrame = 0;
442
447
  this.redraw();
@@ -453,6 +458,7 @@ export class Screen {
453
458
  if (this._busyTimer) { clearInterval(this._busyTimer); this._busyTimer = null; }
454
459
  this._busy = false;
455
460
  this._busyResume = false;
461
+ this.out.write("\x1b[?25h");
456
462
  this.redraw();
457
463
  }
458
464