@tiens.nguyen/gonext-local-worker 1.0.239 → 1.0.241

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.
@@ -1830,6 +1830,32 @@ async function runAgentChatJob(job) {
1830
1830
  (process.env.GONEXT_PROBE_PYTHON ?? process.env.GONEXT_MLX_LM_PYTHON ?? "")
1831
1831
  .trim() || "python3";
1832
1832
  const scriptPath = join(WORKER_DIR, "gonext_agent_chat.py");
1833
+
1834
+ // Workspace scope depends on WHERE the job came from (task #75). The `gonext`
1835
+ // terminal always sends its launch cwd as activeWorkspace; the web app never does —
1836
+ // that's the discriminator.
1837
+ // • Terminal: the real `gonext-local-worker workspace add` registry + that cwd.
1838
+ // • Web agent chat: ONE isolated temp scratch folder — NOT the Mac's registered
1839
+ // project folders. Inheriting them leaked their paths into the web prompt and
1840
+ // let terminal state drive web routing. run_command stays off (not a code repo).
1841
+ const terminalActiveWs = String(payload?.activeWorkspace ?? "").trim();
1842
+ let jobWorkspaces;
1843
+ let jobActiveWorkspace;
1844
+ if (terminalActiveWs) {
1845
+ jobWorkspaces = await readFile(join(homedir(), ".gonext", "workspaces.json"), "utf8")
1846
+ .then((raw) => {
1847
+ const parsed = JSON.parse(raw);
1848
+ return Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
1849
+ })
1850
+ .catch(() => []);
1851
+ jobActiveWorkspace = terminalActiveWs;
1852
+ } else {
1853
+ const webWork = join(homedir(), ".gonext", "web-agent-work");
1854
+ await mkdir(webWork, { recursive: true }).catch(() => {});
1855
+ jobWorkspaces = [{ name: "web", path: webWork, allowRun: false }];
1856
+ jobActiveWorkspace = webWork;
1857
+ }
1858
+
1833
1859
  const input = JSON.stringify({
1834
1860
  messages: payload?.messages ?? [],
1835
1861
  agentBaseURL: payload?.agentBaseURL ?? "",
@@ -1889,17 +1915,12 @@ async function runAgentChatJob(job) {
1889
1915
  ragEmbedModel: payload?.ragEmbedModel ?? "",
1890
1916
  ragEmbedUrl: payload?.ragEmbedUrl ?? "",
1891
1917
  ragTopK: payload?.ragTopK ?? 6,
1892
- // Workspaces the agent may read/edit/test code in read fresh each job so a
1893
- // `gonext-local-worker workspace add` applies without restarting the worker.
1894
- workspaces: await readFile(join(homedir(), ".gonext", "workspaces.json"), "utf8")
1895
- .then((raw) => {
1896
- const parsed = JSON.parse(raw);
1897
- return Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
1898
- })
1899
- .catch(() => []),
1900
- // The `gonext` terminal REPL's launch folder — download_file/unzip_file output
1901
- // lands here (must be a registered workspace; the python side re-validates).
1902
- activeWorkspace: payload?.activeWorkspace ?? "",
1918
+ // Workspaces the agent may read/edit/test code in (see the terminal-vs-web split
1919
+ // computed above): the real registry for the terminal, a lone temp folder for web.
1920
+ workspaces: jobWorkspaces,
1921
+ // download_file/unzip_file output lands here (a registered workspace; python
1922
+ // re-validates). Terminal = its launch cwd; web = the temp scratch folder.
1923
+ activeWorkspace: jobActiveWorkspace,
1903
1924
  });
1904
1925
  // 30 min max for an agent run: multi-step ReAct on a 14B/31B with a cold prompt
1905
1926
  // cache — or a remote Ollama box on slow GPU cold-loading a big model — can run
package/gonext-repl.mjs CHANGED
@@ -106,8 +106,22 @@ const THINKING_WORDS = (() => {
106
106
  })();
107
107
  const pickWord = () =>
108
108
  THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] || "Thinking";
109
- // Rotating half-circle = an in-progress ("blinking") bullet for the step being worked on.
110
- const SPINNER = ["◐", "◓", "◑", "◒"];
109
+ // In-progress ("blinking") spinner for the step being worked on. 30 styles that rotate
110
+ // ~every 8s so a long CPU-bound wait stays interesting — mirrors the web agent's set
111
+ // (keep in sync with the .spin-N @keyframes in web/src/App.css). Single-width glyphs so
112
+ // they never shift the text after them.
113
+ const SPINNERS = [
114
+ "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", "│╱─╲", "▁▃▄▅▆▇▆▅▄▃", "←↖↑↗→↘↓↙", "▏▎▍▌▋▊▉▊▋▌▍▎",
115
+ "◴◷◶◵", "◐◓◑◒", "⣾⣽⣻⢿⡿⣟⣯⣷", "▖▘▝▗", "✶✸✹✺✹✷",
116
+ "⠁⠂⠄⡀⢀⠠⠐⠈", "dqpb", "◢◣◤◥", "✦✧", "◜◝◞◟",
117
+ "▌▀▐▄", "◰◳◲◱", "┤┘┴└├┌┬┐", "▘▝▗▖", "▸▹",
118
+ "⢄⢂⢁⡁⡈⡐⡠", "▙▛▜▟", "▂▃▄▅▆▇█▇▆▅▄▃", "⣿⣷⣯⣟⡿⢿⣻⣽", "⎺⎻⎼⎽",
119
+ "•◦", "░▒▓█▓▒", "◇◈◆", "·✢✳✽", "◠◡",
120
+ ].map((s) => [...s]);
121
+ // The playful word's color cycles ~every 2.5s (xterm-256 palette matching the web accents:
122
+ // green, cyan, blue, indigo, pink, amber).
123
+ const WAIT_COLORS_256 = [120, 87, 111, 147, 218, 221];
124
+ const color256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
111
125
  const BULLET = "●"; // a completed action
112
126
 
113
127
  // ---------- flags ----------
@@ -1014,6 +1028,7 @@ async function runAgentTurn(history) {
1014
1028
  let thinking = false;
1015
1029
  let thinkingSince = 0;
1016
1030
  let thinkWord = pickWord(); // playful word shown UNDER the in-progress line (flavor)
1031
+ let lastWordBucket = -1; // 12s window index → rotate the word once per window (task: 30-spinner)
1017
1032
  let almostDone = false; // set from the worker's "…almost completed thinking…" heartbeat
1018
1033
  let runningCmd = null; // the command currently executing (from a "Running → …" event)
1019
1034
  // The model's CURRENT Thought (task #64): while it streams its reasoning inside the
@@ -1042,10 +1057,20 @@ async function runAgentTurn(history) {
1042
1057
  // silent once any of THIS line has already been shown (see carryFlushedLen).
1043
1058
  if (plainReplyFlow && carryFlushedLen > 0) return;
1044
1059
  // Count from when this phase went quiet, so the seconds reflect the current wait/run.
1045
- if (!thinking) { thinking = true; thinkingSince = lastContentAt; thinkWord = pickWord(); }
1060
+ if (!thinking) {
1061
+ thinking = true; thinkingSince = lastContentAt; thinkWord = pickWord(); lastWordBucket = 0;
1062
+ }
1063
+ const now = Date.now();
1046
1064
  const secs = thinkSecs();
1047
- if (secs % 12 === 0) thinkWord = pickWord(); // rotate the word on long waits
1048
- const glyph = SPINNER[secs % SPINNER.length]; // rotates → reads as "in progress"
1065
+ // Rotate the playful word once per 12s window (guarded so the fast ticker doesn't
1066
+ // re-roll it many times within the same second).
1067
+ const wordBucket = Math.floor((now - thinkingSince) / 12000);
1068
+ if (wordBucket !== lastWordBucket) { lastWordBucket = wordBucket; thinkWord = pickWord(); }
1069
+ // Spinner: STYLE rotates ~every 8s, FRAME advances ~every 120ms, color cycles ~every
1070
+ // 2.5s — all off the wall clock so they keep moving smoothly across phases.
1071
+ const frames = SPINNERS[Math.floor(now / 8000) % SPINNERS.length];
1072
+ const glyph = frames[Math.floor(now / 120) % frames.length];
1073
+ const wc = WAIT_COLORS_256[Math.floor(now / 2500) % WAIT_COLORS_256.length];
1049
1074
  // Line 1 = WHAT is happening now: the running command, else the phase (Thinking /
1050
1075
  // Almost done). Line 2 = the playful word (flavor). Truncate so neither wraps.
1051
1076
  // Priority: a live command > the model's current Thought clause (task #64, so a
@@ -1057,12 +1082,17 @@ async function runAgentTurn(history) {
1057
1082
  const max = Math.max(24, (process.stdout.columns || 80) - 14);
1058
1083
  const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
1059
1084
  clearStatus();
1085
+ // Line 1: cycling-color spinner + yellow phase/seconds. Line 2: the word in the same
1086
+ // cycling color (the web colors the spinner + word together; primary stays the theme).
1060
1087
  process.stdout.write(
1061
- yellow(`${glyph} ${fit(primary)}… (${secs}s)`) + "\n" + dim(` ${thinkWord}…`)
1088
+ `${color256(wc, glyph)} ${yellow(`${fit(primary)}… (${secs}s)`)}` +
1089
+ "\n" + color256(wc, ` ${thinkWord}…`)
1062
1090
  );
1063
1091
  statusShown = true;
1064
1092
  };
1065
- const ticker = setInterval(tick, 1000);
1093
+ // 120ms so the spinner animates smoothly (the frame math above uses the wall clock, so
1094
+ // seconds still tick once/sec). Only redraws when the model has gone quiet >QUIET_MS.
1095
+ const ticker = setInterval(tick, 120);
1066
1096
 
1067
1097
  // Called right before any real (bullet/edit-card/answer) content prints: drop the
1068
1098
  // status lines so content lands cleanly, and end the current phase (so the next phase's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.239",
3
+ "version": "1.0.241",
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",