@tiens.nguyen/gonext-cli 1.0.369 → 1.0.371
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 +57 -27
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -651,7 +651,28 @@ let editMode = "accept"; // "accept" | "manual"
|
|
|
651
651
|
const modeLineText = () =>
|
|
652
652
|
white("✎ " + (editMode === "accept" ? "accept edits on" : "manually edit")) +
|
|
653
653
|
dim(" (shift+tab to switch)");
|
|
654
|
-
|
|
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
|
+
);
|
|
674
|
+
};
|
|
675
|
+
const FOOTER_ROWS = 3; // the rule row + the token row + the mode row
|
|
655
676
|
// True only while the MAIN loop's ">> " prompt is waiting for input. The bar belongs to that
|
|
656
677
|
// prompt, so it must never appear under an ask()/askSecret() question or during a turn.
|
|
657
678
|
let footerActive = false;
|
|
@@ -675,9 +696,11 @@ function drawFooter() {
|
|
|
675
696
|
process.stdout.write(
|
|
676
697
|
(down ? `\x1b[${down}B` : "") +
|
|
677
698
|
"\r\n\x1b[2K" + dim("─".repeat(width)) +
|
|
678
|
-
// CLIPPED: the mode line is ~44 columns,
|
|
679
|
-
//
|
|
680
|
-
// short and the next keystroke would type over the
|
|
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) +
|
|
681
704
|
"\r\n\x1b[2K" + clipVisible(GUTTER + modeLineText(), width - 1) +
|
|
682
705
|
"\x1b[J" + // nothing stale below the bar
|
|
683
706
|
`\x1b[${FOOTER_ROWS + down}A\r` + // back to the caret's row
|
|
@@ -699,12 +722,20 @@ function eraseFooter() {
|
|
|
699
722
|
}
|
|
700
723
|
// Shift+Tab redraw: the bar is below the caret, so a plain redraw in place is enough.
|
|
701
724
|
const redrawModeLine = () => drawFooter();
|
|
725
|
+
// Set by the prepended Enter handler, cleared once the resulting line is consumed. It tells
|
|
726
|
+
// the empty-input path that readline really echoed a newline to the terminal (a live
|
|
727
|
+
// keypress) and there is therefore a row to step back onto — as opposed to a line drained
|
|
728
|
+
// from _lineQueue, which produced no echo to undo.
|
|
729
|
+
let enterEchoed = false;
|
|
702
730
|
// Enter must tear the bar down BEFORE readline echoes its newline (which would land the
|
|
703
731
|
// cursor on the rule row and leave the mode line orphaned below the output). Our normal
|
|
704
732
|
// keypress listener runs AFTER readline's, so this one is prepended to run first.
|
|
705
733
|
if (process.stdin.isTTY) {
|
|
706
734
|
process.stdin.prependListener("keypress", (_ch, key) => {
|
|
707
|
-
if (key && (key.name === "return" || key.name === "enter"))
|
|
735
|
+
if (key && (key.name === "return" || key.name === "enter")) {
|
|
736
|
+
enterEchoed = true;
|
|
737
|
+
eraseFooter();
|
|
738
|
+
}
|
|
708
739
|
});
|
|
709
740
|
}
|
|
710
741
|
if (process.stdin.isTTY) {
|
|
@@ -2751,16 +2782,22 @@ async function main() {
|
|
|
2751
2782
|
let lineRaw;
|
|
2752
2783
|
for (;;) {
|
|
2753
2784
|
lineRaw = await nextLine();
|
|
2785
|
+
const echoed = enterEchoed;
|
|
2786
|
+
enterEchoed = false;
|
|
2754
2787
|
if (lineRaw === null || lineRaw.trim()) break;
|
|
2755
|
-
//
|
|
2756
|
-
//
|
|
2757
|
-
//
|
|
2758
|
-
//
|
|
2788
|
+
// EMPTY (or whitespace-only) input is not a question — it must leave NO trace, or a few
|
|
2789
|
+
// stray Enters stack up a column of bare ">>" rows. readline has already echoed its
|
|
2790
|
+
// newline, so step back UP onto the ">> " row it left behind and re-prompt there:
|
|
2791
|
+
// prompt() erases from the caret down, so the row is rewritten in place and the empty
|
|
2792
|
+
// submit becomes a visual no-op. Nothing to step back over if the line came from
|
|
2793
|
+
// _lineQueue (no keypress, no echo) — hence the `echoed` guard.
|
|
2759
2794
|
if (process.stdout.isTTY) {
|
|
2795
|
+
if (echoed) process.stdout.write("\x1b[1A");
|
|
2760
2796
|
footerActive = true;
|
|
2761
2797
|
rl.prompt();
|
|
2762
2798
|
}
|
|
2763
2799
|
}
|
|
2800
|
+
enterEchoed = false;
|
|
2764
2801
|
footerActive = false; // a line arrived from the queue (no Enter keypress) → disarm
|
|
2765
2802
|
if (lineRaw === null) break; // stdin closed
|
|
2766
2803
|
const line = lineRaw.trim();
|
|
@@ -2842,6 +2879,9 @@ async function main() {
|
|
|
2842
2879
|
const done = await resetGlobalOutputTokens();
|
|
2843
2880
|
if (done) {
|
|
2844
2881
|
globalOutputTokens = 0;
|
|
2882
|
+
// The bar takes the FRESHER of the two totals, so the last turn's copy has to be
|
|
2883
|
+
// zeroed too or the reset wouldn't show up in it.
|
|
2884
|
+
lastTurnTotalOutput = 0;
|
|
2845
2885
|
console.log(GUTTER + green("✓ global + workspace output-token totals reset to 0.\n"));
|
|
2846
2886
|
} else {
|
|
2847
2887
|
console.log(GUTTER + dim("Could not reset the output-token totals (API error).\n"));
|
|
@@ -2887,6 +2927,14 @@ async function main() {
|
|
|
2887
2927
|
try {
|
|
2888
2928
|
const { text: answer, shown, cancelled, inputTokens, outputTokens, turnOutputTokens } =
|
|
2889
2929
|
await runAgentTurn(history);
|
|
2930
|
+
// Feed the bottom bar's token row (task #104). Assigned for EVERY turn — including one
|
|
2931
|
+
// that ended without an answer — so the row never shows a stale count from an older
|
|
2932
|
+
// turn. It replaces the print-once-per-turn footer that used to live here: the bar is
|
|
2933
|
+
// always on screen, so printing the same three figures into the scrollback right above
|
|
2934
|
+
// it just read as a duplicate.
|
|
2935
|
+
lastTurnInputTokens = inputTokens ?? 0;
|
|
2936
|
+
lastTurnOutputTokens = turnOutputTokens ?? 0;
|
|
2937
|
+
lastTurnTotalOutput = outputTokens ?? 0;
|
|
2890
2938
|
if (answer) {
|
|
2891
2939
|
history.push({ role: "assistant", content: answer });
|
|
2892
2940
|
// Persist after every successful turn (not just on clean exit) — an ungraceful
|
|
@@ -2902,24 +2950,6 @@ async function main() {
|
|
|
2902
2950
|
// renderAnswer also WRAPS long lines and re-indents each continuation row, so a wrapped
|
|
2903
2951
|
// paragraph keeps the same left margin instead of soft-wrapping back to column 0.
|
|
2904
2952
|
console.log(shown ? "" : `${LINE_SPACING}${renderAnswer(answer)}\n`);
|
|
2905
|
-
// Task #104: persistent token footer at the bottom of the screen — this turn's
|
|
2906
|
-
// code-model INPUT + this turn's OUTPUT (↓ out), then the model's GLOBAL lifetime
|
|
2907
|
-
// output total (all turns for this user + coding server), labelled with the model
|
|
2908
|
-
// name so it's clear which coder that running total belongs to. Guard on THIS turn's
|
|
2909
|
-
// activity so a plain-reply greeting that did no code-model work shows nothing (#112).
|
|
2910
|
-
if ((inputTokens ?? 0) > 0 || (turnOutputTokens ?? 0) > 0) {
|
|
2911
|
-
const coder = sessionCodingLabel || defaultCodingModel;
|
|
2912
|
-
console.log(
|
|
2913
|
-
GUTTER +
|
|
2914
|
-
dim("tokens · ↑ in ") +
|
|
2915
|
-
fmtTokens(inputTokens ?? 0) +
|
|
2916
|
-
dim(" · ") +
|
|
2917
|
-
cyan(`↓ out ${fmtTokens(turnOutputTokens ?? 0)}`) +
|
|
2918
|
-
dim(" · ") +
|
|
2919
|
-
dim(`${coder ? coder + " " : ""}total `) +
|
|
2920
|
-
cyan(`↓ ${fmtTokens(outputTokens ?? 0)}`)
|
|
2921
|
-
);
|
|
2922
|
-
}
|
|
2923
2953
|
} else {
|
|
2924
2954
|
history.pop(); // aborted/failed/cancelled turn — don't poison the next request
|
|
2925
2955
|
// 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.371",
|
|
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",
|