@tiens.nguyen/gonext-cli 1.0.358 → 1.0.360

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 +70 -13
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -58,15 +58,64 @@ const white = (s) => `\x1b[37m${s}\x1b[0m`;
58
58
  // Left gutter so terminal output doesn't hug the edge (task #128, Phase 1). A terminal has
59
59
  // no CSS padding — a "left margin" is just a space prefix on every printed line, applied
60
60
  // consistently at the prompt, the ● bullets, plain replies, the banner and the live status
61
- // so they share ONE clean left edge. Existing secondary indents already use 2 spaces, so a
62
- // 2-space gutter converges everything to the same column. `gutterLines` insets a multi-line
63
- // block (blank lines left bare — no trailing whitespace).
64
- const GUTTER = " ";
61
+ // so they share ONE clean left edge. 3 spaces reads as a deliberate margin — 2 was too subtle
62
+ // against the window edge (the padding fix). `gutterLines` insets a multi-line block (blank
63
+ // lines left bare — no trailing whitespace).
64
+ const GUTTER = " ";
65
65
  const gutterLines = (s) =>
66
66
  String(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).
75
+ const OUTPUT_INDENT = GUTTER + " ";
76
+ // Render the model's inline **bold** as ANSI bold (markdown polish). The terminal has no
77
+ // markdown renderer, so a heading like `**What I did:**` was printing its literal asterisks.
78
+ // Only paired ** with non-space content between them is consumed (so a stray `**` or `a ** b`
79
+ // is left untouched); `.` never crosses a newline, so a bold span stays on its own line. Ends
80
+ // with \x1b[22m (bold-off) rather than a full \x1b[0m reset so any surrounding colour/gutter
81
+ // styling survives. Applied to the completed agent answer (a whole string) — the live plain-
82
+ // reply stream isn't marked up because ** can straddle a chunk boundary there.
83
+ const renderMdBold = (s) =>
84
+ String(s).replace(/\*\*(?=\S)(.*?\S)\*\*/g, "\x1b[1m$1\x1b[22m");
85
+ // Word-wrap a block to the terminal width so a long line does NOT soft-wrap back to column 0
86
+ // and lose its left margin — instead we hard-break at word boundaries and re-apply `indent` to
87
+ // EVERY continuation row (user-reported: wrapped answer lines had no padding on line 2+). A
88
+ // single token longer than the line (e.g. a URL) is hard-split so it can't overflow. Existing
89
+ // newlines (paragraph breaks / list items) are preserved; blank lines stay bare. The -1 guards
90
+ // against a wide (emoji) char nudging the visible row one past the edge into a real soft-wrap.
91
+ const wrapBlock = (text, indent, width) => {
92
+ const avail = Math.max(8, (width || 80) - indent.length - 1);
93
+ const rows = [];
94
+ for (const para of String(text).split("\n")) {
95
+ if (para === "") { rows.push(""); continue; }
96
+ let cur = "";
97
+ for (let w of para.split(/\s+/).filter(Boolean)) {
98
+ if (w.length > avail) {
99
+ if (cur) { rows.push(cur); cur = ""; }
100
+ while (w.length > avail) { rows.push(w.slice(0, avail)); w = w.slice(avail); }
101
+ cur = w;
102
+ continue;
103
+ }
104
+ const next = cur ? cur + " " + w : w;
105
+ if (next.length <= avail) cur = next;
106
+ else { rows.push(cur); cur = w; }
107
+ }
108
+ if (cur) rows.push(cur);
109
+ }
110
+ return rows;
111
+ };
112
+ // The completed agent answer, ready to print: wrapped to the current terminal width, every row
113
+ // re-indented to the bullet-text column (OUTPUT_INDENT) so line 2+ keeps the same margin, with
114
+ // inline **bold** rendered per row (after wrapping, so the width math is on visible text).
115
+ const renderAnswer = (answer) =>
116
+ wrapBlock(answer, OUTPUT_INDENT, process.stdout.columns || 80)
117
+ .map((l) => (l.length ? OUTPUT_INDENT + renderMdBold(l) : l))
118
+ .join("\n");
70
119
  // Vertical rhythm between ● action/answer bullets (task #129). One blank line so the list
