@tiens.nguyen/gonext-cli 1.0.367 → 1.0.369
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 +110 -13
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* interrupted investigation actually resumes it). /reset clears it.
|
|
22
22
|
*
|
|
23
23
|
* Requires the worker daemon to be running (it claims the job).
|
|
24
|
-
* Commands: /exit /model /reset /
|
|
24
|
+
* Commands: /exit /model /reset /manual-edit [runId] /help Ctrl-C stops following the current run.
|
|
25
25
|
*/
|
|
26
26
|
import { readFile, mkdir, writeFile, unlink } from "node:fs/promises";
|
|
27
27
|
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
@@ -286,7 +286,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
286
286
|
" -v, --verbose show the agent's diagnostic logs ([gonext-agent] …) and python stderr\n" +
|
|
287
287
|
" -h, --help this help\n\n" +
|
|
288
288
|
"Auth comes from ~/.gonext/worker.env; models/budgets/RAG come from your web Settings.\n" +
|
|
289
|
-
"Inside the REPL: /exit · /model · /reset · /
|
|
289
|
+
"Inside the REPL: /exit · /model · /reset · /manual-edit [runId] · /help — Ctrl-C stops following a turn.\n" +
|
|
290
290
|
"Questions are enqueued like web questions and executed by the RUNNING\n" +
|
|
291
291
|
"gonext-cli daemon — its terminal shows the full [gonext-agent] logs.\n" +
|
|
292
292
|
"Conversation history is saved per-folder in ~/.gonext/sessions and resumed next time\n" +
|
|
@@ -360,7 +360,7 @@ const COMMANDS = [
|
|
|
360
360
|
{ name: "/test-auto", desc: "toggle auto-test (verify & fix until it passes) for this folder" },
|
|
361
361
|
{ name: "/rag-local", desc: "toggle storing this folder's RAG knowledge base locally (vs cloud/S3)" },
|
|
362
362
|
{ name: "/server", desc: "pick a deployment server (host/user) for this session" },
|
|
363
|
-
{ name: "/revert", usage: "/
|
|
363
|
+
{ name: "/manual-edit", aliases: ["/revert"], usage: "/manual-edit [runId]", desc: "undo the agent's file edits so you can edit them yourself (latest run)" },
|
|
364
364
|
{ name: "/reset", aliases: ["/new"], desc: "forget this folder's saved conversation and start fresh" },
|
|
365
365
|
{ name: "/output-token", desc: "show the agent code-model OUTPUT-token total for this workspace" },
|
|
366
366
|
{ name: "/reset-output-token-global", desc: "reset the GLOBAL (you + coding server) output-token total to 0" },
|
|
@@ -641,6 +641,72 @@ if (process.stdout.isTTY) {
|
|
|
641
641
|
}
|
|
642
642
|
});
|
|
643
643
|
}
|
|
644
|
+
// ---------- edit-mode bottom bar (display-only + Shift+Tab to cycle) ----------
|
|
645
|
+
// A split rule + a mode indicator pinned BELOW the ">> " input line — the bar is the last
|
|
646
|
+
// thing on screen and the user types ABOVE it: "accept edits on" (the agent applies file
|
|
647
|
+
// edits — today's behaviour) vs "manually edit". DISPLAY-ONLY for now — it tracks the mode
|
|
648
|
+
// so the bar reflects it and Shift+Tab cycles it; wiring it to actually gate the agent's
|
|
649
|
+
// edits is a deliberate follow-up.
|
|
650
|
+
let editMode = "accept"; // "accept" | "manual"
|
|
651
|
+
const modeLineText = () =>
|
|
652
|
+
white("✎ " + (editMode === "accept" ? "accept edits on" : "manually edit")) +
|
|
653
|
+
dim(" (shift+tab to switch)");
|
|
654
|
+
const FOOTER_ROWS = 2; // the rule row + the mode row
|
|
655
|
+
// True only while the MAIN loop's ">> " prompt is waiting for input. The bar belongs to that
|
|
656
|
+
// prompt, so it must never appear under an ask()/askSecret() question or during a turn.
|
|
657
|
+
let footerActive = false;
|
|
658
|
+
// Physical rows the prompt + typed line occupies (it wraps once past the terminal width) —
|
|
659
|
+
// the bar goes under the LAST of them, not under the caret's row.
|
|
660
|
+
const inputRows = () => {
|
|
661
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
662
|
+
return Math.floor((PROMPT_COLS + rl.line.length) / width) + 1;
|
|
663
|
+
};
|
|
664
|
+
const cursorPos = () =>
|
|
665
|
+
(rl.getCursorPos ? rl.getCursorPos() : null) ?? { rows: 0, cols: PROMPT_COLS };
|
|
666
|
+
// Draw the bar below the input line and put the caret back exactly where readline left it.
|
|
667
|
+
// Relative moves only (same reason as the slash overlay): the block must survive the terminal
|
|
668
|
+
// scrolling — and writing past the last screen row scrolls the input line up TOO, so the
|
|
669
|
+
// matching up-move still lands on it.
|
|
670
|
+
function drawFooter() {
|
|
671
|
+
if (!footerActive || !process.stdout.isTTY || slashOverlayActive) return;
|
|
672
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
673
|
+
const pos = cursorPos();
|
|
674
|
+
const down = Math.max(0, inputRows() - 1 - pos.rows); // caret row → last input row
|
|
675
|
+
process.stdout.write(
|
|
676
|
+
(down ? `\x1b[${down}B` : "") +
|
|
677
|
+
"\r\n\x1b[2K" + dim("─".repeat(width)) +
|
|
678
|
+
// CLIPPED: the mode line is ~44 columns, so on a narrow terminal it would WRAP onto a
|
|
679
|
+
// third row — and ESC[nA counts PHYSICAL rows, so the caret would come back one row
|
|
680
|
+
// short and the next keystroke would type over the rule (the #109 failure mode).
|
|
681
|
+
"\r\n\x1b[2K" + clipVisible(GUTTER + modeLineText(), width - 1) +
|
|
682
|
+
"\x1b[J" + // nothing stale below the bar
|
|
683
|
+
`\x1b[${FOOTER_ROWS + down}A\r` + // back to the caret's row
|
|
684
|
+
(pos.cols > 0 ? `\x1b[${pos.cols}C` : "")
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
// Wipe the bar (and anything under it) WITHOUT touching the input line — called the instant
|
|
688
|
+
// Enter is pressed so the command's output starts on clean rows instead of printing over the
|
|
689
|
+
// rule and orphaning the mode line.
|
|
690
|
+
function eraseFooter() {
|
|
691
|
+
const was = footerActive;
|
|
692
|
+
footerActive = false;
|
|
693
|
+
if (!was || !process.stdout.isTTY || slashOverlayActive) return;
|
|
694
|
+
const pos = cursorPos();
|
|
695
|
+
const down = Math.max(0, inputRows() - 1 - pos.rows) + 1; // first footer row
|
|
696
|
+
process.stdout.write(
|
|
697
|
+
`\x1b[${down}B\r\x1b[J\x1b[${down}A\r` + (pos.cols > 0 ? `\x1b[${pos.cols}C` : "")
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
// Shift+Tab redraw: the bar is below the caret, so a plain redraw in place is enough.
|
|
701
|
+
const redrawModeLine = () => drawFooter();
|
|
702
|
+
// Enter must tear the bar down BEFORE readline echoes its newline (which would land the
|
|
703
|
+
// cursor on the rule row and leave the mode line orphaned below the output). Our normal
|
|
704
|
+
// keypress listener runs AFTER readline's, so this one is prepended to run first.
|
|
705
|
+
if (process.stdin.isTTY) {
|
|
706
|
+
process.stdin.prependListener("keypress", (_ch, key) => {
|
|
707
|
+
if (key && (key.name === "return" || key.name === "enter")) eraseFooter();
|
|
708
|
+
});
|
|
709
|
+
}
|
|
644
710
|
if (process.stdin.isTTY) {
|
|
645
711
|
process.stdin.on("keypress", (_ch, key) => {
|
|
646
712
|
// An arrow-key list picker (/server) is up → ↑/↓ move the highlight, Enter chooses.
|
|
@@ -665,6 +731,15 @@ if (process.stdin.isTTY) {
|
|
|
665
731
|
rl.historyIndex = -1;
|
|
666
732
|
return;
|
|
667
733
|
}
|
|
734
|
+
// Shift+Tab cycles the edit mode shown in the bottom bar (display-only). Skip while the
|
|
735
|
+
// slash overlay owns the screen so we don't fight its redraw.
|
|
736
|
+
if (key && key.name === "tab" && key.shift) {
|
|
737
|
+
if (!slashOverlayActive) {
|
|
738
|
+
editMode = editMode === "accept" ? "manual" : "accept";
|
|
739
|
+
redrawModeLine();
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
668
743
|
// Enter is handled by rl.on("line") — don't redraw the hint on the way out.
|
|
669
744
|
if (key && (key.name === "return" || key.name === "enter")) return;
|
|
670
745
|
// ↑/↓ while the menu is up = move the highlight IN PLACE. readline drawing is
|
|
@@ -1591,6 +1666,10 @@ rl._refreshLine = () => {
|
|
|
1591
1666
|
if (following || listPickerActive || slashOverlayActive) return;
|
|
1592
1667
|
if (secretInput) return; // don't redraw the typed key (task #127)
|
|
1593
1668
|
if (_origRefreshLine) _origRefreshLine();
|
|
1669
|
+
// That refresh ends with clearScreenDown — i.e. it just ERASED the bottom bar sitting
|
|
1670
|
+
// under the input line. Redraw it here so every readline redraw (typing, backspace,
|
|
1671
|
+
// history recall, rl.prompt(), a resize) repaints the bar as one atomic step.
|
|
1672
|
+
drawFooter();
|
|
1594
1673
|
};
|
|
1595
1674
|
|
|
1596
1675
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -2525,14 +2604,13 @@ async function main() {
|
|
|
2525
2604
|
bold(cyan("GONEXT AGENT")) + dim(` v${REPL_VERSION}`),
|
|
2526
2605
|
lbl("model") + (p.agentModelId || probe?.modelKey || "?"),
|
|
2527
2606
|
lbl("coder") + (p.codingModelId || dim("—")),
|
|
2528
|
-
lbl("RAG") +
|
|
2529
|
-
|
|
2530
|
-
dim(" auto-test ") + (sessionTestAuto ? "on" : dim("off")),
|
|
2607
|
+
lbl("RAG") + (p.ragEnabled ? (sessionRagMode === "cloud" ? "cloud" : "local") : dim("off")),
|
|
2608
|
+
sessionTestAuto ? green("auto-test on") : dim("auto-test off"),
|
|
2531
2609
|
lbl("folder") + ws + dim(wsSync ? ` (${wsSync.run ? "run" : "read-only"}${wsSync.enabled ? " · sync" : ""})` : ""),
|
|
2532
2610
|
];
|
|
2533
2611
|
if (selectedServer)
|
|
2534
2612
|
statusRows.push(lbl("deploy") + `${selectedServer.name} ${dim(`(${selectedServer.user}@${selectedServer.host})`)}`);
|
|
2535
|
-
statusRows.push(dim(`
|
|
2613
|
+
statusRows.push(dim(`key ${workerKey.slice(0, 8)}…`));
|
|
2536
2614
|
console.log(renderBanner(statusRows));
|
|
2537
2615
|
}
|
|
2538
2616
|
// Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
|
|
@@ -2638,11 +2716,16 @@ async function main() {
|
|
|
2638
2716
|
} else {
|
|
2639
2717
|
// Ctrl+C at a prompt with the slash overlay open = dismiss the overlay first (#109).
|
|
2640
2718
|
if (slashOverlayActive) closeSlashOverlay();
|
|
2719
|
+
// Wipe the bottom bar before printing below the input line, or the note would land on
|
|
2720
|
+
// the rule row and strand the mode line under it.
|
|
2721
|
+
eraseFooter();
|
|
2641
2722
|
process.stdout.write("\n" + GUTTER + dim("(/exit to quit)") + "\n");
|
|
2642
2723
|
// Discard anything half-typed (Ctrl-C at a prompt means "never mind") and
|
|
2643
|
-
// redraw through readline so its cursor tracking stays right
|
|
2724
|
+
// redraw through readline so its cursor tracking stays right — the refresh repaints
|
|
2725
|
+
// the bar under the fresh prompt.
|
|
2644
2726
|
rl.line = "";
|
|
2645
2727
|
rl.cursor = 0;
|
|
2728
|
+
footerActive = process.stdout.isTTY;
|
|
2646
2729
|
rl.prompt();
|
|
2647
2730
|
}
|
|
2648
2731
|
};
|
|
@@ -2655,16 +2738,30 @@ async function main() {
|
|
|
2655
2738
|
let lastCancelledTask = "";
|
|
2656
2739
|
|
|
2657
2740
|
for (;;) {
|
|
2741
|
+
// The bottom bar (split rule + edit-mode indicator) is pinned BELOW the input line, at
|
|
2742
|
+
// the very bottom of the screen. Arming it here is all it takes: rl.prompt() refreshes
|
|
2743
|
+
// the line, and our _refreshLine hook paints the bar underneath (and repaints it on
|
|
2744
|
+
// every keystroke that erases it). Enter tears it down again.
|
|
2745
|
+
footerActive = process.stdout.isTTY;
|
|
2658
2746
|
rl.prompt();
|
|
2659
|
-
// Absorb blank Enter-presses SILENTLY
|
|
2660
|
-
//
|
|
2661
|
-
//
|
|
2662
|
-
//
|
|
2747
|
+
// Absorb blank Enter-presses SILENTLY — they never reach the agent. Lines typed while
|
|
2748
|
+
// it was busy on a long turn (queued, not yet read) drain here all at once the moment
|
|
2749
|
+
// the turn ends; a queued line carries no Enter keypress, so its re-prompt overwrites
|
|
2750
|
+
// in place (\r + refresh) rather than stacking ">> >> >> " on one row.
|
|
2663
2751
|
let lineRaw;
|
|
2664
2752
|
for (;;) {
|
|
2665
2753
|
lineRaw = await nextLine();
|
|
2666
2754
|
if (lineRaw === null || lineRaw.trim()) break;
|
|
2755
|
+
// A bare Enter: its keypress already tore the bar down, and readline left the caret on
|
|
2756
|
+
// a fresh row with no prompt on it. Re-print the prompt so that row isn't blank — the
|
|
2757
|
+
// refresh repaints the bar under it. (Queued lines drain with no keypress at all, so
|
|
2758
|
+
// this re-prompt just overwrites in place and can't stack ">> " on one row.)
|
|
2759
|
+
if (process.stdout.isTTY) {
|
|
2760
|
+
footerActive = true;
|
|
2761
|
+
rl.prompt();
|
|
2762
|
+
}
|
|
2667
2763
|
}
|
|
2764
|
+
footerActive = false; // a line arrived from the queue (no Enter keypress) → disarm
|
|
2668
2765
|
if (lineRaw === null) break; // stdin closed
|
|
2669
2766
|
const line = lineRaw.trim();
|
|
2670
2767
|
if (line === "/exit" || line === "/quit") break;
|
|
@@ -2751,7 +2848,7 @@ async function main() {
|
|
|
2751
2848
|
}
|
|
2752
2849
|
continue;
|
|
2753
2850
|
}
|
|
2754
|
-
if (line.startsWith("/revert")) {
|
|
2851
|
+
if (line.startsWith("/manual-edit") || line.startsWith("/revert")) {
|
|
2755
2852
|
const runId = line.split(/\s+/)[1] ?? "";
|
|
2756
2853
|
await new Promise((res) => {
|
|
2757
2854
|
const p = spawn(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.369",
|
|
4
4
|
"description": "GoNext CLI — the gonext terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|