symbiot 0.54.0 → 0.54.2
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/CHANGELOG.md +8 -0
- package/agents.mjs +7 -6
- package/lanes.mjs +2 -2
- package/package.json +1 -1
- package/server.mjs +2 -1
- package/tasks.mjs +2 -1
- package/ui.mjs +20 -8
- package/work.mjs +26 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
|
|
4
4
|
|
|
5
|
+
## 0.54.2
|
|
6
|
+
|
|
7
|
+
- **Lighter without a graphics card.** When the liquid is drawn by the CPU (no GPU, or a headless browser like the ones agents check Symbiot's screens in), it used 6 cores or more. Now it draws at a third of the resolution, without the breathing, and only when something actually moved, at most 20 times a second: about a third of the CPU. It does the same when your system asks for less motion. With a graphics card, Home also draws half as often at rest and a sixth as often behind an open panel, where it barely shows.
|
|
8
|
+
|
|
9
|
+
## 0.54.1
|
|
10
|
+
|
|
11
|
+
- **No more lag from a big agent log.** An agent's log keeps every run and everything it reads, screenshots included; one reached 144 MB, and Symbiot reread all of it on almost every refresh, so the app stalled for 5–28 seconds at a time and ate a core. Now it reads only the newest run, from the end of the file, and keeps it until the file changes. A log past 8 MB starts afresh at the next run, with the old one kept as `agent.log.old`.
|
|
12
|
+
|
|
5
13
|
## 0.54.0
|
|
6
14
|
|
|
7
15
|
- **Show Symbiot a screenshot.** Paste one into "Talk to Symbiot" (Ctrl+V) or drop it on the bar: it shows as a thumbnail you can remove, goes with what you say, and the AI you connected sees it (Claude, OpenAI, Gemini, or a local model that reads images). If Symbiot hands the work to an agent, the agent gets the screenshot too. Up to 4 at a time; they're kept a week, readable by you only.
|
package/agents.mjs
CHANGED
|
@@ -5,10 +5,10 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join, dirname, resolve, relative } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync, renameSync } from "node:fs";
|
|
9
9
|
import { randomBytes, createHash } from "node:crypto";
|
|
10
10
|
import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
|
|
11
|
-
import { parseRun, lastRunText } from "./work.mjs";
|
|
11
|
+
import { parseRun, lastRunText, readRunLog } from "./work.mjs";
|
|
12
12
|
import { parseFacts } from "./handover.mjs";
|
|
13
13
|
import { scanRoots, repoPathMap } from "./scan.mjs";
|
|
14
14
|
import { knowledgeFolders } from "./knowledge.mjs";
|
|
@@ -273,7 +273,7 @@ function earlierRuns() {
|
|
|
273
273
|
const busy = runningHandoff(r.path), ask = agentQuestions(r.path, r.name), wait = waitingFor(r.path), facts = busy ? [] : factsOf(r.path);
|
|
274
274
|
if (!busy && !ask.questions.length && !wait && !facts.length) continue;
|
|
275
275
|
const log = join(r.path, ".symbiot", "agent.log"), startedAt = (busy && busy.startedAt) || Number(r.startedAt) || Date.now();
|
|
276
|
-
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path,
|
|
276
|
+
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path, readRunLog(log))); if (!busy) end = statSync(log).mtimeMs; } catch {}
|
|
277
277
|
out.push({ id: "earlier-" + digest(r.path).slice(0, 8), name: String(r.name || r.path.split("/").pop()), path: r.path, status: busy ? "running" : "done", earlier: true, elapsed: Math.max(0, end - startedAt), exitCode: null, tail, work, progress, changed: agentChanges(r.path, startedAt), ask, held: heldTasks(r.path), waiting: wait, remember: facts.length ? facts : null, fromHeld: false });
|
|
278
278
|
}
|
|
279
279
|
return out;
|
|
@@ -576,10 +576,12 @@ function _findOrcaCli() {
|
|
|
576
576
|
// ---- background jobs --------------------------------------------------------
|
|
577
577
|
// Run a shell command as a tracked, logged background job that shows up live in
|
|
578
578
|
// the Agents tab. Shared by the agent handoff and the local-model setup.
|
|
579
|
+
const LOG_KEEP = 8 * 1024 * 1024;
|
|
579
580
|
function track(name, cmd, cwd, onExit, env) {
|
|
580
581
|
try {
|
|
581
582
|
const dir = join(cwd, ".symbiot"); mkdirSync(dir, { recursive: true });
|
|
582
583
|
const logp = join(dir, "agent.log");
|
|
584
|
+
try { if (statSync(logp).size > LOG_KEEP) renameSync(logp, logp + ".old"); } catch {} // past 8 MB: kept as agent.log.old, and a fresh one starts
|
|
583
585
|
let fd = "ignore"; try { fd = openSync(logp, "a"); writeSync(fd, `\n=== ${name} ${new Date().toISOString()} ===\n$ ${cmd}\n`); } catch {}
|
|
584
586
|
const entry = { id: randomBytes(4).toString("hex"), name, path: cwd, log: logp, startedAt: Date.now(), status: "running", exitCode: null, endedAt: null };
|
|
585
587
|
const child = spawn(cmd, { shell: true, cwd, detached: true, stdio: ["ignore", fd === "ignore" ? "ignore" : fd, fd === "ignore" ? "ignore" : fd], ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
@@ -623,7 +625,7 @@ const QUESTIONS_MAX = 5;
|
|
|
623
625
|
// the next moves up. The rest are a click away.
|
|
624
626
|
const OPTIONS_SHOWN = 2, IDEAS_SHOWN = 2;
|
|
625
627
|
const qKey = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
626
|
-
const readSymbiot = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
628
|
+
const readSymbiot = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
627
629
|
// "## Questions" → "### question", context lines, "- option" bullets;
|
|
628
630
|
// "## Suggestions" (or Ideas / Follow-ups) → "- idea" bullets. Forgiving: a bare
|
|
629
631
|
// bullet under Questions is a question with no options.
|
|
@@ -880,8 +882,7 @@ function workOf(path, log) {
|
|
|
880
882
|
function agentsList() {
|
|
881
883
|
const seen = new Set();
|
|
882
884
|
return HANDOFFS.map((e) => {
|
|
883
|
-
|
|
884
|
-
const { tail, work, progress } = workOf(e.path, log);
|
|
885
|
+
const { tail, work, progress } = workOf(e.path, readRunLog(e.log));
|
|
885
886
|
const first = !seen.has(e.path); seen.add(e.path);
|
|
886
887
|
const facts = first && e.status !== "running" ? factsOf(e.path) : [];
|
|
887
888
|
return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, work, progress, changed: agentChanges(e.path, e.startedAt), ask: first ? agentQuestions(e.path, e.name) : null, held: first ? heldTasks(e.path) : null, waiting: first && e.status !== "running" ? waitingFor(e.path) : null, remember: facts.length ? facts : null, fromHeld: !!e.fromHeld };
|
package/lanes.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { runHandoff, runningHandoff, waitingFor, agentQuestions } from "./agents
|
|
|
23
23
|
import { addTask, pushTasks } from "./tasks.mjs";
|
|
24
24
|
import { actNow } from "./mind.mjs";
|
|
25
25
|
import { repoPathMap } from "./scan.mjs";
|
|
26
|
-
import { parseRun, lastRunText } from "./work.mjs";
|
|
26
|
+
import { parseRun, lastRunText, readRunLog } from "./work.mjs";
|
|
27
27
|
import { OPS, parseHandoffs } from "./handover.mjs";
|
|
28
28
|
|
|
29
29
|
const LEDGER = join(CONFIG_DIR, "lanes.json");
|
|
@@ -33,7 +33,7 @@ const MAX_KEEP = 200, MAX_CHAIN = 4, TAIL = 4000;
|
|
|
33
33
|
function loadLedger() { try { const d = JSON.parse(readFileSync(LEDGER, "utf8")); return { handoffs: Array.isArray(d.handoffs) ? d.handoffs : [] }; } catch { return { handoffs: [] }; } }
|
|
34
34
|
// It can name accounts and what's in them: yours only (0600).
|
|
35
35
|
function saveLedger(d) { try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(LEDGER, JSON.stringify({ handoffs: d.handoffs.slice(-MAX_KEEP) }, null, 1), { mode: 0o600 }); try { chmodSync(LEDGER, 0o600); } catch {} return true; } catch { return false; } }
|
|
36
|
-
const readSym = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
36
|
+
const readSym = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
37
37
|
const firstLine = (t) => String(t || "").split("\n").find((l) => l.trim()) || "";
|
|
38
38
|
|
|
39
39
|
// A folder's lane: the repo it is, or ops for a run of its own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.2",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { gunzipSync } from "node:zlib";
|
|
10
|
+
import { readRunLog } from "./work.mjs";
|
|
10
11
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
11
12
|
import { createServer } from "node:http";
|
|
12
13
|
import { randomBytes } from "node:crypto";
|
|
@@ -413,7 +414,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
413
414
|
// giving up and sending you to a terminal.
|
|
414
415
|
const attempt = (n) => track("symbiot update", inst, homedir(), (code) => {
|
|
415
416
|
if (code !== 0) {
|
|
416
|
-
|
|
417
|
+
const tail = readRunLog(join(homedir(), ".symbiot", "agent.log")).slice(-3000);
|
|
417
418
|
if (n < 5 && /E404|ETARGET|notarget|No matching version|is not in this registry/i.test(tail)) { UPDATING = { target, retrying: true, attempt: n + 1, at: Date.now() }; setTimeout(() => attempt(n + 1), 30000).unref(); }
|
|
418
419
|
else UPDATING = null;
|
|
419
420
|
return;
|
package/tasks.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { handoverRules, ONLY_YOU, HANDBACK } from "./handover.mjs";
|
|
|
11
11
|
import { userStyleLine } from "./adapt.mjs";
|
|
12
12
|
import { QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, handoffCmd, runningHandoff, writeTasks, droppedTasks, startHeldTasks, connectorsLine } from "./agents.mjs";
|
|
13
13
|
import { gitDefaultBranch } from "./drift.mjs";
|
|
14
|
+
import { readRunLog } from "./work.mjs";
|
|
14
15
|
import { repoPathMap, openWork, detectRepo } from "./scan.mjs";
|
|
15
16
|
|
|
16
17
|
// ---- tasks: a persistent checklist (stored by core.mjs) -------------------
|
|
@@ -342,7 +343,7 @@ function shipWithBump(path, texts, opts) {
|
|
|
342
343
|
// What the last run in a folder said when it finished: agent.log after the last
|
|
343
344
|
// run's "=== name time ===" and "$ command" lines.
|
|
344
345
|
function runSummary(path) {
|
|
345
|
-
|
|
346
|
+
const log = readRunLog(join(path, ".symbiot", "agent.log")); if (!log) return "";
|
|
346
347
|
const at = log.lastIndexOf("\n=== "), rest = at < 0 ? log : log.slice(log.indexOf("\n", at + 1) + 1);
|
|
347
348
|
return rest.replace(/^\$ .*\n?/, "").trim().slice(-8000);
|
|
348
349
|
}
|
package/ui.mjs
CHANGED
|
@@ -1224,7 +1224,7 @@ el.addEventListener('pointerleave',function(){if(!LM.pan)lmHot(null);});
|
|
|
1224
1224
|
var zi=$('lmin'),zo=$('lmout'),zf=$('lmfit');function z(f){var w=el.clientWidth/2,h=el.clientHeight/2,k0=LM.view.k,k=Math.max(0.3,Math.min(4,k0*f));LM.view.x=w-(w-LM.view.x)*k/k0;LM.view.y=h-(h-LM.view.y)*k/k0;LM.view.k=k;}
|
|
1225
1225
|
if(zi)zi.addEventListener('click',function(){z(1.25);});if(zo)zo.addEventListener('click',function(){z(0.8);});if(zf)zf.addEventListener('click',lmFit);}
|
|
1226
1226
|
function lmStart(){if(LM.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;LM.on=true;
|
|
1227
|
-
var loop=function(){var p=$('panel-map'),el=$('lmap');if(!p||!el||!el.offsetParent){LM.on=false;return;}lmFrame();raf(loop);};raf(loop);}
|
|
1227
|
+
var loop=function(){var p=$('panel-map'),el=$('lmap');if(!p||!el||!el.offsetParent){LM.on=false;return;}LM.fr=(LM.fr||0)+1;if(LM.fr%glEvery(true)===0)lmFrame();raf(loop);};raf(loop);}
|
|
1228
1228
|
function lmFrame(){var el=$('lmap'),cv=$('lmc');if(!el||!cv||!LM.items.length)return;if(!LM.G)LM.G=lqGL(cv,64);var w=el.clientWidth,h=el.clientHeight,t=LM.t=(LM.t||0)+0.016,still=LQ.theme&&LQ.theme.still,k=LM.view.k;
|
|
1229
1229
|
var P=LM.items.map(function(a,i){var p=lmS(a);if(!still){p.x+=Math.sin(t*0.4+i*1.7)*2;p.y+=Math.cos(t*0.35+i*2.3)*2;}return p;}),ME=lmS(LM.me);
|
|
1230
1230
|
// labels, rings, cluster names, tip
|
|
@@ -1235,7 +1235,7 @@ var me=$('lmme');if(me&&me.style)me.style.transform='translate('+Math.round(ME.x
|
|
|
1235
1235
|
var tip=$('lmtip');if(tip&&LM.hot){var hi=LM.items.indexOf(LM.hot),hp=P[hi],tw=tip.offsetWidth||280,th=tip.offsetHeight||100,tx=Math.max(10,Math.min(w-tw-10,hp.x+LM.hot.r*k+16)),ty=Math.max(10,Math.min(h-th-10,hp.y-th/2));if(tx<hp.x&&hp.x+LM.hot.r*k+16+tw>w)tx=Math.max(10,hp.x-LM.hot.r*k-16-tw);tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
|
|
1236
1236
|
// faint lines for the weak links
|
|
1237
1237
|
var sv=$('lmlines');if(sv){var ln='';LM.edges.forEach(function(e){if(e.sim>=0.3)return;var a=P[LM.items.indexOf(e.a)],b=P[LM.items.indexOf(e.b)],hot=LM.hot&&(LM.hot===e.a||LM.hot===e.b);ln+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' class='"+(hot?'hot':'')+"'/>";});sv.innerHTML=ln;}
|
|
1238
|
-
var G=LM.G;if(!G)return;var gl=G.gl,d=
|
|
1238
|
+
var G=LM.G;if(!G)return;var gl=G.gl,d=glDpr(2),W=Math.max(1,Math.floor(w*d)),H=Math.max(1,Math.floor(h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
|
|
1239
1239
|
var o=G.out;for(var z=0;z<o.length;z++)o[z]=0;var n=0,put=function(x,y,r,a){if(n>=64||r<=0)return;o[n*4]=x/w;o[n*4+1]=y/h;o[n*4+2]=r;o[n*4+3]=a||0;n++;};
|
|
1240
1240
|
LM.items.forEach(function(a,i){if(!a.fold)put(P[i].x,P[i].y,a.r*k*(LM.hot===a?1.08:1),0);});
|
|
1241
1241
|
// tendrils: a chain of small drops, close enough to always bridge, for the middling links
|
|
@@ -1980,13 +1980,13 @@ var x=BS.beads[k],c=BS.b.cards[x.i],n=x.n;if(x.past){tip.innerHTML="<div class='
|
|
|
1980
1980
|
tip.innerHTML="<div class='bcard' data-i='"+x.i+"'><div class='bi' data-j='"+x.j+"'><div class='btf'>"+esc(n.name||bsName(c).n)+" <span>· "+esc(bsName(c).n)+" · "+agoTxt(n.ts)+(n.from&&CHATFROM[n.from]?" · "+CHATFROM[n.from]:"")+(n.read?" · read":"")+"</span></div><div class='btx'>"+esc(n.text||'')+"</div><div class='bta'>"+(n.mail||n.chat?"<button class='bdraft'>"+(n.drafted?"Drafted · again":"Draft a reply")+"</button>":"")+(n.href?"<button class='bopen"+(n.mail||n.chat?" quiet":"")+"'>Open</button>":"")+(n.chat&&n.drafted?"<button class='bopenwa quiet'>Open in WhatsApp</button>":"")+"</div></div></div>";
|
|
1981
1981
|
tip.className='btip on';if(BWIRE)BWIRE(tip);}
|
|
1982
1982
|
function bsStart(){if(BS.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;BS.on=true;
|
|
1983
|
-
var loop=function(){var p=$('panel-board'),el=$('bstream');if(!p||!el||!p.offsetParent||(el.classList&&el.classList.contains('none'))){BS.on=false;return;}bsFrame();raf(loop);};raf(loop);}
|
|
1983
|
+
var loop=function(){var p=$('panel-board'),el=$('bstream');if(!p||!el||!p.offsetParent||(el.classList&&el.classList.contains('none'))){BS.on=false;return;}BS.fr=(BS.fr||0)+1;if(BS.fr%glEvery(true)===0)bsFrame();raf(loop);};raf(loop);}
|
|
1984
1984
|
function bsFrame(){var el=$('bstream'),cv=$('bsc');if(!el||!cv)return;if(!BS.G)BS.G=lqGL(cv,40);var g=bsGeom();if(BS.b&&BS.gw!==g.w){bsBuild(BS.b);g=bsGeom();}var t=LQ.t||0,still=LQ.theme&&LQ.theme.still;
|
|
1985
1985
|
BS.beads.forEach(function(x,k){var hot=BS.hot===k;x.x+=(x.tx-x.x)*0.12;x.y+=((x.ty+(still?0:Math.sin(t*0.8+k*1.3)*2))-x.y)*0.12;x.r+=((x.tr*(hot?1.35:1))-x.r)*0.15;
|
|
1986
1986
|
var bt=BS.btns&&BS.btns[k];if(bt&&bt.style){var R=Math.max(14,x.r+6);bt.style.width=bt.style.height=(2*R)+'px';bt.style.transform='translate('+Math.round(x.x-R)+'px,'+Math.round(x.y-R)+'px)';}});
|
|
1987
1987
|
var tip=document.querySelector('#bsov .btip');if(tip&&BS.hotWait!=null&&BS.waits&&BS.waits[BS.hotWait]){var wx=BS.waits[BS.hotWait],tw2=tip.offsetWidth||300,th2=tip.offsetHeight||140,tx2=Math.max(12,Math.min(g.w-tw2-12,wx.x-tw2/2)),ty2=wx.y-th2-16;if(ty2<8)ty2=wx.y+16;tip.style.transform='translate('+Math.round(tx2)+'px,'+Math.round(ty2)+'px)';}
|
|
1988
1988
|
else if(tip&&BS.hot!=null&&BS.beads[BS.hot]){var x=BS.beads[BS.hot],tw=tip.offsetWidth||300,th=tip.offsetHeight||120,tx=Math.max(12,Math.min(g.w-tw-12,x.x-tw/2)),ty=x.y-x.r-th-14;if(ty<8)ty=x.y+x.r+14;tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
|
|
1989
|
-
var G=BS.G;if(!G)return;var gl=G.gl,d=
|
|
1989
|
+
var G=BS.G;if(!G)return;var gl=G.gl,d=glDpr(3),W=Math.max(1,Math.floor(g.w*d)),H=Math.max(1,Math.floor(g.h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
|
|
1990
1990
|
var o=G.out;for(var i=0;i<o.length;i++)o[i]=0;var k=0,put=function(px,py,r,a){if(k>=40)return;o[k*4]=px/g.w;o[k*4+1]=py/g.h;o[k*4+2]=Math.max(0,r);o[k*4+3]=a;k++;};
|
|
1991
1991
|
(BS.b.cards||[]).forEach(function(c,i){put(g.x1,60+(g.h-110)*(i+0.5)/Math.max(1,g.n),c.count?9+2*Math.min(c.count,6):5,0);});
|
|
1992
1992
|
BS.beads.forEach(function(x){put(x.x,x.y,x.r,x.need?1:0);});
|
|
@@ -2635,7 +2635,17 @@ var LQ_FS=['precision highp float;',
|
|
|
2635
2635
|
'gl_FragColor=vec4(col,1.0);}'].join('');
|
|
2636
2636
|
function lqFS(n){return LQ_FS.split('NB').join(String(n));}
|
|
2637
2637
|
var LQ_NB=32; // metaballs home can draw: the core, the droplets, the lines to their blobs, the one at your pointer
|
|
2638
|
+
// No graphics card (WebGL drawn by the CPU: SwiftShader, llvmpipe), any steady drawing
|
|
2639
|
+
// keeps several cores spinning (measured: 2.4-6 cores at full speed, 0.9 at 2 a second,
|
|
2640
|
+
// 0.06 not drawing). Then, or when the system asks for less motion, the liquid draws at
|
|
2641
|
+
// a third of the resolution, without the breathing, and only when something moved (Home), at most
|
|
2642
|
+
// 20 a second while you use it and 5 at rest. glDpr: the pixel ratio a canvas draws at;
|
|
2643
|
+
// glEvery: draw 1 frame in n.
|
|
2644
|
+
var GLSOFT=false,GLSTILL=false;try{GLSTILL=!!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);}catch(e){}
|
|
2645
|
+
function glEvery(busy){return GLSOFT||GLSTILL?(busy?3:12):1;}
|
|
2646
|
+
function glDpr(max){return GLSOFT?0.34:Math.min(window.devicePixelRatio||1,max);}
|
|
2638
2647
|
function lqGL(c,n){c=c||$('lq');n=n||LQ_NB;if(!c||!c.getContext)return null;var gl=null;try{gl=c.getContext('webgl',{antialias:false,alpha:false});}catch(e){}if(!gl)return null;
|
|
2648
|
+
try{var dbg=gl.getExtension('WEBGL_debug_renderer_info'),rn=String(dbg?gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL):gl.getParameter(gl.RENDERER)).toLowerCase();if(['swiftshader','llvmpipe','softpipe','software'].some(function(k){return rn.indexOf(k)>=0;}))GLSOFT=true;}catch(e){}
|
|
2639
2649
|
function sh(t,src){var o=gl.createShader(t);gl.shaderSource(o,src);gl.compileShader(o);return o;}
|
|
2640
2650
|
var pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,'attribute vec2 a;void main(){gl_Position=vec4(a,0.0,1.0);}'));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,lqFS(n)));gl.linkProgram(pr);
|
|
2641
2651
|
if(!gl.getProgramParameter(pr,gl.LINK_STATUS))return null;gl.useProgram(pr);
|
|
@@ -2643,9 +2653,9 @@ var bf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,bf);gl.bufferData(gl.ARRA
|
|
|
2643
2653
|
var al=gl.getAttribLocation(pr,'a');gl.enableVertexAttribArray(al);gl.vertexAttribPointer(al,2,gl.FLOAT,false,0,0);
|
|
2644
2654
|
var U={};['uRes','uT','uDpr','uB','uRip','uStyle','uContrast','uExposure'].forEach(function(n){U[n]=gl.getUniformLocation(pr,n);});
|
|
2645
2655
|
return {gl:gl,c:c,U:U,out:new Float32Array(n*4),t0:(window.performance&&performance.now)?performance.now():Date.now()};}
|
|
2646
|
-
function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=
|
|
2656
|
+
function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=glDpr(3),W=Math.max(1,Math.floor(S.w*d)),H=Math.max(1,Math.floor(S.h*d));
|
|
2647
2657
|
if(c.width!==W||c.height!==H){c.width=W;c.height=H;gl.viewport(0,0,W,H);}
|
|
2648
|
-
var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,still=th.still,o=G.out;LQ.t=t;
|
|
2658
|
+
var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,slow=GLSOFT||GLSTILL,still=th.still||slow,o=G.out;LQ.t=t;
|
|
2649
2659
|
for(var i=0;i<o.length;i++)o[i]=0;var NB=o.length/4;
|
|
2650
2660
|
var put=function(k,x,y,r,a,lv){if(k>NB-1)return;var w=still?0:(k===0?1:lv!=null?lv:0.3);o[k*4]=x/S.w+Math.sin(t*1.3+k*1.7)*0.004*w;o[k*4+1]=y/S.h+Math.cos(t*1.1+k*2.3)*0.005*w;o[k*4+2]=Math.max(0,r)*(1+0.035*Math.sin(t*2.0+k)*w);o[k*4+3]=a||0;};
|
|
2651
2661
|
put(0,LQ.core.x,LQ.core.y,LQ.core.cr);var k=1,tend=[];
|
|
@@ -2658,7 +2668,9 @@ tend.forEach(function(dr){var dx=dr.x-LQ.core.x,dy=dr.y-LQ.core.y,len=Math.sqrt(
|
|
|
2658
2668
|
if(LQ.mode!=='rest'&&LQ.mode!=='pool')LQ.drops.forEach(function(dr){if(!dr.blob||dr.gone||dr.cr<6||k+3>NB-1)return;var x1=dr.x+dr.cr;put(k++,x1+8,dr.y,7*Math.min(1,dr.cr/dr.r),1,0.2);put(k++,x1+18,dr.y,6,1,0.2);put(k++,x1+31,dr.y,10,1,0.2);});
|
|
2659
2669
|
var P=LQ.pointer;if(P&&LQ.mode==='aware'&&!LQ.talking){var best=null,bd=1e9;[LQ.core].concat(LQ.drops).forEach(function(g){if(g.cr<18)return;var dd=Math.hypot(P[0]-g.x,P[1]-g.y)-g.cr;if(dd<bd){bd=dd;best=g;}});
|
|
2660
2670
|
if(best&&bd<230){var dx=P[0]-best.x,dy=P[1]-best.y,dl=Math.hypot(dx,dy)||1,reach=Math.min(dl,best.cr+70);put(NB-1,best.x+dx/dl*reach,best.y+dy/dl*reach,8+20*(1-Math.max(0,bd)/230));}}
|
|
2661
|
-
|
|
2671
|
+
// drawn seldom (no GPU, or less motion asked for): still, and only when something moved
|
|
2672
|
+
if(slow){var sig=W+'|'+H+'|'+th.look+th.night+th.contrast;for(var q=0;q<o.length;q++)sig+=','+Math.round(o[q]*400);if(sig===G.sig)return;G.sig=sig;}
|
|
2673
|
+
var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,slow?0:t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
|
|
2662
2674
|
gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);
|
|
2663
2675
|
gl.drawArrays(gl.TRIANGLES,0,3);}
|
|
2664
2676
|
function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
|
|
@@ -2681,7 +2693,7 @@ lqLoad(true);
|
|
|
2681
2693
|
// the only clocks: rest when left alone, fresh data now and then, the hour (night)
|
|
2682
2694
|
setInterval(function(){if(LQ.mode==='aware'&&!LQ.talking&&Date.now()-LQ.lastAct>LQ_REST)LQ.mode='rest';if(LQ.mode!=='pool')lqLoad(false);var h=new Date().getHours(),n=h>=22||h<6;if(n!==LQ.theme.night)lqTheme();},30000);
|
|
2683
2695
|
var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAnimationFrame!=='function')return;var raf=function(f){return window.requestAnimationFrame(f);};
|
|
2684
|
-
function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)
|
|
2696
|
+
function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=(GLSOFT||GLSTILL)?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0)lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU
|
|
2685
2697
|
raf(frame);}
|
|
2686
2698
|
|
|
2687
2699
|
initGraphEvents();syncP();refresh();loadMap();firstTab();lqInit();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
|
package/work.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// time. Another agent (Codex, Aider, Gemini) writes plain text: its last lines
|
|
9
9
|
// come through as what it said. Pure: text in, data out.
|
|
10
10
|
|
|
11
|
+
import { statSync, openSync, readSync, closeSync } from "node:fs";
|
|
12
|
+
|
|
11
13
|
const MAX_STEPS = 60;
|
|
12
14
|
const short = (s, n = 80) => { s = String(s ?? "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
13
15
|
const base = (p) => String(p || "").split("/").filter(Boolean).slice(-2).join("/");
|
|
@@ -103,6 +105,29 @@ function parseRun(text) {
|
|
|
103
105
|
return out;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
// The newest run of an agent.log, from its "=== " header on, read from the end of the
|
|
109
|
+
// file. A log keeps every run and everything they read (one reached 144 MB: 96 runs and
|
|
110
|
+
// the screenshots they looked at), and every view only needs the newest, so it never
|
|
111
|
+
// reads the rest; and it's kept until the file changes, so a view that asks again
|
|
112
|
+
// costs nothing. Past RUN_CAP of one run, it's that run's last RUN_CAP.
|
|
113
|
+
const RUN_CAP = 16 * 1024 * 1024, RUN_CACHE = new Map();
|
|
114
|
+
function readRunLog(file) {
|
|
115
|
+
let st; try { st = statSync(file); } catch { return ""; }
|
|
116
|
+
const hit = RUN_CACHE.get(file); if (hit && hit.size === st.size && hit.mtime === st.mtimeMs) return hit.text;
|
|
117
|
+
let fd; try { fd = openSync(file, "r"); } catch { return ""; }
|
|
118
|
+
let buf = Buffer.alloc(0), at = -1;
|
|
119
|
+
try {
|
|
120
|
+
for (let end = st.size; end > 0 && buf.length < RUN_CAP;) {
|
|
121
|
+
const start = Math.max(0, end - (1 << 20)), b = Buffer.alloc(end - start);
|
|
122
|
+
readSync(fd, b, 0, b.length, start); buf = Buffer.concat([b, buf]); end = start;
|
|
123
|
+
at = buf.lastIndexOf("\n=== "); if (at >= 0) break;
|
|
124
|
+
}
|
|
125
|
+
} catch { buf = Buffer.alloc(0); } finally { try { closeSync(fd); } catch {} }
|
|
126
|
+
const text = (at >= 0 ? buf.subarray(at) : buf).toString("utf8");
|
|
127
|
+
RUN_CACHE.delete(file); RUN_CACHE.set(file, { size: st.size, mtime: st.mtimeMs, text });
|
|
128
|
+
if (RUN_CACHE.size > 40) RUN_CACHE.delete(RUN_CACHE.keys().next().value);
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
106
131
|
// The newest run's part of an agent.log (after its header and command line).
|
|
107
132
|
function lastRunText(log) {
|
|
108
133
|
const s = String(log || ""), at = s.lastIndexOf("\n=== ");
|
|
@@ -111,4 +136,4 @@ function lastRunText(log) {
|
|
|
111
136
|
// What the agent said last: its final answer, from either kind of log.
|
|
112
137
|
function finalOf(log) { return parseRun(lastRunText(log)).final; }
|
|
113
138
|
|
|
114
|
-
export { parseRun, lastRunText, finalOf, describe, testsIn };
|
|
139
|
+
export { parseRun, lastRunText, finalOf, describe, testsIn, readRunLog };
|