@tiens.nguyen/gonext-cli 1.0.368 → 1.0.370
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 +122 -40
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -642,25 +642,94 @@ if (process.stdout.isTTY) {
|
|
|
642
642
|
});
|
|
643
643
|
}
|
|
644
644
|
// ---------- edit-mode bottom bar (display-only + Shift+Tab to cycle) ----------
|
|
645
|
-
// A split
|
|
646
|
-
//
|
|
647
|
-
// —
|
|
648
|
-
//
|
|
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.
|
|
649
650
|
let editMode = "accept"; // "accept" | "manual"
|
|
650
651
|
const modeLineText = () =>
|
|
651
652
|
white("✎ " + (editMode === "accept" ? "accept edits on" : "manually edit")) +
|
|
652
653
|
dim(" (shift+tab to switch)");
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
654
|
+
// Token usage for the bar's middle row (task #104): the LAST turn's code-model input +
|
|
655
|
+
// output, then the coder's lifetime output total. Unlike the old per-turn print, this row is
|
|
656
|
+
// ALWAYS on screen — a turn that did no code-model work honestly reads "0", and the lifetime
|
|
657
|
+
// total is loaded at startup so the row is populated before the first turn.
|
|
658
|
+
let lastTurnInputTokens = 0;
|
|
659
|
+
let lastTurnOutputTokens = 0;
|
|
660
|
+
// This turn's lifetime total. `globalOutputTokens` is updated by an async POST that can lag a
|
|
661
|
+
// turn behind, so the bar takes whichever of the two is fresher (both are zeroed by a reset).
|
|
662
|
+
let lastTurnTotalOutput = 0;
|
|
663
|
+
const tokenLineText = () => {
|
|
664
|
+
const coder = sessionCodingLabel || defaultCodingModel;
|
|
665
|
+
return (
|
|
666
|
+
dim("tokens · ↑ in ") +
|
|
667
|
+
fmtTokens(lastTurnInputTokens) +
|
|
668
|
+
dim(" · ") +
|
|
669
|
+
cyan(`↓ out ${fmtTokens(lastTurnOutputTokens)}`) +
|
|
670
|
+
dim(" · ") +
|
|
671
|
+
dim(`${coder ? coder + " " : ""}total `) +
|
|
672
|
+
cyan(`↓ ${fmtTokens(Math.max(globalOutputTokens, lastTurnTotalOutput))}`)
|
|
673
|
+
);
|
|
663
674
|
};
|
|
675
|
+
const FOOTER_ROWS = 3; // the rule row + the token row + the mode row
|
|
676
|
+
// True only while the MAIN loop's ">> " prompt is waiting for input. The bar belongs to that
|
|
677
|
+
// prompt, so it must never appear under an ask()/askSecret() question or during a turn.
|
|
678
|
+
let footerActive = false;
|
|
679
|
+
// Physical rows the prompt + typed line occupies (it wraps once past the terminal width) —
|
|
680
|
+
// the bar goes under the LAST of them, not under the caret's row.
|
|
681
|
+
const inputRows = () => {
|
|
682
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
683
|
+
return Math.floor((PROMPT_COLS + rl.line.length) / width) + 1;
|
|
684
|
+
};
|
|
685
|
+
const cursorPos = () =>
|
|
686
|
+
(rl.getCursorPos ? rl.getCursorPos() : null) ?? { rows: 0, cols: PROMPT_COLS };
|
|
687
|
+
// Draw the bar below the input line and put the caret back exactly where readline left it.
|
|
688
|
+
// Relative moves only (same reason as the slash overlay): the block must survive the terminal
|
|
689
|
+
// scrolling — and writing past the last screen row scrolls the input line up TOO, so the
|
|
690
|
+
// matching up-move still lands on it.
|
|
691
|
+
function drawFooter() {
|
|
692
|
+
if (!footerActive || !process.stdout.isTTY || slashOverlayActive) return;
|
|
693
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
694
|
+
const pos = cursorPos();
|
|
695
|
+
const down = Math.max(0, inputRows() - 1 - pos.rows); // caret row → last input row
|
|
696
|
+
process.stdout.write(
|
|
697
|
+
(down ? `\x1b[${down}B` : "") +
|
|
698
|
+
"\r\n\x1b[2K" + dim("─".repeat(width)) +
|
|
699
|
+
// CLIPPED: both rows can exceed the width (the mode line is ~44 columns, the token row
|
|
700
|
+
// carries a model name) and would then WRAP onto an extra row — ESC[nA counts PHYSICAL
|
|
701
|
+
// rows, so the caret would come back short and the next keystroke would type over the
|
|
702
|
+
// bar (the #109 failure mode).
|
|
703
|
+
"\r\n\x1b[2K" + clipVisible(GUTTER + tokenLineText(), width - 1) +
|
|
704
|
+
"\r\n\x1b[2K" + clipVisible(GUTTER + modeLineText(), width - 1) +
|
|
705
|
+
"\x1b[J" + // nothing stale below the bar
|
|
706
|
+
`\x1b[${FOOTER_ROWS + down}A\r` + // back to the caret's row
|
|
707
|
+
(pos.cols > 0 ? `\x1b[${pos.cols}C` : "")
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
// Wipe the bar (and anything under it) WITHOUT touching the input line — called the instant
|
|
711
|
+
// Enter is pressed so the command's output starts on clean rows instead of printing over the
|
|
712
|
+
// rule and orphaning the mode line.
|
|
713
|
+
function eraseFooter() {
|
|
714
|
+
const was = footerActive;
|
|
715
|
+
footerActive = false;
|
|
716
|
+
if (!was || !process.stdout.isTTY || slashOverlayActive) return;
|
|
717
|
+
const pos = cursorPos();
|
|
718
|
+
const down = Math.max(0, inputRows() - 1 - pos.rows) + 1; // first footer row
|
|
719
|
+
process.stdout.write(
|
|
720
|
+
`\x1b[${down}B\r\x1b[J\x1b[${down}A\r` + (pos.cols > 0 ? `\x1b[${pos.cols}C` : "")
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
// Shift+Tab redraw: the bar is below the caret, so a plain redraw in place is enough.
|
|
724
|
+
const redrawModeLine = () => drawFooter();
|
|
725
|
+
// Enter must tear the bar down BEFORE readline echoes its newline (which would land the
|
|
726
|
+
// cursor on the rule row and leave the mode line orphaned below the output). Our normal
|
|
727
|
+
// keypress listener runs AFTER readline's, so this one is prepended to run first.
|
|
728
|
+
if (process.stdin.isTTY) {
|
|
729
|
+
process.stdin.prependListener("keypress", (_ch, key) => {
|
|
730
|
+
if (key && (key.name === "return" || key.name === "enter")) eraseFooter();
|
|
731
|
+
});
|
|
732
|
+
}
|
|
664
733
|
if (process.stdin.isTTY) {
|
|
665
734
|
process.stdin.on("keypress", (_ch, key) => {
|
|
666
735
|
// An arrow-key list picker (/server) is up → ↑/↓ move the highlight, Enter chooses.
|
|
@@ -1620,6 +1689,10 @@ rl._refreshLine = () => {
|
|
|
1620
1689
|
if (following || listPickerActive || slashOverlayActive) return;
|
|
1621
1690
|
if (secretInput) return; // don't redraw the typed key (task #127)
|
|
1622
1691
|
if (_origRefreshLine) _origRefreshLine();
|
|
1692
|
+
// That refresh ends with clearScreenDown — i.e. it just ERASED the bottom bar sitting
|
|
1693
|
+
// under the input line. Redraw it here so every readline redraw (typing, backspace,
|
|
1694
|
+
// history recall, rl.prompt(), a resize) repaints the bar as one atomic step.
|
|
1695
|
+
drawFooter();
|
|
1623
1696
|
};
|
|
1624
1697
|
|
|
1625
1698
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -2666,11 +2739,16 @@ async function main() {
|
|
|
2666
2739
|
} else {
|
|
2667
2740
|
// Ctrl+C at a prompt with the slash overlay open = dismiss the overlay first (#109).
|
|
2668
2741
|
if (slashOverlayActive) closeSlashOverlay();
|
|
2742
|
+
// Wipe the bottom bar before printing below the input line, or the note would land on
|
|
2743
|
+
// the rule row and strand the mode line under it.
|
|
2744
|
+
eraseFooter();
|
|
2669
2745
|
process.stdout.write("\n" + GUTTER + dim("(/exit to quit)") + "\n");
|
|
2670
2746
|
// Discard anything half-typed (Ctrl-C at a prompt means "never mind") and
|
|
2671
|
-
// redraw through readline so its cursor tracking stays right
|
|
2747
|
+
// redraw through readline so its cursor tracking stays right — the refresh repaints
|
|
2748
|
+
// the bar under the fresh prompt.
|
|
2672
2749
|
rl.line = "";
|
|
2673
2750
|
rl.cursor = 0;
|
|
2751
|
+
footerActive = process.stdout.isTTY;
|
|
2674
2752
|
rl.prompt();
|
|
2675
2753
|
}
|
|
2676
2754
|
};
|
|
@@ -2683,19 +2761,30 @@ async function main() {
|
|
|
2683
2761
|
let lastCancelledTask = "";
|
|
2684
2762
|
|
|
2685
2763
|
for (;;) {
|
|
2686
|
-
// The bottom bar (split
|
|
2687
|
-
// the
|
|
2688
|
-
|
|
2764
|
+
// The bottom bar (split rule + edit-mode indicator) is pinned BELOW the input line, at
|
|
2765
|
+
// the very bottom of the screen. Arming it here is all it takes: rl.prompt() refreshes
|
|
2766
|
+
// the line, and our _refreshLine hook paints the bar underneath (and repaints it on
|
|
2767
|
+
// every keystroke that erases it). Enter tears it down again.
|
|
2768
|
+
footerActive = process.stdout.isTTY;
|
|
2689
2769
|
rl.prompt();
|
|
2690
|
-
// Absorb blank Enter-presses SILENTLY
|
|
2691
|
-
//
|
|
2692
|
-
//
|
|
2693
|
-
//
|
|
2770
|
+
// Absorb blank Enter-presses SILENTLY — they never reach the agent. Lines typed while
|
|
2771
|
+
// it was busy on a long turn (queued, not yet read) drain here all at once the moment
|
|
2772
|
+
// the turn ends; a queued line carries no Enter keypress, so its re-prompt overwrites
|
|
2773
|
+
// in place (\r + refresh) rather than stacking ">> >> >> " on one row.
|
|
2694
2774
|
let lineRaw;
|
|
2695
2775
|
for (;;) {
|
|
2696
2776
|
lineRaw = await nextLine();
|
|
2697
2777
|
if (lineRaw === null || lineRaw.trim()) break;
|
|
2778
|
+
// A bare Enter: its keypress already tore the bar down, and readline left the caret on
|
|
2779
|
+
// a fresh row with no prompt on it. Re-print the prompt so that row isn't blank — the
|
|
2780
|
+
// refresh repaints the bar under it. (Queued lines drain with no keypress at all, so
|
|
2781
|
+
// this re-prompt just overwrites in place and can't stack ">> " on one row.)
|
|
2782
|
+
if (process.stdout.isTTY) {
|
|
2783
|
+
footerActive = true;
|
|
2784
|
+
rl.prompt();
|
|
2785
|
+
}
|
|
2698
2786
|
}
|
|
2787
|
+
footerActive = false; // a line arrived from the queue (no Enter keypress) → disarm
|
|
2699
2788
|
if (lineRaw === null) break; // stdin closed
|
|
2700
2789
|
const line = lineRaw.trim();
|
|
2701
2790
|
if (line === "/exit" || line === "/quit") break;
|
|
@@ -2776,6 +2865,9 @@ async function main() {
|
|
|
2776
2865
|
const done = await resetGlobalOutputTokens();
|
|
2777
2866
|
if (done) {
|
|
2778
2867
|
globalOutputTokens = 0;
|
|
2868
|
+
// The bar takes the FRESHER of the two totals, so the last turn's copy has to be
|
|
2869
|
+
// zeroed too or the reset wouldn't show up in it.
|
|
2870
|
+
lastTurnTotalOutput = 0;
|
|
2779
2871
|
console.log(GUTTER + green("✓ global + workspace output-token totals reset to 0.\n"));
|
|
2780
2872
|
} else {
|
|
2781
2873
|
console.log(GUTTER + dim("Could not reset the output-token totals (API error).\n"));
|
|
@@ -2821,6 +2913,14 @@ async function main() {
|
|
|
2821
2913
|
try {
|
|
2822
2914
|
const { text: answer, shown, cancelled, inputTokens, outputTokens, turnOutputTokens } =
|
|
2823
2915
|
await runAgentTurn(history);
|
|
2916
|
+
// Feed the bottom bar's token row (task #104). Assigned for EVERY turn — including one
|
|
2917
|
+
// that ended without an answer — so the row never shows a stale count from an older
|
|
2918
|
+
// turn. It replaces the print-once-per-turn footer that used to live here: the bar is
|
|
2919
|
+
// always on screen, so printing the same three figures into the scrollback right above
|
|
2920
|
+
// it just read as a duplicate.
|
|
2921
|
+
lastTurnInputTokens = inputTokens ?? 0;
|
|
2922
|
+
lastTurnOutputTokens = turnOutputTokens ?? 0;
|
|
2923
|
+
lastTurnTotalOutput = outputTokens ?? 0;
|
|
2824
2924
|
if (answer) {
|
|
2825
2925
|
history.push({ role: "assistant", content: answer });
|
|
2826
2926
|
// Persist after every successful turn (not just on clean exit) — an ungraceful
|
|
@@ -2836,24 +2936,6 @@ async function main() {
|
|
|
2836
2936
|
// renderAnswer also WRAPS long lines and re-indents each continuation row, so a wrapped
|
|
2837
2937
|
// paragraph keeps the same left margin instead of soft-wrapping back to column 0.
|
|
2838
2938
|
console.log(shown ? "" : `${LINE_SPACING}${renderAnswer(answer)}\n`);
|
|
2839
|
-
// Task #104: persistent token footer at the bottom of the screen — this turn's
|
|
2840
|
-
// code-model INPUT + this turn's OUTPUT (↓ out), then the model's GLOBAL lifetime
|
|
2841
|
-
// output total (all turns for this user + coding server), labelled with the model
|
|
2842
|
-
// name so it's clear which coder that running total belongs to. Guard on THIS turn's
|
|
2843
|
-
// activity so a plain-reply greeting that did no code-model work shows nothing (#112).
|
|
2844
|
-
if ((inputTokens ?? 0) > 0 || (turnOutputTokens ?? 0) > 0) {
|
|
2845
|
-
const coder = sessionCodingLabel || defaultCodingModel;
|
|
2846
|
-
console.log(
|
|
2847
|
-
GUTTER +
|
|
2848
|
-
dim("tokens · ↑ in ") +
|
|
2849
|
-
fmtTokens(inputTokens ?? 0) +
|
|
2850
|
-
dim(" · ") +
|
|
2851
|
-
cyan(`↓ out ${fmtTokens(turnOutputTokens ?? 0)}`) +
|
|
2852
|
-
dim(" · ") +
|
|
2853
|
-
dim(`${coder ? coder + " " : ""}total `) +
|
|
2854
|
-
cyan(`↓ ${fmtTokens(outputTokens ?? 0)}`)
|
|
2855
|
-
);
|
|
2856
|
-
}
|
|
2857
2939
|
} else {
|
|
2858
2940
|
history.pop(); // aborted/failed/cancelled turn — don't poison the next request
|
|
2859
2941
|
// A turn that ended WITHOUT an answer (Ctrl+C cancel, or an abort/failure) must not
|
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.370",
|
|
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",
|