@tiens.nguyen/gonext-local-worker 1.0.261 → 1.0.262
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 +139 -8
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -383,6 +383,30 @@ rl.on("close", () => {
|
|
|
383
383
|
w(null);
|
|
384
384
|
}
|
|
385
385
|
});
|
|
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";
|
|
393
|
+
let _mouseEnabled = false;
|
|
394
|
+
const enableMouse = () => {
|
|
395
|
+
if (_mouseEnabled || !process.stdin.isTTY) return;
|
|
396
|
+
process.stdout.write(MOUSE_ON);
|
|
397
|
+
_mouseEnabled = true;
|
|
398
|
+
};
|
|
399
|
+
const disableMouse = () => {
|
|
400
|
+
if (!_mouseEnabled) return;
|
|
401
|
+
process.stdout.write(MOUSE_OFF);
|
|
402
|
+
_mouseEnabled = false;
|
|
403
|
+
};
|
|
404
|
+
// Safety net: never leave the user's terminal in mouse-reporting mode (would break their
|
|
405
|
+
// shell's selection) if we exit ungracefully.
|
|
406
|
+
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.
|
|
409
|
+
const MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
386
410
|
const nextLine = () =>
|
|
387
411
|
_lineQueue.length > 0
|
|
388
412
|
? Promise.resolve(_lineQueue.shift())
|
|
@@ -1047,6 +1071,52 @@ const normalizeThought = (buf) => {
|
|
|
1047
1071
|
return "";
|
|
1048
1072
|
};
|
|
1049
1073
|
|
|
1074
|
+
// The FULLER thought (task #96): the same Thought prose normalizeThought summarizes, but
|
|
1075
|
+
// NOT clipped to one clause/100 chars — collapsed to a single spaced string and capped so
|
|
1076
|
+
// the expanded (mouse-click) view has a few lines to show. Returns "" when the live line is
|
|
1077
|
+
// only a tool label (nothing extra to expand). Bounded by fenceThought's own 400-char cap.
|
|
1078
|
+
const thoughtFull = (buf) => {
|
|
1079
|
+
const t = String(buf || "");
|
|
1080
|
+
const ci = t.search(/<code[\s>]/i);
|
|
1081
|
+
const head = (ci >= 0 ? t.slice(0, ci) : t)
|
|
1082
|
+
.replace(/^\s*Thought:\s*/i, "")
|
|
1083
|
+
.replace(/\s+/g, " ")
|
|
1084
|
+
.trim();
|
|
1085
|
+
// Only worth expanding when there's a genuine prose clause (same gate as normalizeThought
|
|
1086
|
+
// path 1) — a bare fragment or pure code has nothing meaningful to unfold.
|
|
1087
|
+
const first = head.split(/\s+/).length;
|
|
1088
|
+
if (head.length < 6 || first < 2 || _thoughtLooksCodey(head)) return "";
|
|
1089
|
+
return head.slice(0, 400);
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// Word-wrap a plain (ANSI-free) string into at most `maxLines` lines of width ≤ `width`,
|
|
1093
|
+
// appending an ellipsis to the last line when text was dropped (task #96 expand view).
|
|
1094
|
+
const wrapThought = (s, width, maxLines) => {
|
|
1095
|
+
const words = String(s || "").split(/\s+/).filter(Boolean);
|
|
1096
|
+
const lines = [];
|
|
1097
|
+
let cur = "";
|
|
1098
|
+
let truncated = false;
|
|
1099
|
+
for (let i = 0; i < words.length; i++) {
|
|
1100
|
+
const w = words[i];
|
|
1101
|
+
const next = cur ? cur + " " + w : w;
|
|
1102
|
+
if (next.length <= width) {
|
|
1103
|
+
cur = next;
|
|
1104
|
+
} else {
|
|
1105
|
+
if (cur) lines.push(cur);
|
|
1106
|
+
cur = w;
|
|
1107
|
+
if (lines.length >= maxLines) { truncated = true; cur = ""; break; }
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (cur && lines.length < maxLines) lines.push(cur);
|
|
1111
|
+
else if (cur) truncated = true;
|
|
1112
|
+
if (truncated && lines.length) {
|
|
1113
|
+
const last = lines[maxLines - 1] ?? lines[lines.length - 1];
|
|
1114
|
+
const room = Math.max(1, width - 1);
|
|
1115
|
+
lines[lines.length - 1] = (last.length > room ? last.slice(0, room) : last).replace(/\s+$/, "") + "…";
|
|
1116
|
+
}
|
|
1117
|
+
return lines;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1050
1120
|
async function runAgentTurn(history) {
|
|
1051
1121
|
const res = await fetch(`${apiBase}/api/worker/agent-ask`, {
|
|
1052
1122
|
method: "POST",
|
|
@@ -1090,6 +1160,7 @@ async function runAgentTurn(history) {
|
|
|
1090
1160
|
let lastApprovalId = null; // the last command-approval request we've already answered
|
|
1091
1161
|
let carry = ""; // trailing partial content line held until its newline arrives
|
|
1092
1162
|
let statusShown = false; // a transient status line is currently on screen
|
|
1163
|
+
let statusLines = 2; // how many rows the status block currently occupies (grows when expanded)
|
|
1093
1164
|
let warnedPending = false;
|
|
1094
1165
|
// Transient poll failures (a network blip or a 5xx like the API's momentary "Worker
|
|
1095
1166
|
// key lookup failed." — a cold Mongo connection, NOT a bad key) must NOT kill a
|
|
@@ -1138,14 +1209,21 @@ async function runAgentTurn(history) {
|
|
|
1138
1209
|
// random word. `fenceThought` accumulates the raw in-fence text for the current step.
|
|
1139
1210
|
let liveThought = "";
|
|
1140
1211
|
let fenceThought = "";
|
|
1212
|
+
// Task #96: the FULLER version of the current thought (for the click-to-expand view) and
|
|
1213
|
+
// whether the user has clicked to expand it. Both reset when the step's Thought changes.
|
|
1214
|
+
let fullLiveThought = "";
|
|
1215
|
+
let thoughtExpanded = false;
|
|
1141
1216
|
const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
|
|
1142
1217
|
|
|
1143
|
-
// The status is TWO in-place lines — the in-progress action, then the playful
|
|
1144
|
-
// under it
|
|
1145
|
-
//
|
|
1218
|
+
// The status is normally TWO in-place lines — the in-progress action, then the playful
|
|
1219
|
+
// word under it — but grows by a few grey lines when the thought is expanded (task #96).
|
|
1220
|
+
// Cursor sits at the end of the LAST line, so clearing = wipe it, then move-up-and-wipe
|
|
1221
|
+
// for each line above, leaving the cursor back at the status's origin for a redraw.
|
|
1146
1222
|
const clearStatus = () => {
|
|
1147
1223
|
if (!statusShown) return;
|
|
1148
|
-
|
|
1224
|
+
let seq = "\r\x1b[K";
|
|
1225
|
+
for (let i = 1; i < statusLines; i++) seq += "\x1b[1A\r\x1b[K";
|
|
1226
|
+
process.stdout.write(seq);
|
|
1149
1227
|
statusShown = false;
|
|
1150
1228
|
};
|
|
1151
1229
|
|
|
@@ -1166,7 +1244,9 @@ async function runAgentTurn(history) {
|
|
|
1166
1244
|
// while (e.g. a 150s+ silent prompt-eval on a huge context), the last clause is no longer
|
|
1167
1245
|
// "what it's doing now" — drop it so the line falls back to the playful word / "Thinking…"
|
|
1168
1246
|
// instead of freezing on an old thought while the timer climbs ("string… (777s)", #95 A).
|
|
1169
|
-
if (liveThought && now - lastContentAt > STALE_THOUGHT_MS)
|
|
1247
|
+
if (liveThought && now - lastContentAt > STALE_THOUGHT_MS) {
|
|
1248
|
+
liveThought = ""; fullLiveThought = ""; thoughtExpanded = false; // #96: nothing to expand once stale
|
|
1249
|
+
}
|
|
1170
1250
|
const secs = thinkSecs();
|
|
1171
1251
|
// Rotate the playful word once per 12s window (guarded so the fast ticker doesn't
|
|
1172
1252
|
// re-roll it many times within the same second).
|
|
@@ -1198,16 +1278,58 @@ async function runAgentTurn(history) {
|
|
|
1198
1278
|
const tokLabel =
|
|
1199
1279
|
dim(` · ${fmtTokens(latestCodeTokens)} tok`) +
|
|
1200
1280
|
orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`);
|
|
1201
|
-
|
|
1281
|
+
let out =
|
|
1202
1282
|
`${green(BULLET)} ${green(`${fit(primary)}… (${secs}s)`)}` +
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
+
}
|
|
1292
|
+
process.stdout.write(out);
|
|
1293
|
+
statusLines = drawn;
|
|
1205
1294
|
statusShown = true;
|
|
1206
1295
|
};
|
|
1207
1296
|
// 120ms so the spinner animates smoothly (the frame math above uses the wall clock, so
|
|
1208
1297
|
// seconds still tick once/sec). Only redraws when the model has gone quiet >QUIET_MS.
|
|
1209
1298
|
const ticker = setInterval(tick, 120);
|
|
1210
1299
|
|
|
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.
|
|
1306
|
+
const onStatusClick = (row) => {
|
|
1307
|
+
if (!statusShown) return;
|
|
1308
|
+
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
|
|
1311
|
+
// Only meaningful when there IS a fuller thought to reveal; otherwise ignore the click
|
|
1312
|
+
// so we don't flicker an empty expand.
|
|
1313
|
+
if (!thoughtExpanded && !(fullLiveThought && !runningCmd)) return;
|
|
1314
|
+
thoughtExpanded = !thoughtExpanded;
|
|
1315
|
+
clearStatus(); // next tick (≤120ms) redraws at the new size; keeps cursor math correct
|
|
1316
|
+
};
|
|
1317
|
+
const onMouseData = (chunk) => {
|
|
1318
|
+
if (!following) return;
|
|
1319
|
+
const s = chunk.toString("latin1");
|
|
1320
|
+
if (s.indexOf("\x1b[<") === -1) return; // fast reject: no SGR mouse report in this chunk
|
|
1321
|
+
MOUSE_RE.lastIndex = 0;
|
|
1322
|
+
let m;
|
|
1323
|
+
while ((m = MOUSE_RE.exec(s))) {
|
|
1324
|
+
const btn = parseInt(m[1], 10);
|
|
1325
|
+
const row = parseInt(m[3], 10);
|
|
1326
|
+
const isPress = m[4] === "M";
|
|
1327
|
+
if (isPress && (btn & 0b11) === 0 && (btn & 64) === 0) onStatusClick(row); // left press
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
if (process.stdin.isTTY) process.stdin.on("data", onMouseData);
|
|
1331
|
+
enableMouse();
|
|
1332
|
+
|
|
1211
1333
|
// Called right before any real (bullet/edit-card/answer) content prints: drop the
|
|
1212
1334
|
// status lines so content lands cleanly, and end the current phase (so the next phase's
|
|
1213
1335
|
// seconds count fresh, and any running command is considered finished).
|
|
@@ -1219,6 +1341,8 @@ async function runAgentTurn(history) {
|
|
|
1219
1341
|
// next step's blinking line starts from "Thinking", not last step's stale clause.
|
|
1220
1342
|
liveThought = "";
|
|
1221
1343
|
fenceThought = "";
|
|
1344
|
+
fullLiveThought = ""; // #96: the fuller thought is spent too…
|
|
1345
|
+
thoughtExpanded = false; // …and each new thinking phase starts collapsed.
|
|
1222
1346
|
lastContentAt = Date.now();
|
|
1223
1347
|
};
|
|
1224
1348
|
|
|
@@ -1277,6 +1401,7 @@ async function runAgentTurn(history) {
|
|
|
1277
1401
|
fenceThought += line + "\n";
|
|
1278
1402
|
const th = normalizeThought(fenceThought);
|
|
1279
1403
|
if (th) liveThought = th;
|
|
1404
|
+
fullLiveThought = thoughtFull(fenceThought); // #96: keep the fuller thought for expand
|
|
1280
1405
|
}
|
|
1281
1406
|
continue;
|
|
1282
1407
|
}
|
|
@@ -1416,6 +1541,7 @@ async function runAgentTurn(history) {
|
|
|
1416
1541
|
if (inStreamFence && fenceThought.length < 400) {
|
|
1417
1542
|
const th = normalizeThought(fenceThought + carry);
|
|
1418
1543
|
if (th) liveThought = th;
|
|
1544
|
+
fullLiveThought = thoughtFull(fenceThought + carry); // #96: fuller thought for expand
|
|
1419
1545
|
}
|
|
1420
1546
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
1421
1547
|
// a newline), so tokens are actively flowing — keep the ticker asleep.
|
|
@@ -1574,6 +1700,11 @@ async function runAgentTurn(history) {
|
|
|
1574
1700
|
} finally {
|
|
1575
1701
|
clearInterval(ticker);
|
|
1576
1702
|
clearStatus();
|
|
1703
|
+
// Task #96: stop mouse-reporting the moment the turn ends (also on cancel/error — this
|
|
1704
|
+
// finally always runs), and detach the click parser, so the user's shell selection is
|
|
1705
|
+
// never captured between turns.
|
|
1706
|
+
disableMouse();
|
|
1707
|
+
if (process.stdin.isTTY) process.stdin.removeListener("data", onMouseData);
|
|
1577
1708
|
following = false;
|
|
1578
1709
|
currentJobId = null;
|
|
1579
1710
|
// 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.262",
|
|
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",
|