@tiens.nguyen/gonext-local-worker 1.0.306 → 1.0.307

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/gonext-repl.mjs +74 -37
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -297,15 +297,14 @@ rl.on("line", (l) => {
297
297
  // command's own output doesn't print over/under a stale hint block. If a menu item was
298
298
  // highlighted (↑/↓), RUN that command instead of the raw typed line.
299
299
  let submitted = l;
300
- if (slashHintRows > 0) {
300
+ if (slashOverlayActive) {
301
+ // A highlighted menu item (↑/↓) RUNS that command instead of the raw typed line.
301
302
  if (slashSel >= 0 && slashNames[slashSel]) submitted = slashNames[slashSel];
302
- process.stdout.write("\x1b[J");
303
- slashHintRows = 0;
304
- slashSel = -1;
305
- slashNames = [];
303
+ closeSlashOverlay(); // wipe the menu, re-enable readline, restore history
304
+ // readline's own newline echo was suppressed during the overlay — emit it so the
305
+ // command's output starts on a fresh line, not over the ">> …" input line (#109).
306
+ process.stdout.write("\r\n");
306
307
  }
307
- // Overlay is gone — restore readline history so ↑/↓ recall past tasks again (bug #109).
308
- slashRestoreHistory();
309
308
  const clean = stripAnsiEscapes(submitted);
310
309
  if (_lineWaiter) {
311
310
  const w = _lineWaiter;
@@ -324,7 +323,6 @@ rl.on("line", (l) => {
324
323
  // runs the highlighted command, and it DISAPPEARS when you clear the slash — no permanent
325
324
  // scrollback copies. Relative cursor moves so it survives terminal scrolling.
326
325
  // (slashSel / slashNames / slashPrefix are declared above, near rl.on("line").)
327
- const inputCol = () => 4 + rl.cursor; // ">> " is 3 cols; content starts at col 4 (1-based)
328
326
  // While the slash-hint overlay is open, EMPTY readline's history so ↑/↓ don't recall a
329
327
  // history entry and trigger readline's OWN line-refresh — that refresh fought our overlay
330
328
  // and reprinted the whole ">> /" + hint block on every arrow press (bug #109). With no
@@ -345,32 +343,62 @@ function slashRestoreHistory() {
345
343
  rl.historyIndex = -1;
346
344
  }
347
345
  }
348
- function clearSlashHint() {
349
- if (slashHintRows === 0) {
346
+ // While the overlay is active WE own all terminal drawing: readline's own echo +
347
+ // line-refresh are no-op'd (see slashOverlayActive in _writeToOutput/_refreshLine
348
+ // below), exactly like the /server + approval pickers. That's what makes the redraw
349
+ // stable — readline can't clearScreenDown over our menu or fight our cursor (bug #109,
350
+ // which the earlier relative-cursor overlay hit whenever readline refreshed or the
351
+ // menu was near the bottom of the screen and scrolled).
352
+ let slashOverlayActive = false;
353
+
354
+ // Redraw the input line (">> " + typed text) AND the command menu below it, fully
355
+ // in place. On first activation we scroll N blank rows into existence below the input
356
+ // line then return to it, so every later redraw writes into that reserved space
357
+ // WITHOUT scrolling — which keeps the save/restore-cursor (ESC 7 / ESC 8) anchor valid.
358
+ function renderSlashOverlay(sel) {
359
+ const line = rl.line;
360
+ const cmds = filteredCommands(line);
361
+ slashNames = cmds.map((c) => c.name);
362
+ if (sel >= cmds.length) sel = cmds.length - 1;
363
+ slashSel = sel;
364
+ slashPrefix = line;
365
+ const menu = hintRows(cmds, sel);
366
+ const n = menu.length;
367
+
368
+ if (!slashOverlayActive) {
369
+ slashOverlayActive = true;
370
+ slashNeutralizeHistory();
371
+ process.stdout.write("\n".repeat(n) + `\x1b[${n}A`); // reserve n rows, back to input
372
+ slashHintRows = n;
373
+ } else if (n !== slashHintRows) {
374
+ // The filtered list changed size: clear the old menu, re-reserve.
375
+ process.stdout.write("\x1b7\n\x1b[J\x1b8");
376
+ process.stdout.write("\n".repeat(n) + `\x1b[${n}A`);
377
+ slashHintRows = n;
378
+ }
379
+
380
+ // Input line (carriage-return, clear, prompt + typed), then the menu below, anchored
381
+ // by ESC 7 (save at end of input) / ESC 8 (restore) so the caret ends on the input.
382
+ let out = "\r\x1b[2K" + PROMPT + line + "\x1b7";
383
+ for (const r of menu) out += "\n\x1b[2K" + r;
384
+ out += "\x1b8";
385
+ process.stdout.write(out);
386
+ }
387
+
388
+ // Close the overlay: erase the menu below, keep the typed input line, hand drawing back
389
+ // to readline. Callers that changed the line to a non-slash value should refresh after.
390
+ function closeSlashOverlay() {
391
+ if (!slashOverlayActive) {
350
392
  slashRestoreHistory();
351
393
  return;
352
394
  }
353
- process.stdout.write("\n\x1b[J" + `\x1b[1A\x1b[${inputCol()}G`);
395
+ process.stdout.write("\x1b7\n\x1b[J\x1b8"); // save, wipe below, restore to input line
396
+ slashOverlayActive = false;
354
397
  slashHintRows = 0;
355
398
  slashSel = -1;
356
399
  slashNames = [];
357
400
  slashRestoreHistory();
358
401
  }
359
- function drawSlashHint(prefix, sel) {
360
- slashNeutralizeHistory();
361
- const cmds = filteredCommands(prefix);
362
- slashNames = cmds.map((c) => c.name);
363
- if (sel >= cmds.length) sel = cmds.length - 1;
364
- slashSel = sel;
365
- const rows = hintRows(cmds, sel);
366
- const body = rows.map((r) => "\x1b[K" + r).join("\n");
367
- let out = "";
368
- if (slashHintRows > 0) out += "\n\x1b[J\x1b[1A"; // wipe previous block, back to input row
369
- out += "\n" + body; // draw below the input line
370
- out += `\x1b[${rows.length}A\x1b[${inputCol()}G`; // back up to input line + column
371
- process.stdout.write(out);
372
- slashHintRows = rows.length;
373
- }
374
402
  if (process.stdin.isTTY) {
375
403
  process.stdin.on("keypress", (_ch, key) => {
376
404
  // An arrow-key list picker (/server) is up → ↑/↓ move the highlight, Enter chooses.
@@ -397,24 +425,27 @@ if (process.stdin.isTTY) {
397
425
  }
398
426
  // Enter is handled by rl.on("line") — don't redraw the hint on the way out.
399
427
  if (key && (key.name === "return" || key.name === "enter")) return;
400
- // ↑/↓ while the menu is up = move the highlight IN PLACE. History is neutralized
401
- // (slashNeutralizeHistory), so readline's ↑/↓ did nothing the input line is intact
402
- // and we just move the selection and redraw the hint below it (bug #109).
403
- if (slashHintRows > 0 && key && (key.name === "up" || key.name === "down")) {
428
+ // ↑/↓ while the menu is up = move the highlight IN PLACE. readline drawing is
429
+ // suppressed (slashOverlayActive) and history neutralized, so its own ↑/↓ do
430
+ // nothing we just move the selection and re-render the overlay (bug #109).
431
+ if (slashOverlayActive && key && (key.name === "up" || key.name === "down")) {
404
432
  rl.historyIndex = -1;
405
433
  const n = slashNames.length;
406
434
  if (n > 0) {
407
435
  slashSel =
408
436
  key.name === "down" ? (slashSel + 1 + n) % n : (slashSel - 1 + n) % n;
409
437
  }
410
- drawSlashHint(slashPrefix, slashSel);
438
+ renderSlashOverlay(slashSel);
411
439
  return;
412
440
  }
413
441
  if (rl.line.startsWith("/")) {
414
- slashPrefix = rl.line;
415
- drawSlashHint(rl.line, -1); // typing resets the selection to none
416
- } else {
417
- clearSlashHint();
442
+ // Typing narrows the list and resets the selection to none.
443
+ renderSlashOverlay(-1);
444
+ } else if (slashOverlayActive) {
445
+ // The line no longer starts with "/" (e.g. backspaced the slash away): tear down
446
+ // the overlay and let readline redraw the now-normal line.
447
+ closeSlashOverlay();
448
+ rl._refreshLine();
418
449
  }
419
450
  });
420
451
  }
@@ -1115,7 +1146,9 @@ let globalOutputTokens = 0;
1115
1146
  // writes to stdout directly, so it's unaffected — only readline's echo is swallowed.
1116
1147
  const _origWriteToOutput = rl._writeToOutput ? rl._writeToOutput.bind(rl) : null;
1117
1148
  rl._writeToOutput = (s) => {
1118
- if (following || listPickerActive) return;
1149
+ // slashOverlayActive: the slash-command overlay owns drawing (input line + menu), so
1150
+ // readline's own echo must be swallowed or it fights the overlay (bug #109).
1151
+ if (following || listPickerActive || slashOverlayActive) return;
1119
1152
  if (_origWriteToOutput) _origWriteToOutput(s);
1120
1153
  else process.stdout.write(s);
1121
1154
  };
@@ -1125,7 +1158,9 @@ rl._writeToOutput = (s) => {
1125
1158
  // ZERO terminal output.
1126
1159
  const _origRefreshLine = rl._refreshLine ? rl._refreshLine.bind(rl) : null;
1127
1160
  rl._refreshLine = () => {
1128
- if (following || listPickerActive) return;
1161
+ // Suppress readline's line refresh (which does clearScreenDown → would wipe our menu)
1162
+ // while the slash overlay owns the screen. We redraw the input line ourselves (#109).
1163
+ if (following || listPickerActive || slashOverlayActive) return;
1129
1164
  if (_origRefreshLine) _origRefreshLine();
1130
1165
  };
1131
1166
 
@@ -2004,6 +2039,8 @@ async function main() {
2004
2039
  process.stdout.write(red("\n^C "));
2005
2040
  }
2006
2041
  } else {
2042
+ // Ctrl+C at a prompt with the slash overlay open = dismiss the overlay first (#109).
2043
+ if (slashOverlayActive) closeSlashOverlay();
2007
2044
  process.stdout.write(dim("\n(/exit to quit)\n"));
2008
2045
  // Discard anything half-typed (Ctrl-C at a prompt means "never mind") and
2009
2046
  // redraw through readline so its cursor tracking stays right.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.306",
3
+ "version": "1.0.307",
4
4
  "description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
5
5
  "type": "module",
6
6
  "license": "MIT",