@tiens.nguyen/gonext-cli 1.0.360 → 1.0.362

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 +142 -63
  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,18 +112,33 @@ 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))
118
121
  .join("\n");
119
- // Vertical rhythm between action/answer bullets (task #129). One blank line so the list
120
- // breathes instead of being cramped. A knob: "" = the old tight list, "\n" = one blank.
121
- // Applied as a LEADING blank before a bullet when the previous line wasn't already blank —
122
- // so blanks land BETWEEN bullets, never trailing, and it's driven by `lastWasBlank` (what
123
- // was actually printed), NOT the status ticker, so it can't reintroduce the #115 race.
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");
130
+ // The blank SEAM between the dim action log and the bright final answer (grouping, senior-UI
131
+ // advice): the completed-action bullets are now a TIGHT group with no blanks between them, so
132
+ // this one gap is what separates the "what I did" scaffolding from the deliverable. A knob:
133
+ // "" = answer butts against the log, "\n" = one blank line. (Task #129 originally used this
134
+ // BETWEEN every bullet; that read too loose once the bullets went dim — see the bullet write.)
124
135
  const LINE_SPACING = "\n";
136
+ // Plain (chat) reply rendering. false = BUFFER the reply and print it WRAPPED at completion
137
+ // (via renderAnswer, from the authoritative resultText) so long lines keep the left margin —
138
+ // the thinking ticker animates during generation, then the wrapped answer appears. true = the
139
+ // old live char-by-char stream, which reads nicely but SOFT-WRAPS a long line back to column 0.
140
+ // This is the one knob to flip back to streaming if the buffered path ever misbehaves live.
141
+ const STREAM_PLAIN_REPLY = false;
125
142
  // ANSI 90 ("bright black") — a reliable neutral grey across terminal themes, unlike
126
143
  // code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
127
144
  const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
