@tiens.nguyen/gonext-cli 1.0.359 → 1.0.361

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 +105 -33
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -67,17 +67,14 @@ const gutterLines = (s) =>
67
67
  .split("\n")
68
68
  .map((l) => (l.length ? GUTTER + l : l))
69
69
  .join("\n");
70
- // The ● action bullets and the final agent answer are ONE block of agent output, so they share
71
- // one left edge: the bullet's TEXT column (GUTTER + "● " = GUTTER + 2). Without this the answer
72
- // sat at the bare GUTTER, 2 cols left of the bullet text above it the ragged jog between the
73
- // actions and "Done!" (padding fix). Plain replies have no bullets, so they keep the GUTTER
74
- // margin (aligned with the >> prompt). `outputLines` insets a block to that shared edge.
75
- const OUTPUT_INDENT = GUTTER + " ";
76
- const outputLines = (s) =>
77
- String(s)
78
- .split("\n")
79
- .map((l) => (l.length ? OUTPUT_INDENT + l : l))
80
- .join("\n");
70
+ // The ● (and ⏺, the spinner glyph) are MARGIN MARKERS, not text they HANG in the gutter so a
71
+ // bullet's text lands on the SAME column as every other line (prompt, banner, plain reply, the
72
+ // final answer). There is ONE text column (GUTTER) and the marker sits to its left, instead of
73
+ // pushing bullet text 2 cols right of everything else. `markLine(glyph)` builds a GUTTER-wide
74
+ // prefix: a 1-col glyph right-aligned before a single trailing space, so text starts at the
75
+ // gutter column exactly like an un-marked line.
76
+ const MARK_PAD = " ".repeat(Math.max(0, GUTTER.length - 2));
77
+ const markLine = (glyph) => MARK_PAD + glyph + " ";
81
78
  // Render the model's inline **bold** as ANSI bold (markdown polish). The terminal has no
82
79
  // markdown renderer, so a heading like `**What I did:**` was printing its literal asterisks.
83
80
  // Only paired ** with non-space content between them is consumed (so a stray `**` or `a ** b`
@@ -87,6 +84,49 @@ const outputLines = (s) =>
87
84
  // reply stream isn't marked up because ** can straddle a chunk boundary there.
88
85
  const renderMdBold = (s) =>
89
86
  String(s).replace(/\*\*(?=\S)(.*?\S)\*\*/g, "\x1b[1m$1\x1b[22m");
87
+ // Word-wrap a block to the terminal width so a long line does NOT soft-wrap back to column 0
88
+ // and lose its left margin — instead we hard-break at word boundaries and re-apply `indent` to
89
+ // EVERY continuation row (user-reported: wrapped answer lines had no padding on line 2+). A
90
+ // single token longer than the line (e.g. a URL) is hard-split so it can't overflow. Existing
91
+ // newlines (paragraph breaks / list items) are preserved; blank lines stay bare. The -1 guards
92
+ // against a wide (emoji) char nudging the visible row one past the edge into a real soft-wrap.
93
+ const wrapBlock = (text, indent, width) => {
94
+ const avail = Math.max(8, (width || 80) - indent.length - 1);
95
+ const rows = [];
96
+ for (const para of String(text).split("\n")) {
97
+ if (para === "") { rows.push(""); continue; }
98
+ let cur = "";
99
+ for (let w of para.split(/\s+/).filter(Boolean)) {
100
+ if (w.length > avail) {
101
+ if (cur) { rows.push(cur); cur = ""; }
102
+ while (w.length > avail) { rows.push(w.slice(0, avail)); w = w.slice(avail); }
103
+ cur = w;
104
+ continue;
105
+ }
106
+ const next = cur ? cur + " " + w : w;
107
+ if (next.length <= avail) cur = next;
108
+ else { rows.push(cur); cur = w; }
109
+ }
110
+ if (cur) rows.push(cur);
111
+ }
112
+ return rows;
113
+ };
114
+ // The completed agent answer, ready to print: wrapped to the current terminal width, every row
115
+ // indented to the shared text column (GUTTER) so it lines up with the bullets' text and the
116
+ // rest of the output, with inline **bold** rendered per row (after wrapping, so the width math
117
+ // is on visible text).
118
+ const renderAnswer = (answer) =>
119
+ wrapBlock(answer, GUTTER, process.stdout.columns || 80)
120
+ .map((l) => (l.length ? GUTTER + renderMdBold(l) : l))
121
+ .join("\n");
122
+ // Wrap a PLAIN (uncoloured) banner/status string to the terminal width and re-indent every row
123
+ // to GUTTER, colouring each row with `colorFn` — so a long startup line keeps its left margin
124
+ // on wrap (narrow windows) instead of soft-wrapping back to column 0. Pass plain text: the
125
+ // colour is applied per row, AFTER the width math (ANSI codes would otherwise skew the wrap).
126
+ const wrapGutter = (plain, colorFn = (s) => s) =>
127
+ wrapBlock(plain, GUTTER, process.stdout.columns || 80)
128
+ .map((l) => (l.length ? GUTTER + colorFn(l) : l))
129
+ .join("\n");
90
130
  // Vertical rhythm between ● action/answer bullets (task #129). One blank line so the list
