@tiens.nguyen/gonext-local-worker 1.0.187 → 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.
Files changed (2) hide show
  1. package/gonext-repl.mjs +43 -3
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -53,6 +53,11 @@ const isCodeCloseLine = (line) => line.trim() === "</code>";
53
53
  // "unzip_file(...) | → Unzipped 256 files…") duplicates what's already visible in the
54
54
  // gonext-local-worker daemon's own terminal — swallow it here to keep the REPL terse.
55
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());
56
61
 
57
62
  // Playful words for the local "thinking…" ticker, reused verbatim from the worker's
58
63
  // thinking_words.txt sibling (falls back to a tiny built-in list if it's missing).
@@ -106,7 +111,17 @@ if (!workerKey || !apiBase) {
106
111
  // awaits (workspace prompt fetch, payload probe) are queued instead of lost — vital
107
112
  // for piped/scripted input, harmless for interactive TTYs. Both ask() and the main
108
113
  // loop consume from this queue (never rl.question, to avoid double-capture).
109
- const rl = createInterface({ input: process.stdin, output: process.stdout });
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 });
110
125
  const _lineQueue = [];
111
126
  let _lineWaiter = null;
112
127
  let _stdinClosed = false;
@@ -256,6 +271,7 @@ async function runAgentTurn(history) {
256
271
  let statusShown = false; // a transient status line is currently on screen
257
272
  let warnedPending = false;
258
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
259
275
 
260
276
  // --- Live "thinking…" ticker ---------------------------------------------------
261
277
  // Between visible tokens (prompt-eval / awaiting the first token) we show ONE in-place
@@ -326,6 +342,7 @@ async function runAgentTurn(history) {
326
342
  // them prints in blue (not dim, so it stands out from "Thought:" prose); blank lines
327
343
  // keep spacing; everything else commits dim to scrollback.
328
344
  let inCode = false;
345
+ let lastWasBlank = true; // suppresses a leading blank line and collapses repeats
329
346
  const consume = (chunk) => {
330
347
  carry += stripThinkTags(chunk);
331
348
  let nl;
@@ -334,6 +351,19 @@ async function runAgentTurn(history) {
334
351
  carry = carry.slice(nl + 1);
335
352
  if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow, no trace
336
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
+ }
337
367
  if (!inCode && isCodeOpenLine(line)) {
338
368
  stampIfThinking(); // hold/refresh the stamp; the code body commits it below
339
369
  inCode = true;
@@ -344,14 +374,21 @@ async function runAgentTurn(history) {
344
374
  if (line.trim() === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
345
375
  onContent();
346
376
  process.stdout.write(blue(line) + "\n");
377
+ lastWasBlank = false;
347
378
  continue;
348
379
  }
349
380
  if (line.trim() === "") {
350
- if (!thinking && !statusShown) process.stdout.write("\n");
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
+ }
351
387
  continue;
352
388
  }
353
389
  onContent();
354
390
  process.stdout.write(dim(line) + "\n");
391
+ lastWasBlank = false;
355
392
  }
356
393
  // A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
357
394
  // a newline), so tokens are actively flowing — keep the ticker asleep.
@@ -430,7 +467,10 @@ async function main() {
430
467
  console.log(dim("Ask about this repo, or /help. Ctrl-C aborts a running turn.\n"));
431
468
 
432
469
  const history = [];
433
- rl.on("SIGINT", () => {
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", () => {
434
474
  if (following) {
435
475
  followAborted = true;
436
476
  process.stdout.write(red("\n^C "));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.187",
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",