@@ -211,7 +228,23 @@ const fmtTokens = (n) => {
211
228
  if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0)}k`;
212
229
  return `${(n / 1_000_000).toFixed(1)}M`;
213
230
  };
214
- const BULLET = "●"; // a completed action
231
+ const BULLET = "●"; // a neutral completed action (informational step)
232
+ const CHECK = "✓"; // a step that succeeded
233
+ const CROSS = "✗"; // a step that FAILED
234
+ const brightText = (s) => s; // default terminal colour (un-dimmed) — the "pops" text weight
235
+ // Classify a finished action bullet for the "spotlight the answer" hierarchy: successes and
236
+ // neutral steps RECEDE (dim), a FAILURE POPS (red ✗ + full-bright text) — it's the one thing
237
+ // the user must not miss while everything else is quiet. Keyed off the worker's step strings
238
+ // ("Command finished/failed → …", "Server up → …", "App launched → …", "HTTP … → HTTP 2xx/4xx/
239
+ // 5xx", "Searching/Downloading/Composing …"). Failure is checked FIRST so a failing HTTP or a
240
+ // "failed" command never reads as success.
241
+ const classifyBullet = (text) => {
242
+ if (/\bfail(ed|s|ure)?\b|\berror(ed)?\b|→\s*(?:HTTP\s*)?[45]\d\d\b/i.test(text))
243
+ return { mark: CROSS, markColor: red, textColor: brightText };
244
+ if (/^(?:Command finished|Server up|App launched)\b|→\s*(?:HTTP\s*)?2\d\d\b|\b(?:passed|succeeded|ready)\b/i.test(text))
245
+ return { mark: CHECK, markColor: green, textColor: dim };
246
+ return { mark: BULLET, markColor: dim, textColor: dim };
247
+ };
215
248
 
216
249
  // ---------- flags ----------
217
250
  const argv = process.argv.slice(2);
@@ -345,7 +378,11 @@ const rl = createInterface({
345
378
  terminal: IS_TTY,
346
379
  completer: slashCompleter,
347
380
  });
348
- const PROMPT = GUTTER + cyan(">> ");
381
+ // ">>" is a MARGIN MARKER like the ● bullets — it hangs in the gutter (no leading gutter) so
382
+ // the user's typed input lands on the SAME text column (GUTTER+1) as the agent's bullets and
383
+ // answer. ">> " is exactly GUTTER-wide, so input aligns; the ">>" right-edge sits at the same
384
+ // column the ● / ✓ / ✗ markers hang at (markers right-aligned in the margin, text in one column).
385
+ const PROMPT = cyan(">> ");
349
386
  rl.setPrompt(PROMPT);
350
387
  // Side effect of terminal:false above: readline no longer intercepts arrow keys for
351
388
  // command-history recall, so pressing ↑/↓/→/← (or Home/End/Delete) sends the RAW escape
@@ -450,9 +487,9 @@ let secretInput = false;
450
487
 
451
488
  // Visible width of PROMPT (">> ") — used to put the caret back on the input line by
452
489
  // COLUMN, since the string itself carries color escapes that don't occupy cells.
453
- // Visible width of PROMPT = the gutter + ">> " (task #128). Used by the slash-overlay
454
- // cursor math to park the caret at the right column, so it must include the gutter.
455
- const PROMPT_COLS = GUTTER.length + 3;
490
+ // Visible width of PROMPT = ">> " (3), since it now hangs with no leading gutter. Used by the
491
+ // slash-overlay cursor math to park the caret at the input's text column (GUTTER+1 = col 4).
492
+ const PROMPT_COLS = 3;
456
493
 
457
494
  // Truncate to `max` VISIBLE columns, keeping the ANSI color escapes (which occupy no
458
495
  // cells) intact. Essential for the overlay: every row it draws must fit on ONE physical
@@ -735,16 +772,16 @@ async function ensureWorkspace() {
735
772
  }
736
773
  wsSync = { enabled: hit.allowSync !== false, name: hit.name, path: hit.path };
737
774
  console.log(
738
- GUTTER +
739
- dim(
775
+ wrapGutter(
740
776
  `workspace: ${hit.name} → ${hit.path} (run ${hit.allowRun ? "enabled" : "disabled"}` +
741
- `${hit.allowSync !== false ? ", cloud sync on" : ""})`
777
+ `${hit.allowSync !== false ? ", cloud sync on" : ""})`,
778
+ dim
742
779
  )
743
780
  );
744
781
  return;
745
782
  }
746
783
  if (!existsSync(cwd) || !statSync(cwd).isDirectory()) return;
747
- console.log(gutterLines(`This folder is not a registered workspace:\n ${cwd}`));
784
+ console.log(wrapGutter(`This folder is not a registered workspace:\n ${cwd}`));
748
785
  const register = await askYesNo(
749
786
  "Register it so the agent can read/edit code here?", true /* default Yes */
750
787
  );
@@ -1862,14 +1899,14 @@ async function runAgentTurn(history) {
1862
1899
  // underlined bright-green hover style when the mouse is over a clickable thought. Then the
1863
1900
  // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
1864
1901
  // (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)}`;
1902
+ // The blinking ● and the spinner are hanging margin markers (markLine): the label/word land
1903
+ // on the gutter text column, matching the finished ● bullets (no left-edge jump when a live
1904
+ // action becomes a bullet). The expanded grey sub-lines are plain text at the gutter. Safe
1905
+ // for the redraw: clearStatus clears whole lines and hover/click hit-testing is ROW-based,
1906
+ // so a horizontal offset changes nothing.
1907
+ let out = `${markLine(green(BULLET))}${hovering ? hoverThought(label) : green(label)}`;
1871
1908
  for (const wl of expLines) out += "\n" + GUTTER + dim(wl);
