@tiens.nguyen/gonext-local-worker 1.0.186 → 1.0.188
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 +95 -20
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -36,6 +36,7 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
|
36
36
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
37
37
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
38
38
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
39
|
+
const blue = (s) => `\x1b[34m${s}\x1b[0m`;
|
|
39
40
|
const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
40
41
|
|
|
41
42
|
// The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
|
|
@@ -43,11 +44,20 @@ const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
|
43
44
|
// also drop the worker's ~~~ stream fences (they only matter to the web renderer).
|
|
44
45
|
const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
|
|
45
46
|
const isFenceLine = (line) => line.trim() === "~~~";
|
|
46
|
-
// The model's
|
|
47
|
-
//
|
|
48
|
-
//
|
|
47
|
+
// The model's raw Python code blob is delimited by <code>/</code> lines. We don't print
|
|
48
|
+
// the tag lines themselves, but the code IN BETWEEN prints in a distinct color (blue)
|
|
49
|
+
// instead of dim prose, so it visually separates from the "Thought:" reasoning.
|
|
49
50
|
const isCodeOpenLine = (line) => line.trim() === "<code>";
|
|
50
51
|
const isCodeCloseLine = (line) => line.trim() === "</code>";
|
|
52
|
+
// The step summary the worker emits right after a tool call finishes (e.g.
|
|
53
|
+
// "unzip_file(...) | → Unzipped 256 files…") duplicates what's already visible in the
|
|
54
|
+
// gonext-local-worker daemon's own terminal — swallow it here to keep the REPL terse.
|
|
55
|
+
const isToolStepSummary = (line) => / \| → /.test(line);
|
|
56
|
+
// The router's internal play-by-play ("Routing your request…", "→ Agent mode (needs
|
|
57
|
+
// tools)", "→ Chat reply", "Choosing a tool…") is implementation detail, not something a
|
|
58
|
+
// user needs — also visible in the daemon's own terminal. Swallowed; see routingAnnounced.
|
|
59
|
+
const isRoutingLine = (line) =>
|
|
60
|
+
/^(Routing your request…|Choosing a tool…|→ Agent mode.*|→ Chat reply)$/.test(line.trim());
|
|
51
61
|
|
|
52
62
|
// Playful words for the local "thinking…" ticker, reused verbatim from the worker's
|
|
53
63
|
// thinking_words.txt sibling (falls back to a tiny built-in list if it's missing).
|
|
@@ -101,7 +111,17 @@ if (!workerKey || !apiBase) {
|
|
|
101
111
|
// awaits (workspace prompt fetch, payload probe) are queued instead of lost — vital
|
|
102
112
|
// for piped/scripted input, harmless for interactive TTYs. Both ask() and the main
|
|
103
113
|
// loop consume from this queue (never rl.question, to avoid double-capture).
|
|
104
|
-
|
|
114
|
+
//
|
|
115
|
+
// terminal: false — we draw our OWN prompt (cyan ">> ") and colorize output ourselves;
|
|
116
|
+
// readline's default terminal:true (auto-enabled whenever output is a TTY) makes it ALSO
|
|
117
|
+
// track cursor position and redraw the input line on every keystroke using ITS OWN
|
|
118
|
+
// (empty, since we never call rl.setPrompt/rl.prompt) prompt — that tracking goes stale
|
|
119
|
+
// the moment we write anything readline doesn't know about (which is everything we print
|
|
120
|
+
// between prompts), and its next redraw emits a stray leading "[" (a malformed/partial
|
|
121
|
+
// cursor-position escape) right before typed input, e.g. "[>> hi". With terminal:false,
|
|
122
|
+
// readline stops managing the line/cursor itself; the OS's own canonical-mode terminal
|
|
123
|
+
// still echoes keystrokes normally, and 'line' events fire exactly the same.
|
|
124
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
105
125
|
const _lineQueue = [];
|
|
106
126
|
let _lineWaiter = null;
|
|
107
127
|
let _stdinClosed = false;
|
|
@@ -251,6 +271,7 @@ async function runAgentTurn(history) {
|
|
|
251
271
|
let statusShown = false; // a transient status line is currently on screen
|
|
252
272
|
let warnedPending = false;
|
|
253
273
|
let jobRunning = false; // only tick "thinking…" once the worker has claimed the job
|
|
274
|
+
let routingAnnounced = false; // one friendly line replaces the whole routing play-by-play
|
|
254
275
|
|
|
255
276
|
// --- Live "thinking…" ticker ---------------------------------------------------
|
|
256
277
|
// Between visible tokens (prompt-eval / awaiting the first token) we show ONE in-place
|
|
@@ -282,48 +303,92 @@ async function runAgentTurn(history) {
|
|
|
282
303
|
};
|
|
283
304
|
const ticker = setInterval(tick, 1000);
|
|
284
305
|
|
|
285
|
-
// Close out a "thinking…" phase right before real output prints.
|
|
306
|
+
// Close out a "thinking…" phase right before real output prints. A model streams in
|
|
307
|
+
// small bursts with natural micro-pauses between them (token batching, network jitter);
|
|
308
|
+
// each pause >700ms would otherwise stamp its OWN "✔ completed thought" line, stacking
|
|
309
|
+
// many near-duplicates through a single Thought/code generation. `stampIfThinking()` —
|
|
310
|
+
// called ONLY at the <code> tag transition, where nothing prints right after it — HOLDS
|
|
311
|
+
// the stamp as the current status line (like the ticker): a later completion overwrites
|
|
312
|
+
// it in place instead of stacking a new line. `onContent()` — called immediately before
|
|
313
|
+
// any real content prints — always COMMITS whatever is pending (a fresh stamp or a held
|
|
314
|
+
// one) with a trailing newline first, so content never lands on the same line as a stamp.
|
|
315
|
+
let pendingStamp = false;
|
|
316
|
+
const stampIfThinking = () => {
|
|
317
|
+
if (thinking) {
|
|
318
|
+
process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`));
|
|
319
|
+
thinking = false;
|
|
320
|
+
statusShown = true;
|
|
321
|
+
pendingStamp = true;
|
|
322
|
+
}
|
|
323
|
+
lastContentAt = Date.now();
|
|
324
|
+
};
|
|
286
325
|
const onContent = () => {
|
|
287
326
|
if (thinking) {
|
|
288
327
|
process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`) + "\n");
|
|
289
328
|
thinking = false;
|
|
290
|
-
|
|
329
|
+
} else if (pendingStamp) {
|
|
330
|
+
process.stdout.write("\n"); // commit the held stamp; content prints right after
|
|
291
331
|
} else {
|
|
292
332
|
clearStatus();
|
|
293
333
|
}
|
|
334
|
+
pendingStamp = false;
|
|
335
|
+
statusShown = false;
|
|
294
336
|
lastContentAt = Date.now();
|
|
295
337
|
};
|
|
296
338
|
|
|
297
|
-
// Split newly-arrived text into lines: heartbeats + ~~~ fences
|
|
298
|
-
//
|
|
299
|
-
// the
|
|
300
|
-
//
|
|
339
|
+
// Split newly-arrived text into lines: heartbeats + ~~~ fences + the post-tool-call step
|
|
340
|
+
// summary ("tool(...) | → result", redundant with the daemon's own terminal) are
|
|
341
|
+
// swallowed; the model's <code>/</code> TAG lines are hidden but the code body between
|
|
342
|
+
// them prints in blue (not dim, so it stands out from "Thought:" prose); blank lines
|
|
343
|
+
// keep spacing; everything else commits dim to scrollback.
|
|
301
344
|
let inCode = false;
|
|
345
|
+
let lastWasBlank = true; // suppresses a leading blank line and collapses repeats
|
|
302
346
|
const consume = (chunk) => {
|
|
303
347
|
carry += stripThinkTags(chunk);
|
|
304
348
|
let nl;
|
|
305
349
|
while ((nl = carry.indexOf("\n")) >= 0) {
|
|
306
350
|
const line = carry.slice(0, nl);
|
|
307
351
|
carry = carry.slice(nl + 1);
|
|
308
|
-
if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow
|
|
352
|
+
if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow, no trace
|
|
353
|
+
if (isToolStepSummary(line)) { lastContentAt = Date.now(); continue; } // swallow
|
|
354
|
+
if (isRoutingLine(line)) {
|
|
355
|
+
// Replace the whole routing play-by-play with ONE friendly line, shown once
|
|
356
|
+
// per turn — the raw labels are implementation detail (visible in the daemon's
|
|
357
|
+
// own terminal if you need them).
|
|
358
|
+
if (!routingAnnounced) {
|
|
359
|
+
routingAnnounced = true;
|
|
360
|
+
onContent();
|
|
361
|
+
process.stdout.write(dim(`${pickWord()}…`) + "\n");
|
|
362
|
+
lastWasBlank = false;
|
|
363
|
+
}
|
|
364
|
+
lastContentAt = Date.now();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
309
367
|
if (!inCode && isCodeOpenLine(line)) {
|
|
310
|
-
//
|
|
311
|
-
// then suppress the code body itself.
|
|
312
|
-
onContent();
|
|
368
|
+
stampIfThinking(); // hold/refresh the stamp; the code body commits it below
|
|
313
369
|
inCode = true;
|
|
314
370
|
continue;
|
|
315
371
|
}
|
|
316
372
|
if (inCode) {
|
|
317
|
-
lastContentAt = Date.now();
|
|
318
|
-
if (
|
|
373
|
+
if (isCodeCloseLine(line)) { inCode = false; lastContentAt = Date.now(); continue; }
|
|
374
|
+
if (line.trim() === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
|
|
375
|
+
onContent();
|
|
376
|
+
process.stdout.write(blue(line) + "\n");
|
|
377
|
+
lastWasBlank = false;
|
|
319
378
|
continue;
|
|
320
379
|
}
|
|
321
380
|
if (line.trim() === "") {
|
|
322
|
-
|
|
381
|
+
// Several sources (swallowed routing lines, swallowed step summaries) each leave
|
|
382
|
+
// their own blank-line companion behind — collapse consecutive blanks into one.
|
|
383
|
+
if (!thinking && !statusShown && !lastWasBlank) {
|
|
384
|
+
process.stdout.write("\n");
|
|
385
|
+
lastWasBlank = true;
|
|
386
|
+
}
|
|
323
387
|
continue;
|
|
324
388
|
}
|
|
325
389
|
onContent();
|
|
326
390
|
process.stdout.write(dim(line) + "\n");
|
|
391
|
+
lastWasBlank = false;
|
|
327
392
|
}
|
|
328
393
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
329
394
|
// a newline), so tokens are actively flowing — keep the ticker asleep.
|
|
@@ -402,7 +467,10 @@ async function main() {
|
|
|
402
467
|
console.log(dim("Ask about this repo, or /help. Ctrl-C aborts a running turn.\n"));
|
|
403
468
|
|
|
404
469
|
const history = [];
|
|
405
|
-
|
|
470
|
+
// With terminal:false, Ctrl-C is no longer intercepted by readline's raw-mode byte
|
|
471
|
+
// scanning (rl.on("SIGINT", …) would never fire) — it now delivers a real OS SIGINT to
|
|
472
|
+
// the process instead, which we catch directly here. Same behavior as before.
|
|
473
|
+
process.on("SIGINT", () => {
|
|
406
474
|
if (following) {
|
|
407
475
|
followAborted = true;
|
|
408
476
|
process.stdout.write(red("\n^C "));
|
|
@@ -413,10 +481,17 @@ async function main() {
|
|
|
413
481
|
|
|
414
482
|
for (;;) {
|
|
415
483
|
process.stdout.write(cyan(">> "));
|
|
416
|
-
|
|
484
|
+
// Absorb blank Enter-presses SILENTLY under the same prompt — without this, lines
|
|
485
|
+
// typed while the agent was busy on a long turn (queued, not yet read) all drain at
|
|
486
|
+
// once the moment the turn ends, each re-printing ">> " with no newline between them
|
|
487
|
+
// (e.g. ">> >> >> "). Only re-print the prompt for a genuine stdin close or input.
|
|
488
|
+
let lineRaw;
|
|
489
|
+
for (;;) {
|
|
490
|
+
lineRaw = await nextLine();
|
|
491
|
+
if (lineRaw === null || lineRaw.trim()) break;
|
|
492
|
+
}
|
|
417
493
|
if (lineRaw === null) break; // stdin closed
|
|
418
494
|
const line = lineRaw.trim();
|
|
419
|
-
if (!line) continue;
|
|
420
495
|
if (line === "/exit" || line === "/quit") break;
|
|
421
496
|
if (line === "/help") {
|
|
422
497
|
console.log(dim(" /exit — quit · /revert [runId] — undo the agent's file edits (latest run)"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.188",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|