@tiens.nguyen/gonext-cli 1.0.370 → 1.0.372
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 +59 -20
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -673,6 +673,18 @@ const tokenLineText = () => {
|
|
|
673
673
|
);
|
|
674
674
|
};
|
|
675
675
|
const FOOTER_ROWS = 3; // the rule row + the token row + the mode row
|
|
676
|
+
// The bar's rows as plain printable strings, one PHYSICAL terminal row each. Both text rows
|
|
677
|
+
// can exceed the width (the mode line is ~44 columns, the token row carries a model name) and
|
|
678
|
+
// would then WRAP onto an extra row — every consumer here counts rows with ESC[nA / ESC[nB, so
|
|
679
|
+
// a wrapped row corrupts the redraw (the #109 failure mode). Hence clipVisible on both.
|
|
680
|
+
const footerRows = () => {
|
|
681
|
+
const width = Math.max(20, process.stdout.columns || 80);
|
|
682
|
+
return [
|
|
683
|
+
dim("─".repeat(width)),
|
|
684
|
+
clipVisible(GUTTER + tokenLineText(), width - 1),
|
|
685
|
+
clipVisible(GUTTER + modeLineText(), width - 1),
|
|
686
|
+
];
|
|
687
|
+
};
|
|
676
688
|
// True only while the MAIN loop's ">> " prompt is waiting for input. The bar belongs to that
|
|
677
689
|
// prompt, so it must never appear under an ask()/askSecret() question or during a turn.
|
|
678
690
|
let footerActive = false;
|
|
@@ -690,18 +702,11 @@ const cursorPos = () =>
|
|
|
690
702
|
// matching up-move still lands on it.
|
|
691
703
|
function drawFooter() {
|
|
692
704
|
if (!footerActive || !process.stdout.isTTY || slashOverlayActive) return;
|
|
693
|
-
const width = Math.max(20, process.stdout.columns || 80);
|
|
694
705
|
const pos = cursorPos();
|
|
695
706
|
const down = Math.max(0, inputRows() - 1 - pos.rows); // caret row → last input row
|
|
696
707
|
process.stdout.write(
|
|
697
708
|
(down ? `\x1b[${down}B` : "") +
|
|
698
|
-
"\r\n\x1b[2K" +
|
|
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) +
|
|
709
|
+
footerRows().map((r) => "\r\n\x1b[2K" + r).join("") +
|
|
705
710
|
"\x1b[J" + // nothing stale below the bar
|
|
706
711
|
`\x1b[${FOOTER_ROWS + down}A\r` + // back to the caret's row
|
|
707
712
|
(pos.cols > 0 ? `\x1b[${pos.cols}C` : "")
|
|
@@ -722,12 +727,20 @@ function eraseFooter() {
|
|
|
722
727
|
}
|
|
723
728
|
// Shift+Tab redraw: the bar is below the caret, so a plain redraw in place is enough.
|
|
724
729
|
const redrawModeLine = () => drawFooter();
|
|
730
|
+
// Set by the prepended Enter handler, cleared once the resulting line is consumed. It tells
|
|
731
|
+
// the empty-input path that readline really echoed a newline to the terminal (a live
|
|
732
|
+
// keypress) and there is therefore a row to step back onto — as opposed to a line drained
|
|
733
|
+
// from _lineQueue, which produced no echo to undo.
|
|
734
|
+
let enterEchoed = false;
|
|
725
735
|
// Enter must tear the bar down BEFORE readline echoes its newline (which would land the
|
|
726
736
|
// cursor on the rule row and leave the mode line orphaned below the output). Our normal
|
|
727
737
|
// keypress listener runs AFTER readline's, so this one is prepended to run first.
|
|
728
738
|
if (process.stdin.isTTY) {
|
|
729
739
|
process.stdin.prependListener("keypress", (_ch, key) => {
|
|
730
|
-
if (key && (key.name === "return" || key.name === "enter"))
|
|
740
|
+
if (key && (key.name === "return" || key.name === "enter")) {
|
|
741
|
+
enterEchoed = true;
|
|
742
|
+
eraseFooter();
|
|
743
|
+
}
|
|
731
744
|
});
|
|
732
745
|
}
|
|
733
746
|
if (process.stdin.isTTY) {
|
|
@@ -1864,7 +1877,9 @@ async function runAgentTurn(history) {
|
|
|
1864
1877
|
let lastApprovalId = null; // the last command-approval request we've already answered
|
|
1865
1878
|
let carry = ""; // trailing partial content line held until its newline arrives
|
|
1866
1879
|
let statusShown = false; // a transient status line is currently on screen
|
|
1867
|
-
let statusLines = 2; //
|
|
1880
|
+
let statusLines = 2; // TOTAL rows the status block occupies, bottom bar included (clearStatus)
|
|
1881
|
+
// …of which these are the thought/word rows — the only ones hover + click hit-test against.
|
|
1882
|
+
let statusContentLines = 2;
|
|
1868
1883
|
let warnedPending = false;
|
|
1869
1884
|
// Transient poll failures (a network blip or a 5xx like the API's momentary "Worker
|
|
1870
1885
|
// key lookup failed." — a cold Mongo connection, NOT a bad key) must NOT kill a
|
|
@@ -1891,6 +1906,10 @@ async function runAgentTurn(history) {
|
|
|
1891
1906
|
// the lifetime per-workspace + global counters once the turn completes.
|
|
1892
1907
|
let latestOutputTokens = 0;
|
|
1893
1908
|
let outputTokensPosted = false; // guard: increment the lifetime counters once per turn
|
|
1909
|
+
// A new turn starts from zero on the bar's token row too — otherwise the previous turn's
|
|
1910
|
+
// in/out figures would sit there, looking live, until the first poll reports real numbers.
|
|
1911
|
+
lastTurnInputTokens = 0;
|
|
1912
|
+
lastTurnOutputTokens = 0;
|
|
1894
1913
|
// First few KB of the raw stream as consumed — a cheap fingerprint to verify the
|
|
1895
1914
|
// completed resultText really EXTENDS what we streamed (an old worker replaces it
|
|
1896
1915
|
// with the shorter bare answer, which must not be sliced by shownChars).
|
|
@@ -2012,11 +2031,16 @@ async function runAgentTurn(history) {
|
|
|
2012
2031
|
// the playful-word line.
|
|
2013
2032
|
const expLines = thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
|
|
2014
2033
|
const drawn = 2 + expLines.length;
|
|
2034
|
+
// The bottom bar rides along UNDER the live status while a turn runs, so it stays on screen
|
|
2035
|
+
// the whole time the agent is thinking instead of only at the prompt. It's part of the same
|
|
2036
|
+
// in-place block: the ticker redraws it every 120ms and clearStatus wipes it with the rest,
|
|
2037
|
+
// so content still lands on clean rows.
|
|
2038
|
+
const bar = process.stdout.isTTY ? footerRows() : [];
|
|
2015
2039
|
const rows = process.stdout.rows || 24;
|
|
2016
2040
|
// Hover-highlight only the THOUGHT line (line 1) — its exact viewport row comes from the
|
|
2017
2041
|
// \x1b[6n probe below (`thoughtRow`); fall back to the viewport-bottom assumption only when
|
|
2018
|
-
// the terminal never answered the probe.
|
|
2019
|
-
const tRow = thoughtRow >= 1 ? thoughtRow : rows - drawn + 1;
|
|
2042
|
+
// the terminal never answered the probe (the block's top = bottom minus ALL its rows).
|
|
2043
|
+
const tRow = thoughtRow >= 1 ? thoughtRow : rows - (drawn + bar.length) + 1;
|
|
2020
2044
|
const hovering = clickable && hoverRow === tRow;
|
|
2021
2045
|
clearStatus();
|
|
2022
2046
|
// Right after clearing, the cursor sits at the status block's TOP-LEFT = the thought line's
|
|
@@ -2034,8 +2058,10 @@ async function runAgentTurn(history) {
|
|
|
2034
2058
|
let out = `${markLine(green(BULLET))}${hovering ? hoverThought(label) : green(label)}`;
|
|
2035
2059
|
for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
|
|
2036
2060
|
out += "\n" + `${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${tokLabel}`;
|
|
2061
|
+
for (const br of bar) out += "\n" + br;
|
|
2037
2062
|
process.stdout.write(out);
|
|
2038
|
-
|
|
2063
|
+
statusContentLines = drawn;
|
|
2064
|
+
statusLines = drawn + bar.length;
|
|
2039
2065
|
statusShown = true;
|
|
2040
2066
|
};
|
|
2041
2067
|
// 120ms so the spinner animates smoothly (the frame math above uses the wall clock, so
|
|
@@ -2053,9 +2079,10 @@ async function runAgentTurn(history) {
|
|
|
2053
2079
|
if (!statusShown) return;
|
|
2054
2080
|
const rows = process.stdout.rows || 24;
|
|
2055
2081
|
const top = thoughtRow >= 1 ? thoughtRow : rows - statusLines + 1; // thought line row
|
|
2056
|
-
// Toggle only on the thought line + its grey expansion — NOT the
|
|
2057
|
-
//
|
|
2058
|
-
|
|
2082
|
+
// Toggle only on the thought line + its grey expansion — NOT the playful-word/token row,
|
|
2083
|
+
// and NOT the bottom bar below it (statusContentLines excludes the bar), so a click there
|
|
2084
|
+
// doesn't expand what it isn't part of.
|
|
2085
|
+
if (row < top || row > top + statusContentLines - 2) return;
|
|
2059
2086
|
// Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
|
|
2060
2087
|
// so we don't flicker an empty expand.
|
|
2061
2088
|
if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
|
|
@@ -2404,6 +2431,12 @@ async function runAgentTurn(history) {
|
|
|
2404
2431
|
if (Number.isFinite(job.agentOutputTokens) && job.agentOutputTokens > latestOutputTokens) {
|
|
2405
2432
|
latestOutputTokens = job.agentOutputTokens;
|
|
2406
2433
|
}
|
|
2434
|
+
// Mirror both into the bottom bar's token row so it counts UP live while the turn runs
|
|
2435
|
+
// (the bar rides under the status block now), instead of sitting on the last turn's
|
|
2436
|
+
// figures until this one finishes. The turn-end assignment writes the same numbers again.
|
|
2437
|
+
lastTurnInputTokens = latestCodeTokens;
|
|
2438
|
+
lastTurnOutputTokens = latestOutputTokens;
|
|
2439
|
+
lastTurnTotalOutput = globalOutputTokens + latestOutputTokens;
|
|
2407
2440
|
const text = typeof job.resultText === "string" ? job.resultText : "";
|
|
2408
2441
|
if (job.jobStatus === "completed") {
|
|
2409
2442
|
// Task #104: fold this turn's output-token total into the lifetime per-workspace +
|
|
@@ -2774,16 +2807,22 @@ async function main() {
|
|
|
2774
2807
|
let lineRaw;
|
|
2775
2808
|
for (;;) {
|
|
2776
2809
|
lineRaw = await nextLine();
|
|
2810
|
+
const echoed = enterEchoed;
|
|
2811
|
+
enterEchoed = false;
|
|
2777
2812
|
if (lineRaw === null || lineRaw.trim()) break;
|
|
2778
|
-
//
|
|
2779
|
-
//
|
|
2780
|
-
//
|
|
2781
|
-
//
|
|
2813
|
+
// EMPTY (or whitespace-only) input is not a question — it must leave NO trace, or a few
|
|
2814
|
+
// stray Enters stack up a column of bare ">>" rows. readline has already echoed its
|
|
2815
|
+
// newline, so step back UP onto the ">> " row it left behind and re-prompt there:
|
|
2816
|
+
// prompt() erases from the caret down, so the row is rewritten in place and the empty
|
|
2817
|
+
// submit becomes a visual no-op. Nothing to step back over if the line came from
|
|
2818
|
+
// _lineQueue (no keypress, no echo) — hence the `echoed` guard.
|
|
2782
2819
|
if (process.stdout.isTTY) {
|
|
2820
|
+
if (echoed) process.stdout.write("\x1b[1A");
|
|
2783
2821
|
footerActive = true;
|
|
2784
2822
|
rl.prompt();
|
|
2785
2823
|
}
|
|
2786
2824
|
}
|
|
2825
|
+
enterEchoed = false;
|
|
2787
2826
|
footerActive = false; // a line arrived from the queue (no Enter keypress) → disarm
|
|
2788
2827
|
if (lineRaw === null) break; // stdin closed
|
|
2789
2828
|
const line = lineRaw.trim();
|
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.372",
|
|
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",
|