@tiens.nguyen/gonext-cli 1.0.360 → 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 +71 -30
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -67,12 +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).
75
- const OUTPUT_INDENT = GUTTER + " ";
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 + " ";
76
78
  // Render the model's inline **bold** as ANSI bold (markdown polish). The terminal has no
77
79
  // markdown renderer, so a heading like `**What I did:**` was printing its literal asterisks.
78
80
  // Only paired ** with non-space content between them is consumed (so a stray `**` or `a ** b`
@@ -110,11 +112,20 @@ const wrapBlock = (text, indent, width) => {
110
112
  return rows;
111
113
  };
112
114
  // 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
+ // 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).
115
118
  const renderAnswer = (answer) =>
116
- wrapBlock(answer, OUTPUT_INDENT, process.stdout.columns || 80)
117
- .map((l) => (l.length ? OUTPUT_INDENT + renderMdBold(l) : l))
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))
118
129
  .join("\n");
119
130
  // Vertical rhythm between ● action/answer bullets (task #129). One blank line so the list
120
131
  // breathes instead of being cramped. A knob: "" = the old tight list, "\n" = one blank.
@@ -735,16 +746,16 @@ async function ensureWorkspace() {
735
746
  }
736
747
  wsSync = { enabled: hit.allowSync !== false, name: hit.name, path: hit.path };
737
748
  console.log(
738
- GUTTER +
739
- dim(
749
+ wrapGutter(
740
750
  `workspace: ${hit.name} → ${hit.path} (run ${hit.allowRun ? "enabled" : "disabled"}` +
741
- `${hit.allowSync !== false ? ", cloud sync on" : ""})`
751
+ `${hit.allowSync !== false ? ", cloud sync on" : ""})`,
752
+ dim
742
753
  )
743
754
  );
744
755
  return;
745
756
  }
746
757
  if (!existsSync(cwd) || !statSync(cwd).isDirectory()) return;
747
- 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}`));
748
759
  const register = await askYesNo(
749
760
  "Register it so the agent can read/edit code here?", true /* default Yes */
750
761
  );
@@ -1862,14 +1873,14 @@ async function runAgentTurn(history) {
1862
1873
  // underlined bright-green hover style when the mouse is over a clickable thought. Then the
1863
1874
  // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
1864
1875
  // (task #83/#84), e.g. " ⠙ Caffeinating… · 12.3k tok · 8.1k max".
1865
- // Gutter the live status block's first line (task #128) so the blinking ● lines up with
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.
1870
- 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)}`;
1871
1882
  for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
1872
- out += "\n" + `${GUTTER}${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1883
+ out += "\n" + `${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${tokLabel}`;
1873
1884
  process.stdout.write(out);
1874
1885
  statusLines = drawn;
1875
1886
  statusShown = true;
@@ -2010,7 +2021,7 @@ async function runAgentTurn(history) {
2010
2021
  if (isEditCardHeader(line)) {
2011
2022
  onContent();
2012
2023
  inEditCard = true;
2013
- process.stdout.write(GUTTER + green("⏺") + bold(line.slice(1)) + "\n");
2024
+ process.stdout.write(markLine(green("⏺")) + bold(line.slice(1).replace(/^\s+/, "")) + "\n");
2014
2025
  lastWasBlank = false;
2015
2026
  lastContentAt = Date.now();
2016
2027
  continue;
@@ -2129,7 +2140,20 @@ async function runAgentTurn(history) {
2129
2140
  // first (which follows the prompt) and none trailing. Deterministic (keyed off
2130
2141
  // lastWasBlank, not the ticker) so #115's status-race stays fixed.
2131
2142
  if (LINE_SPACING && !lastWasBlank) process.stdout.write(LINE_SPACING);
2132
- 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
+ );
2133
2157
  }
2134
2158
  lastWasBlank = false;
2135
2159
  }
@@ -2380,7 +2404,23 @@ async function runAgentTurn(history) {
2380
2404
 
2381
2405
  // ---------- REPL ----------
2382
2406
  async function main() {
2383
- 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
+ }
2384
2424
  // Task #127, Phase 2: the REPL enqueues jobs that the polling DAEMON executes, so ensure
2385
2425
  // one is running — no GoTerminal needed. Idempotent + BC4-safe: startDaemon() no-ops when
2386
2426
  // a daemon (ours, GoTerminal's, or a manual one) is already alive. Best-effort; a failure
@@ -2427,14 +2467,15 @@ async function main() {
2427
2467
  await saveSessionRagMode(resolve(process.cwd()), sessionRagMode);
2428
2468
  }
2429
2469
  console.log(
2430
- GUTTER +
2431
- dim(
2470
+ wrapGutter(
2432
2471
  `agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
2433
2472
  (p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
2434
2473
  (p.ragEnabled ? ` · RAG ${sessionRagMode === "cloud" ? "cloud" : "local"}` : "") +
2435
2474
  ` · auto-test: ${sessionTestAuto ? "on" : "off"}` +
2436
- (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "")
2437
- ) + (sessionTestAuto ? "" : dim(" (/test-auto to enable)"))
2475
+ (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "") +
2476
+ (sessionTestAuto ? "" : " (/test-auto to enable)"),
2477
+ dim
2478
+ )
2438
2479
  );
2439
2480
  // Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
2440
2481
  // (download + start mlx_lm.server). BC3: only runs when NOTHING answers — an already-up
@@ -2494,14 +2535,14 @@ async function main() {
2494
2535
  }
2495
2536
  process.exit(1);
2496
2537
  }
2497
- 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");
2498
2539
 
2499
2540
  const cwd = resolve(process.cwd());
2500
2541
  const history = await loadSession(cwd);
2501
2542
  if (history.length > 0) {
2502
2543
  const turns = history.filter((m) => m.role === "user").length;
2503
2544
  console.log(
2504
- 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"
2505
2546
  );
2506
2547
  }
2507
2548
  // Ctrl-C arrives on ONE of two paths depending on the readline mode: with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-cli",
3
- "version": "1.0.360",
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",