@tiens.nguyen/gonext-local-worker 1.0.261 → 1.0.263
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 +159 -17
- 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,6 +386,33 @@ rl.on("close", () => {
|
|
|
383
386
|
w(null);
|
|
384
387
|
}
|
|
385
388
|
});
|
|
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";
|
|
398
|
+
let _mouseEnabled = false;
|
|
399
|
+
const enableMouse = () => {
|
|
400
|
+
if (_mouseEnabled || !process.stdin.isTTY) return;
|
|
401
|
+
process.stdout.write(MOUSE_ON);
|
|
402
|
+
_mouseEnabled = true;
|
|
403
|
+
};
|
|
404
|
+
const disableMouse = () => {
|
|
405
|
+
if (!_mouseEnabled) return;
|
|
406
|
+
process.stdout.write(MOUSE_OFF);
|
|
407
|
+
_mouseEnabled = false;
|
|
408
|
+
};
|
|
409
|
+
// Safety net: never leave the user's terminal in mouse-reporting mode (would break their
|
|
410
|
+
// shell's selection) if we exit ungracefully.
|
|
411
|
+
process.on("exit", disableMouse);
|
|
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.
|
|
415
|
+
const MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
386
416
|
const nextLine = () =>
|
|
387
417
|
_lineQueue.length > 0
|
|
388
418
|
? Promise.resolve(_lineQueue.shift())
|
|
@@ -1047,6 +1077,52 @@ const normalizeThought = (buf) => {
|
|
|
1047
1077
|
return "";
|
|
1048
1078
|
};
|
|
1049
1079
|
|
|
1080
|
+
// The FULLER thought (task #96): the same Thought prose normalizeThought summarizes, but
|
|
1081
|
+
// NOT clipped to one clause/100 chars — collapsed to a single spaced string and capped so
|
|
1082
|
+
// the expanded (mouse-click) view has a few lines to show. Returns "" when the live line is
|
|
1083
|
+
// only a tool label (nothing extra to expand). Bounded by fenceThought's own 400-char cap.
|
|
1084
|
+
const thoughtFull = (buf) => {
|
|
1085
|
+
const t = String(buf || "");
|
|
1086
|
+
const ci = t.search(/<code[\s>]/i);
|
|
1087
|
+
const head = (ci >= 0 ? t.slice(0, ci) : t)
|
|
1088
|
+
.replace(/^\s*Thought:\s*/i, "")
|
|
1089
|
+
.replace(/\s+/g, " ")
|
|
1090
|
+
.trim();
|
|
1091
|
+
// Only worth expanding when there's a genuine prose clause (same gate as normalizeThought
|
|
1092
|
+
// path 1) — a bare fragment or pure code has nothing meaningful to unfold.
|
|
1093
|
+
const first = head.split(/\s+/).length;
|
|
1094
|
+
if (head.length < 6 || first < 2 || _thoughtLooksCodey(head)) return "";
|
|
1095
|
+
return head.slice(0, 400);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// Word-wrap a plain (ANSI-free) string into at most `maxLines` lines of width ≤ `width`,
|
|
1099
|
+
// appending an ellipsis to the last line when text was dropped (task #96 expand view).
|
|
1100
|
+
const wrapThought = (s, width, maxLines) => {
|
|
1101
|
+
const words = String(s || "").split(/\s+/).filter(Boolean);
|
|
1102
|
+
const lines = [];
|
|
1103
|
+
let cur = "";
|
|
1104
|
+
let truncated = false;
|
|
1105
|
+
for (let i = 0; i < words.length; i++) {
|
|
1106
|
+
const w = words[i];
|
|
1107
|
+
const next = cur ? cur + " " + w : w;
|
|
1108
|
+
if (next.length <= width) {
|
|
1109
|
+
cur = next;
|
|
1110
|
+
} else {
|
|
1111
|
+
if (cur) lines.push(cur);
|
|
1112
|
+
cur = w;
|
|
1113
|
+
if (lines.length >= maxLines) { truncated = true; cur = ""; break; }
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (cur && lines.length < maxLines) lines.push(cur);
|
|
1117
|
+
else if (cur) truncated = true;
|
|
1118
|
+
if (truncated && lines.length) {
|
|
1119
|
+
const last = lines[maxLines - 1] ?? lines[lines.length - 1];
|
|
1120
|
+
const room = Math.max(1, width - 1);
|
|
1121
|
+
lines[lines.length - 1] = (last.length > room ? last.slice(0, room) : last).replace(/\s+$/, "") + "…";
|
|
1122
|
+
}
|
|
1123
|
+
return lines;
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1050
1126
|
async function runAgentTurn(history) {
|
|
1051
1127
|
const res = await fetch(`${apiBase}/api/worker/agent-ask`, {
|
|
1052
1128
|
method: "POST",
|
|
@@ -1090,6 +1166,7 @@ async function runAgentTurn(history) {
|
|
|
1090
1166
|
let lastApprovalId = null; // the last command-approval request we've already answered
|
|
1091
1167
|
let carry = ""; // trailing partial content line held until its newline arrives
|
|
1092
1168
|
let statusShown = false; // a transient status line is currently on screen
|
|
1169
|
+
let statusLines = 2; // how many rows the status block currently occupies (grows when expanded)
|
|
1093
1170
|
let warnedPending = false;
|
|
1094
1171
|
// Transient poll failures (a network blip or a 5xx like the API's momentary "Worker
|
|
1095
1172
|
// key lookup failed." — a cold Mongo connection, NOT a bad key) must NOT kill a
|
|
@@ -1138,14 +1215,22 @@ async function runAgentTurn(history) {
|
|
|
1138
1215
|
// random word. `fenceThought` accumulates the raw in-fence text for the current step.
|
|
1139
1216
|
let liveThought = "";
|
|
1140
1217
|
let fenceThought = "";
|
|
1218
|
+
// Task #96: the FULLER version of the current thought (for the click-to-expand view) and
|
|
1219
|
+
// whether the user has clicked to expand it. Both reset when the step's Thought changes.
|
|
1220
|
+
let fullLiveThought = "";
|
|
1221
|
+
let thoughtExpanded = false;
|
|
1222
|
+
let hoverRow = -1; // last mouse viewport row (from motion events) → highlight when it's on the thought line
|
|
1141
1223
|
const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
|
|
1142
1224
|
|
|
1143
|
-
// The status is TWO in-place lines — the in-progress action, then the playful
|
|
1144
|
-
// under it
|
|
1145
|
-
//
|
|
1225
|
+
// The status is normally TWO in-place lines — the in-progress action, then the playful
|
|
1226
|
+
// word under it — but grows by a few grey lines when the thought is expanded (task #96).
|
|
1227
|
+
// Cursor sits at the end of the LAST line, so clearing = wipe it, then move-up-and-wipe
|
|
1228
|
+
// for each line above, leaving the cursor back at the status's origin for a redraw.
|
|
1146
1229
|
const clearStatus = () => {
|
|
1147
1230
|
if (!statusShown) return;
|
|
1148
|
-
|
|
1231
|
+
let seq = "\r\x1b[K";
|
|
1232
|
+
for (let i = 1; i < statusLines; i++) seq += "\x1b[1A\r\x1b[K";
|
|
1233
|
+
process.stdout.write(seq);
|
|
1149
1234
|
statusShown = false;
|
|
1150
1235
|
};
|
|
1151
1236
|
|
|
@@ -1166,7 +1251,9 @@ async function runAgentTurn(history) {
|
|
|
1166
1251
|
// while (e.g. a 150s+ silent prompt-eval on a huge context), the last clause is no longer
|
|
1167
1252
|
// "what it's doing now" — drop it so the line falls back to the playful word / "Thinking…"
|
|
1168
1253
|
// instead of freezing on an old thought while the timer climbs ("string… (777s)", #95 A).
|
|
1169
|
-
if (liveThought && now - lastContentAt > STALE_THOUGHT_MS)
|
|
1254
|
+
if (liveThought && now - lastContentAt > STALE_THOUGHT_MS) {
|
|
1255
|
+
liveThought = ""; fullLiveThought = ""; thoughtExpanded = false; // #96: nothing to expand once stale
|
|
1256
|
+
}
|
|
1170
1257
|
const secs = thinkSecs();
|
|
1171
1258
|
// Rotate the playful word once per 12s window (guarded so the fast ticker doesn't
|
|
1172
1259
|
// re-roll it many times within the same second).
|
|
@@ -1187,27 +1274,73 @@ async function runAgentTurn(history) {
|
|
|
1187
1274
|
: liveThought || (almostDone ? "Almost done" : "Thinking");
|
|
1188
1275
|
const max = Math.max(24, (process.stdout.columns || 80) - 14);
|
|
1189
1276
|
const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
|
|
1190
|
-
clearStatus();
|
|
1191
|
-
// Line 1: green ● + green phase/seconds (matches the web's green streaming label).
|
|
1192
|
-
// Line 2 (indented): the SPINNER + the playful word together, both in the cycling
|
|
1193
|
-
// color — the spinner belongs with the word (dot on the label line, spinner leading
|
|
1194
|
-
// the flavor line).
|
|
1195
|
-
// Task #83/#84: after the playful word, show the running agent-code-model token
|
|
1196
|
-
// count (dim) plus the per-workspace PEAK single-request count (orange), e.g.
|
|
1197
|
-
// " ⠙ Caffeinating… · 12.3k tok · 8.1k max". Both start at 0 and climb.
|
|
1198
1277
|
const tokLabel =
|
|
1199
1278
|
dim(` · ${fmtTokens(latestCodeTokens)} tok`) +
|
|
1200
1279
|
orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
)
|
|
1280
|
+
// Task #96: the thought line is "clickable" only when there's a fuller thought to reveal
|
|
1281
|
+
// (a live command / bare "Thinking" has nothing to expand). When clicked, unfold it below
|
|
1282
|
+
// as up to 4 wrapped GREY lines (dim). The whole status block rides the viewport bottom as
|
|
1283
|
+
// output scrolls, so line 1 (the thought) sits at row `rows - drawn + 1` — if the mouse is
|
|
1284
|
+
// hovering THAT row, render the label as an underlined bright-green "link".
|
|
1285
|
+
const clickable = !!(fullLiveThought && !runningCmd);
|
|
1286
|
+
const expLines = thoughtExpanded && clickable ? wrapThought(fullLiveThought, max, 4) : [];
|
|
1287
|
+
const drawn = 2 + expLines.length;
|
|
1288
|
+
const rows = process.stdout.rows || 24;
|
|
1289
|
+
const hovering = clickable && hoverRow === rows - drawn + 1;
|
|
1290
|
+
const label = `${fit(primary)}… (${secs}s)`;
|
|
1291
|
+
clearStatus();
|
|
1292
|
+
// Line 1: green ● + green phase/seconds (matches the web's green streaming label), or the
|
|
1293
|
+
// underlined bright-green hover style when the mouse is over a clickable thought.
|
|
1294
|
+
// Line 2 (indented): the SPINNER + the playful word together, both in the cycling color.
|
|
1295
|
+
// Task #83/#84: after the word, the running agent-code token count (dim) + per-workspace
|
|
1296
|
+
// PEAK single-request count (orange), e.g. " ⠙ Caffeinating… · 12.3k tok · 8.1k max".
|
|
1297
|
+
let out =
|
|
1298
|
+
`${green(BULLET)} ${hovering ? hoverThought(label) : green(label)}` +
|
|
1299
|
+
"\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
|
|
1300
|
+
for (const wl of expLines) out += "\n " + dim(wl);
|
|
1301
|
+
process.stdout.write(out);
|
|
1302
|
+
statusLines = drawn;
|
|
1205
1303
|
statusShown = true;
|
|
1206
1304
|
};
|
|
1207
1305
|
// 120ms so the spinner animates smoothly (the frame math above uses the wall clock, so
|
|
1208
1306
|
// seconds still tick once/sec). Only redraws when the model has gone quiet >QUIET_MS.
|
|
1209
1307
|
const ticker = setInterval(tick, 120);
|
|
1210
1308
|
|
|
1309
|
+
// --- Hover + click on the thinking line (task #96) -------------------------------------
|
|
1310
|
+
// HOVER: motion events keep `hoverRow`; the tick highlights the thought line (underlined
|
|
1311
|
+
// bright-green "link") whenever the mouse is on its row. CLICK: a left-click on the status
|
|
1312
|
+
// block toggles the fuller thought (3-4 grey lines). The block sits at the bottom of the
|
|
1313
|
+
// viewport (new output keeps scrolling it there), so we hit-test rows against the bottom
|
|
1314
|
+
// `statusLines` rows. Mouse reporting is on only for the turn and always turned back off in
|
|
1315
|
+
// the finally + on process exit, so the user's shell is never left in mouse-capture mode.
|
|
1316
|
+
const onStatusClick = (row) => {
|
|
1317
|
+
if (!statusShown) return;
|
|
1318
|
+
const rows = process.stdout.rows || 24;
|
|
1319
|
+
const top = rows - statusLines + 1; // 1-based viewport row of the first status line
|
|
1320
|
+
if (row < top || row > rows) return; // click landed outside the status block
|
|
1321
|
+
// Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
|
|
1322
|
+
// so we don't flicker an empty expand.
|
|
1323
|
+
if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
|
|
1324
|
+
thoughtExpanded = !thoughtExpanded;
|
|
1325
|
+
clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
|
|
1326
|
+
};
|
|
1327
|
+
const onMouseData = (chunk) => {
|
|
1328
|
+
if (!following) return;
|
|
1329
|
+
const s = chunk.toString("latin1");
|
|
1330
|
+
if (s.indexOf("\x1b[<") === -1) return; // fast reject: no SGR mouse report in this chunk
|
|
1331
|
+
MOUSE_RE.lastIndex = 0;
|
|
1332
|
+
let m;
|
|
1333
|
+
while ((m = MOUSE_RE.exec(s))) {
|
|
1334
|
+
const btn = parseInt(m[1], 10);
|
|
1335
|
+
const row = parseInt(m[3], 10);
|
|
1336
|
+
const isPress = m[4] === "M";
|
|
1337
|
+
if (btn & 32) { hoverRow = row; continue; } // MOTION (hover/drag) → track for the highlight
|
|
1338
|
+
if (isPress && (btn & 0b11) === 0 && (btn & 64) === 0) onStatusClick(row); // left click → expand
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
if (process.stdin.isTTY) process.stdin.on("data", onMouseData);
|
|
1342
|
+
enableMouse();
|
|
1343
|
+
|
|
1211
1344
|
// Called right before any real (bullet/edit-card/answer) content prints: drop the
|
|
1212
1345
|
// status lines so content lands cleanly, and end the current phase (so the next phase's
|
|
1213
1346
|
// seconds count fresh, and any running command is considered finished).
|
|
@@ -1219,6 +1352,8 @@ async function runAgentTurn(history) {
|
|
|
1219
1352
|
// next step's blinking line starts from "Thinking", not last step's stale clause.
|
|
1220
1353
|
liveThought = "";
|
|
1221
1354
|
fenceThought = "";
|
|
1355
|
+
fullLiveThought = ""; // #96: the fuller thought is spent too…
|
|
1356
|
+
thoughtExpanded = false; // …and each new thinking phase starts collapsed.
|
|
1222
1357
|
lastContentAt = Date.now();
|
|
1223
1358
|
};
|
|
1224
1359
|
|
|
@@ -1277,6 +1412,7 @@ async function runAgentTurn(history) {
|
|
|
1277
1412
|
fenceThought += line + "\n";
|
|
1278
1413
|
const th = normalizeThought(fenceThought);
|
|
1279
1414
|
if (th) liveThought = th;
|
|
1415
|
+
fullLiveThought = thoughtFull(fenceThought); // #96: keep the fuller thought for expand
|
|
1280
1416
|
}
|
|
1281
1417
|
continue;
|
|
1282
1418
|
}
|
|
@@ -1416,6 +1552,7 @@ async function runAgentTurn(history) {
|
|
|
1416
1552
|
if (inStreamFence && fenceThought.length < 400) {
|
|
1417
1553
|
const th = normalizeThought(fenceThought + carry);
|
|
1418
1554
|
if (th) liveThought = th;
|
|
1555
|
+
fullLiveThought = thoughtFull(fenceThought + carry); // #96: fuller thought for expand
|
|
1419
1556
|
}
|
|
1420
1557
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
1421
1558
|
// a newline), so tokens are actively flowing — keep the ticker asleep.
|
|
@@ -1574,6 +1711,11 @@ async function runAgentTurn(history) {
|
|
|
1574
1711
|
} finally {
|
|
1575
1712
|
clearInterval(ticker);
|
|
1576
1713
|
clearStatus();
|
|
1714
|
+
// Task #96: stop mouse-reporting the moment the turn ends (also on cancel/error — this
|
|
1715
|
+
// finally always runs), and detach the click parser, so the user's shell selection is
|
|
1716
|
+
// never captured between turns.
|
|
1717
|
+
disableMouse();
|
|
1718
|
+
if (process.stdin.isTTY) process.stdin.removeListener("data", onMouseData);
|
|
1577
1719
|
following = false;
|
|
1578
1720
|
currentJobId = null;
|
|
1579
1721
|
// Drop anything typed (but not submitted) during the turn — echo was muted, so it's
|
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.263",
|
|
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",
|