1872
- out += "\n" + `${GUTTER}${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1909
+ out += "\n" + `${markLine(color256(wc, glyph))}${color256(wc, `${thinkWord}…`)}${tokLabel}`;
1873
1910
  process.stdout.write(out);
1874
1911
  statusLines = drawn;
1875
1912
  statusShown = true;
@@ -2010,7 +2047,7 @@ async function runAgentTurn(history) {
2010
2047
  if (isEditCardHeader(line)) {
2011
2048
  onContent();
2012
2049
  inEditCard = true;
2013
- process.stdout.write(GUTTER + green("⏺") + bold(line.slice(1)) + "\n");
2050
+ process.stdout.write(markLine(green("⏺")) + bold(line.slice(1).replace(/^\s+/, "")) + "\n");
2014
2051
  lastWasBlank = false;
2015
2052
  lastContentAt = Date.now();
2016
2053
  continue;
@@ -2094,7 +2131,9 @@ async function runAgentTurn(history) {
2094
2131
  // • PLAIN-REPLY answer → this text IS the answer, so its blanks are real
2095
2132
  // paragraph breaks. Keep them (collapsing runs) and clear the status block
2096
2133
  // first, which is what the old `!statusShown` guard was really protecting.
2097
- if (plainReplyFlow && !lastWasBlank) {
2134
+ // When buffering the plain reply, blanks live in resultText and are re-emitted by the
2135
+ // caller's wrapped reprint — swallow them here (and keep the ticker up: no onContent).
2136
+ if (plainReplyFlow && STREAM_PLAIN_REPLY && !lastWasBlank) {
2098
2137
  onContent();
2099
2138
  process.stdout.write("\n");
2100
2139
  lastWasBlank = true;
@@ -2109,27 +2148,44 @@ async function runAgentTurn(history) {
2109
2148
  const mRun = /^Running → (.+)$/.exec(line.trim());
2110
2149
  if (mRun) { runningCmd = mRun[1]; thinking = false; lastContentAt = Date.now(); continue; }
2111
2150
  }
2112
- if (alreadyFlushed === 0) onContent();
2151
+ // Skip the status-clear for a BUFFERED plain reply (STREAM_PLAIN_REPLY=false): nothing
2152
+ // prints yet, so the thinking ticker should keep animating until completion.
2153
+ if (alreadyFlushed === 0 && !(plainReplyFlow && !STREAM_PLAIN_REPLY)) onContent();
2113
2154
  if (plainReplyFlow) {
2114
- // Plain-reply content IS the answer print it in the default color and remember
2115
- // it's visible so the caller doesn't reprint it once the turn completes.
2116
- // Gutter (task #128) ONLY at line-start: when alreadyFlushed>0 this is a re-flush of
2117
- // a growing SAME physical line, so a gutter here would land mid-line.
2118
- process.stdout.write(
2119
- (alreadyFlushed === 0 ? GUTTER : "") + line.slice(alreadyFlushed) + "\n"
2120
- );
2155
+ // Plain-reply content IS the answer. When STREAMING, print it live (default colour) and
2156
+ // mark it shown so the caller doesn't reprint. When BUFFERING, only accumulate — the
2157
+ // caller reprints the whole thing WRAPPED from resultText (shown stays false), so a long
2158
+ // line keeps its margin instead of soft-wrapping. Gutter only at a line start.
2121
2159
  liveAnswer += line.slice(alreadyFlushed) + "\n";
2122
- answerShownLive = true;
2160
+ if (STREAM_PLAIN_REPLY) {
2161
+ process.stdout.write(
2162
+ (alreadyFlushed === 0 ? GUTTER : "") + line.slice(alreadyFlushed) + "\n"
2163
+ );
2164
+ answerShownLive = true;
2165
+ }
2123
2166
  } else {
2124
- // The remaining out-of-fence ACTION events ("Command passed → …", "Searching code
2125
- // → …", "Server up → …", "Composing answer…") each print as ONE solid bullet —
2126
- // the clean, summarized action list (task #41), now with a blank line between them
2127
- // for breathing room (task #129). The spacer is a LEADING blank emitted only when
2128
- // the previous line wasn't already blank one gap BETWEEN bullets, none before the
2129
- // first (which follows the prompt) and none trailing. Deterministic (keyed off
2130
- // lastWasBlank, not the ticker) so #115's status-race stays fixed.
2131
- if (LINE_SPACING && !lastWasBlank) process.stdout.write(LINE_SPACING);
2132
- process.stdout.write(GUTTER + white(BULLET) + " " + line.trim() + "\n");
2167
+ // The out-of-fence ACTION events ("Command finished/failed → …", "Server up → …",
2168
+ // "Searching code → …", "Composing answer…") print as ONE solid bullet each a clean,
2169
+ // summarized action list (task #41). They form a TIGHT group now (no blank between them,
2170
+ // superseding #129's uniform spacing): completed actions are dim scaffolding, so a
2171
+ // compact grey log reads better than a sparse one, and the ONE blank that matters is
2172
+ // before the bright answer (the caller's LINE_SPACING seam), separating the log from the
2173
+ // deliverable. #115's status-race stays gone — there are simply no per-bullet blanks.
2174
+ //
2175
+ // Glyph vocabulary (senior-UI advice): a step that SUCCEEDED gets a green ✓ (with dim
2176
+ // text — a receded success), a FAILURE gets a red ✗ + full-bright text so it POPS (the
2177
+ // one thing not to miss), and a neutral/informational step keeps the dim ● . The marker
2178
+ // hangs in the gutter (markLine); a long bullet wraps with every continuation row
2179
+ // re-indented to the SAME gutter, so it shares one text column with the prompt/answer.
2180
+ const st = classifyBullet(line.trim());
2181
+ const bRows = wrapBlock(line.trim(), GUTTER, process.stdout.columns || 80);
2182
+ process.stdout.write(
2183
+ bRows
2184
+ .map((r, i) =>
2185
+ i === 0 ? markLine(st.markColor(st.mark)) + st.textColor(r) : GUTTER + st.textColor(r)
2186
+ )
2187
+ .join("\n") + "\n"
2188
+ );
2133
2189
  }
2134
2190
  lastWasBlank = false;
2135
2191
  }
@@ -2137,7 +2193,8 @@ async function runAgentTurn(history) {
2137
2193
  // reply) and can take many seconds to generate — without this, nothing would print
2138
2194
  // until the whole thing finally lands, right back to the "wait then dump" problem
2139
2195
  // this was meant to fix. Only safe in the plain-reply flow (see plainReplyFlow above).
2140
- if (plainReplyFlow && !inCode && carry.length > carryFlushedLen) {
2196
+ // Buffered mode skips this entirely the reply is reprinted wrapped at completion.
2197
+ if (STREAM_PLAIN_REPLY && plainReplyFlow && !inCode && carry.length > carryFlushedLen) {
2141
2198
  if (carryFlushedLen === 0) onContent();
2142
2199
  // Gutter (task #128) only at the pending line's START; a continuation appends bare.
2143
2200
  process.stdout.write(
@@ -2158,8 +2215,10 @@ async function runAgentTurn(history) {
2158
2215
  fullLiveThought = thoughtFull(fenceThought + carry); // #96: fuller thought for expand
2159
2216
  }
2160
2217
  // A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
2161
- // a newline), so tokens are actively flowing — keep the ticker asleep.
2162
- if (carry.trim() !== "") lastContentAt = Date.now();
2218
+ // a newline), so tokens are actively flowing — keep the ticker asleep. EXCEPT a buffered
2219
+ // plain reply: nothing prints during generation, so let the ticker keep animating
2220
+ // "Thinking…" (don't mark content as flowing) until the wrapped answer lands at completion.
2221
+ if (carry.trim() !== "" && !(plainReplyFlow && !STREAM_PLAIN_REPLY)) lastContentAt = Date.now();
2163
2222
  };
2164
2223
 
2165
2224
  try {
@@ -2248,11 +2307,14 @@ async function runAgentTurn(history) {
2248
2307
  // nothing (the reported "Hello! How" cut) or a garbage tail. Verify with the
2249
2308
  // seenRaw fingerprint; on a mismatch, recover by answer-prefix instead: print
2250
2309
  // whatever of the final answer wasn't already shown live.
2310
+ // Buffered plain reply: nothing streamed live (answerShownLive stays false), so skip
2311
+ // the suffix reconciliation entirely — the caller reprints the whole reply WRAPPED from
2312
+ // resultText (shown=false), which is also more truncation-proof than the live-stream math.
2251
2313
  const extendsStream = text.length >= shownChars && text.startsWith(seenRaw);
2252
- if (plainReplyFlow && extendsStream && text.length > shownChars) {
2314
+ if (STREAM_PLAIN_REPLY && plainReplyFlow && extendsStream && text.length > shownChars) {
2253
2315
  consume(text.slice(shownChars));
2254
2316
  shownChars = text.length;
2255
- } else if (plainReplyFlow && !extendsStream && answerShownLive) {
2317
+ } else if (STREAM_PLAIN_REPLY && plainReplyFlow && !extendsStream && answerShownLive) {
2256
2318
  const ans = answerFrom(text);
2257
2319
  let rest = ans.startsWith(liveAnswer) ? ans.slice(liveAnswer.length) : null;
2258
2320
  if (rest === null) {
@@ -2380,7 +2442,23 @@ async function runAgentTurn(history) {
2380
2442
 
2381
2443
  // ---------- REPL ----------
2382
2444
  async function main() {
2383
- console.log(GUTTER + cyan("GoNext agent REPL") + dim(` v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`));
2445
+ // Wrap the header so a narrow window doesn't soft-wrap "key wk_…" to column 0; keep the
2446
+ // "GoNext agent REPL" label cyan on row 0, dim the rest (and any wrapped continuation rows).
2447
+ {
2448
+ const LABEL = "GoNext agent REPL";
2449
+ const header = `${LABEL} v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`;
2450
+ console.log(
2451
+ wrapBlock(header, GUTTER, process.stdout.columns || 80)
2452
+ .map((l, i) =>
2453
+ !l.length
2454
+ ? l
2455
+ : i === 0 && l.startsWith(LABEL)
2456
+ ? GUTTER + cyan(LABEL) + dim(l.slice(LABEL.length))
2457
+ : GUTTER + dim(l)
2458
+ )
2459
+ .join("\n")
2460
+ );
2461
+ }
2384
2462
  // Task #127, Phase 2: the REPL enqueues jobs that the polling DAEMON executes, so ensure
2385
2463
  // one is running — no GoTerminal needed. Idempotent + BC4-safe: startDaemon() no-ops when
2386
2464
  // a daemon (ours, GoTerminal's, or a manual one) is already alive. Best-effort; a failure
@@ -2427,14 +2505,15 @@ async function main() {
2427
2505
  await saveSessionRagMode(resolve(process.cwd()), sessionRagMode);
2428
2506
  }
2429
2507
  console.log(
2430
- GUTTER +
2431
- dim(
2508
+ wrapGutter(
2432
2509
  `agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