91
131
  // breathes instead of being cramped. A knob: "" = the old tight list, "\n" = one blank.
92
132
  // Applied as a LEADING blank before a bullet when the previous line wasn't already blank —
@@ -706,16 +746,16 @@ async function ensureWorkspace() {
706
746
  }
707
747
  wsSync = { enabled: hit.allowSync !== false, name: hit.name, path: hit.path };
708
748
  console.log(
709
- GUTTER +
710
- dim(
749
+ wrapGutter(
711
750
  `workspace: ${hit.name} → ${hit.path} (run ${hit.allowRun ? "enabled" : "disabled"}` +
712
- `${hit.allowSync !== false ? ", cloud sync on" : ""})`
751
+ `${hit.allowSync !== false ? ", cloud sync on" : ""})`,
752
+ dim
713
753
  )
714
754
  );
715
755
  return;
716
756
  }
717
757
  if (!existsSync(cwd) || !statSync(cwd).isDirectory()) return;
718
- console.log(gutterLines(`This folder is not a registered workspace:\n ${cwd}`));
758
+ console.log(wrapGutter(`This folder is not a registered workspace:\n ${cwd}`));
719
759
  const register = await askYesNo(
720
760
  "Register it so the agent can read/edit code here?", true /* default Yes */
721
761
  );
@@ -1833,14 +1873,14 @@ async function runAgentTurn(history) {
1833
1873
  // underlined bright-green hover style when the mouse is over a clickable thought. Then the
1834
1874
  // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
1835
1875
  // (task #83/#84), e.g. " ⠙ Caffeinating… · 12.3k tok · 8.1k max".
1836
- // Gutter the live status block's first line (task #128) so the blinking ● lines up with
1837
- // the finished ● bullets during a turn (no left-edge jump). The sub-lines below use the
1838
- // same GUTTER so the whole block shares one left edge. Safe for the redraw: clearStatus
1839
- // clears whole lines and the hover/click hit-testing is ROW-based, so a horizontal offset
1840
- // changes nothing.
1841
- let out = `${GUTTER}${green(BULLET)} ${hovering ? hoverThought(label) : green(label)}`;
1876
+ // The blinking ● and the spinner are hanging margin markers (markLine): the label/word land
1877
+ // on the gutter text column, matching the finished ● bullets (no left-edge jump when a live
1878
+ // action becomes a bullet). The expanded grey sub-lines are plain text at the gutter. Safe
1879
+ // for the redraw: clearStatus clears whole lines and hover/click hit-testing is ROW-based,
1880
+ // so a horizontal offset changes nothing.
1881
+ let out = `${markLine(green(BULLET))}${hovering ? hoverThought(label) : green(label)}`;
1842
1882
  for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
1843
- out += "\n" + `${GUTTER}${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1883
+ out += "\n" + `${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${tokLabel}`;
1844
1884
  process.stdout.write(out);
1845
1885
  statusLines = drawn;
1846
1886
  statusShown = true;
@@ -1981,7 +2021,7 @@ async function runAgentTurn(history) {
1981
2021
  if (isEditCardHeader(line)) {
1982
2022
  onContent();
1983
2023
  inEditCard = true;
1984
- process.stdout.write(GUTTER + green("⏺") + bold(line.slice(1)) + "\n");
2024
+ process.stdout.write(markLine(green("⏺")) + bold(line.slice(1).replace(/^\s+/, "")) + "\n");
1985
2025
  lastWasBlank = false;
1986
2026
  lastContentAt = Date.now();
1987
2027
  continue;
@@ -2100,7 +2140,20 @@ async function runAgentTurn(history) {
2100
2140
  // first (which follows the prompt) and none trailing. Deterministic (keyed off
2101
2141
  // lastWasBlank, not the ticker) so #115's status-race stays fixed.
2102
2142
  if (LINE_SPACING && !lastWasBlank) process.stdout.write(LINE_SPACING);
2103
- process.stdout.write(GUTTER + white(BULLET) + " " + line.trim() + "\n");
2143
+ // Completed actions are SCAFFOLDING, not the deliverable commit them DIM the instant
2144
+ // they print (they're history the moment they land), so the eye lands on the full-bright
2145
+ // final answer below (the "spotlight the answer" design). The loud "happening now"
2146
+ // element stays the green live ticker, not this settled history. Dimming at print time
2147
+ // (not re-colouring afterwards) avoids cursor-walking back over wrapped/scrolled rows.
2148
+ // The ● is a hanging margin marker (markLine): text lands on the gutter column, and a
2149
+ // long bullet wraps so every continuation row re-indents to that SAME gutter — the whole
2150
+ // bullet shares one text column with the prompt/banner instead of soft-wrapping to col 0.
2151
+ const bRows = wrapBlock(line.trim(), GUTTER, process.stdout.columns || 80);
2152
+ process.stdout.write(
2153
+ bRows
2154
+ .map((r, i) => (i === 0 ? markLine(dim(BULLET)) + dim(r) : GUTTER + dim(r)))
2155
+ .join("\n") + "\n"
2156
+ );
2104
2157
  }
2105
2158
  lastWasBlank = false;
2106
2159
  }
@@ -2351,7 +2404,23 @@ async function runAgentTurn(history) {
2351
2404
 
2352
2405
  // ---------- REPL ----------
2353
2406
  async function main() {
2354
- console.log(GUTTER + cyan("GoNext agent REPL") + dim(` v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`));
2407
+ // Wrap the header so a narrow window doesn't soft-wrap "key wk_…" to column 0; keep the
2408
+ // "GoNext agent REPL" label cyan on row 0, dim the rest (and any wrapped continuation rows).
2409
+ {
2410
+ const LABEL = "GoNext agent REPL";
2411
+ const header = `${LABEL} v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`;
2412
+ console.log(
2413
+ wrapBlock(header, GUTTER, process.stdout.columns || 80)
2414
+ .map((l, i) =>
2415
+ !l.length
2416
+ ? l
2417
+ : i === 0 && l.startsWith(LABEL)
2418
+ ? GUTTER + cyan(LABEL) + dim(l.slice(LABEL.length))
2419
+ : GUTTER + dim(l)
2420
+ )
2421
+ .join("\n")
2422
+ );
2423
+ }
2355
2424
  // Task #127, Phase 2: the REPL enqueues jobs that the polling DAEMON executes, so ensure
2356
2425
  // one is running — no GoTerminal needed. Idempotent + BC4-safe: startDaemon() no-ops when
2357
2426
  // a daemon (ours, GoTerminal's, or a manual one) is already alive. Best-effort; a failure
@@ -2398,14 +2467,15 @@ async function main() {
2398
2467
  await saveSessionRagMode(resolve(process.cwd()), sessionRagMode);
2399
2468
  }
2400
2469
  console.log(
2401
- GUTTER +
2402
- dim(
2470
+ wrapGutter(
2403
2471
  `agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
2404
2472
  (p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
2405
2473
  (p.ragEnabled ? ` · RAG ${sessionRagMode === "cloud" ? "cloud" : "local"}` : "") +
2406
2474
  ` · auto-test: ${sessionTestAuto ? "on" : "off"}` +
2407
- (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "")
2408
- ) + (sessionTestAuto ? "" : dim(" (/test-auto to enable)"))
2475
+ (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "") +
2476
+ (sessionTestAuto ? "" : " (/test-auto to enable)"),
2477
+ dim
2478
+ )
2409
2479
  );