71
120
  // breathes instead of being cramped. A knob: "" = the old tight list, "\n" = one blank.
72
121
  // Applied as a LEADING blank before a bullet when the previous line wasn't already blank —
@@ -1814,12 +1863,13 @@ async function runAgentTurn(history) {
1814
1863
  // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
1815
1864
  // (task #83/#84), e.g. " ⠙ Caffeinating… · 12.3k tok · 8.1k max".
1816
1865
  // Gutter the live status block's first line (task #128) so the blinking ● lines up with
1817
- // the finished ● bullets during a turn (no left-edge jump). The sub-lines below already
1818
- // use a 2-space indent = the gutter. Safe for the redraw: clearStatus clears whole lines
1819
- // and the hover/click hit-testing is ROW-based, so a horizontal offset changes nothing.
1866
+ // the finished ● bullets during a turn (no left-edge jump). The sub-lines below use the
1867
+ // same GUTTER so the whole block shares one left edge. Safe for the redraw: clearStatus
1868
+ // clears whole lines and the hover/click hit-testing is ROW-based, so a horizontal offset
1869
+ // changes nothing.
1820
1870
  let out = `${GUTTER}${green(BULLET)} ${hovering ? hoverThought(label) : green(label)}`;
1821
- for (const wl of expLines) out += "\n " + dim(wl);
1822
- out += "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1871
+ for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
1872
+ out += "\n" + `${GUTTER}${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1823
1873
  process.stdout.write(out);
1824
1874
  statusLines = drawn;
1825
1875
  statusShown = true;
@@ -2647,9 +2697,15 @@ async function main() {
2647
2697
  // termination (killed terminal, force-quit) would otherwise lose the whole
2648
2698
  // session even though most of it completed fine.
2649
2699
  await saveSession(cwd, history);
2650
- // A plain-reply answer already streamed live in the loop above — printing it
2651
- // again here would just duplicate it; a blank line is enough for spacing.
2652
- console.log(shown ? "" : `\n\n${gutterLines(answer)}\n`);
2700
+ // A plain-reply answer already streamed live in the loop above (shown=true) — printing
2701
+ // it again here would just duplicate it, so only a spacer blank line. The AGENT answer
2702
+ // (shown=false) is printed here from resultText, indented to the bullet-text column
2703
+ // (renderAnswer) so it shares the ● bullets' left edge, and with ONE leading blank
2704
+ // (LINE_SPACING) matching the between-bullets rhythm — was a hard "\n\n" double gap at
2705
+ // the seam, bigger than the gaps between bullets above it (the seam padding fix).
2706
+ // renderAnswer also WRAPS long lines and re-indents each continuation row, so a wrapped
2707
+ // paragraph keeps the same left margin instead of soft-wrapping back to column 0.
2708
+ console.log(shown ? "" : `${LINE_SPACING}${renderAnswer(answer)}\n`);
2653
2709
  // Task #104: persistent token footer at the bottom of the screen — this turn's
2654
2710
  // code-model INPUT + this turn's OUTPUT (↓ out), then the model's GLOBAL lifetime
2655
2711
  // output total (all turns for this user + coding server), labelled with the model
@@ -2658,7 +2714,8 @@ async function main() {
2658
2714
  if ((inputTokens ?? 0) > 0 || (turnOutputTokens ?? 0) > 0) {
2659
2715
  const coder = sessionCodingLabel || defaultCodingModel;
2660
2716
  console.log(
2661
- dim("tokens · ↑ in ") +
2717
+ GUTTER +
2718
+ dim("tokens · ↑ in ") +
2662
2719
  fmtTokens(inputTokens ?? 0) +
2663
2720
  dim(" · ") +
2664
2721
  cyan(`↓ out ${fmtTokens(turnOutputTokens ?? 0)}`) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-cli",
3
- "version": "1.0.358",
3
+ "version": "1.0.360",
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",