@tiens.nguyen/gonext-local-worker 1.0.203 → 1.0.204

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 +43 -26
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -43,11 +43,11 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
43
43
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
44
44
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
45
45
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
46
+ const white = (s) => `\x1b[37m${s}\x1b[0m`;
46
47
  // ANSI 90 ("bright black") — a reliable neutral grey across terminal themes, unlike
47
48
  // code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
48
49
  const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
49
50
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
50
- const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
51
51
 
52
52
  // The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
53
53
  // SWALLOW them from the terminal (the local ticker below drives progress instead), and
@@ -400,7 +400,6 @@ async function runAgentTurn(history) {
400
400
  let warnedPending = false;
401
401
  let jobRunning = false; // only tick "thinking…" once the worker has claimed the job
402
402
  let answerShownLive = false; // plain-reply content already printed — don't re-print it
403
- let routingAnnounced = false; // one friendly line replaces the whole routing play-by-play
404
403
  // True once we've SPECIFICALLY seen "→ Chat reply" (the plain-reply/trivial-chat
405
404
  // branch), as opposed to "→ Agent mode…" (the tool-use branch). Only the plain-reply
406
405
  // path streams its answer via answer_stream with NO heartbeat/tool-summary/code lines
@@ -419,11 +418,18 @@ async function runAgentTurn(history) {
419
418
  let lastContentAt = Date.now();
420
419
  let thinking = false;
421
420
  let thinkingSince = 0;
421
+ let thinkWord = pickWord(); // playful word shown UNDER the in-progress line (flavor)
422
422
  let almostDone = false; // set from the worker's "…almost completed thinking…" heartbeat
423
+ let runningCmd = null; // the command currently executing (from a "Running → …" event)
423
424
  const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
424
425
 
426
+ // The status is TWO in-place lines — the in-progress action, then the playful word
427
+ // under it. Cursor sits at the end of line 2, so clearing = wipe line 2, move up, wipe
428
+ // line 1, leaving the cursor back at the status's origin for a redraw or real content.
425
429
  const clearStatus = () => {
426
- if (statusShown) { process.stdout.write(CLEAR_LINE); statusShown = false; }
430
+ if (!statusShown) return;
431
+ process.stdout.write("\r\x1b[K\x1b[1A\r\x1b[K");
432
+ statusShown = false;
427
433
  };
428
434
 
429
435
  const tick = () => {
@@ -433,22 +439,33 @@ async function runAgentTurn(history) {
433
439
  // done — overwriting the ticker there corrupts the visible answer mid-word. Stay
434
440
  // silent once any of THIS line has already been shown (see carryFlushedLen).
435
441
  if (plainReplyFlow && carryFlushedLen > 0) return;
436
- // Count from when the stream went quiet, so the seconds reflect the true wait.
437
- if (!thinking) { thinking = true; thinkingSince = lastContentAt; }
442
+ // Count from when this phase went quiet, so the seconds reflect the current wait/run.
443
+ if (!thinking) { thinking = true; thinkingSince = lastContentAt; thinkWord = pickWord(); }
438
444
  const secs = thinkSecs();
439
- const glyph = SPINNER[secs % SPINNER.length]; // rotates reads as "in progress"
440
- const label = almostDone ? "almost done" : "working";
441
- process.stdout.write(CLEAR_LINE + yellow(`${glyph} ${label}… (${secs}s)`));
445
+ if (secs % 12 === 0) thinkWord = pickWord(); // rotate the word on long waits
446
+ const glyph = SPINNER[secs % SPINNER.length]; // rotates → reads as "in progress"
447
+ // Line 1 = WHAT is happening now: the running command, else the phase (Thinking /
448
+ // Almost done). Line 2 = the playful word (flavor). Truncate so neither wraps.
449
+ const primary = runningCmd
450
+ ? `Running ${runningCmd}`
451
+ : almostDone ? "Almost done" : "Thinking";
452
+ const max = Math.max(24, (process.stdout.columns || 80) - 14);
453
+ const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
454
+ clearStatus();
455
+ process.stdout.write(
456
+ yellow(`${glyph} ${fit(primary)}… (${secs}s)`) + "\n" + dim(` ${thinkWord}…`)
457
+ );
442
458
  statusShown = true;
443
459
  };
444
460
  const ticker = setInterval(tick, 1000);
445
461
 
446
462
  // Called right before any real (bullet/edit-card/answer) content prints: drop the
447
- // blinking-bullet status line so content lands cleanly on its own line. No "completed
448
- // thought" stamp each finished action prints its own solid ● bullet instead.
463
+ // status lines so content lands cleanly, and end the current phase (so the next phase's
464
+ // seconds count fresh, and any running command is considered finished).
449
465
  const onContent = () => {
450
466
  clearStatus();
451
- if (thinking) { process.stdout.write(CLEAR_LINE); thinking = false; }
467
+ thinking = false;
468
+ runningCmd = null;
452
469
  lastContentAt = Date.now();
453
470
  };
454
471
 
@@ -494,17 +511,10 @@ async function runAgentTurn(history) {
494
511
  if (inStreamFence) continue;
495
512
  if (isToolStepSummary(line)) { lastContentAt = Date.now(); continue; } // swallow
496
513
  if (isRoutingLine(line)) {
497
- // Replace the whole routing play-by-play with ONE friendly line, shown once
498
- // per turn the raw labels are implementation detail (visible in the daemon's
499
- // own terminal if you need them).
514
+ // Swallow the router's play-by-play entirely the blinking-bullet ticker (which
515
+ // already shows the playful word + seconds) is the single in-progress indicator,
516
+ // so a separate static "Booting the big brain…" line here would just duplicate it.
500
517
  if (line.trim() === "→ Chat reply") plainReplyFlow = true;
501
- if (!routingAnnounced) {
502
- routingAnnounced = true;
503
- onContent();
504
- process.stdout.write(dim(`${pickWord()}…`) + "\n");
505
- lastWasBlank = false;
506
- }
507
- lastContentAt = Date.now();
508
518
  continue;
509
519
  }
510
520
  // Edit card (a file change made by the agent's edit tools) — rendered as a
@@ -593,6 +603,14 @@ async function runAgentTurn(history) {
593
603
  }
594
604
  continue;
595
605
  }
606
+ // Agent flow: a "Running → <cmd>" is the START of a command — show it as the live
607
+ // in-progress status (the blinking bullet names it) rather than a solid bullet; its
608
+ // completion ("Command passed/failed → …" / "Server up → …") prints the done bullet.
609
+ // So the pair collapses to: "◑ Running <cmd>…" while it runs, then "● <result>".
610
+ if (!plainReplyFlow) {
611
+ const mRun = /^Running → (.+)$/.exec(line.trim());
612
+ if (mRun) { runningCmd = mRun[1]; thinking = false; lastContentAt = Date.now(); continue; }
613
+ }
596
614
  if (alreadyFlushed === 0) onContent();
597
615
  if (plainReplyFlow) {
598
616
  // Plain-reply content IS the answer — print it in the default color and remember
@@ -600,11 +618,10 @@ async function runAgentTurn(history) {
600
618
  process.stdout.write(line.slice(alreadyFlushed) + "\n");
601
619
  answerShownLive = true;
602
620
  } else {
603
- // Agent flow: with the raw Thought/code stream suppressed above, the only lines
604
- // that reach here are the worker's out-of-fence ACTION events ("Running npx…",
605
- // "Searching code → import (11 hits)", "Server up …", "Composing answer…").
606
- // Render each as ONE solid bullet — the clean, summarized action list (task #41).
607
- process.stdout.write(green(BULLET) + " " + line.trim() + "\n");
621
+ // The remaining out-of-fence ACTION events ("Command passed …", "Searching code
622
+ // …", "Server up …", "Composing answer…") each print as ONE solid bullet —
623
+ // the clean, summarized action list (task #41).
624
+ process.stdout.write(white(BULLET) + " " + line.trim() + "\n");
608
625
  }
609
626
  lastWasBlank = false;
610
627
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.203",
3
+ "version": "1.0.204",
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",