@tiens.nguyen/gonext-local-worker 1.0.262 → 1.0.264

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 +66 -38
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -48,6 +48,9 @@ const white = (s) => `\x1b[37m${s}\x1b[0m`;
48
48
  // code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
49
49
  const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
50
50
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
51
+ // Hover highlight for the clickable thinking line (task #96): bold + underline + bright
52
+ // green so it reads as an interactive "link" the moment the mouse is over it.
53
+ const hoverThought = (s) => `\x1b[1;4;92m${s}\x1b[0m`;
51
54
 
52
55
  // The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
53
56
  // SWALLOW them from the terminal (the local ticker below drives progress instead), and
@@ -383,13 +386,15 @@ rl.on("close", () => {
383
386
  w(null);
384
387
  }
385
388
  });
386
- // Mouse click-to-expand (task #96): while a turn runs we turn ON xterm button tracking so a
387
- // click on the live "thinking" status can expand it (see runAgentTurn). Trade-off the user
388
- // accepted: with tracking on, the terminal's own drag-to-select/copy is captured by us — the
389
- // user holds Option/Shift to select text meanwhile. ?1000 = report button press+release only
390
- // (no motion, so we don't flood on mouse-move); ?1006 = SGR extended coords (no 223-col cap).
391
- const MOUSE_ON = "\x1b[?1000h\x1b[?1006h";
392
- const MOUSE_OFF = "\x1b[?1000l\x1b[?1006l";
389
+ // Mouse hover+click on the thinking line (task #96): while a turn runs we turn ON xterm
390
+ // ANY-MOTION tracking so we can both HIGHLIGHT the line as the mouse hovers it AND expand it
391
+ // on click (see runAgentTurn). Trade-off the user accepted: with tracking on, the terminal's
392
+ // own drag-to-select/copy is captured by us — the user holds Option/Shift to select text
393
+ // meanwhile. ?1003 = report every button press/release AND all mouse MOTION (needed for hover
394
+ // with no button held); ?1006 = SGR extended coords (no 223-col cap). We disable ?1000/?1002
395
+ // too on teardown in case a terminal had them latched.
396
+ const MOUSE_ON = "\x1b[?1003h\x1b[?1006h";
397
+ const MOUSE_OFF = "\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l";
393
398
  let _mouseEnabled = false;
