@tiens.nguyen/gonext-local-worker 1.0.231 → 1.0.233

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 +83 -19
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -239,6 +239,13 @@ let slashSel = -1; // highlighted row in the current filtered list (-1 = none)
239
239
  let slashNames = []; // command names in the current filtered list (for Enter → run)
240
240
  let slashPrefix = ""; // the "/…" text the user typed (restored when ↑/↓ recall history)
241
241
  rl.on("line", (l) => {
242
+ // A list picker owns the keyboard (its Enter resolves via the keypress handler) — never
243
+ // let that Enter submit a stray empty line into the command loop.
244
+ if (listPickerActive) {
245
+ rl.line = "";
246
+ rl.cursor = 0;
247
+ return;
248
+ }
242
249
  // While a turn is running, IGNORE typed input entirely — no type-ahead, no queued
243
250
  // next question. Only Ctrl+C (handled via SIGINT) does anything mid-turn. Clear the
244
251
  // line buffer so nothing the user typed leaks into the next prompt.
@@ -300,6 +307,14 @@ function drawSlashHint(prefix, sel) {
300
307
  }
301
308
  if (process.stdin.isTTY) {
302
309
  process.stdin.on("keypress", (_ch, key) => {
310
+ // An arrow-key list picker (/server) is up → ↑/↓ move the highlight, Enter chooses.
311
+ if (listPickerActive) {
312
+ onListKey(key);
313
+ rl.line = "";
314
+ rl.cursor = 0;
315
+ rl.historyIndex = -1;
316
+ return;
317
+ }
303
318
  // A command-approval picker is up → arrow keys move the highlight, Enter chooses.
304
319
  if (approvalActive) {
305
320
  onApprovalKey(key);
@@ -665,29 +680,23 @@ async function chooseServer() {
665
680
  console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
666
681
  return;
667
682
  }
668
- const activeHost = selectedServer?.host || "";
669
- console.log(dim(" Choose a deployment server for this session:"));
670
- servers.forEach((s, i) => {
671
- const mark = s.host === activeHost ? green(" ") : " ";
672
- console.log(` ${mark} ${i + 1}. ${s.name} ${dim(`(${s.user}@${s.host})`)}`);
673
- });
674
- console.log(` 0. ${dim("none (clear selection)")}`);
675
- const pick = (await ask(dim(" number (or Enter to keep current): "))).trim();
676
- if (!pick) {
677
- console.log("");
683
+ // Arrow-key picker: ↑/↓ to move, Enter to choose (no number typing). The list ends with
684
+ // a "none" row to clear the selection. Pre-highlight the current pick if there is one.
685
+ const labels = servers.map((s) => `${s.name} (${s.user}@${s.host})`);
686
+ labels.push("none (clear selection)");
687
+ const activeIdx = servers.findIndex((s) => s.host === selectedServer?.host);
688
+ console.log(dim(" Choose a deployment server (↑/↓ then Enter):"));
689
+ const idx = await pickFromList(labels, activeIdx >= 0 ? activeIdx : 0);
690
+ if (idx < 0) {
691
+ console.log(dim(" (cancelled)\n"));
678
692
  return;
679
693
  }
680
- const n = Number.parseInt(pick, 10);
681
- if (n === 0) {
694
+ if (idx === servers.length) {
695
+ // the trailing "none" row
682
696
  selectedServer = null;
683
697
  console.log(dim(" ✓ deployment server cleared.\n"));
684
698
  return;
685
699
  }
686
- const idx = n - 1;
687
- if (!Number.isInteger(idx) || idx < 0 || idx >= servers.length) {
688
- console.log(red(" invalid choice.\n"));
689
- return;
690
- }
691
700
  const s = servers[idx];
692
701
  selectedServer = { name: s.name, host: s.host, user: s.user };
693
702
  console.log(green(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
@@ -784,6 +793,59 @@ function approvalPrompt(command) {
784
793
  };
785
794
  });
786
795
  }
796
+ // Generic arrow-key list picker (↑/↓ + Enter) — used by /server so you navigate instead
797
+ // of typing a number. Same mechanism as the Yes/No approval picker above, generalized to
798
+ // N rows. Returns the chosen index (or -1 on Esc/Ctrl+C). Works OUTSIDE a turn, so it also
799
+ // mutes readline echo/refresh (see the _writeToOutput/_refreshLine guards) via listPickerActive.
800
+ let listPickerActive = false;
801
+ let listSel = 0;
802
+ let listItems = [];
803
+ let listResolve = null;
804
+ function drawList(redraw) {
805
+ const n = listItems.length;
806
+ const body = listItems
807
+ .map((it, i) => "\x1b[K" + (i === listSel ? green("▸ " + it) : dim(" " + it)))
808
+ .join("\n");
809
+ process.stdout.write((redraw ? `\x1b[${n}A` : "") + body + "\n");
810
+ }
811
+ function finishList(index) {
812
+ if (!listPickerActive) return;
813
+ listPickerActive = false;
814
+ process.stdout.write(`\x1b[${listItems.length}A\x1b[J`); // wipe the list block
815
+ const r = listResolve;
816
+ listResolve = null;
817
+ listItems = [];
818
+ if (r) r(index);
819
+ }
820
+ function onListKey(key) {
821
+ if (!key) return;
822
+ const n = listItems.length;
823
+ if (key.name === "up") {
824
+ listSel = (listSel - 1 + n) % n;
825
+ drawList(true);
826
+ } else if (key.name === "down") {
827
+ listSel = (listSel + 1) % n;
828
+ drawList(true);
829
+ } else if (key.name === "return" || key.name === "enter") {
830
+ finishList(listSel);
831
+ } else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
832
+ finishList(-1);
833
+ }
834
+ }
835
+ function pickFromList(items, initialSel = 0) {
836
+ return new Promise((resolve) => {
837
+ if (!process.stdin.isTTY || items.length === 0) {
838
+ resolve(-1);
839
+ return;
840
+ }
841
+ listItems = items;
842
+ listSel = Math.max(0, Math.min(initialSel, items.length - 1));
843
+ listResolve = resolve;
844
+ listPickerActive = true;
845
+ drawList(false);
846
+ });
847
+ }
848
+
787
849
  // Per-session coding-model override chosen via /model (empty = use the account default).
788
850
  // Sent to /agent-ask as codingModelOverride; the API validates it against the whitelist.
789
851
  let sessionCodingModel = "";
@@ -801,7 +863,7 @@ let selectedServer = null;
801
863
  // writes to stdout directly, so it's unaffected — only readline's echo is swallowed.
802
864
  const _origWriteToOutput = rl._writeToOutput ? rl._writeToOutput.bind(rl) : null;
803
865
  rl._writeToOutput = (s) => {
804
- if (following) return;
866
+ if (following || listPickerActive) return;
805
867
  if (_origWriteToOutput) _origWriteToOutput(s);
806
868
  else process.stdout.write(s);
807
869
  };
@@ -811,7 +873,7 @@ rl._writeToOutput = (s) => {
811
873
  // ZERO terminal output.
812
874
  const _origRefreshLine = rl._refreshLine ? rl._refreshLine.bind(rl) : null;
813
875
  rl._refreshLine = () => {
814
- if (following) return;
876
+ if (following || listPickerActive) return;
815
877
  if (_origRefreshLine) _origRefreshLine();
816
878
  };
817
879
 
@@ -1377,6 +1439,8 @@ async function main() {
1377
1439
  // the OS delivers a real SIGINT to the process. Attach the same handler to both —
1378
1440
  // only one can ever fire for a given mode, so there's no double-handling.
1379
1441
  const onInterrupt = () => {
1442
+ // Ctrl+C while a picker is up = cancel it.
1443
+ if (listPickerActive) finishList(-1);
1380
1444
  // Ctrl+C while the approval picker is up = decline it (and fall through to also
1381
1445
  // cancel the turn) — otherwise the poll loop stays blocked awaiting a choice.
1382
1446
  if (approvalActive) finishApproval(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.231",
3
+ "version": "1.0.233",
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",