symbiot 0.45.0 → 0.47.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 +15 -0
- package/agents.mjs +26 -6
- package/home.mjs +37 -4
- package/lanes.mjs +3 -1
- package/mind.mjs +10 -2
- package/package.json +3 -2
- package/server.mjs +4 -1
- package/ui.mjs +212 -36
- package/watch.mjs +2 -1
- package/work.mjs +113 -0
- package/writeups.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.47.0
|
|
6
|
+
|
|
7
|
+
- **Tasks and agents in the liquid.** Opening Tasks or Agents re-forms the liquid: each agent at work is a sphere with small spheres revolving round it, named for what it's doing in plain words; work ready for your OK sits at the top; tasks waiting their turn sit below. One **Go** (or just say "go") starts everything waiting.
|
|
8
|
+
- **Tags float on their spheres.** Each sphere carries just its name, floating with a shadow. Tap the sphere to open its tag for a line about it and **Open** for the details.
|
|
9
|
+
- **Less to read.** An agent's block shows what it's doing and its to-do list; steps, files, tokens and cost are under "details".
|
|
10
|
+
- **No stray box.** The moving silver edge round open panels is gone.
|
|
11
|
+
|
|
12
|
+
## 0.46.0 — 2026-10-06
|
|
13
|
+
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **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).
|
|
19
|
+
|
|
5
20
|
## 0.45.0 — 2026-10-06
|
|
6
21
|
|
|
7
22
|
- **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
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// how you use them; this is the live data around them. Home's talk goes to the
|
|
8
8
|
// same Symbiot as every chat (mind.mjs converse), told what home shows.
|
|
9
9
|
import { watchBoard } from "./watch.mjs";
|
|
10
|
-
import { pendingReview } from "./tasks.mjs";
|
|
11
|
-
import { agentsList } from "./agents.mjs";
|
|
10
|
+
import { pendingReview, pushTasks } from "./tasks.mjs";
|
|
11
|
+
import { agentsList, runHandoff } from "./agents.mjs";
|
|
12
|
+
import { loadTasks } from "./core.mjs";
|
|
12
13
|
import { lanesState } from "./lanes.mjs";
|
|
13
14
|
import { converse, actIn, actNow, taskIn } from "./mind.mjs";
|
|
14
15
|
import { repoPathMap } from "./scan.mjs";
|
|
@@ -68,7 +69,39 @@ async function homeAsk(question, { ask, now = Date.now(), state } = {}) {
|
|
|
68
69
|
agent: (req, known, repo) => (repo ? actIn(req, repo, { map, known, title: "Home" }) : actNow(req, { title: "Home", known })),
|
|
69
70
|
task: (text, repo) => taskIn(text, repo, { map }),
|
|
70
71
|
} });
|
|
71
|
-
return { answer: r.reply, ...(r.did ? { did: r.did } : {}) };
|
|
72
|
+
return { answer: r.reply, ...(r.did ? { did: r.did } : {}), steps: r.steps || [] };
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
// ---- the work scene: the liquid when you open Tasks or Agents ---------------------
|
|
76
|
+
// Plain words for someone who doesn't read diffs: what each agent is doing now,
|
|
77
|
+
// what's waiting its turn, what's done and waiting for your OK, and whether
|
|
78
|
+
// there's anything a Go would start. The details (steps, files, cost) stay one
|
|
79
|
+
// tap away, in the Agents panel.
|
|
80
|
+
const plain = (s, n = 64) => { s = String(s || "").replace(/[\`*_#>]/g, "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1).replace(/\s+\S*$/, "") + "…" : s; };
|
|
81
|
+
function workScene({ deps = {} } = {}) {
|
|
82
|
+
const agents = deps.agents || (() => { try { return agentsList(); } catch { return []; } });
|
|
83
|
+
const pending = deps.pending || (() => { try { return pendingReview(); } catch { return []; } });
|
|
84
|
+
const tasks = deps.tasks || (() => { try { return loadTasks(); } catch { return []; } });
|
|
85
|
+
const seen = new Set(), running = [];
|
|
86
|
+
for (const a of agents()) {
|
|
87
|
+
if (a.status !== "running" || seen.has(a.path)) continue; seen.add(a.path);
|
|
88
|
+
const w = a.work || {}, todo = (w.todos || []).find((t) => t.status === "in_progress"), pg = a.progress;
|
|
89
|
+
running.push({ id: "run:" + a.path, path: a.path, name: a.name, doing: plain(todo ? todo.active : w.doing || "Working on it", 56), progress: pg || null, waiting: !!(a.ask && a.ask.questions && a.ask.questions.length) });
|
|
90
|
+
}
|
|
91
|
+
const ready = pending().filter((r) => r.path && !r.running && ((r.tasks || []).length || (r.files || []).length)).map((r) => ({ id: "ready:" + r.repo, repo: r.repo, count: (r.tasks || []).length }));
|
|
92
|
+
const busy = new Set(running.map((r) => r.name)), open = tasks().filter((t) => !t.done && !t.archived && !t.review && t.repo);
|
|
93
|
+
const waiting = open.map((t) => ({ id: "task:" + t.id, text: plain(t.text, 56), repo: t.repo, busy: busy.has(t.repo) }));
|
|
94
|
+
return { running, ready, waiting: waiting.slice(0, 12), waitingCount: waiting.length, canGo: waiting.filter((w) => !w.busy).length };
|
|
95
|
+
}
|
|
96
|
+
// Go: everything waiting goes to its repo's agent, the way Send to repos and an
|
|
97
|
+
// agent per repo would: briefs written, an agent started in each (or queued
|
|
98
|
+
// behind one already there). { started, queued, repos } or { error }.
|
|
99
|
+
function workGo({ push = pushTasks, run = runHandoff } = {}) {
|
|
100
|
+
const r = push({});
|
|
101
|
+
if (r.empty) return { started: 0, queued: 0, repos: [], note: "Nothing waiting to start." };
|
|
102
|
+
let started = 0, queued = 0; const repos = [];
|
|
103
|
+
for (const w of r.written || []) { const e = run(w.path); if (e && e.id && !e.busy && !e.blocked) started++; else queued++; repos.push(w.name); }
|
|
104
|
+
return { started, queued, repos, ...(r.unresolved && r.unresolved.length ? { unresolved: r.unresolved.map((u) => u.name) } : {}) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { homeState, homeContext, homeAsk, workScene, workGo };
|
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.47.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/server.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { linksState, linkSite, checkLink, unlinkSite } from "./links.mjs";
|
|
|
26
26
|
import { mindState, forget } from "./mind.mjs";
|
|
27
27
|
import { lanesTick, lanesState } from "./lanes.mjs";
|
|
28
28
|
import { adaptState, noteUse } from "./adapt.mjs";
|
|
29
|
-
import { homeState, homeAsk } from "./home.mjs";
|
|
29
|
+
import { homeState, homeAsk, workScene, workGo } from "./home.mjs";
|
|
30
30
|
import { phoneState, setPhoneLink, newCode, unpairPhone, pairComputer, forgetComputer, pollComputer, startPhone } from "./phone.mjs";
|
|
31
31
|
|
|
32
32
|
// The in-app update installs the EXACT newest version (not the `latest` tag, which
|
|
@@ -280,6 +280,9 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
280
280
|
// Home (home.mjs): the liquid's live data, and its talk; Adapt (adapt.mjs): its
|
|
281
281
|
// shape from how you use it (commit=1 when it wakes from rest, never mid-gesture).
|
|
282
282
|
if (u.pathname === "/api/home") return json(res, homeState({ fresh: u.searchParams.get("fresh") === "1" }));
|
|
283
|
+
// the work scene: what agents are doing, what's waiting, what's ready; Go starts what's waiting
|
|
284
|
+
if (u.pathname === "/api/work") return json(res, workScene());
|
|
285
|
+
if (u.pathname === "/api/work/go" && req.method === "POST") return json(res, workGo());
|
|
283
286
|
if (u.pathname === "/api/home/ask" && req.method === "POST") { const b = await readBody(req); return json(res, await homeAsk(b.question)); }
|
|
284
287
|
if (u.pathname === "/api/adapt") return json(res, adaptState({ from: String(u.searchParams.get("from") || ""), commit: u.searchParams.get("commit") === "1", ...(u.searchParams.has("touch") ? { touch: u.searchParams.get("touch") === "1" } : {}) }));
|
|
285
288
|
if (u.pathname === "/api/adapt/use" && req.method === "POST") { const b = await readBody(req); return json(res, noteUse(b)); }
|
package/ui.mjs
CHANGED
|
@@ -19,8 +19,15 @@ body.lq-liquid.lq-light #liquid{background:#EDEFF3}
|
|
|
19
19
|
#lq{position:absolute;inset:0;width:100%;height:100%;display:block}
|
|
20
20
|
body.lq-forced #lq{display:none}
|
|
21
21
|
#lqdrops{position:absolute;inset:0;pointer-events:none}
|
|
22
|
-
.lqd{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;
|
|
23
|
-
.lqd
|
|
22
|
+
.lqd{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;border-radius:999px;border:1px solid rgba(220,228,240,.35);background:rgba(4,5,7,.72);backdrop-filter:blur(10px);color:#EEF2F8;font:600 13.5px/1.2 var(--sans);pointer-events:auto;transition:opacity .5s ease,border-radius .25s ease;will-change:transform;box-shadow:0 16px 30px -12px rgba(0,0,0,.7),0 3px 8px rgba(0,0,0,.35)}
|
|
23
|
+
.lqd .lt{all:unset;box-sizing:border-box;display:flex;align-items:center;justify-content:center;min-height:44px;padding:8px 15px;cursor:pointer;text-align:center;max-width:240px}
|
|
24
|
+
.lqd .lt span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
25
|
+
.lqd .lgo{display:none;margin:4px 10px 10px;min-height:38px;padding:0 16px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;font:700 12.5px var(--sans);cursor:pointer}
|
|
26
|
+
.lqd.open{z-index:5;border-radius:20px}.lqd.open .lt span{white-space:normal}.lqd.open .lt{max-width:280px}.lqd.open .lgo{display:block}
|
|
27
|
+
.lqd .lt:focus-visible,.lqd .lgo:focus-visible{outline:2px solid currentColor;outline-offset:2px;border-radius:999px}
|
|
28
|
+
.lqmb{min-height:44px;padding:8px 14px;border-radius:999px;border:1px solid rgba(220,228,240,.35);background:rgba(4,5,7,.72);backdrop-filter:blur(10px);color:#EEF2F8;font:600 13.5px var(--sans);cursor:pointer}
|
|
29
|
+
body.lq-light .lqmb{background:rgba(255,255,255,.86);color:#10151C;border-color:rgba(20,30,45,.25)}
|
|
30
|
+
.lqd small{display:none;font-weight:500;font-size:12px;line-height:1.35;color:#A9B3C2;max-width:260px;padding:0 15px 2px;text-align:center}.lqd.open small{display:block}
|
|
24
31
|
.lqd.lq-you{border-color:rgba(255,255,255,.9)}
|
|
25
32
|
.lqd:hover{border-color:rgba(255,255,255,.95)}
|
|
26
33
|
.lqd:focus-visible,#lqcore:focus-visible,#lqask:focus-visible{outline:2px solid currentColor;outline-offset:3px}
|
|
@@ -33,7 +40,7 @@ body.lq-light.lq-contrast .lqd{background:#FFF;color:#000;border:2px solid #000}
|
|
|
33
40
|
body.lq-light.lq-contrast .lqd small{color:#000}
|
|
34
41
|
body.lq-solid .lqd,body.lq-solid .lqglass{backdrop-filter:none;background:#05070A}
|
|
35
42
|
body.lq-light.lq-solid .lqd,body.lq-light.lq-solid .lqglass{background:#FFFFFF}
|
|
36
|
-
body.lq-touch .lqd{min-height:48px
|
|
43
|
+
body.lq-touch .lqd .lt{min-height:48px}
|
|
37
44
|
body.lq-keys .lqd:focus{outline:2px solid currentColor;outline-offset:3px}
|
|
38
45
|
#lqcore{position:absolute;left:0;top:0;min-height:44px;padding:0 16px;border:1px solid rgba(220,228,240,.22);border-radius:999px;background:rgba(4,5,7,.6);backdrop-filter:blur(8px);color:#C3CBD7;font:500 13px var(--sans);letter-spacing:.05em;cursor:pointer;transition:opacity .6s ease;white-space:nowrap}
|
|
39
46
|
body.lq-light #lqcore{color:#2B3644;background:rgba(255,255,255,.78);border-color:rgba(20,30,45,.18)}
|
|
@@ -60,6 +67,81 @@ body.lq-liquid footer{position:fixed;right:12px;top:38px;border:0;padding:0;z-in
|
|
|
60
67
|
body.lq-pooled #lqtalk,body.lq-pooled #lqmore,body.lq-pooled #lqcore{display:none}
|
|
61
68
|
body.lq-talking .lqd{opacity:.55}
|
|
62
69
|
@media (prefers-reduced-motion: reduce){.lqd,#lqcore{transition:none}}
|
|
70
|
+
/* ---- the Symbiot look on every part: glass over the liquid, chrome, a silver edge ---- */
|
|
71
|
+
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)}
|
|
72
|
+
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}
|
|
73
|
+
body.lq-liquid.lq-solid.lq-pooled main{backdrop-filter:none;background:var(--ink)}
|
|
74
|
+
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)}
|
|
75
|
+
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)}
|
|
76
|
+
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)}
|
|
77
|
+
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)}
|
|
78
|
+
body.lq-liquid.lq-light button.ghost{background:rgba(255,255,255,.7);border-color:rgba(20,30,45,.18)}
|
|
79
|
+
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)}
|
|
80
|
+
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)}
|
|
81
|
+
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)}
|
|
82
|
+
body.lq-liquid .tgroup{color:#C9D1DC;letter-spacing:.16em}
|
|
83
|
+
body.lq-liquid.lq-light .tgroup{color:#3B4656}
|
|
84
|
+
body.lq-liquid h2,body.lq-liquid h3{letter-spacing:-.01em}
|
|
85
|
+
body.lq-liquid .fchip.on{background:linear-gradient(180deg,#FFFFFF,#C9D1DC);color:#06080B;border-color:#FFFFFF}
|
|
86
|
+
body.lq-contrast.lq-liquid .task,body.lq-contrast.lq-liquid .agent{border:2px solid currentColor}
|
|
87
|
+
|
|
88
|
+
/* the work scene: tasks and agents in the liquid, with spheres revolving round what's at work */
|
|
89
|
+
#lqorbits{position:absolute;inset:0;pointer-events:none}
|
|
90
|
+
.lqorbit{position:absolute;left:0;top:0;pointer-events:none;transition:opacity .5s ease}
|
|
91
|
+
.lqorbit span{position:absolute;inset:0;animation:lqorbit 3.4s linear infinite}
|
|
92
|
+
.lqorbit span:nth-child(2){animation-duration:4.6s;animation-delay:-1.6s}
|
|
93
|
+
.lqorbit span:nth-child(3){animation-duration:5.8s;animation-delay:-3.9s;animation-direction:reverse}
|
|
94
|
+
.lqorbit i{position:absolute;left:50%;top:0;width:10px;height:10px;margin:-5px 0 0 -5px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#FFFFFF 0%,#C9CFD9 35%,#6F7887 75%,#2A2F38 100%);box-shadow:0 0 8px rgba(255,255,255,.35)}
|
|
95
|
+
.lqorbit.ask i{background:radial-gradient(circle at 35% 30%,#FFF4E2 0%,#F2A541 50%,#7A4A10 100%)}
|
|
96
|
+
@keyframes lqorbit{to{transform:rotate(360deg)}}
|
|
97
|
+
@media (prefers-reduced-motion: reduce){.lqorbit span{animation:none}.lqorbit span:nth-child(2){transform:rotate(120deg)}.lqorbit span:nth-child(3){transform:rotate(240deg)}}
|
|
98
|
+
#lqgo,#lqback{display:none}
|
|
99
|
+
body.lq-work #lqgo.on{display:inline-flex;position:absolute;left:50%;bottom:86px;transform:translateX(-50%);z-index:4;align-items:center;gap:8px;min-height:52px;padding:0 26px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;font:700 16px var(--sans);letter-spacing:.02em;cursor:pointer;box-shadow:0 16px 40px -16px rgba(0,0,0,.75)}
|
|
100
|
+
body.lq-work #lqback{display:inline-flex;position:absolute;left:16px;top:52px;z-index:4;align-items:center;min-height:44px;padding:0 16px;border-radius:999px;border:1px solid rgba(220,228,240,.3);background:rgba(4,5,7,.6);backdrop-filter:blur(8px);color:#EEF2F8;font:600 14px var(--sans);cursor:pointer}
|
|
101
|
+
body.lq-light.lq-work #lqback{background:rgba(255,255,255,.8);color:#10151C;border-color:rgba(20,30,45,.2)}
|
|
102
|
+
body.lq-pooled #lqgo,body.lq-pooled #lqback,body.lq-pooled #lqorbits{display:none}
|
|
103
|
+
.lqd.lq-ready{border-color:#F2A541}
|
|
104
|
+
/* the live work of an agent: what it's doing, its to-dos, its steps, its files, its pace */
|
|
105
|
+
.wk{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:12px;margin-top:10px}
|
|
106
|
+
.wkbox{border-radius:14px;padding:10px 12px;background:rgba(255,255,255,.035);border:1px solid rgba(220,228,240,.12)}
|
|
107
|
+
body.lq-light .wkbox{background:#F6F8FB;border-color:rgba(20,30,45,.1)}
|
|
108
|
+
.wkh{font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--faint);margin-bottom:6px;display:flex;gap:6px;align-items:center}
|
|
109
|
+
.wkstats{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
110
|
+
.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}
|
|
111
|
+
.wkstat b{color:var(--bone)}
|
|
112
|
+
.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}
|
|
113
|
+
body.lq-light .wkdoing{background-image:linear-gradient(90deg,#4A5565 0%,#0F1720 40%,#4A5565 60%,#4A5565 100%)}
|
|
114
|
+
@keyframes lqshim{0%{background-position:120% 0}100%{background-position:-120% 0}}
|
|
115
|
+
.wktodo{display:flex;gap:8px;align-items:flex-start;font-size:12.5px;line-height:1.4;padding:3px 0}
|
|
116
|
+
.wktodo i{flex:none;width:12px;height:12px;margin-top:2px;border-radius:50%;border:1.5px solid var(--faint)}
|
|
117
|
+
.wktodo.in_progress i{border-color:var(--bone);background:conic-gradient(var(--bone) 0 50%,transparent 50%);animation:lqspin 1.4s linear infinite}
|
|
118
|
+
.wktodo.completed i{border-color:var(--green);background:var(--green)}
|
|
119
|
+
.wktodo.completed span{color:var(--faint);text-decoration:line-through}
|
|
120
|
+
@keyframes lqspin{to{transform:rotate(360deg)}}
|
|
121
|
+
.wkstep{display:flex;gap:8px;align-items:center;font-size:12.5px;padding:3px 0;min-width:0}
|
|
122
|
+
.wkstep svg{flex:none;opacity:.85}
|
|
123
|
+
.wkstep .wt{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
124
|
+
.wkstep .wt b{font-weight:600;color:var(--bone)}
|
|
125
|
+
.wkstep .wm{flex:none;font-size:11px;color:var(--faint)}
|
|
126
|
+
.wkstep.error .wt{color:#FF8A75}.wkstep.running .wt{color:var(--bone)}
|
|
127
|
+
.wktest{font-size:11px;padding:1px 7px;border-radius:999px;background:rgba(61,220,151,.14);color:var(--green);white-space:nowrap}
|
|
128
|
+
.wktest.bad{background:rgba(255,138,117,.16);color:#FF8A75}
|
|
129
|
+
.wkfile{display:flex;gap:8px;align-items:center;font-size:12px;padding:2px 0}
|
|
130
|
+
.wkfile .fn{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
131
|
+
.wkfile .fb{flex:1;height:6px;border-radius:3px;background:rgba(220,228,240,.12);overflow:hidden}
|
|
132
|
+
.wkfile .fb i{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,#8E97A6,#FFFFFF)}
|
|
133
|
+
body.lq-light .wkfile .fb i{background:linear-gradient(90deg,#6F7887,#0F1720)}
|
|
134
|
+
.wkfinal{margin-top:10px;font-size:13px;line-height:1.55;white-space:pre-wrap}
|
|
135
|
+
.wkraw summary{cursor:pointer;font-size:12px;color:var(--faint);margin-top:10px}
|
|
136
|
+
.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)}
|
|
137
|
+
.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%)}
|
|
138
|
+
@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)}}
|
|
139
|
+
.msg .steps,.lqmsg .steps{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
|
|
140
|
+
.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}
|
|
141
|
+
.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}
|
|
142
|
+
body.lq-light .thinking{background-image:linear-gradient(90deg,#4A5565 0%,#0F1720 40%,#4A5565 60%,#4A5565 100%)}
|
|
143
|
+
@media (prefers-reduced-motion: reduce){.wkdoing,.thinking,.orb.run,.wktodo.in_progress i{animation:none}}
|
|
144
|
+
|
|
63
145
|
|
|
64
146
|
*{box-sizing:border-box}html,body{margin:0;height:100%}
|
|
65
147
|
body{background:var(--ink);color:var(--text);font-family:var(--sans);font-size:14px;display:flex;flex-direction:column}
|
|
@@ -85,9 +167,14 @@ a{color:var(--green);cursor:pointer}.hidden{display:none}
|
|
|
85
167
|
.note{font-size:12px;margin-top:10px}.ok{color:var(--green)}.err{color:var(--amber)}
|
|
86
168
|
.updatebar{display:none;align-items:center;gap:12px;padding:9px 16px;font-size:13px;font-weight:600;background:var(--green);color:var(--ink)}
|
|
87
169
|
.updatebar.show{display:flex}
|
|
170
|
+
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)}
|
|
171
|
+
body.lq-liquid .updatebar.show.reconnect{background:var(--amber)}
|
|
172
|
+
body.lq-liquid #appbar.updatebar.show{top:58px}
|
|
88
173
|
.updatebar.reconnect{background:var(--amber)}
|
|
89
174
|
.whatsnew{display:none;padding:10px 16px;font-size:13px;background:var(--ink2);border-bottom:1px solid var(--line);max-height:40vh;overflow:auto}
|
|
90
175
|
.whatsnew.show{display:block}
|
|
176
|
+
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)}
|
|
177
|
+
body.lq-liquid.lq-light .whatsnew.show{background:rgba(255,255,255,.9);border-color:rgba(20,30,45,.15)}
|
|
91
178
|
.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
179
|
.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
180
|
footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
@@ -207,7 +294,10 @@ label.check input{width:auto}
|
|
|
207
294
|
</style></head><body>
|
|
208
295
|
<div id="liquid" aria-label="Symbiot">
|
|
209
296
|
<canvas id="lq" aria-hidden="true"></canvas>
|
|
297
|
+
<div id="lqorbits"></div>
|
|
210
298
|
<div id="lqdrops"></div>
|
|
299
|
+
<button type="button" id="lqback">‹ Home</button>
|
|
300
|
+
<button type="button" id="lqgo"></button>
|
|
211
301
|
<button type="button" id="lqcore" title="talk to Symbiot"></button>
|
|
212
302
|
<div id="lqmore"></div>
|
|
213
303
|
<div id="lqtalk" aria-live="polite"></div>
|
|
@@ -439,10 +529,10 @@ edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;v
|
|
|
439
529
|
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
530
|
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
531
|
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+"'"+
|
|
532
|
+
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)+")'>";
|
|
533
|
+
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+"'/>";});
|
|
534
|
+
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"):"");
|
|
535
|
+
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
536
|
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
537
|
document.getElementById("graph").innerHTML=s+"</g>";}
|
|
448
538
|
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=g.stats.base||"your home folder";
|
|
@@ -498,7 +588,8 @@ else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id
|
|
|
498
588
|
// connected model, grounded in the task's repo (and its pending changes).
|
|
499
589
|
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
590
|
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
|
|
591
|
+
function stepsHtml(st){return st&&st.length?"<div class='steps'>"+st.map(function(x){return "<span>"+esc(x)+"</span>";}).join('')+"</div>":"";}
|
|
592
|
+
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
593
|
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
594
|
function wireAsk(row,id){var b=row.querySelector('.ask');if(b)b.addEventListener('click',function(){toggleChat(row,id);});}
|
|
504
595
|
function toggleChat(row,id){var btn=row.querySelector('.ask');var nx=row.nextElementSibling;
|
|
@@ -646,6 +737,33 @@ var LANEWORD={started:'working on it',held:'queued: that lane is busy',done:'don
|
|
|
646
737
|
function loadLanes(){api('/api/lanes').then(function(d){var el=document.getElementById('laneslist');if(!el||!d)return;var hs=d.handoffs||[];
|
|
647
738
|
el.innerHTML=hs.length?"<div class='tgroup'>Handovers <span class='tcount'>"+hs.length+"</span></div>"+hs.slice(0,12).map(function(h){
|
|
648
739
|
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(''):'';});}
|
|
740
|
+
|
|
741
|
+
// ---- an agent's work, live (work.mjs parses its stream) ------------------------------
|
|
742
|
+
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'/>"};
|
|
743
|
+
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>";}
|
|
744
|
+
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';}
|
|
745
|
+
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>";}
|
|
746
|
+
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>";}
|
|
747
|
+
function workHtml(a){var w=a.work,pg=a.progress,run=a.status==='running',h='';
|
|
748
|
+
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;}
|
|
749
|
+
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>");
|
|
750
|
+
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>");
|
|
751
|
+
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>");
|
|
752
|
+
if(run&&w.doing)h+="<div class='wkdoing'>"+esc(w.doing)+"…</div>";
|
|
753
|
+
var first=(w.final||'').split(String.fromCharCode(10))[0];if(!run&&first)h+="<div class='wkfinal'>"+esc(first.length>180?first.slice(0,179)+'\u2026':first)+"</div>";
|
|
754
|
+
var boxes=[];
|
|
755
|
+
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>";
|
|
756
|
+
(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>";});
|
|
757
|
+
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>");}
|
|
758
|
+
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>";
|
|
759
|
+
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>";});
|
|
760
|
+
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>");}
|
|
761
|
+
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>";
|
|
762
|
+
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>");}
|
|
763
|
+
var todoBox=boxes.length&&(w.todos&&w.todos.length||pg)?boxes.shift():'';
|
|
764
|
+
if(todoBox)h+="<div class='wk'>"+todoBox+"</div>";
|
|
765
|
+
h+="<details class='wkraw'><summary>details: steps, files, cost</summary><div class='wkstats'>"+stats.join('')+"</div>"+(boxes.length?"<div class='wk'>"+boxes.join('')+"</div>":"")+(!run&&w.final?"<div class='wkfinal'>"+esc(w.final)+"</div>":"")+"</details>";
|
|
766
|
+
return h;}
|
|
649
767
|
function loadAgents(){loadLanes();api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
|
|
650
768
|
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
769
|
var anyRunning=list.some(function(a){return a.status==='running';});
|
|
@@ -660,8 +778,8 @@ if(nQs(a))st+=" · <span style='color:var(--amber)'>needs your answers</sp
|
|
|
660
778
|
if(a.held!=null){var hn=a.held.length,ht=a.held.map(function(t){return "• "+esc(t).replace(/'/g,''');}).join(' ');
|
|
661
779
|
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
780
|
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
|
-
|
|
781
|
+
var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
|
|
782
|
+
b+=workHtml(a);
|
|
665
783
|
var ch=a.changed;
|
|
666
784
|
if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
667
785
|
var parts=[];
|
|
@@ -673,7 +791,7 @@ if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
|
673
791
|
b+="<div class='changed muted'>No file changes detected (the agent may have only planned or asked).</div>";
|
|
674
792
|
}
|
|
675
793
|
b+=waitHtml(a)+askHtml(a);
|
|
676
|
-
b+="<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>";
|
|
794
|
+
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
795
|
return "<div class='agent'>"+b+"</div>";}).join("");
|
|
678
796
|
el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
|
|
679
797
|
restoreDrafts(el);wireAsks(el);wireWaits(el);
|
|
@@ -1165,6 +1283,33 @@ function doUpdate(){updBusy=true;try{localStorage.setItem('symbiot_update_tried'
|
|
|
1165
1283
|
setInterval(heartbeat,4000);heartbeat(true);
|
|
1166
1284
|
window.addEventListener('focus',function(){heartbeat(true);}); // re-check for updates when you come back to the window
|
|
1167
1285
|
|
|
1286
|
+
|
|
1287
|
+
// ---- the work scene ------------------------------------------------------------------
|
|
1288
|
+
// Tasks and agents, in the liquid: what each agent is doing (spheres revolve round
|
|
1289
|
+
// it while it works), what's done and waiting for your OK, and what's waiting its
|
|
1290
|
+
// turn. One Go starts what's waiting. A tap opens the details; nothing else to read.
|
|
1291
|
+
function lqWork(){LQ.scene='work';LQ.mode='aware';var b=document.body;if(b&&b.classList){b.classList.add('lq-work');b.classList.remove('lq-pooled');}lqLoadWork();
|
|
1292
|
+
if(LQ.workTimer)clearInterval(LQ.workTimer);LQ.workTimer=setInterval(function(){if(LQ.scene==='work'&&LQ.mode!=='pool')lqLoadWork();},5000);}
|
|
1293
|
+
function lqHome(){LQ.scene='home';var b=document.body;if(b&&b.classList){b.classList.remove('lq-work');b.classList.remove('lq-pooled');}if(LQ.workTimer){clearInterval(LQ.workTimer);LQ.workTimer=null;}LQ.mode='aware';lqLoad(true);}
|
|
1294
|
+
function lqLoadWork(){api('/api/work').then(function(w){LQ.workData=w||{};lqBuildWork();}).catch(function(){});}
|
|
1295
|
+
function lqBuildWork(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;
|
|
1296
|
+
var vx=Math.max(0.5,Math.min(S.s*1.3,(S.w/2-90)/400)),vy=Math.max(0.35,Math.min(S.s*0.8,(S.h-cy-230)/400,(cy-160)/400)),rs=Math.min(S.s,(vx+vy)/1.5);
|
|
1297
|
+
var run=(w.running||[]).slice(0,3),ready=(w.ready||[]).slice(0,2),wait=(w.waiting||[]).slice(0,5),extra=(w.waitingCount||0)-wait.length;
|
|
1298
|
+
run.forEach(function(r,i){var ang=Math.PI+(i+1)*Math.PI/(run.length+1);var pg=r.progress?' · '+r.progress.done+' of '+r.progress.total+' done':'';
|
|
1299
|
+
list.push({id:r.id,kind:'run',shape:'agents',ask:r.waiting,title:r.doing,sub:r.name+pg+(r.waiting?' · has a question for you':''),r:58*rs,tx:cx+Math.cos(ang)*190*vx,ty:cy+Math.sin(ang)*130*vy});});
|
|
1300
|
+
ready.forEach(function(r,i){var ang=-Math.PI/2+(i-(ready.length-1)/2)*0.9;list.push({id:r.id,kind:'ready',shape:'tasks',title:'Ready for your OK',sub:r.repo+(r.count?' · '+r.count+' task'+(r.count>1?'s':'')+' done':''),r:48*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*300*vy});});
|
|
1301
|
+
wait.forEach(function(t,i){var ang=Math.PI/2+(i-(wait.length-1)/2)*0.55;list.push({id:t.id,kind:'wait',shape:'tasks',title:t.text,sub:t.repo+(t.busy?' · after the current one':' · waiting'),r:30*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*260*vy});});
|
|
1302
|
+
if(extra>0)list.push({id:'morewait',kind:'more',shape:'tasks',title:'+'+extra+' more waiting',sub:'see them all',r:24*rs,tx:S.w-Math.max(80,S.w*0.1),ty:S.h-230});
|
|
1303
|
+
lqRelax(list,S,cx,cy,60*rs);
|
|
1304
|
+
var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
|
|
1305
|
+
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;});
|
|
1306
|
+
var nr=(w.running||[]).length,nw=w.waitingCount||0,nk=(w.ready||[]).length;
|
|
1307
|
+
LQ.coreText=(nr?nr+' at work':'Nobody at work')+(nw?' · '+nw+' waiting':'')+(nk?' · '+nk+' ready for your OK':'');
|
|
1308
|
+
var go=$('lqgo');if(go){var n=w.canGo||0;go.textContent=n?'Go \u00b7 start '+n+' task'+(n>1?'s':''):'';if(go.classList)go.classList.toggle('on',n>0);}
|
|
1309
|
+
lqLabels();}
|
|
1310
|
+
function lqGoWork(){var go=$('lqgo');if(go)go.disabled=true;api('/api/work/go',{}).then(function(r){if(go)go.disabled=false;
|
|
1311
|
+
var t=r&&r.error?r.error:r.started||r.queued?(r.started?'Started '+r.started:'')+(r.started&&r.queued?', ':'')+(r.queued?r.queued+' queued behind work already going':'')+'.':(r&&r.note)||'Nothing waiting to start.';
|
|
1312
|
+
LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.5,LQ.t];lqLoadWork();}).catch(function(){if(go)go.disabled=false;});}
|
|
1168
1313
|
// ---- the liquid: the app's home ---------------------------------------------------
|
|
1169
1314
|
// One silver surface is the app. Its droplets: what only you can do, what's new on
|
|
1170
1315
|
// what you watch, and the parts of the app, sized and placed from how you use them
|
|
@@ -1184,15 +1329,21 @@ if(a.style.color){var ca=getComputedStyle(a).color,cb=getComputedStyle(b).color;
|
|
|
1184
1329
|
var bd=document.body;if(bd&&bd.classList){bd.classList.toggle('lq-light',th.light);bd.classList.toggle('lq-contrast',th.contrast);bd.classList.toggle('lq-solid',th.solid||th.contrast);bd.classList.toggle('lq-forced',th.forced);}
|
|
1185
1330
|
LQ.theme=th;return th;}
|
|
1186
1331
|
function lqSize(){var el=$('liquid');var w=(el&&el.clientWidth)||window.innerWidth||1280,h=(el&&el.clientHeight)||window.innerHeight||800;return {w:w,h:h,s:Math.max(0.55,Math.min(1.15,Math.min(w,h)/860))};}
|
|
1187
|
-
function lqLoad(commit){Promise.all([api('/api/adapt?from='+encodeURIComponent(LQ.last)+(commit?'&commit=1':'')+(LQ.touch?'&touch=1':'')),api('/api/home')]).then(function(r){LQ.adapt=r[0]||{};LQ.home=r[1]||{};lqBuild();}).catch(function(){});}
|
|
1332
|
+
function lqLoad(commit){if(LQ.scene==='work'){lqLoadWork();return;}Promise.all([api('/api/adapt?from='+encodeURIComponent(LQ.last)+(commit?'&commit=1':'')+(LQ.touch?'&touch=1':'')),api('/api/home')]).then(function(r){LQ.adapt=r[0]||{};LQ.home=r[1]||{};lqBuild();}).catch(function(){});}
|
|
1188
1333
|
function lqBuild(){var a=LQ.adapt||{},h=LQ.home||{},S=lqSize(),cx=S.w/2,cy=S.h*0.47,list=[];LQ.bw=S.w;LQ.bh=S.h;
|
|
1189
1334
|
var you=(h.you||[]).slice(0,3),feeds=(h.feeds||[]).slice(0,2),lay=(a.layout&&a.layout.items)||[],more=((a.layout&&a.layout.more)||[]).slice();
|
|
1190
1335
|
var vx=Math.max(0.5,Math.min(S.s*1.3,(S.w/2-90)/400)),vy=Math.max(0.35,Math.min(S.s*0.8,(S.h-cy-230)/400,(cy-160)/400));var rs=Math.min(S.s,(vx+vy)/1.5);var room=Math.max(3,LQ_MAX-you.length-feeds.length-1);lay.slice(room).forEach(function(it){more.push(it.id);});
|
|
1191
1336
|
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
1337
|
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
1338
|
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
|
-
|
|
1339
|
+
// Solve it. If this screen can't hold them all clear of each other (lqRelax), the
|
|
1340
|
+
// least likely part goes under "more" (Hick: fewer, not cramped) and it solves
|
|
1341
|
+
// again. What only you can do and your feeds always stay out.
|
|
1342
|
+
var base=list.slice(),cur=null,tries=0;base.forEach(function(d){d.r0=d.r;d.tx0=d.tx;d.ty0=d.ty;});
|
|
1343
|
+
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});
|
|
1344
|
+
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;
|
|
1345
|
+
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);}
|
|
1346
|
+
list=cur;var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
|
|
1196
1347
|
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
1348
|
var n=(h.you||[]).length,w=h.working||0;
|
|
1198
1349
|
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,36 +1353,58 @@ lqLabels();}
|
|
|
1202
1353
|
// Force-directed layout, solved once per change: push overlapping targets apart
|
|
1203
1354
|
// (each droplet's circle plus its label below or above), and off the core, within
|
|
1204
1355
|
// the screen. Deterministic, so the same data gives the same layout.
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
a.
|
|
1356
|
+
// The one rule for how far apart two droplets sit: past where their metal would
|
|
1357
|
+
// bridge (r²/d² fields sum to 1 midway at d = 2·√(r1²+r2²)), with a margin so the
|
|
1358
|
+
// wobble can't flicker a bridge open and shut, and room for their labels when
|
|
1359
|
+
// they share a column. Used once, when the layout is solved; nothing fights it live.
|
|
1360
|
+
// Tags float on their spheres, so two on the same row also keep a tag's width apart.
|
|
1361
|
+
function lqSep(a,b,dx,dy){var m=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);return dy!=null&&Math.abs(dy)<56?Math.max(m,232):m;}
|
|
1362
|
+
// Solve the layout: push targets apart until every pair keeps lqSep (and clear
|
|
1363
|
+
// of the core), within the screen. If they can't all fit, every droplet gives up
|
|
1364
|
+
// a little of its size (the liquid is conserved, not crowded) and it solves again.
|
|
1365
|
+
function lqRelax(L,S,cx,cy,coreR){var n=L.length;
|
|
1366
|
+
for(var pass=0;pass<6;pass++){
|
|
1367
|
+
for(var it=0;it<220;it++){var moved=0;for(var i=0;i<n;i++){var a=L[i];
|
|
1368
|
+
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,dy);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++;}}
|
|
1369
|
+
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++;}
|
|
1370
|
+
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;}
|
|
1371
|
+
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,ddy)-1){bad=true;break;}}
|
|
1372
|
+
if(!bad)return true;L.forEach(function(x){x.r*=0.9;});coreR*=0.9;}return false;}
|
|
1209
1373
|
function lqLabels(){var el=$('lqdrops');if(!el)return;
|
|
1210
|
-
el.innerHTML=LQ.drops.map(function(d,i){return "<
|
|
1211
|
-
LQ.btns=[];el.querySelectorAll('.lqd').forEach(function(b){LQ.btns.push(b);
|
|
1374
|
+
el.innerHTML=LQ.drops.map(function(d,i){return "<div class='lqd lq-"+d.kind+(LQ.openTag===d.id?' open':'')+"' data-i='"+i+"'><button type='button' class='lt' aria-expanded='"+(LQ.openTag===d.id)+"'><span>"+esc(d.title)+"</span></button>"+(d.sub?"<small>"+esc(d.sub)+"</small>":"")+"<button type='button' class='lgo'>Open ›</button></div>";}).join('');
|
|
1375
|
+
LQ.btns=[];LQ.lts=[];el.querySelectorAll('.lqd').forEach(function(b){LQ.btns.push(b);var d=LQ.drops[+b.getAttribute('data-i')],lt=b.querySelector('.lt'),go=b.querySelector('.lgo');LQ.lts.push(lt);
|
|
1376
|
+
if(lt)lt.addEventListener('click',function(ev){if(d.kind==='more'){lqOpen(d,ev);return;}lqTag(d.id);});
|
|
1377
|
+
if(go)go.addEventListener('click',function(ev){LQ.openTag=null;lqOpen(d,ev);});});
|
|
1378
|
+
var oe=$('lqorbits');if(oe){oe.innerHTML=LQ.drops.map(function(d,i){return d.kind==='run'?"<div class='lqorbit"+(d.ask?' ask':'')+"' data-i='"+i+"'><span><i></i></span><span><i></i></span><span><i></i></span></div>":'';}).join('');LQ.orbs=[];oe.querySelectorAll('.lqorbit').forEach(function(o){LQ.orbs.push(o);});}
|
|
1379
|
+
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
1380
|
var c=$('lqcore');if(c)c.textContent=LQ.coreText||'';}
|
|
1381
|
+
// Tapping a sphere (or its name) opens its tag; tapping it again, or elsewhere, closes it.
|
|
1382
|
+
function lqTag(id){LQ.openTag=LQ.openTag===id?null:id;lqAct();LQ.btns.forEach(function(b,i){var d=LQ.drops[+b.getAttribute('data-i')],on=!!d&&d.id===LQ.openTag;if(b.classList)b.classList.toggle('open',on);var lt=LQ.lts&&LQ.lts[i];if(lt&<.setAttribute)lt.setAttribute('aria-expanded',on?'true':'false');});}
|
|
1213
1383
|
function lqVia(ev){return LQ.talking?'talk':ev&&ev.pointerType==='touch'?'touch':ev&&ev.detail===0?'key':'click';}
|
|
1214
1384
|
function lqUse(shape,ev,via){api('/api/adapt/use',{shape:shape,from:LQ.last,via:via||lqVia(ev)});LQ.last=shape;}
|
|
1215
1385
|
function lqOpen(d,ev){if(!d)return;lqAct();
|
|
1216
1386
|
if(d.kind==='more'){LQ.moreOpen=!LQ.moreOpen;lqMore(d);return;}
|
|
1217
|
-
var S=lqSize();LQ.ripple=[d.x/S.w,d.y/S.h,LQ.t];lqUse(d.shape||'board',ev);
|
|
1387
|
+
var S=lqSize();LQ.ripple=[d.x/S.w,d.y/S.h,LQ.t];if(LQ.scene==='work'){lqPool(d.shape||'agents');return;}lqUse(d.shape||'board',ev);lqGo(d.shape||'board');}
|
|
1218
1388
|
function lqMore(d){var el=$('lqmore');if(!el)return;if(!LQ.moreOpen||!d){el.innerHTML='';return;}
|
|
1219
|
-
el.innerHTML=(d.more||[]).map(function(s){return "<button type='button' class='
|
|
1220
|
-
el.querySelectorAll('.lqm').forEach(function(b){b.addEventListener('click',function(ev){var s=b.getAttribute('data-s');LQ.moreOpen=false;el.innerHTML='';lqUse(s,ev);
|
|
1389
|
+
el.innerHTML=(d.more||[]).map(function(s){return "<button type='button' class='lqmb lqm' data-s='"+s+"'>"+esc(LQNAMES[s]||s)+"</button>";}).join('');
|
|
1390
|
+
el.querySelectorAll('.lqm').forEach(function(b){b.addEventListener('click',function(ev){var s=b.getAttribute('data-s');LQ.moreOpen=false;el.innerHTML='';lqUse(s,ev);lqGo(s);});});}
|
|
1391
|
+
function lqGo(shape){if(shape==='tasks'||shape==='agents'){lqWork();return;}lqPool(shape);}
|
|
1221
1392
|
function lqPool(shape){tabPicked=true;setTab(shape);var b=document.body;if(b&&b.classList)b.classList.add('lq-pooled');LQ.mode='pool';var m=document.querySelector('main');if(m&&m.focus)m.focus();}
|
|
1222
1393
|
function lqSink(){var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
|
|
1223
1394
|
function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
|
|
1224
1395
|
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'
|
|
1396
|
+
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
1397
|
function lqSay(){var i=$('lqask');var q=((i&&i.value)||'').trim();if(!q)return;i.value='';lqAct();
|
|
1227
1398
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
1228
1399
|
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
|
-
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');
|
|
1400
|
+
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');lqGo(hit);return;}
|
|
1401
|
+
if(/^(go|start|go for it|start them|start it)[.! ]*$/.test(low)){lqTalkMode(true);lqGoWork();return;}
|
|
1402
|
+
if(/^(home|back)[.! ]*$/.test(low)&&LQ.scene==='work'){lqHome();return;}
|
|
1230
1403
|
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);});}
|
|
1404
|
+
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
1405
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
1233
1406
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
1234
|
-
function lqStep(){var S0=lqSize();if(
|
|
1407
|
+
function lqStep(){var S0=lqSize();if(S0.w!==LQ.bw||S0.h!==LQ.bh){if(LQ.scene==='work'&&LQ.workData)lqBuildWork();else if(LQ.adapt)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;
|
|
1235
1408
|
var core=LQ.core,ctx=cx,cty=talk?S.h-44:cy,ctr=(rest?140:pool?50:talk?54+30*(LQ.talkWeight||0.35):92)*S.s;
|
|
1236
1409
|
if(!core.x){core.x=cx;core.y=cy;}core.x+=(ctx-core.x)*(still?1:0.06);core.y+=(cty-core.y)*(still?1:0.06);core.cr+=(ctr-core.cr)*(still?1:0.05);
|
|
1237
1410
|
var D=LQ.drops,N=D.length;
|
|
@@ -1240,17 +1413,16 @@ if(rest){tx=cx+Math.cos(i*1.3)*16;ty=cy+Math.sin(i*1.3)*16;tr=d.r*0.6;}
|
|
|
1240
1413
|
else if(pool){tx=cx+(i-(N-1)/2)*40*S.s;ty=S.h-40;tr=13*S.s;}
|
|
1241
1414
|
else if(talk){ty=S.h*0.12+(d.ty-S.h*0.12)*0.55;}
|
|
1242
1415
|
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
1416
|
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
1417
|
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
1418
|
d.cr+=(tr-d.cr)*(still?1:0.06);
|
|
1248
|
-
var b=LQ.btns[i];if(b&&b.offsetWidth)d.lw=b.offsetWidth;if(b&&b.style){
|
|
1419
|
+
var b=LQ.btns[i];if(b&&b.offsetWidth)d.lw=b.offsetWidth;if(b&&b.style){b.style.transform='translate('+Math.round(d.x)+'px,'+Math.round(d.y)+'px) translate(-50%,-50%)';var show=!rest&&!pool;b.style.opacity=show?'1':'0';b.style.pointerEvents=show?'auto':'none';var lt=LQ.lts&&LQ.lts[i];if(lt)lt.tabIndex=show?0:-1;}}
|
|
1420
|
+
(LQ.orbs||[]).forEach(function(o){var d=D[+o.getAttribute('data-i')];if(!d||!o.style)return;var R=Math.round(d.cr+20);o.style.width=o.style.height=(2*R)+'px';o.style.transform='translate('+Math.round(d.x-R)+'px,'+Math.round(d.y-R)+'px)';o.style.opacity=rest||pool?'0':'1';});
|
|
1249
1421
|
var cb=$('lqcore');if(cb&&cb.style){cb.style.transform='translate('+Math.round(cx)+'px,'+(rest?Math.round(core.y+core.cr+18):56)+'px) translateX(-50%)';var ct=rest?(LQ.coreText||'All handled'):talk||pool?'':(LQ.coreText||'');cb.textContent=ct;cb.style.opacity=ct?'1':'0';cb.style.pointerEvents=ct?'auto':'none';}}
|
|
1250
1422
|
// The liquid: a metaball surface (Σ r²/d² = 1) shaded as chrome, on the GPU.
|
|
1251
1423
|
var LQ_FS=['precision highp float;',
|
|
1252
1424
|
'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.
|
|
1425
|
+
'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
1426
|
'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
|
|
1255
1427
|
'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
1428
|
'void main(){vec2 p=vec2(gl_FragCoord.x,uRes.y-gl_FragCoord.y);float f=field(p);float e=2.0*uDpr;',
|
|
@@ -1281,10 +1453,12 @@ function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=Math.min(window.devicePixelRat
|
|
|
1281
1453
|
if(c.width!==W||c.height!==H){c.width=W;c.height=H;gl.viewport(0,0,W,H);}
|
|
1282
1454
|
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
1455
|
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);};
|
|
1456
|
+
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
1457
|
put(0,LQ.core.x,LQ.core.y,LQ.core.cr);var k=1,tend=[];
|
|
1286
1458
|
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
|
-
|
|
1459
|
+
// a tendril is a chain of blobs close enough to always bridge (spaced at under 2.8r),
|
|
1460
|
+
// never one blob floating in the gap: drawn whole or not at all
|
|
1461
|
+
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
1462
|
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
1463
|
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
1464
|
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]);
|
|
@@ -1292,16 +1466,18 @@ gl.uniform1f(U.uLight,th.light?1:0);gl.uniform1f(U.uContrast,th.contrast?1:0);va
|
|
|
1292
1466
|
gl.drawArrays(gl.TRIANGLES,0,3);}
|
|
1293
1467
|
function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
|
|
1294
1468
|
['(prefers-color-scheme: light)','(prefers-contrast: more)','(forced-colors: active)','(prefers-reduced-transparency: reduce)','(prefers-reduced-motion: reduce)'].forEach(function(q){try{var mq=window.matchMedia&&window.matchMedia(q);if(mq&&mq.addEventListener)mq.addEventListener('change',lqTheme);else if(mq&&mq.addListener)mq.addListener(lqTheme);}catch(e){}});
|
|
1295
|
-
var L=$('liquid');if(L&&L.addEventListener){L.addEventListener('pointermove',function(ev){LQ.pointer=[ev.clientX,ev.clientY];if(ev.pointerType==='touch')LQ.touch=true;lqAct();});L.addEventListener('pointerdown',function(ev){if(ev.pointerType==='touch')LQ.touch=true;lqAct();});}
|
|
1469
|
+
var L=$('liquid');if(L&&L.addEventListener){L.addEventListener('click',function(ev){var t=ev&&ev.target;if(t&&t.closest&&t.closest('button,input,form,.lqd'))return;var x=ev.clientX,y=ev.clientY,hit=null;LQ.drops.forEach(function(d){if(!hit&&Math.hypot(x-d.x,y-d.y)<=d.cr+8)hit=d;});if(hit){if(hit.kind==='more')lqOpen(hit,ev);else lqTag(hit.id);}else if(LQ.openTag)lqTag(LQ.openTag);});L.addEventListener('pointermove',function(ev){LQ.pointer=[ev.clientX,ev.clientY];if(ev.pointerType==='touch')LQ.touch=true;lqAct();});L.addEventListener('pointerdown',function(ev){if(ev.pointerType==='touch')LQ.touch=true;lqAct();});}
|
|
1296
1470
|
var f=$('lqform');if(f&&f.addEventListener)f.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();lqSay();});
|
|
1297
|
-
var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('focus',function(){lqTalkMode(true);lqAct();});ia.addEventListener('blur',function(){if(!ia.value&&!LQ.talk.length)lqTalkMode(false);});ia.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape'){if(LQ.mode==='pool'){lqSink();return;}ia.value='';LQ.talk=[];lqTalkShow(false);lqTalkMode(false);if(ia.blur)ia.blur();}});}
|
|
1471
|
+
var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('focus',function(){lqTalkMode(true);lqAct();});ia.addEventListener('blur',function(){if(!ia.value&&!LQ.talk.length)lqTalkMode(false);});ia.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape'){if(LQ.mode==='pool'){lqSink();return;}if(LQ.scene==='work'&&!ia.value){lqHome();return;}ia.value='';LQ.talk=[];lqTalkShow(false);lqTalkMode(false);if(ia.blur)ia.blur();}});}
|
|
1298
1472
|
var co=$('lqcore');if(co&&co.addEventListener)co.addEventListener('click',function(){var i=$('lqask');if(i&&i.focus)i.focus();});
|
|
1299
1473
|
var sk=$('lqsink');if(sk&&sk.addEventListener)sk.addEventListener('click',lqSink);
|
|
1474
|
+
var bk=$('lqback');if(bk&&bk.addEventListener)bk.addEventListener('click',lqHome);
|
|
1475
|
+
var gb=$('lqgo');if(gb&&gb.addEventListener)gb.addEventListener('click',lqGoWork);
|
|
1300
1476
|
var mn=document.querySelector('main');if(mn&&mn.addEventListener)mn.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape')lqSink();});
|
|
1301
1477
|
lqLoad(true);
|
|
1302
1478
|
// the only clocks: rest when left alone, fresh data now and then, the hour (night)
|
|
1303
1479
|
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);};
|
|
1480
|
+
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
1481
|
function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0){lqStep();if(G)lqDraw(G);}}raf(frame);}
|
|
1306
1482
|
raf(frame);}
|
|
1307
1483
|
|
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
|
}
|