@tiens.nguyen/gonext-local-worker 1.0.186 → 1.0.187

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 +52 -17
  2. 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,15 @@ 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 own raw Python code blob, delimited by <code>/</code> lines in its
47
- // Thought+Code output. It's redundant with the step summary that follows it (e.g.
48
- // "Downloading url"), so we don't print it — only the "Thought:" prose survives.
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);
51
56
 
52
57
  // Playful words for the local "thinking…" ticker, reused verbatim from the worker's
53
58
  // thinking_words.txt sibling (falls back to a tiny built-in list if it's missing).
@@ -282,22 +287,44 @@ async function runAgentTurn(history) {
282
287
  };
283
288
  const ticker = setInterval(tick, 1000);
284
289
 
285
- // Close out a "thinking…" phase right before real output prints.
290
+ // Close out a "thinking…" phase right before real output prints. A model streams in
291
+ // small bursts with natural micro-pauses between them (token batching, network jitter);
292
+ // each pause >700ms would otherwise stamp its OWN "✔ completed thought" line, stacking
293
+ // many near-duplicates through a single Thought/code generation. `stampIfThinking()` —
294
+ // called ONLY at the <code> tag transition, where nothing prints right after it — HOLDS
295
+ // the stamp as the current status line (like the ticker): a later completion overwrites
296
+ // it in place instead of stacking a new line. `onContent()` — called immediately before
297
+ // any real content prints — always COMMITS whatever is pending (a fresh stamp or a held
298
+ // one) with a trailing newline first, so content never lands on the same line as a stamp.
299
+ let pendingStamp = false;
300
+ const stampIfThinking = () => {
301
+ if (thinking) {
302
+ process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`));
303
+ thinking = false;
304
+ statusShown = true;
305
+ pendingStamp = true;
306
+ }
307
+ lastContentAt = Date.now();
308
+ };
286
309
  const onContent = () => {
287
310
  if (thinking) {
288
311
  process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`) + "\n");
289
312
  thinking = false;
290
- statusShown = false;
313
+ } else if (pendingStamp) {
314
+ process.stdout.write("\n"); // commit the held stamp; content prints right after
291
315
  } else {
292
316
  clearStatus();
293
317
  }
318
+ pendingStamp = false;
319
+ statusShown = false;
294
320
  lastContentAt = Date.now();
295
321
  };
296
322
 
297
- // Split newly-arrived text into lines: heartbeats + ~~~ fences are swallowed (the ticker
298
- // shows progress); the model's raw <code>…</code> blob is swallowed too (redundant with
299
- // the step summary that follows it, e.g. "Downloading url"); blank lines keep spacing;
300
- // everything else (the "Thought:" prose) commits dim to scrollback.
323
+ // Split newly-arrived text into lines: heartbeats + ~~~ fences + the post-tool-call step
324
+ // summary ("tool(...) | result", redundant with the daemon's own terminal) are
325
+ // swallowed; the model's <code>/</code> TAG lines are hidden but the code body between
326
+ // them prints in blue (not dim, so it stands out from "Thought:" prose); blank lines
327
+ // keep spacing; everything else commits dim to scrollback.
301
328
  let inCode = false;
302
329
  const consume = (chunk) => {
303
330
  carry += stripThinkTags(chunk);
@@ -305,17 +332,18 @@ async function runAgentTurn(history) {
305
332
  while ((nl = carry.indexOf("\n")) >= 0) {
306
333
  const line = carry.slice(0, nl);
307
334
  carry = carry.slice(nl + 1);
308
- if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow
335
+ if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow, no trace
336
+ if (isToolStepSummary(line)) { lastContentAt = Date.now(); continue; } // swallow
309
337
  if (!inCode && isCodeOpenLine(line)) {
310
- // Reasoning just ended and code is about to run — stamp the thought complete,
311
- // then suppress the code body itself.
312
- onContent();
338
+ stampIfThinking(); // hold/refresh the stamp; the code body commits it below
313
339
  inCode = true;
314
340
  continue;
315
341
  }
316
342
  if (inCode) {
317
- lastContentAt = Date.now(); // content IS arriving, just not printed
318
- if (isCodeCloseLine(line)) inCode = false;
343
+ if (isCodeCloseLine(line)) { inCode = false; lastContentAt = Date.now(); continue; }
344
+ if (line.trim() === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
345
+ onContent();
346
+ process.stdout.write(blue(line) + "\n");
319
347
  continue;
320
348
  }
321
349
  if (line.trim() === "") {
@@ -413,10 +441,17 @@ async function main() {
413
441
 
414
442
  for (;;) {
415
443
  process.stdout.write(cyan(">> "));
416
- const lineRaw = await nextLine();
444
+ // Absorb blank Enter-presses SILENTLY under the same prompt — without this, lines
445
+ // typed while the agent was busy on a long turn (queued, not yet read) all drain at
446
+ // once the moment the turn ends, each re-printing ">> " with no newline between them
447
+ // (e.g. ">> >> >> "). Only re-print the prompt for a genuine stdin close or input.
448
+ let lineRaw;
449
+ for (;;) {
450
+ lineRaw = await nextLine();
451
+ if (lineRaw === null || lineRaw.trim()) break;
452
+ }
417
453
  if (lineRaw === null) break; // stdin closed
418
454
  const line = lineRaw.trim();
419
- if (!line) continue;
420
455
  if (line === "/exit" || line === "/quit") break;
421
456
  if (line === "/help") {
422
457
  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.186",
3
+ "version": "1.0.187",
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",