@tiens.nguyen/gonext-cli 1.0.361 → 1.0.363
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 +80 -42
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -127,12 +127,18 @@ const wrapGutter = (plain, colorFn = (s) => s) =>
|
|
|
127
127
|
wrapBlock(plain, GUTTER, process.stdout.columns || 80)
|
|
128
128
|
.map((l) => (l.length ? GUTTER + colorFn(l) : l))
|
|
129
129
|
.join("\n");
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
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.)
|
|
135
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;
|
|
136
142
|
// ANSI 90 ("bright black") — a reliable neutral grey across terminal themes, unlike
|
|
137
143
|
// code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
|
|
138
144
|
const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
@@ -222,7 +228,23 @@ const fmtTokens = (n) => {
|
|
|
222
228
|
if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0)}k`;
|
|
223
229
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
224
230
|
};
|
|
225
|
-
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
|
+
};
|
|
226
248
|
|
|
227
249
|
// ---------- flags ----------
|
|
228
250
|
const argv = process.argv.slice(2);
|
|
@@ -356,7 +378,11 @@ const rl = createInterface({
|
|
|
356
378
|
terminal: IS_TTY,
|
|
357
379
|
completer: slashCompleter,
|
|
358
380
|
});
|
|
359
|
-
|
|
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(">> ");
|
|
360
386
|
rl.setPrompt(PROMPT);
|
|
361
387
|
// Side effect of terminal:false above: readline no longer intercepts arrow keys for
|
|
362
388
|
// command-history recall, so pressing ↑/↓/→/← (or Home/End/Delete) sends the RAW escape
|
|
@@ -461,9 +487,9 @@ let secretInput = false;
|
|
|
461
487
|
|
|
462
488
|
// Visible width of PROMPT (">> ") — used to put the caret back on the input line by
|
|
463
489
|
// COLUMN, since the string itself carries color escapes that don't occupy cells.
|
|
464
|
-
// Visible width of PROMPT =
|
|
465
|
-
// cursor math to park the caret at the
|
|
466
|
-
const PROMPT_COLS =
|
|
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;
|
|
467
493
|
|
|
468
494
|
// Truncate to `max` VISIBLE columns, keeping the ANSI color escapes (which occupy no
|
|
469
495
|
// cells) intact. Essential for the overlay: every row it draws must fit on ONE physical
|
|
@@ -2105,7 +2131,9 @@ async function runAgentTurn(history) {
|
|
|
2105
2131
|
// • PLAIN-REPLY answer → this text IS the answer, so its blanks are real
|
|
2106
2132
|
// paragraph breaks. Keep them (collapsing runs) and clear the status block
|
|
2107
2133
|
// first, which is what the old `!statusShown` guard was really protecting.
|
|
2108
|
-
|
|
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) {
|
|
2109
2137
|
onContent();
|
|
2110
2138
|
process.stdout.write("\n");
|
|
2111
2139
|
lastWasBlank = true;
|
|
@@ -2120,38 +2148,42 @@ async function runAgentTurn(history) {
|
|
|
2120
2148
|
const mRun = /^Running → (.+)$/.exec(line.trim());
|
|
2121
2149
|
if (mRun) { runningCmd = mRun[1]; thinking = false; lastContentAt = Date.now(); continue; }
|
|
2122
2150
|
}
|
|
2123
|
-
|
|
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();
|
|
2124
2154
|
if (plainReplyFlow) {
|
|
2125
|
-
// Plain-reply content IS the answer
|
|
2126
|
-
// it
|
|
2127
|
-
//
|
|
2128
|
-
//
|
|
2129
|
-
process.stdout.write(
|
|
2130
|
-
(alreadyFlushed === 0 ? GUTTER : "") + line.slice(alreadyFlushed) + "\n"
|
|
2131
|
-
);
|
|
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.
|
|
2132
2159
|
liveAnswer += line.slice(alreadyFlushed) + "\n";
|
|
2133
|
-
|
|
2160
|
+
if (STREAM_PLAIN_REPLY) {
|
|
2161
|
+
process.stdout.write(
|
|
2162
|
+
(alreadyFlushed === 0 ? GUTTER : "") + line.slice(alreadyFlushed) + "\n"
|
|
2163
|
+
);
|
|
2164
|
+
answerShownLive = true;
|
|
2165
|
+
}
|
|
2134
2166
|
} else {
|
|
2135
|
-
// The
|
|
2136
|
-
//
|
|
2137
|
-
//
|
|
2138
|
-
//
|
|
2139
|
-
//
|
|
2140
|
-
//
|
|
2141
|
-
//
|
|
2142
|
-
|
|
2143
|
-
//
|
|
2144
|
-
//
|
|
2145
|
-
//
|
|
2146
|
-
//
|
|
2147
|
-
//
|
|
2148
|
-
|
|
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.
|
|
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());
|
|
2151
2181
|
const bRows = wrapBlock(line.trim(), GUTTER, process.stdout.columns || 80);
|
|
2152
2182
|
process.stdout.write(
|
|
2153
2183
|
bRows
|
|
2154
|
-
.map((r, i) =>
|
|
2184
|
+
.map((r, i) =>
|
|
2185
|
+
i === 0 ? markLine(st.markColor(st.mark)) + st.textColor(r) : GUTTER + st.textColor(r)
|
|
2186
|
+
)
|
|
2155
2187
|
.join("\n") + "\n"
|
|
2156
2188
|
);
|
|
2157
2189
|
}
|
|
@@ -2161,7 +2193,8 @@ async function runAgentTurn(history) {
|
|
|
2161
2193
|
// reply) and can take many seconds to generate — without this, nothing would print
|
|
2162
2194
|
// until the whole thing finally lands, right back to the "wait then dump" problem
|
|
2163
2195
|
// this was meant to fix. Only safe in the plain-reply flow (see plainReplyFlow above).
|
|
2164
|
-
|
|
2196
|
+
// Buffered mode skips this entirely — the reply is reprinted wrapped at completion.
|
|
2197
|
+
if (STREAM_PLAIN_REPLY && plainReplyFlow && !inCode && carry.length > carryFlushedLen) {
|
|
2165
2198
|
if (carryFlushedLen === 0) onContent();
|
|
2166
2199
|
// Gutter (task #128) only at the pending line's START; a continuation appends bare.
|
|
2167
2200
|
process.stdout.write(
|
|
@@ -2182,8 +2215,10 @@ async function runAgentTurn(history) {
|
|
|
2182
2215
|
fullLiveThought = thoughtFull(fenceThought + carry); // #96: fuller thought for expand
|
|
2183
2216
|
}
|
|
2184
2217
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
2185
|
-
// a newline), so tokens are actively flowing — keep the ticker asleep.
|
|
2186
|
-
|
|
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();
|
|
2187
2222
|
};
|
|
2188
2223
|
|
|
2189
2224
|
try {
|
|
@@ -2272,11 +2307,14 @@ async function runAgentTurn(history) {
|
|
|
2272
2307
|
// nothing (the reported "Hello! How" cut) or a garbage tail. Verify with the
|
|
2273
2308
|
// seenRaw fingerprint; on a mismatch, recover by answer-prefix instead: print
|
|
2274
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.
|
|
2275
2313
|
const extendsStream = text.length >= shownChars && text.startsWith(seenRaw);
|
|
2276
|
-
if (plainReplyFlow && extendsStream && text.length > shownChars) {
|
|
2314
|
+
if (STREAM_PLAIN_REPLY && plainReplyFlow && extendsStream && text.length > shownChars) {
|
|
2277
2315
|
consume(text.slice(shownChars));
|
|
2278
2316
|
shownChars = text.length;
|
|
2279
|
-
} else if (plainReplyFlow && !extendsStream && answerShownLive) {
|
|
2317
|
+
} else if (STREAM_PLAIN_REPLY && plainReplyFlow && !extendsStream && answerShownLive) {
|
|
2280
2318
|
const ans = answerFrom(text);
|
|
2281
2319
|
let rest = ans.startsWith(liveAnswer) ? ans.slice(liveAnswer.length) : null;
|
|
2282
2320
|
if (rest === null) {
|
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.363",
|
|
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",
|