@tiens.nguyen/gonext-local-worker 1.0.305 → 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 +97 -35
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -297,12 +297,13 @@ 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
308
  const clean = stripAnsiEscapes(submitted);
308
309
  if (_lineWaiter) {
@@ -322,27 +323,81 @@ rl.on("line", (l) => {
322
323
  // runs the highlighted command, and it DISAPPEARS when you clear the slash — no permanent
323
324
  // scrollback copies. Relative cursor moves so it survives terminal scrolling.
324
325
  // (slashSel / slashNames / slashPrefix are declared above, near rl.on("line").)
325
- const inputCol = () => 4 + rl.cursor; // ">> " is 3 cols; content starts at col 4 (1-based)
326
- function clearSlashHint() {
327
- if (slashHintRows === 0) return;
328
- process.stdout.write("\n\x1b[J" + `\x1b[1A\x1b[${inputCol()}G`);
329
- slashHintRows = 0;
330
- slashSel = -1;
331
- slashNames = [];
326
+ // While the slash-hint overlay is open, EMPTY readline's history so ↑/↓ don't recall a
327
+ // history entry and trigger readline's OWN line-refresh — that refresh fought our overlay
328
+ // and reprinted the whole ">> /" + hint block on every arrow press (bug #109). With no
329
+ // history to recall, readline's ↑/↓ are inert and our keypress handler owns the highlight.
330
+ // Restored the instant the overlay closes, so normal task-history recall still works.
331
+ let _slashSavedHistory = null;
332
+ function slashNeutralizeHistory() {
333
+ if (_slashSavedHistory === null && Array.isArray(rl.history)) {
334
+ _slashSavedHistory = rl.history;
335
+ rl.history = [];
336
+ rl.historyIndex = -1;
337
+ }
338
+ }
339
+ function slashRestoreHistory() {
340
+ if (_slashSavedHistory !== null) {
341
+ rl.history = _slashSavedHistory;
342
+ _slashSavedHistory = null;
343
+ rl.historyIndex = -1;
344
+ }
332
345
  }
333
- function drawSlashHint(prefix, sel) {
334
- const cmds = filteredCommands(prefix);
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);
335
361
  slashNames = cmds.map((c) => c.name);
336
362
  if (sel >= cmds.length) sel = cmds.length - 1;
337
363
  slashSel = sel;
338
- const rows = hintRows(cmds, sel);
339
- const body = rows.map((r) => "\x1b[K" + r).join("\n");
340
- let out = "";
341
- if (slashHintRows > 0) out += "\n\x1b[J\x1b[1A"; // wipe previous block, back to input row
342
- out += "\n" + body; // draw below the input line
343
- out += `\x1b[${rows.length}A\x1b[${inputCol()}G`; // back up to input line + column
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";
344
385
  process.stdout.write(out);
345
- slashHintRows = rows.length;
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) {
392
+ slashRestoreHistory();
393
+ return;
394
+ }
395
+ process.stdout.write("\x1b7\n\x1b[J\x1b8"); // save, wipe below, restore to input line
396
+ slashOverlayActive = false;
397
+ slashHintRows = 0;
398
+ slashSel = -1;
399
+ slashNames = [];
400
+ slashRestoreHistory();
346
401
  }
347
402
  if (process.stdin.isTTY) {
348
403
  process.stdin.on("keypress", (_ch, key) => {
@@ -370,26 +425,27 @@ if (process.stdin.isTTY) {
370
425
  }
371
426
  // Enter is handled by rl.on("line") — don't redraw the hint on the way out.
372
427
  if (key && (key.name === "return" || key.name === "enter")) return;
373
- // ↑/↓ while the menu is up = move the highlight, NOT recall shell history. readline
374
- // already ran history-nav (may have changed the line); restore the typed "/…" prefix
375
- // and redraw with the new selection.
376
- if (slashHintRows > 0 && key && (key.name === "up" || key.name === "down")) {
377
- rl.line = slashPrefix;
378
- rl.cursor = slashPrefix.length;
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")) {
432
+ rl.historyIndex = -1;
379
433
  const n = slashNames.length;
380
434
  if (n > 0) {
381
435
  slashSel =
382
436
  key.name === "down" ? (slashSel + 1 + n) % n : (slashSel - 1 + n) % n;
383
437
  }
384
- rl._refreshLine();
385
- drawSlashHint(slashPrefix, slashSel);
438
+ renderSlashOverlay(slashSel);
386
439
  return;
387
440
  }
388
441
  if (rl.line.startsWith("/")) {
389
- slashPrefix = rl.line;
390
- drawSlashHint(rl.line, -1); // typing resets the selection to none
391
- } else {
392
- 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();
393
449
  }
394
450
  });
395
451
  }
@@ -1090,7 +1146,9 @@ let globalOutputTokens = 0;
1090
1146
  // writes to stdout directly, so it's unaffected — only readline's echo is swallowed.
1091
1147
  const _origWriteToOutput = rl._writeToOutput ? rl._writeToOutput.bind(rl) : null;
1092
1148
  rl._writeToOutput = (s) => {
1093
- 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;
1094
1152
  if (_origWriteToOutput) _origWriteToOutput(s);
1095
1153
  else process.stdout.write(s);
1096
1154
  };
@@ -1100,7 +1158,9 @@ rl._writeToOutput = (s) => {
1100
1158
  // ZERO terminal output.
1101
1159
  const _origRefreshLine = rl._refreshLine ? rl._refreshLine.bind(rl) : null;
1102
1160
  rl._refreshLine = () => {
1103
- 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;
1104
1164
  if (_origRefreshLine) _origRefreshLine();
1105
1165
  };
1106
1166
 
@@ -1979,6 +2039,8 @@ async function main() {
1979
2039
  process.stdout.write(red("\n^C "));
1980
2040
  }
1981
2041
  } else {
2042
+ // Ctrl+C at a prompt with the slash overlay open = dismiss the overlay first (#109).
2043
+ if (slashOverlayActive) closeSlashOverlay();
1982
2044
  process.stdout.write(dim("\n(/exit to quit)\n"));
1983
2045
  // Discard anything half-typed (Ctrl-C at a prompt means "never mind") and
1984
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.305",
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",