@tiens.nguyen/gonext-cli 1.0.371 → 1.0.373

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 +93 -25
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -362,6 +362,7 @@ const COMMANDS = [
362
362
  { name: "/server", desc: "pick a deployment server (host/user) for this session" },
363
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
+ { name: "/hover", desc: "toggle mouse hover/click on the thinking line (costs scrolling while it runs)" },
365
366
  { name: "/output-token", desc: "show the agent code-model OUTPUT-token total for this workspace" },
366
367
  { name: "/reset-output-token-global", desc: "reset the GLOBAL (you + coding server) output-token total to 0" },
367
368
  { name: "/help", desc: "list these commands" },
@@ -673,6 +674,18 @@ const tokenLineText = () => {
673
674
  );
674
675
  };
675
676
  const FOOTER_ROWS = 3; // the rule row + the token row + the mode row
677
+ // The bar's rows as plain printable strings, one PHYSICAL terminal row each. Both text rows
678
+ // can exceed the width (the mode line is ~44 columns, the token row carries a model name) and
679
+ // would then WRAP onto an extra row — every consumer here counts rows with ESC[nA / ESC[nB, so
680
+ // a wrapped row corrupts the redraw (the #109 failure mode). Hence clipVisible on both.
681
+ const footerRows = () => {
682
+ const width = Math.max(20, process.stdout.columns || 80);
683
+ return [
684
+ dim("─".repeat(width)),
685
+ clipVisible(GUTTER + tokenLineText(), width - 1),
686
+ clipVisible(GUTTER + modeLineText(), width - 1),
687
+ ];
688
+ };
676
689
  // True only while the MAIN loop's ">> " prompt is waiting for input. The bar belongs to that
677
690
  // prompt, so it must never appear under an ask()/askSecret() question or during a turn.
678
691
  let footerActive = false;
@@ -690,18 +703,11 @@ const cursorPos = () =>
690
703
  // matching up-move still lands on it.