2433
2510
  (p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
2434
2511
  (p.ragEnabled ? ` · RAG ${sessionRagMode === "cloud" ? "cloud" : "local"}` : "") +
2435
2512
  ` · auto-test: ${sessionTestAuto ? "on" : "off"}` +
2436
- (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "")
2437
- ) + (sessionTestAuto ? "" : dim(" (/test-auto to enable)"))
2513
+ (selectedServer ? ` · deploy: ${selectedServer.name} (${selectedServer.user}@${selectedServer.host})` : "") +
2514
+ (sessionTestAuto ? "" : " (/test-auto to enable)"),
2515
+ dim
2516
+ )
2438
2517
  );
2439
2518
  // Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
2440
2519
  // (download + start mlx_lm.server). BC3: only runs when NOTHING answers — an already-up
@@ -2494,14 +2573,14 @@ async function main() {
2494
2573
  }
2495
2574
  process.exit(1);
2496
2575
  }
2497
- console.log(GUTTER + dim("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.\n"));
2576
+ console.log(wrapGutter("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.", dim) + "\n");
2498
2577
 
2499
2578
  const cwd = resolve(process.cwd());
2500
2579
  const history = await loadSession(cwd);
2501
2580
  if (history.length > 0) {
2502
2581
  const turns = history.filter((m) => m.role === "user").length;
2503
2582
  console.log(
2504
- GUTTER + dim(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.\n`)
2583
+ wrapGutter(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.`, dim) + "\n"
2505
2584
  );
2506
2585
  }
2507
2586
  // 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.362",
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",