2410
2480
  // Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
2411
2481
  // (download + start mlx_lm.server). BC3: only runs when NOTHING answers — an already-up
@@ -2465,14 +2535,14 @@ async function main() {
2465
2535
  }
2466
2536
  process.exit(1);
2467
2537
  }
2468
- console.log(GUTTER + dim("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.\n"));
2538
+ console.log(wrapGutter("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.", dim) + "\n");
2469
2539
 
2470
2540
  const cwd = resolve(process.cwd());
2471
2541
  const history = await loadSession(cwd);
2472
2542
  if (history.length > 0) {
2473
2543
  const turns = history.filter((m) => m.role === "user").length;
2474
2544
  console.log(
2475
- GUTTER + dim(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.\n`)
2545
+ wrapGutter(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.`, dim) + "\n"
2476
2546
  );
2477
2547
  }
2478
2548
  // Ctrl-C arrives on ONE of two paths depending on the readline mode: with
@@ -2671,10 +2741,12 @@ async function main() {
2671
2741
  // A plain-reply answer already streamed live in the loop above (shown=true) — printing
2672
2742
  // it again here would just duplicate it, so only a spacer blank line. The AGENT answer
2673
2743
  // (shown=false) is printed here from resultText, indented to the bullet-text column
2674
- // (outputLines) so it shares the ● bullets' left edge, and with ONE leading blank
2744
+ // (renderAnswer) so it shares the ● bullets' left edge, and with ONE leading blank
2675
2745
  // (LINE_SPACING) matching the between-bullets rhythm — was a hard "\n\n" double gap at
2676
2746
  // the seam, bigger than the gaps between bullets above it (the seam padding fix).
2677
- console.log(shown ? "" : `${LINE_SPACING}${outputLines(renderMdBold(answer))}\n`);
2747
+ // renderAnswer also WRAPS long lines and re-indents each continuation row, so a wrapped
2748
+ // paragraph keeps the same left margin instead of soft-wrapping back to column 0.
2749
+ console.log(shown ? "" : `${LINE_SPACING}${renderAnswer(answer)}\n`);
2678
2750
  // Task #104: persistent token footer at the bottom of the screen — this turn's
2679
2751
  // code-model INPUT + this turn's OUTPUT (↓ out), then the model's GLOBAL lifetime
2680
2752
  // output total (all turns for this user + coding server), labelled with the model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-cli",
3
- "version": "1.0.359",
3
+ "version": "1.0.361",
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",