symbiot 0.45.0 → 0.46.0
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 +26 -6
- package/home.mjs +1 -1
- package/lanes.mjs +3 -1
- package/mind.mjs +10 -2
- package/package.json +3 -2
- package/ui.mjs +135 -23
- package/watch.mjs +2 -1
- package/work.mjs +113 -0
- package/writeups.mjs +1 -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.46.0 — 2026-10-06
|
|
6
|
+
|
|
7
|
+
- **The Symbiot look on every part.** Whatever you open pools over the liquid as glass, with a slow silver edge: cards are glass, the main buttons chrome, the map's nodes small silver spheres.
|
|
8
|
+
- **Watch an agent work, as it works.** What it's doing right now, its own to-do list ticking off (with a progress ring), each step in plain words with how long it took, test results, the files it changed, its pace, its model, turns, tokens and cost, and finally its answer.
|
|
9
|
+
- **Update prompts show again.** Since 0.45.0 the "Update & restart" bar was hidden behind the liquid; it now sits on top, as a chrome bar at the top of the window.
|
|
10
|
+
- **Calm, readable droplets.** Droplets keep clear of each other, past where their metal would merge, so none of them flicker or shake their labels; when a screen can't hold them all, the least used go under "more" instead of crowding.
|
|
11
|
+
- **See Symbiot think.** Under each chat reply: what it did to answer (what it recalled, the links it looked up, that it matched how you talk, what it handed over or remembered).
|
|
12
|
+
|
|
5
13
|
## 0.45.0 — 2026-10-06
|
|
6
14
|
|
|
7
15
|
- **The liquid.** Symbiot's home is now one surface of liquid silver that shapes itself to you, with no settings. Its droplets are what only you can do, what's new on what you watch, and the parts of the app, which pool open over it. Talk to it in the bar at the bottom; leave it alone and it rests as one orb.
|
package/agents.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
9
9
|
import { randomBytes, createHash } from "node:crypto";
|
|
10
10
|
import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
|
|
11
|
+
import { parseRun, lastRunText } from "./work.mjs";
|
|
11
12
|
|
|
12
13
|
const HANDOFFS = []; // live registry of agents Symbiot has handed work to
|
|
13
14
|
// ---- hand a repo (+ its tasks) to the user's agent — generic, settings-based
|
|
@@ -138,7 +139,7 @@ function runHandoff(repoPath, { force = false } = {}) {
|
|
|
138
139
|
let opts = {}; try { opts = JSON.parse(readSymbiot(repoPath, "handoff.json")) || {}; } catch {}
|
|
139
140
|
const busy = runningHandoff(repoPath); if (busy) return { busy: true, id: busy.id || "", pid: busy.pid, auto: !!busy.auto };
|
|
140
141
|
releaseHeldTasks(repoPath); // held for an agent another process started, which has since exited
|
|
141
|
-
tmpl = withConnectors(tmpl, repoPath);
|
|
142
|
+
tmpl = withStream(withConnectors(tmpl, repoPath));
|
|
142
143
|
if (!force) { const w = waitingFor(repoPath); if (w) { askedFor(repoPath); return { blocked: true, waiting: true, questions: 0, note: waitNote(w) }; } }
|
|
143
144
|
const blocked = !force && blockedAgain(repoPath, tmpl); if (blocked) return blocked;
|
|
144
145
|
try { unlinkSync(join(repoPath, ".symbiot", WAITING)); } catch {} // the step's done (or this is Start it anyway): this is the run it waited for
|
|
@@ -180,8 +181,8 @@ function earlierRuns() {
|
|
|
180
181
|
const busy = runningHandoff(r.path), ask = agentQuestions(r.path, r.name), wait = waitingFor(r.path);
|
|
181
182
|
if (!busy && !ask.questions.length && !wait) continue;
|
|
182
183
|
const log = join(r.path, ".symbiot", "agent.log"), startedAt = (busy && busy.startedAt) || Number(r.startedAt) || Date.now();
|
|
183
|
-
let tail = "", end = Date.now(); try { tail = readFileSync(log, "utf8")
|
|
184
|
-
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, changed: agentChanges(r.path, startedAt), ask, held: heldTasks(r.path), waiting: wait, fromHeld: false });
|
|
184
|
+
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path, readFileSync(log, "utf8"))); if (!busy) end = statSync(log).mtimeMs; } catch {}
|
|
185
|
+
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, fromHeld: false });
|
|
185
186
|
}
|
|
186
187
|
return out;
|
|
187
188
|
}
|
|
@@ -614,13 +615,32 @@ function heldTasks(path) {
|
|
|
614
615
|
// newest job per folder — the questions/ideas it left, any tasks held for it and
|
|
615
616
|
// a step of yours it waits on. Then runs from before the app started that still
|
|
616
617
|
// need you (earlierRuns).
|
|
618
|
+
// A run's work, to draw: parsed from its log (work.mjs), with how far through its
|
|
619
|
+
// TASKS.md it is. tail is what to show as text: what it said, for a streaming
|
|
620
|
+
// run; the log's end, for another agent.
|
|
621
|
+
// Claude Code in print mode (claude -p) streams each step as JSON, so the work
|
|
622
|
+
// can be drawn as it happens (work.mjs). Added to a claude -p command that has no
|
|
623
|
+
// output format of its own; anything else (an Orca tab, another agent) as it is.
|
|
624
|
+
function withStream(cmd) {
|
|
625
|
+
const c = String(cmd || "");
|
|
626
|
+
if (!/^\s*claude\b/.test(c) || !/\s-p\b|\s--print\b/.test(c) || /--output-format\b/.test(c)) return c;
|
|
627
|
+
return c + " --output-format stream-json --verbose";
|
|
628
|
+
}
|
|
629
|
+
function workOf(path, log) {
|
|
630
|
+
const w = parseRun(lastRunText(log)), md = readSymbiot(path, "TASKS.md");
|
|
631
|
+
const done = (md.match(/^\s*-\s*\[x\]/gim) || []).length, total = done + (md.match(/^\s*-\s*\[ \]/gm) || []).length;
|
|
632
|
+
const tail = w.stream ? (w.final || w.said.join("\n\n") || (w.doing ? w.doing + "…" : "")) : String(log).slice(-1200);
|
|
633
|
+
const work = w.stream ? { model: w.model, doing: w.doing, steps: w.steps.slice(-16), todos: w.todos, cost: w.cost, turns: w.turns, tokens: w.tokens, tests: w.tests, files: w.files, pace: w.pace, errors: w.errors, final: w.final.slice(0, 1200), count: w.steps.length } : null;
|
|
634
|
+
return { tail, work, progress: total ? { done, total } : null };
|
|
635
|
+
}
|
|
617
636
|
function agentsList() {
|
|
618
637
|
const seen = new Set();
|
|
619
638
|
return HANDOFFS.map((e) => {
|
|
620
|
-
let
|
|
639
|
+
let log = ""; try { log = readFileSync(e.log, "utf8"); } catch {}
|
|
640
|
+
const { tail, work, progress } = workOf(e.path, log);
|
|
621
641
|
const first = !seen.has(e.path); seen.add(e.path);
|
|
622
|
-
return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, 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, fromHeld: !!e.fromHeld };
|
|
642
|
+
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, fromHeld: !!e.fromHeld };
|
|
623
643
|
}).concat(earlierRuns());
|
|
624
644
|
}
|
|
625
645
|
|
|
626
|
-
export { HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, fillHandoff, runHandoff, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, writeTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList };
|
|
646
|
+
export { withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, fillHandoff, runHandoff, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, writeTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList };
|
package/home.mjs
CHANGED
|
@@ -68,7 +68,7 @@ async function homeAsk(question, { ask, now = Date.now(), state } = {}) {
|
|
|
68
68
|
agent: (req, known, repo) => (repo ? actIn(req, repo, { map, known, title: "Home" }) : actNow(req, { title: "Home", known })),
|
|
69
69
|
task: (text, repo) => taskIn(text, repo, { map }),
|
|
70
70
|
} });
|
|
71
|
-
return { answer: r.reply, ...(r.did ? { did: r.did } : {}) };
|
|
71
|
+
return { answer: r.reply, ...(r.did ? { did: r.did } : {}), steps: r.steps || [] };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export { homeState, homeContext, homeAsk };
|
package/lanes.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { runHandoff, runningHandoff, waitingFor } from "./agents.mjs";
|
|
|
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
27
|
import { OPS, parseHandoffs } from "./handover.mjs";
|
|
27
28
|
|
|
28
29
|
const LEDGER = join(CONFIG_DIR, "lanes.json");
|
|
@@ -83,7 +84,8 @@ function dispatch(path, { map = repoPathMap(), act = actNow, run = runHandoff, a
|
|
|
83
84
|
// What an agent said last: its log after the newest run's header.
|
|
84
85
|
function lastWords(path) {
|
|
85
86
|
const log = readSym(path, "agent.log"), at = log.lastIndexOf("\n=== "); if (at < 0) return "";
|
|
86
|
-
const
|
|
87
|
+
const w = parseRun(lastRunText(log)); // a streaming run: its final answer, not its JSON
|
|
88
|
+
const run = w.stream ? (w.final || w.said.join("\n")) : log.slice(at).split("\n").slice(3).join("\n").trim(); // past the header and the command
|
|
87
89
|
return run.length > TAIL ? "…" + run.slice(-TAIL) : run;
|
|
88
90
|
}
|
|
89
91
|
// When the newest run in a folder started (its log header), or 0.
|
package/mind.mjs
CHANGED
|
@@ -190,7 +190,12 @@ function parseReply(raw) {
|
|
|
190
190
|
// own last turns. act: { agent(request) -> result, task(text, repo) -> result },
|
|
191
191
|
// what "do" runs here. Gives { reply, did?, remembered }.
|
|
192
192
|
async function converse({ where, role = "", context = "", history = "", question, act = {}, ask = write, map = null, now = Date.now() }) {
|
|
193
|
-
const d = loadMind(),
|
|
193
|
+
const d = loadMind(), hits = recall(question, now, d), known = recallText(hits), elsewhere = lately(where, d);
|
|
194
|
+
// what it did to answer, to show under the reply (as the app shows an agent's work)
|
|
195
|
+
const steps = [];
|
|
196
|
+
if (hits.length) steps.push(`recalled ${hits.length === 1 ? hits[0].name : hits.length + " things: " + hits.slice(0, 3).map((h) => h.name).join(", ")}`);
|
|
197
|
+
const places = [...new Set(d.log.filter((l) => l.where !== where).slice(-6).map((l) => l.where))];
|
|
198
|
+
if (places.length) steps.push(`picked up what you said on ${places.slice(0, 2).join(" and ")}`);
|
|
194
199
|
let lanes = map; if (!lanes) { try { lanes = repoPathMap(); } catch { lanes = {}; } }
|
|
195
200
|
// how they talk, from what they've typed into any chat (adapt.mjs: accommodation)
|
|
196
201
|
const voice = styleLine(styleOf(d.log.filter((l) => l.role === "user").map((l) => l.text).concat(question)));
|
|
@@ -219,7 +224,10 @@ async function converse({ where, role = "", context = "", history = "", question
|
|
|
219
224
|
const d2 = loadMind(), remembered = rememberIn(d2, j.remember, where, now);
|
|
220
225
|
logTurn(where, "user", question, now, d2); logTurn(where, "ai", reply, now, d2);
|
|
221
226
|
saveMind(d2);
|
|
222
|
-
|
|
227
|
+
if (voice) steps.push("matched how you talk");
|
|
228
|
+
if (did && !did.error) steps.push(did.kind === "agent" ? (did.lane ? `handed it to ${did.lane}'s agent` : "handed it to an agent") : did.lane ? `added a task to ${did.lane}` : "added a task");
|
|
229
|
+
if (remembered) steps.push(`remembered ${remembered} new thing${remembered > 1 ? "s" : ""}`);
|
|
230
|
+
return { reply, ...(did ? { did } : {}), remembered, steps };
|
|
223
231
|
}
|
|
224
232
|
// The usual "task" act, for a caller with no lanes of its own to pass.
|
|
225
233
|
const addToTasks = (text, repo) => { let map = {}; try { map = repoPathMap(); } catch {} return taskIn(text, repo, { map }); };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"lanes.mjs",
|
|
29
29
|
"adapt.mjs",
|
|
30
30
|
"home.mjs",
|
|
31
|
+
"work.mjs",
|
|
31
32
|
"phone.mjs",
|
|
32
33
|
"desktop.mjs",
|
|
33
34
|
"README.md",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
66
67
|
},
|
|
67
68
|
"scripts": {
|
|
68
|
-
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/peek.mjs && node test/mind.mjs && node test/lanes.mjs && node test/adapt.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
69
|
+
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/peek.mjs && node test/mind.mjs && node test/lanes.mjs && node test/adapt.mjs && node test/work.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
69
70
|
"lint": "eslint ."
|
|
70
71
|
},
|
|
71
72
|
"devDependencies": {
|
package/ui.mjs
CHANGED
|
@@ -60,6 +60,67 @@ body.lq-liquid footer{position:fixed;right:12px;top:38px;border:0;padding:0;z-in
|
|
|
60
60
|
body.lq-pooled #lqtalk,body.lq-pooled #lqmore,body.lq-pooled #lqcore{display:none}
|
|
61
61
|
body.lq-talking .lqd{opacity:.55}
|
|
62
62
|
@media (prefers-reduced-motion: reduce){.lqd,#lqcore{transition:none}}
|
|
63
|
+
/* ---- the Symbiot look on every part: glass over the liquid, chrome, a silver edge ---- */
|
|
64
|
+
body.lq-liquid.lq-pooled main{background:rgba(6,8,11,.86);backdrop-filter:blur(20px) saturate(1.25);border:1px solid transparent;background-clip:padding-box;box-shadow:0 0 0 1px rgba(214,222,234,.22),0 40px 90px -30px rgba(0,0,0,.85),inset 0 1px 0 rgba(255,255,255,.12)}
|
|
65
|
+
body.lq-liquid.lq-light.lq-pooled main{background:rgba(246,248,251,.9);box-shadow:0 0 0 1px rgba(20,30,45,.14),0 40px 90px -40px rgba(20,30,45,.45),inset 0 1px 0 #FFFFFF}
|
|
66
|
+
body.lq-liquid.lq-solid.lq-pooled main{backdrop-filter:none;background:var(--ink)}
|
|
67
|
+
.lqedge{position:fixed;top:64px;left:max(12px,4vw);right:max(12px,4vw);bottom:88px;z-index:6;border-radius:24px;pointer-events:none;padding:1px;background:linear-gradient(115deg,rgba(255,255,255,.0) 0%,rgba(255,255,255,.75) 18%,rgba(140,150,166,.3) 32%,rgba(255,255,255,.0) 48%,rgba(230,236,244,.6) 66%,rgba(255,255,255,0) 82%);background-size:260% 260%;-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;animation:lqsheen 9s linear infinite;display:none}
|
|
68
|
+
body.lq-liquid.lq-pooled .lqedge{display:block}
|
|
69
|
+
@keyframes lqsheen{0%{background-position:0% 50%}100%{background-position:260% 50%}}
|
|
70
|
+
body.lq-liquid .task,body.lq-liquid .agent,body.lq-liquid .rcard,body.lq-liquid .bcard,body.lq-liquid .drift,body.lq-liquid .q,body.lq-liquid .out,body.lq-liquid .tchat{background:linear-gradient(180deg,rgba(255,255,255,.075),rgba(255,255,255,.02));border:1px solid rgba(220,228,240,.16);border-radius:16px;box-shadow:inset 0 1px 0 rgba(255,255,255,.09),0 12px 30px -24px rgba(0,0,0,.9)}
|
|
71
|
+
body.lq-liquid.lq-light .task,body.lq-liquid.lq-light .agent,body.lq-liquid.lq-light .rcard,body.lq-liquid.lq-light .bcard,body.lq-liquid.lq-light .drift,body.lq-liquid.lq-light .q,body.lq-liquid.lq-light .out,body.lq-liquid.lq-light .tchat{background:linear-gradient(180deg,#FFFFFF,#F2F4F8);border-color:rgba(20,30,45,.12);box-shadow:inset 0 1px 0 #FFFFFF,0 12px 28px -24px rgba(20,30,45,.5)}
|
|
72
|
+
body.lq-liquid button.act{background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;border:1px solid rgba(255,255,255,.9);border-radius:999px;box-shadow:0 6px 18px -10px rgba(0,0,0,.7)}
|
|
73
|
+
body.lq-liquid button.ghost{background:rgba(255,255,255,.05);border:1px solid rgba(220,228,240,.28);border-radius:999px;color:var(--bone);backdrop-filter:blur(6px)}
|
|
74
|
+
body.lq-liquid.lq-light button.ghost{background:rgba(255,255,255,.7);border-color:rgba(20,30,45,.18)}
|
|
75
|
+
body.lq-liquid input,body.lq-liquid select,body.lq-liquid textarea{border-radius:12px;border:1px solid rgba(220,228,240,.2);background:rgba(255,255,255,.04);color:var(--bone)}
|
|
76
|
+
body.lq-liquid.lq-light input,body.lq-liquid.lq-light select,body.lq-liquid.lq-light textarea{background:#FFFFFF;border-color:rgba(20,30,45,.18)}
|
|
77
|
+
body.lq-liquid input:focus,body.lq-liquid select:focus,body.lq-liquid textarea:focus{outline:none;border-color:rgba(255,255,255,.7);box-shadow:0 0 0 3px rgba(200,210,224,.18)}
|
|
78
|
+
body.lq-liquid .tgroup{color:#C9D1DC;letter-spacing:.16em}
|
|
79
|
+
body.lq-liquid.lq-light .tgroup{color:#3B4656}
|
|
80
|
+
body.lq-liquid h2,body.lq-liquid h3{letter-spacing:-.01em}
|
|
81
|
+
body.lq-liquid .fchip.on{background:linear-gradient(180deg,#FFFFFF,#C9D1DC);color:#06080B;border-color:#FFFFFF}
|
|
82
|
+
body.lq-contrast.lq-liquid .task,body.lq-contrast.lq-liquid .agent{border:2px solid currentColor}
|
|
83
|
+
/* the live work of an agent: what it's doing, its to-dos, its steps, its files, its pace */
|
|
84
|
+
.wk{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:12px;margin-top:10px}
|
|
85
|
+
.wkbox{border-radius:14px;padding:10px 12px;background:rgba(255,255,255,.035);border:1px solid rgba(220,228,240,.12)}
|
|
86
|
+
body.lq-light .wkbox{background:#F6F8FB;border-color:rgba(20,30,45,.1)}
|
|
87
|
+
.wkh{font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--faint);margin-bottom:6px;display:flex;gap:6px;align-items:center}
|
|
88
|
+
.wkstats{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
89
|
+
.wkstat{font-size:11.5px;padding:3px 9px;border-radius:999px;border:1px solid rgba(220,228,240,.2);color:var(--text);white-space:nowrap}
|
|
90
|
+
.wkstat b{color:var(--bone)}
|
|
91
|
+
.wkdoing{margin-top:10px;font-size:14px;font-weight:600;background:linear-gradient(90deg,#8C96A6 0%,#FFFFFF 40%,#8C96A6 60%,#8C96A6 100%);background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:lqshim 2.2s linear infinite}
|
|
92
|
+
body.lq-light .wkdoing{background-image:linear-gradient(90deg,#4A5565 0%,#0F1720 40%,#4A5565 60%,#4A5565 100%)}
|
|
93
|
+
@keyframes lqshim{0%{background-position:120% 0}100%{background-position:-120% 0}}
|
|
94
|
+
.wktodo{display:flex;gap:8px;align-items:flex-start;font-size:12.5px;line-height:1.4;padding:3px 0}
|
|
95
|
+
.wktodo i{flex:none;width:12px;height:12px;margin-top:2px;border-radius:50%;border:1.5px solid var(--faint)}
|
|
96
|
+
.wktodo.in_progress i{border-color:var(--bone);background:conic-gradient(var(--bone) 0 50%,transparent 50%);animation:lqspin 1.4s linear infinite}
|
|
97
|
+
.wktodo.completed i{border-color:var(--green);background:var(--green)}
|
|
98
|
+
.wktodo.completed span{color:var(--faint);text-decoration:line-through}
|
|
99
|
+
@keyframes lqspin{to{transform:rotate(360deg)}}
|
|
100
|
+
.wkstep{display:flex;gap:8px;align-items:center;font-size:12.5px;padding:3px 0;min-width:0}
|
|
101
|
+
.wkstep svg{flex:none;opacity:.85}
|
|
102
|
+
.wkstep .wt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
103
|
+
.wkstep .wt b{font-weight:600;color:var(--bone)}
|
|
104
|
+
.wkstep .wm{flex:none;font-size:11px;color:var(--faint)}
|
|
105
|
+
.wkstep.error .wt{color:#FF8A75}.wkstep.running .wt{color:var(--bone)}
|
|
106
|
+
.wktest{font-size:11px;padding:1px 7px;border-radius:999px;background:rgba(61,220,151,.14);color:var(--green);white-space:nowrap}
|
|
107
|
+
.wktest.bad{background:rgba(255,138,117,.16);color:#FF8A75}
|
|
108
|
+
.wkfile{display:flex;gap:8px;align-items:center;font-size:12px;padding:2px 0}
|
|
109
|
+
.wkfile .fn{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
110
|
+
.wkfile .fb{flex:1;height:6px;border-radius:3px;background:rgba(220,228,240,.12);overflow:hidden}
|
|
111
|
+
.wkfile .fb i{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,#8E97A6,#FFFFFF)}
|
|
112
|
+
body.lq-light .wkfile .fb i{background:linear-gradient(90deg,#6F7887,#0F1720)}
|
|
113
|
+
.wkfinal{margin-top:10px;font-size:13px;line-height:1.55;white-space:pre-wrap}
|
|
114
|
+
.wkraw summary{cursor:pointer;font-size:12px;color:var(--faint);margin-top:10px}
|
|
115
|
+
.orb{flex:none;display:inline-block;width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#FFFFFF 0%,#C9CFD9 30%,#6F7887 70%,#2A2F38 100%);box-shadow:0 0 0 1px rgba(255,255,255,.25)}
|
|
116
|
+
.orb.run{animation:lqpulse 1.6s ease-in-out infinite}.orb.fail{background:radial-gradient(circle at 35% 30%,#FFE1DA 0%,#FF8A75 45%,#7A2A1C 100%)}
|
|
117
|
+
@keyframes lqpulse{0%,100%{transform:scale(1);box-shadow:0 0 0 1px rgba(255,255,255,.25)}50%{transform:scale(1.18);box-shadow:0 0 14px 2px rgba(255,255,255,.35)}}
|
|
118
|
+
.msg .steps,.lqmsg .steps{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
|
|
119
|
+
.steps span{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid rgba(220,228,240,.22);color:var(--faint);white-space:nowrap}
|
|
120
|
+
.thinking{background:linear-gradient(90deg,#8C96A6 0%,#FFFFFF 40%,#8C96A6 60%,#8C96A6 100%);background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent !important;animation:lqshim 2.2s linear infinite}
|
|
121
|
+
body.lq-light .thinking{background-image:linear-gradient(90deg,#4A5565 0%,#0F1720 40%,#4A5565 60%,#4A5565 100%)}
|
|
122
|
+
@media (prefers-reduced-motion: reduce){.lqedge,.wkdoing,.thinking,.orb.run,.wktodo.in_progress i{animation:none}}
|
|
123
|
+
|
|
63
124
|
|
|
64
125
|
*{box-sizing:border-box}html,body{margin:0;height:100%}
|
|
65
126
|
body{background:var(--ink);color:var(--text);font-family:var(--sans);font-size:14px;display:flex;flex-direction:column}
|
|
@@ -85,9 +146,14 @@ a{color:var(--green);cursor:pointer}.hidden{display:none}
|
|
|
85
146
|
.note{font-size:12px;margin-top:10px}.ok{color:var(--green)}.err{color:var(--amber)}
|
|
86
147
|
.updatebar{display:none;align-items:center;gap:12px;padding:9px 16px;font-size:13px;font-weight:600;background:var(--green);color:var(--ink)}
|
|
87
148
|
.updatebar.show{display:flex}
|
|
149
|
+
body.lq-liquid .updatebar.show{position:fixed;top:10px;left:50%;transform:translateX(-50%);width:max-content;max-width:calc(100vw - 24px);z-index:20;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;box-shadow:0 14px 40px -14px rgba(0,0,0,.7)}
|
|
150
|
+
body.lq-liquid .updatebar.show.reconnect{background:var(--amber)}
|
|
151
|
+
body.lq-liquid #appbar.updatebar.show{top:58px}
|
|
88
152
|
.updatebar.reconnect{background:var(--amber)}
|
|
89
153
|
.whatsnew{display:none;padding:10px 16px;font-size:13px;background:var(--ink2);border-bottom:1px solid var(--line);max-height:40vh;overflow:auto}
|
|
90
154
|
.whatsnew.show{display:block}
|
|
155
|
+
body.lq-liquid .whatsnew.show{position:fixed;top:60px;left:50%;transform:translateX(-50%);width:min(660px,calc(100vw - 32px));z-index:8;border:1px solid rgba(220,228,240,.25);border-radius:20px;background:rgba(6,8,11,.82);backdrop-filter:blur(16px);box-shadow:0 30px 70px -30px rgba(0,0,0,.85)}
|
|
156
|
+
body.lq-liquid.lq-light .whatsnew.show{background:rgba(255,255,255,.9);border-color:rgba(20,30,45,.15)}
|
|
91
157
|
.updatebar button,.updatebar a.b{font:inherit;font-weight:700;border:0;border-radius:8px;padding:5px 12px;background:var(--ink);color:var(--bone);cursor:pointer;text-decoration:none}
|
|
92
158
|
.ver{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px;margin-left:2px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
93
159
|
footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
@@ -230,7 +296,7 @@ label.check input{width:auto}
|
|
|
230
296
|
<button class="tab" data-tab="settings">Settings</button>
|
|
231
297
|
</div>
|
|
232
298
|
<main>
|
|
233
|
-
<div id="lqsinkrow"><button class="ghost" type="button" id="lqsink" title="close it: it sinks back into the liquid (Esc)">Sink back</button></div>
|
|
299
|
+
<div class="lqedge" aria-hidden="true"></div><div id="lqsinkrow"><button class="ghost" type="button" id="lqsink" title="close it: it sinks back into the liquid (Esc)">Sink back</button></div>
|
|
234
300
|
<section id="panel-map">
|
|
235
301
|
<div class="profile muted" id="profile">Mapping your work…</div>
|
|
236
302
|
<div class="maprow">
|
|
@@ -439,10 +505,10 @@ edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;v
|
|
|
439
505
|
nodes.forEach(function(n){n.vx+=(GW/2-n.x)*0.002;n.vy+=(GH/2-n.y)*0.002;n.x+=Math.max(-9,Math.min(9,n.vx));n.y+=Math.max(-9,Math.min(9,n.vy));n.vx*=0.86;n.vy*=0.86;n.x=Math.max(30,Math.min(GW-30,n.x));n.y=Math.max(24,Math.min(GH-28,n.y));});}}
|
|
440
506
|
function nbrs(id){var s={};s[id]=1;GRAPH.edges.forEach(function(e){if(e.source===id)s[e.target]=1;if(e.target===id)s[e.source]=1;});return s;}
|
|
441
507
|
function render(){if(!GRAPH)return;var idx={};GRAPH.nodes.forEach(function(n){idx[n.id]=n;});var nb=sel?nbrs(sel):null;
|
|
442
|
-
var s="<g id='vp' transform='translate("+view.x.toFixed(1)+","+view.y.toFixed(1)+") scale("+view.k.toFixed(3)+")'>";
|
|
443
|
-
GRAPH.edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var op=nb?((nb[e.source]&&nb[e.target])?0.75:0.06):0.45;s+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' stroke='#
|
|
444
|
-
GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=COLORS[n.type]||"#888";var op=nb?(nb[n.id]?1:0.14):0.95;var
|
|
445
|
-
s+="<g class='node' data-id='"+esc(n.id)+"' opacity='"+op+"'><circle cx='"+n.x.toFixed(1)+"' cy='"+n.y.toFixed(1)+"' r='"+r.toFixed(1)+"' fill='"+col+"'"+
|
|
508
|
+
var s="<defs><radialGradient id='lqsilver' cx='35%' cy='30%' r='75%'><stop offset='0%' stop-color='#FFFFFF'/><stop offset='35%' stop-color='#C9CFD9'/><stop offset='75%' stop-color='#6F7887'/><stop offset='100%' stop-color='#2A2F38'/></radialGradient></defs><g id='vp' transform='translate("+view.x.toFixed(1)+","+view.y.toFixed(1)+") scale("+view.k.toFixed(3)+")'>";
|
|
509
|
+
GRAPH.edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var op=nb?((nb[e.source]&&nb[e.target])?0.75:0.06):0.45;s+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' stroke='"+(LQ.theme&&LQ.theme.light?'#8E97A6':'#5C6878')+"' stroke-width='1' opacity='"+op+"'/>";});
|
|
510
|
+
GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=COLORS[n.type]||"#888";var op=nb?(nb[n.id]?1:0.14):0.95;var title=esc(n.label)+(n.meta?(" - "+(n.meta.commits||0)+" commits"):"");
|
|
511
|
+
s+="<g class='node' data-id='"+esc(n.id)+"' opacity='"+op+"'><circle cx='"+n.x.toFixed(1)+"' cy='"+n.y.toFixed(1)+"' r='"+r.toFixed(1)+"' fill='url(#lqsilver)' stroke='"+col+"' stroke-width='"+(n.id===sel?2.5:1.2)+"'"+(n.id===sel?" stroke-opacity='1'":" stroke-opacity='0.7'")+"><title>"+title+"</title></circle>";
|
|
446
512
|
var cls=n.type==="person"?"lbl-me":"";s+="<text x='"+n.x.toFixed(1)+"' y='"+(n.y+r+12).toFixed(1)+"' text-anchor='middle' class='"+cls+"'>"+esc(n.label)+"</text></g>";});
|
|
447
513
|
document.getElementById("graph").innerHTML=s+"</g>";}
|
|
448
514
|
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=g.stats.base||"your home folder";
|
|
@@ -498,7 +564,8 @@ else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id
|
|
|
498
564
|
// connected model, grounded in the task's repo (and its pending changes).
|
|
499
565
|
function askBtn(t){var n=(t.chat||[]).length;return "<button class='ask"+(n?" has":"")+"' title='"+(n?"questions & answers about this task":"ask a question about this task")+"'>💬"+(n?" "+Math.ceil(n/2):"")+"</button>";}
|
|
500
566
|
function taskById(id){var all=PENDTASKS.concat(ALLTASKS);for(var i=0;i<all.length;i++)if(all[i].id===id)return all[i];return null;}
|
|
501
|
-
function
|
|
567
|
+
function stepsHtml(st){return st&&st.length?"<div class='steps'>"+st.map(function(x){return "<span>"+esc(x)+"</span>";}).join('')+"</div>":"";}
|
|
568
|
+
function chatMsgs(chat){return (chat||[]).map(function(m){return "<div class='msg "+(m.role==='user'?'q':'a')+"'>"+esc(m.text)+(m.role==='user'?'':stepsHtml(m.steps))+"</div>";}).join("");}
|
|
502
569
|
var CHATHINT="<div class='muted' style='font-size:12px'>Ask anything about this task — what it means, how to approach it, or what the agent changed. Answers draw on the repo's commits, README and rules.</div>";
|
|
503
570
|
function wireAsk(row,id){var b=row.querySelector('.ask');if(b)b.addEventListener('click',function(){toggleChat(row,id);});}
|
|
504
571
|
function toggleChat(row,id){var btn=row.querySelector('.ask');var nx=row.nextElementSibling;
|
|
@@ -646,6 +713,32 @@ var LANEWORD={started:'working on it',held:'queued: that lane is busy',done:'don
|
|
|
646
713
|
function loadLanes(){api('/api/lanes').then(function(d){var el=document.getElementById('laneslist');if(!el||!d)return;var hs=d.handoffs||[];
|
|
647
714
|
el.innerHTML=hs.length?"<div class='tgroup'>Handovers <span class='tcount'>"+hs.length+"</span></div>"+hs.slice(0,12).map(function(h){
|
|
648
715
|
return "<div class='task' title='"+escQ(h.result||h.error||h.text)+"'><span class='t'><b>"+esc(h.from)+"</b> → <b>"+esc(h.to)+"</b>: "+esc(h.text)+"</span><span class='rp'"+(h.status==='error'?" style='color:var(--amber)'":"")+">"+esc(LANEWORD[h.status]||h.status)+"</span></div>";}).join(''):'';});}
|
|
716
|
+
|
|
717
|
+
// ---- an agent's work, live (work.mjs parses its stream) ------------------------------
|
|
718
|
+
var WKICON={read:"<path d='M2 4h5l1 1h6v8H2z'/>",edit:"<path d='M3 13l1-3 7-7 2 2-7 7z'/>",run:"<path d='M3 5l3 3-3 3M8 12h5'/>",search:"<circle cx='7' cy='7' r='4'/><path d='M10 10l3 3'/>",web:"<circle cx='8' cy='8' r='5.5'/><path d='M2.5 8h11M8 2.5c2 2 2 9 0 11M8 2.5c-2 2-2 9 0 11'/>",agent:"<path d='M8 2l1.5 4.5L14 8l-4.5 1.5L8 14l-1.5-4.5L2 8l4.5-1.5z'/>",connector:"<path d='M6 2v4M10 2v4M4 6h8v2a4 4 0 0 1-8 0zM8 12v2'/>",tool:"<circle cx='8' cy='8' r='2.5'/>"};
|
|
719
|
+
function wkIcon(k){return "<svg width='14' height='14' viewBox='0 0 16 16' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'>"+(WKICON[k]||WKICON.tool)+"</svg>";}
|
|
720
|
+
function wkMs(ms){if(ms==null)return '';return ms<1000?ms+'ms':ms<60000?(ms/1000).toFixed(ms<10000?1:0)+'s':Math.round(ms/60000)+'m';}
|
|
721
|
+
function wkRing(done,total){var r=15,c=2*Math.PI*r,f=total?done/total:0;return "<svg width='40' height='40' viewBox='0 0 40 40' aria-label='"+done+" of "+total+" tasks done'><defs><linearGradient id='wkg' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='#FFFFFF'/><stop offset='1' stop-color='#6F7887'/></linearGradient></defs><circle cx='20' cy='20' r='"+r+"' fill='none' stroke='rgba(140,150,166,.25)' stroke-width='4'/><circle cx='20' cy='20' r='"+r+"' fill='none' stroke='url(#wkg)' stroke-width='4' stroke-linecap='round' stroke-dasharray='"+(c*f).toFixed(1)+" "+c.toFixed(1)+"' transform='rotate(-90 20 20)'/><text x='20' y='24' text-anchor='middle' font-size='10' font-weight='700' fill='currentColor'>"+done+"/"+total+"</text></svg>";}
|
|
722
|
+
function wkSpark(p){if(!p||p.length<2)return '';var m=Math.max.apply(null,p)||1,w=120,h=26,pts=p.map(function(v,i){return (i*w/(p.length-1)).toFixed(1)+','+(h-2-(v/m)*(h-4)).toFixed(1);}).join(' ');return "<svg width='"+w+"' height='"+h+"' viewBox='0 0 "+w+" "+h+"' aria-label='steps per minute'><polyline points='"+pts+"' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linejoin='round' opacity='.85'/></svg>";}
|
|
723
|
+
function workHtml(a){var w=a.work,pg=a.progress,run=a.status==='running',h='';
|
|
724
|
+
if(!w){if(run)h+="<div class='bar'><i></i></div>";if(pg)h+="<div class='wkstats'><span class='wkstat'><b>"+pg.done+"/"+pg.total+"</b> tasks ticked</span></div>";return h;}
|
|
725
|
+
var stats=[];if(w.model)stats.push("<span class='wkstat'>"+esc(w.model.split('[')[0])+"</span>");stats.push("<span class='wkstat'><b>"+w.count+"</b> step"+(w.count===1?'':'s')+"</span>");
|
|
726
|
+
if(w.turns!=null)stats.push("<span class='wkstat'><b>"+w.turns+"</b> turns</span>");if(w.tokens)stats.push("<span class='wkstat'><b>"+(w.tokens>999999?(w.tokens/1e6).toFixed(1)+'M':w.tokens>999?Math.round(w.tokens/1000)+'k':w.tokens)+"</b> tokens</span>");
|
|
727
|
+
if(w.cost!=null)stats.push("<span class='wkstat'><b>$"+w.cost.toFixed(2)+"</b></span>");if(w.tests)stats.push("<span class='wktest"+(w.tests.failed?' bad':'')+"'>"+w.tests.passed+" passed · "+w.tests.failed+" failed</span>");if(w.errors)stats.push("<span class='wktest bad'>"+w.errors+" error"+(w.errors>1?'s':'')+"</span>");
|
|
728
|
+
h+="<div class='wkstats'>"+stats.join('')+"</div>";
|
|
729
|
+
if(run&&w.doing)h+="<div class='wkdoing'>"+esc(w.doing)+"…</div>";
|
|
730
|
+
var boxes=[];
|
|
731
|
+
if(w.todos&&w.todos.length||pg){var td="<div class='wkbox'><div class='wkh'>"+(pg?wkRing(pg.done,pg.total):'')+"<span>its to-do list</span></div>";
|
|
732
|
+
(w.todos||[]).forEach(function(t){td+="<div class='wktodo "+esc(t.status)+"'><i></i><span>"+esc(t.status==='in_progress'?t.active:t.text)+"</span></div>";});
|
|
733
|
+
if(!(w.todos||[]).length)td+="<div class='muted' style='font-size:12px'>No list of its own: "+pg.done+" of "+pg.total+" tasks ticked in TASKS.md.</div>";boxes.push(td+"</div>");}
|
|
734
|
+
if(w.steps&&w.steps.length){var sp="<div class='wkbox'><div class='wkh'><span>what it did</span><span style='margin-left:auto'>"+wkSpark(w.pace)+"</span></div>";
|
|
735
|
+
w.steps.slice(-10).forEach(function(x){sp+="<div class='wkstep "+esc(x.status)+"'>"+wkIcon(x.kind)+"<span class='wt'><b>"+esc(x.verb)+"</b> "+esc(x.target||'')+"</span>"+(x.tests?"<span class='wktest"+(x.tests.failed?' bad':'')+"'>"+x.tests.passed+"✓ "+x.tests.failed+"✗</span>":"")+"<span class='wm'>"+(x.status==='running'?'…':wkMs(x.ms))+"</span></div>";});
|
|
736
|
+
if(w.count>10)sp+="<div class='muted' style='font-size:11px;margin-top:4px'>and "+(w.count-10)+" earlier step"+(w.count-10>1?'s':'')+"</div>";boxes.push(sp+"</div>");}
|
|
737
|
+
var fs=Object.keys(w.files||{});if(fs.length){var mx=Math.max.apply(null,fs.map(function(f){return w.files[f];}))||1,fb="<div class='wkbox'><div class='wkh'><span>files it changed</span></div>";
|
|
738
|
+
fs.sort(function(x,y){return w.files[y]-w.files[x];}).slice(0,8).forEach(function(f){fb+="<div class='wkfile'><span class='fn'>"+esc(f)+"</span><span class='fb'><i style='width:"+Math.round(100*w.files[f]/mx)+"%'></i></span><span class='wm'>"+w.files[f]+"×</span></div>";});boxes.push(fb+"</div>");}
|
|
739
|
+
if(boxes.length)h+="<div class='wk'>"+boxes.join('')+"</div>";
|
|
740
|
+
if(!run&&w.final)h+="<div class='wkfinal'>"+esc(w.final)+"</div>";
|
|
741
|
+
return h;}
|
|
649
742
|
function loadAgents(){loadLanes();api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
|
|
650
743
|
if(!list||!list.length){AGENTLIST=[];el.innerHTML="<div class='muted' style='margin-top:12px'>No agents yet. In <b>Tasks</b>, tick ideas and hit <b>Send to repos</b> (with an agent command set in Settings) — you'll watch it work here.</div>";stopAgentsPoll();return;}
|
|
651
744
|
var anyRunning=list.some(function(a){return a.status==='running';});
|
|
@@ -660,8 +753,8 @@ if(nQs(a))st+=" · <span style='color:var(--amber)'>needs your answers</sp
|
|
|
660
753
|
if(a.held!=null){var hn=a.held.length,ht=a.held.map(function(t){return "• "+esc(t).replace(/'/g,''');}).join(' ');
|
|
661
754
|
ht+=(hn?' ':'')+(a.status==='running'?"Sent while this agent was running. They wait in .symbiot/TASKS.next.md, replace TASKS.md when it finishes (keeping its ticks), and an agent starts on them then.":"They wait in .symbiot/TASKS.next.md for the agent running in this folder to finish. After that, an agent starts on them the next time the Tasks tab checks this repo, or send again.");
|
|
662
755
|
st+=" · <span style='color:var(--amber);cursor:help' title='"+ht+"'>⏸ "+(hn?hn+" task"+(hn>1?"s":"")+" held":"tasks held")+"</span>";}
|
|
663
|
-
var b="<div class='dh'><span class='
|
|
664
|
-
|
|
756
|
+
var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
|
|
757
|
+
b+=workHtml(a);
|
|
665
758
|
var ch=a.changed;
|
|
666
759
|
if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
667
760
|
var parts=[];
|
|
@@ -673,7 +766,7 @@ if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
|
673
766
|
b+="<div class='changed muted'>No file changes detected (the agent may have only planned or asked).</div>";
|
|
674
767
|
}
|
|
675
768
|
b+=waitHtml(a)+askHtml(a);
|
|
676
|
-
b+="<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>";
|
|
769
|
+
b+=a.work?("<details class='wkraw'><summary>what it said, in full</summary><pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(nothing yet)')+"</pre></details>"):("<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>");
|
|
677
770
|
return "<div class='agent'>"+b+"</div>";}).join("");
|
|
678
771
|
el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
|
|
679
772
|
restoreDrafts(el);wireAsks(el);wireWaits(el);
|
|
@@ -1191,8 +1284,14 @@ var vx=Math.max(0.5,Math.min(S.s*1.3,(S.w/2-90)/400)),vy=Math.max(0.35,Math.min(
|
|
|
1191
1284
|
lay.slice(0,room).forEach(function(it){list.push({id:'shape:'+it.id,kind:'shape',shape:it.id,title:LQNAMES[it.id]||it.id,sub:'',r:it.r*rs,tx:cx+Math.cos(it.angle)*it.d*vx,ty:cy+Math.sin(it.angle)*it.d*vy});});
|
|
1192
1285
|
you.forEach(function(y,i){var ang=-Math.PI/2+(i-(you.length-1)/2)*0.6;list.push({id:y.id,kind:'you',shape:y.shape,title:y.title,sub:y.sub,r:50*rs,tx:cx+Math.cos(ang)*230*vx,ty:cy+Math.sin(ang)*230*vy*1.05,tendril:true});});
|
|
1193
1286
|
feeds.forEach(function(f,i){var ang=Math.PI/2+(i-(feeds.length-1)/2)*0.7;list.push({id:f.id,kind:'feed',shape:f.shape,title:f.title,sub:f.sub,r:(28+5*Math.min(f.count||1,5))*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*300*vy});});
|
|
1194
|
-
|
|
1195
|
-
|
|
1287
|
+
// Solve it. If this screen can't hold them all clear of each other (lqRelax), the
|
|
1288
|
+
// least likely part goes under "more" (Hick: fewer, not cramped) and it solves
|
|
1289
|
+
// again. What only you can do and your feeds always stay out.
|
|
1290
|
+
var base=list.slice(),cur=null,tries=0;base.forEach(function(d){d.r0=d.r;d.tx0=d.tx;d.ty0=d.ty;});
|
|
1291
|
+
for(;;){cur=base.slice();if(more.length)cur.push({id:'more',kind:'more',title:'more',sub:more.map(function(m){return LQNAMES[m]||m;}).join(' · '),more:more.slice(),r0:24*S.s,tx0:S.w-Math.max(70,S.w*0.08),ty0:150});
|
|
1292
|
+
cur.forEach(function(d){d.r=d.r0;d.tx=d.tx0;d.ty=d.ty0;});if(lqRelax(cur,S,cx,cy,92*rs)||tries++>=8)break;
|
|
1293
|
+
var q=-1;for(var z=base.length-1;z>=0;z--)if(base[z].kind==='shape'){q=z;break;}if(q<0)break;more.push(base[q].shape);base.splice(q,1);}
|
|
1294
|
+
list=cur;var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
|
|
1196
1295
|
LQ.drops=list.map(function(d){var o=old[d.id];d.x=o?o.x:cx;d.y=o?o.y:cy;d.vx=o?o.vx:0;d.vy=o?o.vy:0;d.cr=o?o.cr:0;return d;});
|
|
1197
1296
|
var n=(h.you||[]).length,w=h.working||0;
|
|
1198
1297
|
LQ.coreText=n?(n+(n>1?' things need':' thing needs')+' only you'+(w?' · '+w+' agent'+(w>1?'s':'')+' working':'')):(w?w+' agent'+(w>1?'s':'')+' working · nothing needs you':'All handled');
|
|
@@ -1202,13 +1301,26 @@ lqLabels();}
|
|
|
1202
1301
|
// Force-directed layout, solved once per change: push overlapping targets apart
|
|
1203
1302
|
// (each droplet's circle plus its label below or above), and off the core, within
|
|
1204
1303
|
// the screen. Deterministic, so the same data gives the same layout.
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
a.
|
|
1304
|
+
// The one rule for how far apart two droplets sit: past where their metal would
|
|
1305
|
+
// bridge (r²/d² fields sum to 1 midway at d = 2·√(r1²+r2²)), with a margin so the
|
|
1306
|
+
// wobble can't flicker a bridge open and shut, and room for their labels when
|
|
1307
|
+
// they share a column. Used once, when the layout is solved; nothing fights it live.
|
|
1308
|
+
function lqSep(a,b,dx){return Math.max(a.r+b.r+70,2.4*Math.sqrt(a.r*a.r+b.r*b.r))+(Math.abs(dx)<150?48:0);}
|
|
1309
|
+
// Solve the layout: push targets apart until every pair keeps lqSep (and clear
|
|
1310
|
+
// of the core), within the screen. If they can't all fit, every droplet gives up
|
|
1311
|
+
// a little of its size (the liquid is conserved, not crowded) and it solves again.
|
|
1312
|
+
function lqRelax(L,S,cx,cy,coreR){var n=L.length;
|
|
1313
|
+
for(var pass=0;pass<6;pass++){
|
|
1314
|
+
for(var it=0;it<220;it++){var moved=0;for(var i=0;i<n;i++){var a=L[i];
|
|
1315
|
+
for(var j=i+1;j<n;j++){var b=L[j],dx=a.tx-b.tx,dy=a.ty-b.ty,dd=Math.sqrt(dx*dx+dy*dy)||0.01,mn=lqSep(a,b,dx);if(dd<mn){var f=(mn-dd)/2/dd;a.tx+=dx*f;a.ty+=dy*f;b.tx-=dx*f;b.ty-=dy*f;moved++;}}
|
|
1316
|
+
var cdx=a.tx-cx,cdy=a.ty-cy,cd=Math.sqrt(cdx*cdx+cdy*cdy)||0.01,cm=Math.max(a.r+coreR+70,2.4*Math.sqrt(a.r*a.r+coreR*coreR));if(cd<cm){a.tx+=cdx*(cm-cd)/cd;a.ty+=cdy*(cm-cd)/cd;moved++;}
|
|
1317
|
+
a.tx=Math.max(a.r+60,Math.min(S.w-a.r-60,a.tx));a.ty=Math.max(140+a.r,Math.min(S.h-200-a.r,a.ty));}if(!moved)break;}
|
|
1318
|
+
var bad=false;for(var i2=0;i2<n&&!bad;i2++)for(var j2=i2+1;j2<n;j2++){var p=L[i2],q=L[j2],ddx=p.tx-q.tx,ddy=p.ty-q.ty;if(Math.sqrt(ddx*ddx+ddy*ddy)<lqSep(p,q,ddx)-1){bad=true;break;}}
|
|
1319
|
+
if(!bad)return true;L.forEach(function(x){x.r*=0.9;});coreR*=0.9;}return false;}
|
|
1209
1320
|
function lqLabels(){var el=$('lqdrops');if(!el)return;
|
|
1210
1321
|
el.innerHTML=LQ.drops.map(function(d,i){return "<button type='button' class='lqd lq-"+d.kind+"' data-i='"+i+"'><span>"+esc(d.title)+"</span>"+(d.sub?"<small>"+esc(d.sub)+"</small>":"")+"</button>";}).join('');
|
|
1211
1322
|
LQ.btns=[];el.querySelectorAll('.lqd').forEach(function(b){LQ.btns.push(b);b.addEventListener('click',function(ev){lqOpen(LQ.drops[+b.getAttribute('data-i')],ev);});});
|
|
1323
|
+
LQ.drops.forEach(function(d){if(d.x===d.tx&&d.y===d.ty)return;if(!LQ.frame){d.x=d.tx;d.y=d.ty;d.cr=d.r;}});lqStep();
|
|
1212
1324
|
var c=$('lqcore');if(c)c.textContent=LQ.coreText||'';}
|
|
1213
1325
|
function lqVia(ev){return LQ.talking?'talk':ev&&ev.pointerType==='touch'?'touch':ev&&ev.detail===0?'key':'click';}
|
|
1214
1326
|
function lqUse(shape,ev,via){api('/api/adapt/use',{shape:shape,from:LQ.last,via:via||lqVia(ev)});LQ.last=shape;}
|
|
@@ -1222,13 +1334,13 @@ function lqPool(shape){tabPicked=true;setTab(shape);var b=document.body;if(b&&b.
|
|
|
1222
1334
|
function lqSink(){var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
|
|
1223
1335
|
function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
|
|
1224
1336
|
function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
|
|
1225
|
-
function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;el.innerHTML=LQ.talk.map(function(m){return "<div class='lqmsg"+(m.me?' me':'')+"'>"+esc(m.text)+"</div>";}).join('')+(wait?"<div class='lqmsg'
|
|
1337
|
+
function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;el.innerHTML=LQ.talk.map(function(m){return "<div class='lqmsg"+(m.me?' me':'')+"'>"+esc(m.text)+stepsHtml(m.steps)+"</div>";}).join('')+(wait?"<div class='lqmsg thinking'>Thinking: recalling what it knows, reading what's here…</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;}
|
|
1226
1338
|
function lqSay(){var i=$('lqask');var q=((i&&i.value)||'').trim();if(!q)return;i.value='';lqAct();
|
|
1227
1339
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
1228
1340
|
Object.keys(LQNAMES).forEach(function(k){var nm=LQNAMES[k].toLowerCase();if(hit)return;['open ','show ','go to ','take me to '].forEach(function(v){if(low.indexOf(v+nm)===0||low.indexOf(v+'my '+nm)===0)hit=k;});if(low===nm)hit=k;});
|
|
1229
1341
|
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');lqPool(hit);return;}
|
|
1230
1342
|
LQ.talk.push({me:true,text:q});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
1231
|
-
api('/api/home/ask',{question:q}).then(function(r){LQ.talk.push({me:false,text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
1343
|
+
api('/api/home/ask',{question:q}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
1232
1344
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
1233
1345
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
1234
1346
|
function lqStep(){var S0=lqSize();if(LQ.adapt&&(S0.w!==LQ.bw||S0.h!==LQ.bh))lqBuild();var S=S0,cx=S.w/2,cy=S.h*0.47,k=0.022,c=2*Math.sqrt(k),th=LQ.theme,still=th.still,rest=LQ.mode==='rest',pool=LQ.mode==='pool',talk=LQ.talking&&!pool;
|
|
@@ -1240,8 +1352,6 @@ if(rest){tx=cx+Math.cos(i*1.3)*16;ty=cy+Math.sin(i*1.3)*16;tr=d.r*0.6;}
|
|
|
1240
1352
|
else if(pool){tx=cx+(i-(N-1)/2)*40*S.s;ty=S.h-40;tr=13*S.s;}
|
|
1241
1353
|
else if(talk){ty=S.h*0.12+(d.ty-S.h*0.12)*0.55;}
|
|
1242
1354
|
var ax=k*(tx-d.x)-c*d.vx,ay=k*(ty-d.y)-c*d.vy;
|
|
1243
|
-
if(!rest&&!pool){for(var j=0;j<N;j++){if(i===j)continue;var e=D[j],dx=d.x-e.x,dy=d.y-e.y,dd=Math.sqrt(dx*dx+dy*dy)||1,mn=d.cr+e.cr+78+(Math.abs(dx)<((d.lw||110)+(e.lw||110))/2?52:0);if(dd<mn){var f=0.06*(mn-dd)/dd;ax+=dx*f;ay+=dy*f;}}
|
|
1244
|
-
var dx0=d.x-core.x,dy0=d.y-core.y,d0=Math.sqrt(dx0*dx0+dy0*dy0)||1,m0=d.cr+core.cr+80;if(d0<m0){var f0=0.06*(m0-d0)/d0;ax+=dx0*f0;ay+=dy0*f0;}}
|
|
1245
1355
|
if(still){d.x=tx;d.y=ty;d.vx=0;d.vy=0;}else{d.vx+=ax;d.vy+=ay;d.x+=d.vx;d.y+=d.vy;}
|
|
1246
1356
|
if(!rest&&!pool){d.x=Math.max(d.cr+12,Math.min(S.w-d.cr-12,d.x));d.y=Math.max(140+d.cr,Math.min(S.h-150-d.cr-50,d.y));}
|
|
1247
1357
|
d.cr+=(tr-d.cr)*(still?1:0.06);
|
|
@@ -1250,7 +1360,7 @@ var cb=$('lqcore');if(cb&&cb.style){cb.style.transform='translate('+Math.round(c
|
|
|
1250
1360
|
// The liquid: a metaball surface (Σ r²/d² = 1) shaded as chrome, on the GPU.
|
|
1251
1361
|
var LQ_FS=['precision highp float;',
|
|
1252
1362
|
'uniform vec2 uRes;uniform float uT;uniform float uDpr;uniform vec3 uB[16];uniform vec3 uRip;uniform float uLight;uniform float uContrast;uniform vec4 uAccent;uniform float uExposure;',
|
|
1253
|
-
'float field(vec2 p){float f=0.0;for(int i=0;i<16;i++){vec3 b=uB[i];float r=b.z*uDpr;vec2 d=p-b.xy*uRes;f+=r*r/(dot(d,d)+1.0);}vec2 q=p/uDpr;return f*(1.0+0.
|
|
1363
|
+
'float field(vec2 p){float f=0.0;for(int i=0;i<16;i++){vec3 b=uB[i];float r=b.z*uDpr;vec2 d=p-b.xy*uRes;f+=r*r/(dot(d,d)+1.0);}vec2 q=p/uDpr;return f*(1.0+0.025*sin(q.x*0.011+uT*0.7)*cos(q.y*0.009-uT*0.55)+0.012*sin(q.x*0.031-q.y*0.027+uT*1.3));}',
|
|
1254
1364
|
'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
|
|
1255
1365
|
'vec3 env(vec3 r){float y=r.y;vec3 c=mix(mix(vec3(0.02,0.022,0.03),vec3(0.36,0.38,0.43),uLight),vec3(0.82,0.84,0.90),smoothstep(-0.35,0.55,y));c+=vec3(1.0)*pow(max(0.0,1.0-abs(y-0.18+0.04*sin(uT*0.3))*5.0),4.0)*1.1;c+=vec3(0.75,0.82,1.0)*pow(max(0.0,r.x),10.0)*0.9;c+=vec3(1.0,0.93,0.86)*pow(max(0.0,-r.x),14.0)*0.5;c*=0.92+0.08*sin(r.x*4.0+r.y*3.0+uT*0.4);return c;}',
|
|
1256
1366
|
'void main(){vec2 p=vec2(gl_FragCoord.x,uRes.y-gl_FragCoord.y);float f=field(p);float e=2.0*uDpr;',
|
|
@@ -1281,10 +1391,12 @@ function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=Math.min(window.devicePixelRat
|
|
|
1281
1391
|
if(c.width!==W||c.height!==H){c.width=W;c.height=H;gl.viewport(0,0,W,H);}
|
|
1282
1392
|
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;
|
|
1283
1393
|
for(var i=0;i<48;i++)o[i]=0;
|
|
1284
|
-
var put=function(k,x,y,r){if(k>15)return;var w=still?0:1;o[k*3]=x/S.w+Math.sin(t*1.3+k*1.7)*0.004*w;o[k*3+1]=y/S.h+Math.cos(t*1.1+k*2.3)*0.005*w;o[k*3+2]=Math.max(0,r)*(1+0.035*Math.sin(t*2.0+k)*w);};
|
|
1394
|
+
var put=function(k,x,y,r){if(k>15)return;var w=still?0:(k===0?1:0.3);o[k*3]=x/S.w+Math.sin(t*1.3+k*1.7)*0.004*w;o[k*3+1]=y/S.h+Math.cos(t*1.1+k*2.3)*0.005*w;o[k*3+2]=Math.max(0,r)*(1+0.035*Math.sin(t*2.0+k)*w);};
|
|
1285
1395
|
put(0,LQ.core.x,LQ.core.y,LQ.core.cr);var k=1,tend=[];
|
|
1286
1396
|
LQ.drops.forEach(function(dr){put(k++,dr.x,dr.y,dr.cr);if(dr.tendril&&LQ.mode==='aware'&&!LQ.talking)tend.push(dr);});
|
|
1287
|
-
|
|
1397
|
+
// a tendril is a chain of blobs close enough to always bridge (spaced at under 2.8r),
|
|
1398
|
+
// never one blob floating in the gap: drawn whole or not at all
|
|
1399
|
+
tend.forEach(function(dr){var dx=dr.x-LQ.core.x,dy=dr.y-LQ.core.y,len=Math.sqrt(dx*dx+dy*dy)||1,gap=len-LQ.core.cr-dr.cr;if(gap<=8)return;var n=Math.max(1,Math.min(3,Math.ceil(gap/40))),sp=gap/(n+1),rr=sp/2;if(k+n>15)return;for(var m=1;m<=n;m++){var at=LQ.core.cr+sp*m;put(k++,LQ.core.x+dx/len*at,LQ.core.y+dy/len*at,rr);}});
|
|
1288
1400
|
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;}});
|
|
1289
1401
|
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(15,best.x+dx/dl*reach,best.y+dy/dl*reach,8+20*(1-Math.max(0,bd)/230));}}
|
|
1290
1402
|
var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform3fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
|
|
@@ -1301,7 +1413,7 @@ var mn=document.querySelector('main');if(mn&&mn.addEventListener)mn.addEventList
|
|
|
1301
1413
|
lqLoad(true);
|
|
1302
1414
|
// the only clocks: rest when left alone, fresh data now and then, the hour (night)
|
|
1303
1415
|
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);
|
|
1304
|
-
var G=lqGL();if(typeof window.requestAnimationFrame!=='function')return;var raf=function(f){return window.requestAnimationFrame(f);};
|
|
1416
|
+
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);};
|
|
1305
1417
|
function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0){lqStep();if(G)lqDraw(G);}}raf(frame);}
|
|
1306
1418
|
raf(frame);}
|
|
1307
1419
|
|
package/watch.mjs
CHANGED
|
@@ -301,7 +301,8 @@ async function boardChat(id, question, { hours = 72, now = Date.now(), ask = wri
|
|
|
301
301
|
const answer = r.reply;
|
|
302
302
|
// read again: a check may have saved the file while the model answered
|
|
303
303
|
const d2 = loadWatch(), w2 = d2.watches.find((x) => x.id === id); if (!w2) return { answer, chat: [] };
|
|
304
|
-
|
|
304
|
+
const steps = [...(peeks.length ? [`looked up ${peeks.length} link${peeks.length > 1 ? "s" : ""}`] : []), ...(r.steps || [])];
|
|
305
|
+
w2.chat = [...(w2.chat || []), { role: "user", text: question, ts: now }, { role: "ai", text: answer, ts: now, ...(steps.length ? { steps } : {}) }].slice(-TALK_KEEP);
|
|
305
306
|
saveWatch(d2);
|
|
306
307
|
return { answer, chat: w2.chat, ...(peeks.length ? { links: peeks } : {}), ...(r.did ? { did: r.did } : {}) };
|
|
307
308
|
}
|
package/work.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// symbiot — Work: what an agent is doing, as it does it. Claude Code runs with
|
|
2
|
+
// --output-format stream-json (agents.mjs withStream), so every step lands in the
|
|
3
|
+
// run's .symbiot/agent.log as it happens: each tool it calls and what came back,
|
|
4
|
+
// its own to-do list (TaskCreate/TaskUpdate, or the older TodoWrite) with the
|
|
5
|
+
// "Reading x.txt" line for what's in progress, what it says between steps, and at
|
|
6
|
+
// the end what it answered, what it cost, how many turns and tokens. parseRun
|
|
7
|
+
// turns that into something to draw: steps, to-dos, tests, files, a pace over
|
|
8
|
+
// time. Another agent (Codex, Aider, Gemini) writes plain text: its last lines
|
|
9
|
+
// come through as what it said. Pure: text in, data out.
|
|
10
|
+
|
|
11
|
+
const MAX_STEPS = 60;
|
|
12
|
+
const short = (s, n = 80) => { s = String(s ?? "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
13
|
+
const base = (p) => String(p || "").split("/").filter(Boolean).slice(-2).join("/");
|
|
14
|
+
|
|
15
|
+
// One tool call, as a person would say it: { kind, verb, target }.
|
|
16
|
+
function describe(name, input = {}) {
|
|
17
|
+
const n = String(name || "");
|
|
18
|
+
if (n === "Read") return { kind: "read", verb: "Read", target: base(input.file_path) };
|
|
19
|
+
if (n === "Edit" || n === "MultiEdit") return { kind: "edit", verb: "Edited", target: base(input.file_path) };
|
|
20
|
+
if (n === "Write") return { kind: "edit", verb: "Wrote", target: base(input.file_path) };
|
|
21
|
+
if (n === "NotebookEdit") return { kind: "edit", verb: "Edited", target: base(input.notebook_path) };
|
|
22
|
+
if (n === "Bash") return { kind: "run", verb: "Ran", target: short(input.description || input.command, 70) };
|
|
23
|
+
if (n === "Grep") return { kind: "search", verb: "Searched for", target: short(input.pattern, 50) };
|
|
24
|
+
if (n === "Glob") return { kind: "search", verb: "Listed", target: short(input.pattern, 50) };
|
|
25
|
+
if (n === "WebFetch") return { kind: "web", verb: "Read", target: short(input.url, 60) };
|
|
26
|
+
if (n === "WebSearch") return { kind: "web", verb: "Searched the web for", target: short(input.query, 60) };
|
|
27
|
+
if (n === "Task" || n === "Agent") return { kind: "agent", verb: "Asked a helper to", target: short(input.description || input.prompt, 60) };
|
|
28
|
+
if (/^mcp__/.test(n)) { const parts = n.split("__"); return { kind: "connector", verb: "Used", target: short((parts[1] || "").replace(/^claude_ai_/, "").replace(/_/g, " ") + (parts[2] ? " · " + parts[2].replace(/_/g, " ") : ""), 60) }; }
|
|
29
|
+
return { kind: "tool", verb: "Used", target: n };
|
|
30
|
+
}
|
|
31
|
+
// Tools that are bookkeeping, not work: the to-do list (shown as to-dos) and
|
|
32
|
+
// looking up its own tools.
|
|
33
|
+
const QUIET = new Set(["TaskCreate", "TaskUpdate", "TaskList", "TaskGet", "TodoWrite", "ToolSearch", "ExitPlanMode"]);
|
|
34
|
+
|
|
35
|
+
// A test run's tally from its output: { passed, failed } or null.
|
|
36
|
+
function testsIn(text) {
|
|
37
|
+
const t = String(text || ""); let passed = 0, failed = 0, seen = false;
|
|
38
|
+
for (const m of t.matchAll(/(\d+)\s+passed,\s+(\d+)\s+failed/g)) { passed += +m[1]; failed += +m[2]; seen = true; }
|
|
39
|
+
if (!seen) { const p = t.match(/(\d+)\s+(?:tests?\s+)?passed/), f = t.match(/(\d+)\s+(?:tests?\s+)?failed/); if (p || f) { passed = p ? +p[1] : 0; failed = f ? +f[1] : 0; seen = true; } }
|
|
40
|
+
return seen ? { passed, failed } : null;
|
|
41
|
+
}
|
|
42
|
+
const resultText = (c) => (typeof c === "string" ? c : Array.isArray(c) ? c.map((x) => (x && typeof x.text === "string" ? x.text : "")).join("\n") : "");
|
|
43
|
+
|
|
44
|
+
// The text of one run's log (everything after its "=== name time ===" header and
|
|
45
|
+
// command line) → { stream, model, steps, todos, doing, said, final, cost, turns,
|
|
46
|
+
// tokens, tests, files, pace, errors }.
|
|
47
|
+
function parseRun(text) {
|
|
48
|
+
const lines = String(text || "").split("\n");
|
|
49
|
+
const events = []; let plain = [];
|
|
50
|
+
for (const l of lines) { if (l.startsWith("{")) { try { events.push(JSON.parse(l)); continue; } catch {} } if (l.trim()) plain.push(l); }
|
|
51
|
+
const out = { stream: events.length > 0, model: "", steps: [], todos: [], doing: "", said: [], final: "", cost: null, turns: null, tokens: null, tests: null, files: {}, pace: [], errors: 0 };
|
|
52
|
+
if (!out.stream) { out.said = plain.slice(-12).map((l) => short(l, 160)); out.final = plain.slice(-40).join("\n").trim(); return out; }
|
|
53
|
+
const byId = new Map(), tasks = new Map(); let thinking = false;
|
|
54
|
+
for (const e of events) {
|
|
55
|
+
const at = e.timestamp ? Date.parse(e.timestamp) : null;
|
|
56
|
+
if (e.type === "system" && e.subtype === "init") { out.model = String(e.model || ""); continue; }
|
|
57
|
+
if (e.type === "assistant" && e.message && Array.isArray(e.message.content)) {
|
|
58
|
+
for (const b of e.message.content) {
|
|
59
|
+
if (b.type === "thinking") { thinking = true; continue; }
|
|
60
|
+
if (b.type === "text" && b.text && b.text.trim()) { out.said.push(short(b.text, 220)); thinking = false; continue; }
|
|
61
|
+
if (b.type !== "tool_use") continue;
|
|
62
|
+
thinking = false;
|
|
63
|
+
const inp = b.input || {};
|
|
64
|
+
if (b.name === "TodoWrite" && Array.isArray(inp.todos)) { tasks.clear(); inp.todos.forEach((t, i) => tasks.set(String(i + 1), { text: short(t.content, 120), status: t.status || "pending", active: short(t.activeForm || t.content, 90) })); continue; }
|
|
65
|
+
if (b.name === "TaskCreate") { byId.set(b.id, { create: { text: short(inp.subject || inp.description, 120), active: short(inp.activeForm || inp.subject, 90) } }); continue; }
|
|
66
|
+
if (b.name === "TaskUpdate") { const t = tasks.get(String(inp.taskId)); if (t && inp.status) t.status = inp.status; if (t && inp.subject) t.text = short(inp.subject, 120); continue; }
|
|
67
|
+
if (QUIET.has(b.name)) continue;
|
|
68
|
+
const d = describe(b.name, inp), step = { ...d, at, status: "running" };
|
|
69
|
+
if (d.kind === "edit" && d.target) out.files[d.target] = (out.files[d.target] || 0) + 1;
|
|
70
|
+
byId.set(b.id, step); out.steps.push(step);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (e.type === "user" && e.message && Array.isArray(e.message.content)) {
|
|
75
|
+
for (const b of e.message.content) {
|
|
76
|
+
if (b.type !== "tool_result") continue;
|
|
77
|
+
const s = byId.get(b.tool_use_id), txt = resultText(b.content);
|
|
78
|
+
if (s && s.create) { const m = txt.match(/Task #(\w+)/); tasks.set(m ? m[1] : String(tasks.size + 1), { text: s.create.text, status: "pending", active: s.create.active }); continue; }
|
|
79
|
+
if (!s) continue;
|
|
80
|
+
s.status = b.is_error ? "error" : "done"; if (b.is_error) { out.errors++; s.note = short(txt, 120); }
|
|
81
|
+
if (at && s.at) s.ms = at - s.at;
|
|
82
|
+
if (s.kind === "run") { const tt = testsIn(txt); if (tt) { s.tests = tt; out.tests = tt; } }
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (e.type === "result") {
|
|
87
|
+
out.final = String(e.result || "").trim();
|
|
88
|
+
out.cost = typeof e.total_cost_usd === "number" ? e.total_cost_usd : null;
|
|
89
|
+
out.turns = typeof e.num_turns === "number" ? e.num_turns : null;
|
|
90
|
+
const u = e.usage || {}; out.tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0) || null;
|
|
91
|
+
if (e.is_error) out.errors++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
out.todos = [...tasks.values()];
|
|
95
|
+
const now = out.todos.find((t) => t.status === "in_progress"), last = out.steps[out.steps.length - 1];
|
|
96
|
+
out.doing = out.final ? "" : now ? now.active : last && last.status === "running" ? `${last.verb} ${last.target}`.trim() : thinking ? "Thinking" : "";
|
|
97
|
+
// pace: steps per minute over the run, up to 24 buckets (a sparkline)
|
|
98
|
+
const times = out.steps.map((s) => s.at).filter(Boolean);
|
|
99
|
+
if (times.length > 1) { const t0 = times[0], span = Math.max(times[times.length - 1] - t0, 60000), n = Math.min(24, Math.max(6, Math.ceil(span / 60000))), w = span / n; out.pace = new Array(n).fill(0); for (const t of times) out.pace[Math.min(n - 1, Math.floor((t - t0) / w))]++; }
|
|
100
|
+
out.steps = out.steps.slice(-MAX_STEPS).map(({ at, ...s }) => ({ ...s, ...(at ? { at } : {}) }));
|
|
101
|
+
out.said = out.said.slice(-6);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The newest run's part of an agent.log (after its header and command line).
|
|
106
|
+
function lastRunText(log) {
|
|
107
|
+
const s = String(log || ""), at = s.lastIndexOf("\n=== ");
|
|
108
|
+
return at < 0 ? s : s.slice(at).split("\n").slice(3).join("\n");
|
|
109
|
+
}
|
|
110
|
+
// What the agent said last: its final answer, from either kind of log.
|
|
111
|
+
function finalOf(log) { return parseRun(lastRunText(log)).final; }
|
|
112
|
+
|
|
113
|
+
export { parseRun, lastRunText, finalOf, describe, testsIn };
|
package/writeups.mjs
CHANGED
|
@@ -161,7 +161,7 @@ async function taskChat(id, question) {
|
|
|
161
161
|
// Re-read: other requests may have changed tasks.json while the model ran.
|
|
162
162
|
const t = loadTasks(); const cur = t.find((x) => x.id === id); const now = Date.now();
|
|
163
163
|
if (!cur) return { answer, chat: [] };
|
|
164
|
-
cur.chat = [...(cur.chat || []), { role: "user", text: question, ts: now }, { role: "ai", text: answer, ts: now }].slice(-CHAT_KEEP);
|
|
164
|
+
cur.chat = [...(cur.chat || []), { role: "user", text: question, ts: now }, { role: "ai", text: answer, ts: now, ...(r.steps && r.steps.length ? { steps: r.steps } : {}) }].slice(-CHAT_KEEP);
|
|
165
165
|
saveTasks(t);
|
|
166
166
|
return { answer, chat: cur.chat, ...(r.did ? { did: r.did } : {}) };
|
|
167
167
|
}
|