691
704
  function drawFooter() {
692
705
  if (!footerActive || !process.stdout.isTTY || slashOverlayActive) return;
693
- const width = Math.max(20, process.stdout.columns || 80);
694
706
  const pos = cursorPos();
695
707
  const down = Math.max(0, inputRows() - 1 - pos.rows); // caret row → last input row
696
708
  process.stdout.write(
697
709
  (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) +
710
+ footerRows().map((r) => "\r\n\x1b[2K" + r).join("") +
705
711
  "\x1b[J" + // nothing stale below the bar
706
712
  `\x1b[${FOOTER_ROWS + down}A\r` + // back to the caret's row
707
713
  (pos.cols > 0 ? `\x1b[${pos.cols}C` : "")
@@ -757,6 +763,10 @@ if (process.stdin.isTTY) {
757
763
  return;
758
764
  }
759
765
  if (following) {
766
+ // Ctrl+T expands/collapses the model's current Thought — the keyboard equivalent of the
767
+ // hover-click, so the fuller thought stays reachable with the mouse capture off (the
768
+ // capture is what would eat the scroll wheel; see hoverMouse).
769
+ if (key && key.ctrl && key.name === "t" && toggleActiveThought) toggleActiveThought();
760
770
  rl.line = "";
761
771
  rl.cursor = 0;
762
772
  rl.historyIndex = -1;
@@ -805,18 +815,24 @@ rl.on("close", () => {
805
815
  w(null);
806
816
  }
807
817
  });
808
- // Mouse hover+click on the thinking line (task #96): while a turn runs we turn ON xterm
809
- // ANY-MOTION tracking so we can both HIGHLIGHT the line as the mouse hovers it AND expand it
810
- // on click (see runAgentTurn). Trade-off the user accepted: with tracking on, the terminal's
811
- // own drag-to-select/copy is captured by us the user holds Option/Shift to select text
812
- // meanwhile. ?1003 = report every button press/release AND all mouse MOTION (needed for hover
813
- // with no button held); ?1006 = SGR extended coords (no 223-col cap). We disable ?1000/?1002
814
- // too on teardown in case a terminal had them latched.
818
+ // Mouse hover+click on the thinking line (task #96): turns ON xterm ANY-MOTION tracking for
819
+ // the duration of a turn so we can HIGHLIGHT the line as the mouse hovers it AND expand it on
820
+ // click. ?1003 = report every button press/release AND all mouse MOTION (needed for hover with
821
+ // no button held); ?1006 = SGR extended coords (no 223-col cap). We disable ?1000/?1002 too on
822
+ // teardown in case a terminal had them latched.
823
+ //
824
+ // OFF BY DEFAULT, and it has to be: with ANY mouse reporting enabled the terminal hands us the
825
+ // WHEEL events (SGR button 64/65) instead of scrolling its own scrollback, so the user cannot
826
+ // scroll while a turn runs — and no tracking mode reports motion without also grabbing the
827
+ // wheel, so hover and scrolling are mutually exclusive. Scrolling wins by default (it also
828
+ // gives back native drag-to-select); `/hover` opts back into the mouse for a session, and
829
+ // Ctrl+T expands the thought from the keyboard either way.
830
+ let hoverMouse = false;
815
831
  const MOUSE_ON = "\x1b[?1003h\x1b[?1006h";
816
832
  const MOUSE_OFF = "\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l";
817
833
  let _mouseEnabled = false;
818
834
  const enableMouse = () => {
819
- if (_mouseEnabled || !process.stdin.isTTY) return;
835
+ if (!hoverMouse || _mouseEnabled || !process.stdin.isTTY) return;
820
836
  process.stdout.write(MOUSE_ON);
821
837
  _mouseEnabled = true;
822
838
  };
@@ -1394,6 +1410,10 @@ let approvalResolve = null;
1394
1410
  // first — otherwise the 120ms ticker keeps redrawing that block on top of the picker and
1395
1411
  // its relative-cursor math lands on the wrong rows (duplicated/interleaved options).
1396
1412
  let clearActiveStatus = null;
1413
+ // The active turn's thought expand/collapse (its local toggle), or null between turns — lets
1414
+ // Ctrl+T reveal the full Thought from the KEYBOARD, so the feature doesn't depend on the mouse
1415
+ // capture that would block scrolling (see hoverMouse).
1416
+ let toggleActiveThought = null;
1397
1417
  function drawApprovalOptions(redraw) {
1398
1418
  // Two option lines, redrawn in place. On redraw the cursor sits one row below "No";
1399
1419
  // step up 2 rows, clear+rewrite both, landing back where we started.
@@ -1872,7 +1892,9 @@ async function runAgentTurn(history) {
1872
1892
  let lastApprovalId = null; // the last command-approval request we've already answered
1873
1893
  let carry = ""; // trailing partial content line held until its newline arrives
1874
1894
  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)
1895
+ let statusLines = 2; // TOTAL rows the status block occupies, bottom bar included (clearStatus)
1896
+ // …of which these are the thought/word rows — the only ones hover + click hit-test against.
1897
+ let statusContentLines = 2;
1876
1898
  let warnedPending = false;
1877
1899
  // Transient poll failures (a network blip or a 5xx like the API's momentary "Worker
1878
1900
  // key lookup failed." — a cold Mongo connection, NOT a bad key) must NOT kill a
@@ -1899,6 +1921,10 @@ async function runAgentTurn(history) {
1899
1921
  // the lifetime per-workspace + global counters once the turn completes.
1900
1922
  let latestOutputTokens = 0;
1901
1923
  let outputTokensPosted = false; // guard: increment the lifetime counters once per turn
1924
+ // A new turn starts from zero on the bar's token row too — otherwise the previous turn's
1925
+ // in/out figures would sit there, looking live, until the first poll reports real numbers.
1926
+ lastTurnInputTokens = 0;
1927
+ lastTurnOutputTokens = 0;
1902
1928
  // First few KB of the raw stream as consumed — a cheap fingerprint to verify the
1903
1929
  // completed resultText really EXTENDS what we streamed (an old worker replaces it
1904
1930
  // with the shorter bare answer, which must not be sliced by shownChars).
@@ -2020,16 +2046,23 @@ async function runAgentTurn(history) {
2020
2046
  // the playful-word line.
2021
2047
  const expLines = thoughtExpanded && rest ? wrapThought(rest, max, 4) : [];
2022
2048
  const drawn = 2 + expLines.length;
2049
+ // The bottom bar rides along UNDER the live status while a turn runs, so it stays on screen
2050
+ // the whole time the agent is thinking instead of only at the prompt. It's part of the same
2051
+ // in-place block: the ticker redraws it every 120ms and clearStatus wipes it with the rest,
2052
+ // so content still lands on clean rows.
2053
+ const bar = process.stdout.isTTY ? footerRows() : [];
2023
2054
  const rows = process.stdout.rows || 24;
2024
2055
  // Hover-highlight only the THOUGHT line (line 1) — its exact viewport row comes from the
2025
2056
  // \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;
2057
+ // the terminal never answered the probe (the block's top = bottom minus ALL its rows).
2058
+ const tRow = thoughtRow >= 1 ? thoughtRow : rows - (drawn + bar.length) + 1;
2028
2059
  const hovering = clickable && hoverRow === tRow;
2029
2060
  clearStatus();
2030
2061
  // Right after clearing, the cursor sits at the status block's TOP-LEFT = the thought line's
2031
2062
  // row. Probe for it (throttled) so hover/click hit-testing is exact wherever the block is.
2032
- if (process.stdin.isTTY && now - lastDsrAt > 400) { lastDsrAt = now; process.stdout.write("\x1b[6n"); }
2063
+ // Only worth probing when the mouse is captured thoughtRow exists for hover/click
2064
+ // hit-testing, and with `/hover` off nothing reads it (and nothing would consume the reply).
2065
+ if (hoverMouse && process.stdin.isTTY && now - lastDsrAt > 400) { lastDsrAt = now; process.stdout.write("\x1b[6n"); }
2033
2066
  // Line 1: green ● + green phase/seconds (matches the web's green streaming label), or the
2034
2067
  // underlined bright-green hover style when the mouse is over a clickable thought. Then the
2035
2068
  // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
@@ -2042,8 +2075,10 @@ async function runAgentTurn(history) {
2042
2075
  let out = `${markLine(green(BULLET))}${hovering ? hoverThought(label) : green(label)}`;
2043
2076
  for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
2044
2077
  out += "\n" + `${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${tokLabel}`;
2078
+ for (const br of bar) out += "\n" + br;
2045
2079
  process.stdout.write(out);
2046
- statusLines = drawn;
2080
+ statusContentLines = drawn;
2081
+ statusLines = drawn + bar.length;
2047
2082
  statusShown = true;
2048
2083
  };
2049
2084
  // 120ms so the spinner animates smoothly (the frame math above uses the wall clock, so
@@ -2061,15 +2096,24 @@ async function runAgentTurn(history) {
2061
2096
  if (!statusShown) return;
2062
2097
  const rows = process.stdout.rows || 24;
2063
2098
  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;
2099
+ // Toggle only on the thought line + its grey expansion — NOT the playful-word/token row,
2100
+ // and NOT the bottom bar below it (statusContentLines excludes the bar), so a click there
2101
+ // doesn't expand what it isn't part of.
2102
+ if (row < top || row > top + statusContentLines - 2) return;
2067
2103
  // Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
2068
2104
  // so we don't flicker an empty expand.
2105
+ if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
2106
+ toggleThought();
2107
+ };
2108
+ // Same expand/collapse, reachable without the mouse: Ctrl+T while a turn runs (see
2109
+ // toggleActiveThought). Ignored when there's nothing more to reveal, so it never flickers an
2110
+ // empty expansion.
2111
+ const toggleThought = () => {
2069
2112
  if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
2070
2113
  thoughtExpanded = !thoughtExpanded;
2071
2114
  clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
2072
2115
  };
2116
+ toggleActiveThought = toggleThought;
2073
2117
  const onMouseData = (chunk) => {
2074
2118
  if (!following) return;
2075
2119
  const s = chunk.toString("latin1");
@@ -2088,7 +2132,9 @@ async function runAgentTurn(history) {
2088
2132
  if (isPress && (btn & 0b11) === 0 && (btn & 64) === 0) onStatusClick(row); // left click → expand
2089
2133
  }
2090
2134
  };
2091
- if (process.stdin.isTTY) process.stdin.on("data", onMouseData);
2135
+ // Only listen for reports when we're actually capturing the mouse — with `/hover` off there
2136
+ // are none, and no \x1b[6n probe reply to consume either.
2137
+ if (hoverMouse && process.stdin.isTTY) process.stdin.on("data", onMouseData);
2092
2138
  enableMouse();
2093
2139
 
2094
2140
  // Called right before any real (bullet/edit-card/answer) content prints: drop the
@@ -2412,6 +2458,12 @@ async function runAgentTurn(history) {
2412
2458
  if (Number.isFinite(job.agentOutputTokens) && job.agentOutputTokens > latestOutputTokens) {
2413
2459
  latestOutputTokens = job.agentOutputTokens;
2414
2460
  }
2461
+ // Mirror both into the bottom bar's token row so it counts UP live while the turn runs
2462
+ // (the bar rides under the status block now), instead of sitting on the last turn's
2463
+ // figures until this one finishes. The turn-end assignment writes the same numbers again.
2464
+ lastTurnInputTokens = latestCodeTokens;
2465
+ lastTurnOutputTokens = latestOutputTokens;
2466
+ lastTurnTotalOutput = globalOutputTokens + latestOutputTokens;
2415
2467
  const text = typeof job.resultText === "string" ? job.resultText : "";
2416
2468
  if (job.jobStatus === "completed") {
2417
2469
  // Task #104: fold this turn's output-token total into the lifetime per-workspace +
@@ -2561,6 +2613,7 @@ async function runAgentTurn(history) {
2561
2613
  clearInterval(ticker);
2562
2614
  clearStatus();
2563
2615
  clearActiveStatus = null; // no turn owns the status wiper between turns
2616
+ toggleActiveThought = null; // …nor the Ctrl+T thought toggle
2564
2617
  // Task #96: stop mouse-reporting the moment the turn ends (also on cancel/error — this
2565
2618
  // finally always runs), and detach the click parser, so the user's shell selection is
2566
2619
  // never captured between turns.
@@ -2806,6 +2859,21 @@ async function main() {
2806
2859
  printCommands();
2807
2860
  continue;
2808
2861
  }
2862
+ if (line === "/hover") {
2863
+ // Mouse hover/click on the thinking line (#96) vs. scrolling during a turn: the terminal
2864
+ // only reports wheel events to US when mouse tracking is on, so they're exclusive. Off by
2865
+ // default; this opts in for the session. Ctrl+T expands the thought either way.
2866
+ hoverMouse = !hoverMouse;
2867
+ console.log(
2868
+ (hoverMouse ? green("hover: ON") : dim("hover: OFF")) +
2869
+ dim(
2870
+ hoverMouse
2871
+ ? " — mouse hover/click expands the thinking line; scrolling and drag-select are captured while a turn runs.\n"
2872
+ : " — scrolling and drag-select work during a turn; use Ctrl+T to expand the thinking line.\n"
2873
+ )
2874
+ );
2875
+ continue;
2876
+ }
2809
2877
  if (line === "/model") {
2810
2878
  await chooseModel();
2811
2879
  continue;
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.373",
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",