@tiens.nguyen/gonext-local-worker 1.0.230 → 1.0.232

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 +109 -22
  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);
@@ -516,6 +531,19 @@ async function loadSessionTestAuto(cwd) {
516
531
  }
517
532
  }
518
533
 
534
+ // The /server deploy-target pick is also remembered per folder, so a fresh terminal in
535
+ // the same project still knows where to deploy (task #69 — it was session-only and got
536
+ // lost on restart). Returns {name,host,user} or null.
537
+ async function loadSessionServer(cwd) {
538
+ try {
539
+ const raw = JSON.parse(await readFile(sessionFilePath(cwd), "utf8"));
540
+ const s = raw?.deployServer;
541
+ return s && typeof s.host === "string" && typeof s.user === "string" ? s : null;
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+
519
547
  async function saveSession(cwd, history) {
520
548
  try {
521
549
  await mkdir(SESSIONS_DIR, { recursive: true });
@@ -523,7 +551,13 @@ async function saveSession(cwd, history) {
523
551
  await writeFile(
524
552
  sessionFilePath(cwd),
525
553
  JSON.stringify(
526
- { cwd, updatedAt: new Date().toISOString(), testAuto: sessionTestAuto, history: trimmed },
554
+ {
555
+ cwd,
556
+ updatedAt: new Date().toISOString(),
557
+ testAuto: sessionTestAuto,
558
+ deployServer: selectedServer, // remember the /server pick per folder (task #69)
559
+ history: trimmed,
560
+ },
527
561
  null,
528
562
  2
529
563
  ) + "\n"
@@ -646,29 +680,23 @@ async function chooseServer() {
646
680
  console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
647
681
  return;
648
682
  }
649
- const activeHost = selectedServer?.host || "";
650
- console.log(dim(" Choose a deployment server for this session:"));
651
- servers.forEach((s, i) => {
652
- const mark = s.host === activeHost ? green(" ") : " ";
653
- console.log(` ${mark} ${i + 1}. ${s.name} ${dim(`(${s.user}@${s.host})`)}`);
654
- });
655
- console.log(` 0. ${dim("none (clear selection)")}`);
656
- const pick = (await ask(dim(" number (or Enter to keep current): "))).trim();
657
- if (!pick) {
658
- 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"));
659
692
  return;
660
693
  }
661
- const n = Number.parseInt(pick, 10);
662
- if (n === 0) {
694
+ if (idx === servers.length) {
695
+ // the trailing "none" row
663
696
  selectedServer = null;
664
697
  console.log(dim(" ✓ deployment server cleared.\n"));
665
698
  return;
666
699
  }
667
- const idx = n - 1;
668
- if (!Number.isInteger(idx) || idx < 0 || idx >= servers.length) {
669
- console.log(red(" invalid choice.\n"));
670
- return;
671
- }
672
700
  const s = servers[idx];
673
701
  selectedServer = { name: s.name, host: s.host, user: s.user };
674
702
  console.log(green(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
@@ -765,6 +793,59 @@ function approvalPrompt(command) {
765
793
  };
766
794
  });
767
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
+
768
849
  // Per-session coding-model override chosen via /model (empty = use the account default).
769
850
  // Sent to /agent-ask as codingModelOverride; the API validates it against the whitelist.
770
851
  let sessionCodingModel = "";
@@ -782,7 +863,7 @@ let selectedServer = null;
782
863
  // writes to stdout directly, so it's unaffected — only readline's echo is swallowed.
783
864
  const _origWriteToOutput = rl._writeToOutput ? rl._writeToOutput.bind(rl) : null;
784
865
  rl._writeToOutput = (s) => {
785
- if (following) return;
866
+ if (following || listPickerActive) return;
786
867
  if (_origWriteToOutput) _origWriteToOutput(s);
787
868
  else process.stdout.write(s);
788
869
  };
@@ -792,7 +873,7 @@ rl._writeToOutput = (s) => {
792
873
  // ZERO terminal output.
793
874
  const _origRefreshLine = rl._refreshLine ? rl._refreshLine.bind(rl) : null;
794
875
  rl._refreshLine = () => {
795
- if (following) return;
876
+ if (following || listPickerActive) return;
796
877
  if (_origRefreshLine) _origRefreshLine();
797
878
  };
798
879
 
@@ -1321,8 +1402,10 @@ async function runAgentTurn(history) {
1321
1402
  async function main() {
1322
1403
  console.log(cyan("GoNext agent REPL") + dim(` · API ${apiBase} · key ${workerKey.slice(0, 8)}…`));
1323
1404
  await ensureWorkspace();
1324
- // Restore this folder's remembered auto-test choice so the banner + agent-ask reflect it.
1405
+ // Restore this folder's remembered auto-test + deploy-target so the banner + agent-ask
1406
+ // reflect them (both survive a fresh terminal in the same project).
1325
1407
  sessionTestAuto = await loadSessionTestAuto(resolve(process.cwd()));
1408
+ selectedServer = await loadSessionServer(resolve(process.cwd()));
1326
1409
  // Show which models will answer, straight from user settings (also validates auth).
1327
1410
  try {
1328
1411
  const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
@@ -1332,7 +1415,8 @@ async function main() {
1332
1415
  `agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
1333
1416
  (p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
1334
1417
  (p.ragEnabled ? " · RAG on" : "") +
1335
- ` · auto-test: ${sessionTestAuto ? "on" : "off"}`
1418
+ ` · auto-test: ${sessionTestAuto ? "on" : "off"}` +
1419
+ (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "")
1336
1420
  ) + (sessionTestAuto ? "" : dim(" (/test-auto to enable)"))
1337
1421
  );
1338
1422
  } catch (err) {
@@ -1355,6 +1439,8 @@ async function main() {
1355
1439
  // the OS delivers a real SIGINT to the process. Attach the same handler to both —
1356
1440
  // only one can ever fire for a given mode, so there's no double-handling.
1357
1441
  const onInterrupt = () => {
1442
+ // Ctrl+C while a picker is up = cancel it.
1443
+ if (listPickerActive) finishList(-1);
1358
1444
  // Ctrl+C while the approval picker is up = decline it (and fall through to also
1359
1445
  // cancel the turn) — otherwise the poll loop stays blocked awaiting a choice.
1360
1446
  if (approvalActive) finishApproval(false);
@@ -1415,6 +1501,7 @@ async function main() {
1415
1501
  }
1416
1502
  if (line === "/server") {
1417
1503
  await chooseServer();
1504
+ await saveSession(cwd, history); // remember the pick for this folder
1418
1505
  continue;
1419
1506
  }
1420
1507
  if (line === "/test-auto") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.230",
3
+ "version": "1.0.232",
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",