@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.
- package/gonext-repl.mjs +109 -22
- 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
|
-
{
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
servers.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
662
|
-
|
|
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
|
|
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.
|
|
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",
|