394
399
  const enableMouse = () => {
395
400
  if (_mouseEnabled || !process.stdin.isTTY) return;
@@ -404,9 +409,15 @@ const disableMouse = () => {
404
409
  // Safety net: never leave the user's terminal in mouse-reporting mode (would break their
405
410
  // shell's selection) if we exit ungracefully.
406
411
  process.on("exit", disableMouse);
407
- // SGR mouse report: ESC [ < btn ; col ; row (M=press, m=release). A left-button PRESS is
408
- // btn low-2-bits 0 with the wheel bit (64) clear.
412
+ // SGR mouse report: ESC [ < btn ; col ; row (M=press, m=release). btn bit 32 = MOTION
413
+ // (hover/drag), bit 64 = wheel; low 2 bits = which button (0=left). A left CLICK is a press
414
+ // (M) with motion+wheel bits clear and low bits 0; a HOVER is any event with the motion bit.
409
415
  const MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
416
+ // Cursor Position Report reply to our `\x1b[6n` probe: ESC [ row ; col R. We probe right after
417
+ // clearing the status (cursor is then at the status block's TOP row) to learn the thought
418
+ // line's ABSOLUTE viewport row — so hover/click hit-testing is exact even when the block isn't
419
+ // at the bottom of the screen (task #96 fix). No `<`, so it never collides with MOUSE_RE.
420
+ const DSR_RE = /\x1b\[(\d+);(\d+)R/g;
410
421
  const nextLine = () =>
411
422
  _lineQueue.length > 0
412
423
  ? Promise.resolve(_lineQueue.shift())
@@ -1213,6 +1224,9 @@ async function runAgentTurn(history) {
1213
1224
  // whether the user has clicked to expand it. Both reset when the step's Thought changes.
1214
1225
  let fullLiveThought = "";
1215
1226
  let thoughtExpanded = false;
1227
+ let hoverRow = -1; // last mouse viewport row (from motion events) → highlight when it's on the thought line
1228
+ let thoughtRow = -1; // absolute viewport row of the thought line (line 1), learned via \x1b[6n
1229
+ let lastDsrAt = 0; // throttle the cursor-position probe
1216
1230
  const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
1217
1231
 
1218
1232
  // The status is normally TWO in-place lines — the in-progress action, then the playful
@@ -1267,28 +1281,33 @@ async function runAgentTurn(history) {
1267
1281
  : liveThought || (almostDone ? "Almost done" : "Thinking");
1268
1282
  const max = Math.max(24, (process.stdout.columns || 80) - 14);
1269
1283
  const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
1270
- clearStatus();
1271
- // Line 1: green ● + green phase/seconds (matches the web's green streaming label).
1272
- // Line 2 (indented): the SPINNER + the playful word together, both in the cycling
1273
- // color — the spinner belongs with the word (dot on the label line, spinner leading
1274
- // the flavor line).
1275
- // Task #83/#84: after the playful word, show the running agent-code-model token
1276
- // count (dim) plus the per-workspace PEAK single-request count (orange), e.g.
1277
- // " ⠙ Caffeinating… · 12.3k tok · 8.1k max". Both start at 0 and climb.
1278
1284
  const tokLabel =
1279
1285
  dim(` · ${fmtTokens(latestCodeTokens)} tok`) +
1280
1286
  orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`);
1281
- let out =
1282
- `${green(BULLET)} ${green(`${fit(primary)}… (${secs}s)`)}` +
1283
- "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1284
- let drawn = 2;
1285
- // Task #96: when the user has clicked the thought line, unfold the fuller thought below
1286
- // it as up to 4 wrapped GREY lines (dim). Only when there's genuinely more to show than
1287
- // the truncated primary (a live command / bare "Thinking" has nothing to expand).
1288
- if (thoughtExpanded && fullLiveThought && !runningCmd) {
1289
- const wrapped = wrapThought(fullLiveThought, max, 4);
1290
- for (const wl of wrapped) { out += "\n " + dim(wl); drawn++; }
1291
- }
1287
+ // Task #96: the thought line is "clickable" only when there's a fuller thought to reveal
1288
+ // (a live command / bare "Thinking" has nothing to expand). When clicked, the fuller
1289
+ // thought unfolds as up to 4 wrapped GREY lines BETWEEN the thought and the playful-word
1290
+ // line. Hover-highlight only the THOUGHT line (line 1) — its exact viewport row comes from
1291
+ // the \x1b[6n probe below (`thoughtRow`); we fall back to the viewport-bottom assumption
1292
+ // only when the terminal never answered the probe.
1293
+ const clickable = !!(fullLiveThought && !runningCmd);
1294
+ const expLines = thoughtExpanded && clickable ? wrapThought(fullLiveThought, max, 4) : [];
1295
+ const drawn = 2 + expLines.length;
1296
+ const rows = process.stdout.rows || 24;
1297
+ const tRow = thoughtRow >= 1 ? thoughtRow : rows - drawn + 1;
1298
+ const hovering = clickable && hoverRow === tRow;
1299
+ const label = `${fit(primary)}… (${secs}s)`;
1300
+ clearStatus();
1301
+ // Right after clearing, the cursor sits at the status block's TOP-LEFT = the thought line's
1302
+ // row. Probe for it (throttled) so hover/click hit-testing is exact wherever the block is.
1303
+ if (process.stdin.isTTY && now - lastDsrAt > 400) { lastDsrAt = now; process.stdout.write("\x1b[6n"); }
1304
+ // Line 1: green ● + green phase/seconds (matches the web's green streaming label), or the
1305
+ // underlined bright-green hover style when the mouse is over a clickable thought. Then the
1306
+ // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
1307
+ // (task #83/#84), e.g. " ⠙ Caffeinating… · 12.3k tok · 8.1k max".
1308
+ let out = `${green(BULLET)} ${hovering ? hoverThought(label) : green(label)}`;
1309
+ for (const wl of expLines) out += "\n " + dim(wl);
1310
+ out += "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1292
1311
  process.stdout.write(out);
1293
1312
  statusLines = drawn;
1294
1313
  statusShown = true;
@@ -1297,17 +1316,20 @@ async function runAgentTurn(history) {
1297
1316
  // seconds still tick once/sec). Only redraws when the model has gone quiet >QUIET_MS.
1298
1317
  const ticker = setInterval(tick, 120);
1299
1318
 
1300
- // --- Click-to-expand the thinking line (task #96) --------------------------------------
1301
- // A left-click on the live status block toggles the fuller thought (3-4 grey lines). The
1302
- // status block sits at the bottom of the viewport (new output keeps scrolling it there),
1303
- // so we hit-test the click row against the bottom `statusLines` rows. We enable xterm
1304
- // mouse reporting only for the duration of the turn and always turn it back off in the
1305
- // finally + on process exit, so the user's shell is never left in mouse-capture mode.
1319
+ // --- Hover + click on the thinking line (task #96) -------------------------------------
1320
+ // HOVER: motion events keep `hoverRow`; the tick highlights the thought line (underlined
1321
+ // bright-green "link") whenever the mouse is on its row. CLICK: a left-click on the status
1322
+ // block toggles the fuller thought (3-4 grey lines). The block sits at the bottom of the
1323
+ // viewport (new output keeps scrolling it there), so we hit-test rows against the bottom
1324
+ // `statusLines` rows. Mouse reporting is on only for the turn and always turned back off in
1325
+ // the finally + on process exit, so the user's shell is never left in mouse-capture mode.
1306
1326
  const onStatusClick = (row) => {
1307
1327
  if (!statusShown) return;
1308
1328
  const rows = process.stdout.rows || 24;
1309
- const top = rows - statusLines + 1; // 1-based viewport row of the first status line
1310
- if (row < top || row > rows) return; // click landed outside the status block
1329
+ const top = thoughtRow >= 1 ? thoughtRow : rows - statusLines + 1; // thought line row
1330
+ // Toggle only on the thought line + its grey expansion NOT the last row (the playful
1331
+ // word / token line), so a click there doesn't expand what it isn't part of.
1332
+ if (row < top || row > top + statusLines - 2) return;
1311
1333
  // Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
1312
1334
  // so we don't flicker an empty expand.
1313
1335
  if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
@@ -1317,14 +1339,19 @@ async function runAgentTurn(history) {
1317
1339
  const onMouseData = (chunk) => {
1318
1340
  if (!following) return;
1319
1341
  const s = chunk.toString("latin1");
1320
- if (s.indexOf("\x1b[<") === -1) return; // fast reject: no SGR mouse report in this chunk
1342
+ if (s.indexOf("\x1b[") === -1) return; // fast reject: no CSI (mouse report OR cursor reply) here
1343
+ // Cursor-position reply → the thought line's absolute row (see the \x1b[6n probe in tick).
1344
+ DSR_RE.lastIndex = 0;
1345
+ let d;
1346
+ while ((d = DSR_RE.exec(s))) thoughtRow = parseInt(d[1], 10);
1321
1347
  MOUSE_RE.lastIndex = 0;
1322
1348
  let m;
1323
1349
  while ((m = MOUSE_RE.exec(s))) {
1324
1350
  const btn = parseInt(m[1], 10);
1325
1351
  const row = parseInt(m[3], 10);
1326
1352
  const isPress = m[4] === "M";
1327
- if (isPress && (btn & 0b11) === 0 && (btn & 64) === 0) onStatusClick(row); // left press
1353
+ if (btn & 32) { hoverRow = row; continue; } // MOTION (hover/drag) track for the highlight
1354
+ if (isPress && (btn & 0b11) === 0 && (btn & 64) === 0) onStatusClick(row); // left click → expand
1328
1355
  }
1329
1356
  };
1330
1357
  if (process.stdin.isTTY) process.stdin.on("data", onMouseData);
@@ -1343,6 +1370,7 @@ async function runAgentTurn(history) {
1343
1370
  fenceThought = "";
1344
1371
  fullLiveThought = ""; // #96: the fuller thought is spent too…
1345
1372
  thoughtExpanded = false; // …and each new thinking phase starts collapsed.
1373
+ lastDsrAt = 0; // …content just scrolled → re-probe the thought row next tick.
1346
1374
  lastContentAt = Date.now();
1347
1375
  };
1348
1376
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.262",
3
+ "version": "1.0.264",
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",