@tiens.nguyen/gonext-cli 1.0.371 → 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.
Files changed (2) hide show
  1. package/gonext-repl.mjs +40 -15
  2. 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" + 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) +
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` : "")
@@ -1872,7 +1877,9 @@ async function runAgentTurn(history) {
1872
1877
  let lastApprovalId = null; // the last command-approval request we've already answered
1873
1878
  let carry = ""; // trailing partial content line held until its newline arrives
1874
1879
  let statusShown = false; // a transient status line is currently on screen
1875
- let statusLines = 2; // how many rows the status block currently occupies (grows when expanded)
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;
1876
1883
  let warnedPending = false;
1877
1884
  // Transient poll failures (a network blip or a 5xx like the API's momentary "Worker
1878
1885
  // key lookup failed." — a cold Mongo connection, NOT a bad key) must NOT kill a
@@ -1899,6 +1906,10 @@ async function runAgentTurn(history) {
1899
1906
  // the lifetime per-workspace + global counters once the turn completes.
1900
1907
  let latestOutputTokens = 0;
1901
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;
1902
1913
  // First few KB of the raw stream as consumed — a cheap fingerprint to verify the
1903
1914
  // completed resultText really EXTENDS what we streamed (an old worker replaces it
1904
1915
  // with the shorter bare answer, which must not be sliced by shownChars).
@@ -2020,11 +2031,16 @@ async function runAgentTurn(history) {
2020
2031
  // the playful-word line.
2021
2032
  const expLines = thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
2022
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() : [];
2023
2039
  const rows = process.stdout.rows || 24;
2024
2040
  // Hover-highlight only the THOUGHT line (line 1) — its exact viewport row comes from the
2025
2041
  // \x1b[6n probe below (`thoughtRow`); fall back to the viewport-bottom assumption only when
2026
- // the terminal never answered the probe.
2027
- 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;
2028
2044
  const hovering = clickable && hoverRow === tRow;
2029
2045
  clearStatus();
2030
2046
  // Right after clearing, the cursor sits at the status block's TOP-LEFT = the thought line's
@@ -2042,8 +2058,10 @@ async function runAgentTurn(history) {
2042
2058
  let out = `${markLine(green(BULLET))}${hovering ? hoverThought(label) : green(label)}`;
2043
2059
  for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
2044
2060
  out += "\n" + `${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${tokLabel}`;
2061
+ for (const br of bar) out += "\n" + br;
2045
2062
  process.stdout.write(out);
2046
- statusLines = drawn;
2063
+ statusContentLines = drawn;
2064
+ statusLines = drawn + bar.length;
2047
2065
  statusShown = true;
2048
2066
  };
2049
2067
  // 120ms so the spinner animates smoothly (the frame math above uses the wall clock, so
@@ -2061,9 +2079,10 @@ async function runAgentTurn(history) {
2061
2079
  if (!statusShown) return;
2062
2080
  const rows = process.stdout.rows || 24;
2063
2081
  const top = thoughtRow >= 1 ? thoughtRow : rows - statusLines + 1; // thought line row
2064
- // Toggle only on the thought line + its grey expansion — NOT the last row (the playful
2065
- // word / token line), so a click there doesn't expand what it isn't part of.
2066
- if (row < top || row > top + statusLines - 2) return;
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;
2067
2086
  // Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
2068
2087
  // so we don't flicker an empty expand.
2069
2088
  if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
@@ -2412,6 +2431,12 @@ async function runAgentTurn(history) {
2412
2431
  if (Number.isFinite(job.agentOutputTokens) && job.agentOutputTokens > latestOutputTokens) {
2413
2432
  latestOutputTokens = job.agentOutputTokens;
2414
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;
2415
2440
  const text = typeof job.resultText === "string" ? job.resultText : "";
2416
2441
  if (job.jobStatus === "completed") {
2417
2442
  // Task #104: fold this turn's output-token total into the lifetime per-workspace +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-cli",
3
- "version": "1.0.371",
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",