@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.
- package/gonext-repl.mjs +105 -33
- 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 ●
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
1837
|
-
// the finished ● bullets
|
|
1838
|
-
//
|
|
1839
|
-
// clears whole lines and
|
|
1840
|
-
// changes nothing.
|
|
1841
|
-
let out = `${
|
|
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" + `${
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
// (
|
|
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
|
-
|
|
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.
|
|
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",
|