symbiot 0.51.2 → 0.52.1
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 +9 -0
- package/README.md +12 -0
- package/agents.mjs +75 -3
- package/handover.mjs +1 -1
- package/home.mjs +20 -1
- package/package.json +3 -3
- package/server.mjs +4 -1
- package/ui.mjs +93 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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.52.1
|
|
6
|
+
|
|
7
|
+
- **Agents stop asking you for permission inside your work.** An agent reaches your knowledge folders from the start, and an ops agent your repos too. When it needs more and proposes a list that stays inside the folders you gave Symbiot, asks for nothing that could run anything, and doesn't publish or reach another machine, Symbiot turns it on by itself and the agent carries on: no question reaches you. Only a list that reaches outside your work (`~/.ssh`, `~/.config`, another machine) still asks, and it says why. Asks already waiting clear the same way.
|
|
8
|
+
|
|
9
|
+
## 0.52.0
|
|
10
|
+
|
|
11
|
+
- **Your agents, on the Tasks screen.** No need to open Agents: a line at the top says where everything stands ("1 question for you · 1 ready for your OK · 2 at work"), and a blob per project shows what its agent did or is doing, what it asks you (its answers as buttons, or your own words), Review and approve when its work is ready, and the extra tasks it suggests, each with **+ task** or **Skip**. The liquid moves left to make room.
|
|
12
|
+
- **Agents, organised.** Runs are grouped: what needs you first, then what's at work, then what's finished, folded away. Handovers show only what's under way; the finished ones fold into one line, and repeats show once (×2).
|
|
13
|
+
|
|
5
14
|
## 0.51.2 — 2026-10-07
|
|
6
15
|
|
|
7
16
|
- Replaced the Quit button with an X in the top corner, and Home warns about your AI, agent or connectors only when one is missing.
|
package/README.md
CHANGED
|
@@ -439,6 +439,18 @@ editor presets only open a tab and exit, so Symbiot can't see the agent you run
|
|
|
439
439
|
in them. Send to repos stays disabled until its
|
|
440
440
|
handoffs have started, so a double click can't send twice.
|
|
441
441
|
|
|
442
|
+
### What agents may do without asking
|
|
443
|
+
|
|
444
|
+
Your work is the folders you gave Symbiot (Settings → Folders, and Knowledge folders),
|
|
445
|
+
except the hidden folders at the top of your home (`~/.ssh`, `~/.config`). An agent
|
|
446
|
+
reaches your knowledge folders from the start, and an ops agent your repos too. When
|
|
447
|
+
one needs more, it proposes an allow list; if the list stays inside your work, can't
|
|
448
|
+
run anything at all (a shell, sudo, a wildcard `rm`) and doesn't publish or reach
|
|
449
|
+
another machine (`git push`, `npm publish`, `aws`, `docker`, `ssh`), Symbiot turns it
|
|
450
|
+
on for that agent's folder and the agent carries on, with no question for you. Every
|
|
451
|
+
list also blocks editing Symbiot's own settings. Only what reaches outside your work
|
|
452
|
+
comes to you, with the reason.
|
|
453
|
+
|
|
442
454
|
### Questions from your agent
|
|
443
455
|
|
|
444
456
|
An agent working in your terminal or IDE stops to ask you things: which way to go,
|
package/agents.mjs
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
// tab, and the questions an unattended agent leaves for the user.
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
6
|
+
import { join, dirname, resolve, relative } from "node:path";
|
|
7
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, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
|
|
11
11
|
import { parseRun, lastRunText } from "./work.mjs";
|
|
12
12
|
import { parseFacts } from "./handover.mjs";
|
|
13
|
+
import { scanRoots, repoPathMap } from "./scan.mjs";
|
|
14
|
+
import { knowledgeFolders } from "./knowledge.mjs";
|
|
13
15
|
|
|
14
16
|
const HANDOFFS = []; // live registry of agents Symbiot has handed work to
|
|
15
17
|
// ---- hand a repo (+ its tasks) to the user's agent — generic, settings-based
|
|
@@ -159,7 +161,7 @@ function runHandoff(repoPath, { force = false } = {}) {
|
|
|
159
161
|
let opts = {}; try { opts = JSON.parse(readSymbiot(repoPath, "handoff.json")) || {}; } catch {}
|
|
160
162
|
const busy = runningHandoff(repoPath); if (busy) return { busy: true, id: busy.id || "", pid: busy.pid, auto: !!busy.auto };
|
|
161
163
|
releaseHeldTasks(repoPath); // held for an agent another process started, which has since exited
|
|
162
|
-
tmpl = withStream(withConnectors(tmpl, repoPath));
|
|
164
|
+
tmpl = withStream(withScope(withConnectors(tmpl, repoPath), repoPath));
|
|
163
165
|
if (!force) { const w = waitingFor(repoPath); if (w) { askedFor(repoPath); return { blocked: true, waiting: true, questions: 0, note: waitNote(w) }; } }
|
|
164
166
|
const blocked = !force && blockedAgain(repoPath, tmpl); if (blocked) return blocked;
|
|
165
167
|
try { unlinkSync(join(repoPath, ".symbiot", WAITING)); } catch {} // the step's done (or this is Start it anyway): this is the run it waited for
|
|
@@ -168,6 +170,7 @@ function runHandoff(repoPath, { force = false } = {}) {
|
|
|
168
170
|
const e = track(typeof opts.name === "string" && opts.name ? opts.name.slice(0, 80) : repoPath.split("/").pop(), fillHandoff(tmpl, repoPath), repoPath, (code) => {
|
|
169
171
|
try { if (JSON.parse(readFileSync(lock, "utf8")).pid === e.pid) unlinkSync(lock); } catch {}
|
|
170
172
|
noteBlocked(repoPath, tmpl, e.startedAt, code);
|
|
173
|
+
setTimeout(() => { try { autoAllow(repoPath); } catch {} }, 300); // a list it proposed inside your work: turned on, and on it goes
|
|
171
174
|
startHeldTasks(repoPath); // tasks sent while it ran land now; start on them as that Send would have
|
|
172
175
|
}, env);
|
|
173
176
|
if (!e) return null;
|
|
@@ -673,6 +676,75 @@ function installAllowlist(path) {
|
|
|
673
676
|
try { mkdirSync(join(path, ".claude"), { recursive: true }); writeFileSync(f, JSON.stringify(cur, null, 2) + "\n", { mode: 0o600 }); } catch { return null; }
|
|
674
677
|
return [`the allow list for this folder (${allow.length} rule${allow.length === 1 ? "" : "s"})`];
|
|
675
678
|
}
|
|
679
|
+
// ---- the owner's work, and allow lists inside it ---------------------------------------
|
|
680
|
+
// What the owner told Symbiot is their work: the folders it scans (Settings → Folders) and
|
|
681
|
+
// their knowledge folders, but not a hidden folder at the top of their home (~/.ssh,
|
|
682
|
+
// ~/.config: keys and settings), bar the folders Symbiot's own agents run in. An agent
|
|
683
|
+
// reaches its knowledge folders from the start (withScope), and an allow list it proposes
|
|
684
|
+
// that stays inside this work, asks for nothing wide, and doesn't publish or reach another
|
|
685
|
+
// machine is turned on by itself when its run ends (autoAllow): no question for the user.
|
|
686
|
+
const PUBLISH_RULE = /^Bash\(\s*(git push|git\s+-C\s+\S+\s+push|npm publish|yarn publish|pnpm publish|aws|gcloud|az|kubectl|docker|podman|terraform|ssh|scp|sftp|rsync|ftp|heroku|vercel|netlify|fly|flyctl)\b/i;
|
|
687
|
+
function workRoots() {
|
|
688
|
+
const roots = new Set();
|
|
689
|
+
try { for (const r of scanRoots()) roots.add(r); } catch {}
|
|
690
|
+
try { for (const k of knowledgeFolders()) roots.add(k.path); } catch {}
|
|
691
|
+
return [...roots].filter(Boolean).map((r) => r.replace(/\/+$/, "") || "/");
|
|
692
|
+
}
|
|
693
|
+
function inWork(p, roots = workRoots()) {
|
|
694
|
+
if (!p) return false;
|
|
695
|
+
p = resolve(String(p).replace(/^~(?=\/|$)/, homedir()).replace(/[/*]+$/, "")) || "/";
|
|
696
|
+
const drafts = join(CONFIG_DIR, "drafts");
|
|
697
|
+
if (p === drafts || p.startsWith(drafts + "/")) return true;
|
|
698
|
+
if (!roots.some((r) => p === r || p.startsWith(r === "/" ? "/" : r + "/"))) return false;
|
|
699
|
+
const rel = relative(homedir(), p);
|
|
700
|
+
if (!rel.startsWith("..") && rel.split("/")[0].startsWith(".")) return false; // ~/.ssh, ~/.config…
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
// The paths a rule names: Read(/x/**), Bash(find /x:*), additionalDirectories' /x.
|
|
704
|
+
const rulePaths = (r) => (String(r).match(/(?:~|\/)[^\s:*()"',]*/g) || []).filter((x) => x === "~" || x.length > 1);
|
|
705
|
+
// Whether a proposed list can be turned on without asking: { ok } or { why }.
|
|
706
|
+
function allowlistInWork(path) {
|
|
707
|
+
let p; try { p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; } catch { return { why: "no list" }; }
|
|
708
|
+
const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
|
|
709
|
+
const allow = list(p.allow), dirs = list(p.additionalDirectories), roots = [...workRoots(), path];
|
|
710
|
+
const wide = allow.filter(wideRule); if (wide.length) return { why: "wide: " + wide.join(", ") };
|
|
711
|
+
const pub = allow.filter((r) => PUBLISH_RULE.test(r)); if (pub.length) return { why: "publishes or reaches another machine: " + pub.join(", ") };
|
|
712
|
+
const out = [...dirs, ...allow.flatMap(rulePaths)].filter((x) => !inWork(x, roots));
|
|
713
|
+
if (out.length) return { why: "outside your work: " + [...new Set(out)].slice(0, 3).join(", ") };
|
|
714
|
+
return { ok: true, allow, dirs };
|
|
715
|
+
}
|
|
716
|
+
// A run ended asking for its proposed list: inside the work, it's turned on, the
|
|
717
|
+
// question is answered for the user, and the agent starts again. Once per proposal
|
|
718
|
+
// (.symbiot/autoallow.json), so an agent can't loop on it; past that it's the user's.
|
|
719
|
+
function autoAllow(path) {
|
|
720
|
+
if (!path || runningHandoff(path)) return null;
|
|
721
|
+
const f = join(path, ".symbiot", "allowlist.proposed.json"); let raw = ""; try { raw = readFileSync(f, "utf8"); } catch { return null; }
|
|
722
|
+
const q = agentQuestions(path, "").questions.find((x) => (x.options || []).some((o) => /allowlist\.proposed\.json/i.test(o)) || /allowlist\.proposed\.json/i.test(x.context || ""));
|
|
723
|
+
if (!q) return null;
|
|
724
|
+
const mark = join(path, ".symbiot", "autoallow.json"), d = digest(raw); let done = []; try { done = JSON.parse(readFileSync(mark, "utf8")); } catch {}
|
|
725
|
+
if (done.includes(d)) return null;
|
|
726
|
+
const v = allowlistInWork(path); if (!v.ok) return { asked: true, why: v.why };
|
|
727
|
+
const g = installAllowlist(path); if (!g || g.refused) return null;
|
|
728
|
+
try { writeFileSync(mark, JSON.stringify([...done, d].slice(-20))); } catch {}
|
|
729
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
730
|
+
try { writeFileSync(join(path, ".symbiot", "ANSWERS.md"), (readSymbiot(path, "ANSWERS.md") || "# Answers from the user\nAnswers to the questions in QUESTIONS.md, newest last. Follow them; ask again in QUESTIONS.md if one is unclear.\n").replace(/\s*$/, "\n") + `\n### ${q.q}\nSymbiot turned your list on (it stays inside the user's work and asks for nothing wide): .claude/settings.local.json in this folder. Carry on.\n_answered ${day}_\n`); } catch {}
|
|
731
|
+
try { unlinkSync(join(path, ".symbiot", WAITING)); } catch {}
|
|
732
|
+
const e = runHandoff(path, { force: true });
|
|
733
|
+
return { allowed: true, rerun: e && e.id ? e.id : null };
|
|
734
|
+
}
|
|
735
|
+
// Every run that stopped asking for its list, now: on start and every so often, so
|
|
736
|
+
// asks from before this version clear too.
|
|
737
|
+
function autoAllowSweep() { const out = []; for (const r of loadRuns()) { try { const a = autoAllow(r.path); if (a && a.allowed) out.push(r.path); } catch {} } return out; }
|
|
738
|
+
// Every agent reaches the owner's knowledge folders from the start; an ops run (no repo
|
|
739
|
+
// of its own) their repos too, the way a person at their desk would.
|
|
740
|
+
function withScope(tmpl, repoPath) {
|
|
741
|
+
if (!isClaudeCmd(tmpl)) return tmpl;
|
|
742
|
+
const dirs = new Set(); try { for (const k of knowledgeFolders()) dirs.add(k.path); } catch {}
|
|
743
|
+
if (/\/drafts\/act-[^/]+$/.test(String(repoPath || ""))) { try { for (const p of Object.values(repoPathMap() || {})) dirs.add(p); } catch {} }
|
|
744
|
+
dirs.delete(repoPath);
|
|
745
|
+
let cmd = tmpl; for (const d of dirs) if (d && !cmd.includes(`--add-dir "${d}"`)) cmd += ` --add-dir "${d}"`;
|
|
746
|
+
return cmd;
|
|
747
|
+
}
|
|
676
748
|
function grantFromStep(step, path = "", notes = {}) {
|
|
677
749
|
const t = String(step || "");
|
|
678
750
|
if (path && /allowlist\.proposed\.json|settings\.local\.json|press allow|\ballow list\b/i.test(t)) { const g = installAllowlist(path); if (g && g.refused) { notes.refused = g.refused; return null; } if (g) return g; }
|
|
@@ -771,4 +843,4 @@ function agentsList() {
|
|
|
771
843
|
}).concat(earlierRuns());
|
|
772
844
|
}
|
|
773
845
|
|
|
774
|
-
export { FACTS, factsOf, knownRun, 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, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList };
|
|
846
|
+
export { FACTS, factsOf, knownRun, 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, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList , autoAllow, autoAllowSweep, allowlistInWork, inWork, withScope };
|
package/handover.mjs
CHANGED
|
@@ -24,7 +24,7 @@ function handoverRules(lanes = [], here = "") {
|
|
|
24
24
|
}
|
|
25
25
|
// The rule for options in QUESTIONS.md: the user gets only what no agent can do,
|
|
26
26
|
// and the option says which kind it is.
|
|
27
|
-
const ONLY_YOU = "- Only what no agent can do is the user's: their body (a phone in their hand, a cable, which network they're on), their identity or secrets (signing in, a 2FA code, a token from a provider's console) or a decision that's theirs (closing an account, spending money, sending something in their name). Start such an option with `👤 You (only you: <which>):`, e.g. `👤 You (only you: your Meta token): paste it into .env`. Anything an agent could do, yours or another lane's, is never a 👤 step: do it, or hand it over. An option where picking it is enough starts with `🤖 Agent:`. Tag actions only: an option that just reports what the user saw or decides (`it notified me`, `not tried yet`) gets no tag. A permission your run doesn't have (a command, a folder) is never a chore for the user
|
|
27
|
+
const ONLY_YOU = "- Only what no agent can do is the user's: their body (a phone in their hand, a cable, which network they're on), their identity or secrets (signing in, a 2FA code, a token from a provider's console) or a decision that's theirs (closing an account, spending money, sending something in their name). Start such an option with `👤 You (only you: <which>):`, e.g. `👤 You (only you: your Meta token): paste it into .env`. Anything an agent could do, yours or another lane's, is never a 👤 step: do it, or hand it over. An option where picking it is enough starts with `🤖 Agent:`. Tag actions only: an option that just reports what the user saw or decides (`it notified me`, `not tried yet`) gets no tag. A permission your run doesn't have (a command, a folder) is never a chore for the user. Write the rules, as narrow as the task needs, to `.symbiot/allowlist.proposed.json` (`{\"permissions\": {\"allow\": [...], \"deny\": [...], \"additionalDirectories\": [...]}}`), ask for it in QUESTIONS.md with the option `👤 You (only you: a permission): allow the list in .symbiot/allowlist.proposed.json`, and stop. If it stays inside the user's work (the folders they gave Symbiot, not ~/.ssh or ~/.config), asks for nothing wide, and doesn't publish or reach another machine, Symbiot turns it on by itself and runs you again: no question reaches the user. Their knowledge folders are already yours to read. Never ask them to copy files or run claude in a terminal for it.";
|
|
28
28
|
|
|
29
29
|
// What a run hands back to Symbiot itself (handback.mjs reads it): facts for
|
|
30
30
|
// memory, and emails that wait on a reply, which Symbiot then watches for; and
|
package/home.mjs
CHANGED
|
@@ -178,7 +178,8 @@ function workScene({ deps = {} } = {}) {
|
|
|
178
178
|
const isParked = (repo) => !!map[repo] && parked.has(map[repo]);
|
|
179
179
|
const laneOf = (a) => { const l = laneOfPath(a.path, map); return l === "" ? RUNS_LANE : l || a.name; };
|
|
180
180
|
const seen = new Set(), running = [], asks = {};
|
|
181
|
-
|
|
181
|
+
const AG = agents();
|
|
182
|
+
for (const a of AG) {
|
|
182
183
|
if (seen.has(a.path)) continue; seen.add(a.path);
|
|
183
184
|
const nq = (a.ask && a.ask.questions && a.ask.questions.length) || 0;
|
|
184
185
|
if (nq) asks[laneOf(a)] = (asks[laneOf(a)] || 0) + nq;
|
|
@@ -195,6 +196,24 @@ function workScene({ deps = {} } = {}) {
|
|
|
195
196
|
ready.forEach((r) => { at(r.repo).ready = r.count || 1; });
|
|
196
197
|
open.forEach((t) => { const p = at(t.repo); p.waiting++; p.last = Math.max(p.last, Number(t.ts) || 0); });
|
|
197
198
|
Object.keys(asks).forEach((l) => { at(l).asks = asks[l]; });
|
|
199
|
+
// The relay (the Tasks screen, so you needn't open Agents): per project, what its
|
|
200
|
+
// latest run did or is doing in a line, its open questions with their answers,
|
|
201
|
+
// and the extra tasks its agents suggested, each with the folder that asked.
|
|
202
|
+
const DAY = 86400000, now = Date.now(), seenR = new Set();
|
|
203
|
+
for (const a of AG) {
|
|
204
|
+
if (seenR.has(a.path)) continue; seenR.add(a.path);
|
|
205
|
+
const l = laneOf(a), qs = (a.ask && a.ask.questions) || [], ideas = ((a.ask && a.ask.suggestions) || []).filter((s) => s && s.text && !s.added);
|
|
206
|
+
const age = a.startedAt ? now - a.startedAt : a.elapsed != null ? a.elapsed : Infinity, recent = a.status === "running" || age < DAY;
|
|
207
|
+
if (!by[l] && !qs.length && !ideas.length && !recent) continue;
|
|
208
|
+
const p = at(l);
|
|
209
|
+
if (!p.summary && (recent || qs.length)) {
|
|
210
|
+
const w = a.work || {}, todo = (w.todos || []).find((x) => x.status === "in_progress"), t = a.status === "running" ? ((todo && todo.active) || w.doing || "Working on it") : (w.final || a.tail || "");
|
|
211
|
+
const line = plain(String(t).split(/\n\s*\n/)[0], 220);
|
|
212
|
+
if (line) { p.summary = line; p.state = a.status === "running" ? "at work" : qs.length ? "asks you" : "done"; p.path = a.path; }
|
|
213
|
+
}
|
|
214
|
+
for (const q of qs) { p.qs = p.qs || []; if (p.qs.length < 3) p.qs.push({ q: q.q, options: (q.options || []).slice(0, 3), path: a.path }); }
|
|
215
|
+
for (const s of ideas) { p.ideas = p.ideas || []; if (p.ideas.length < 4) p.ideas.push({ text: plain(s.text, 200), full: String(s.text), repo: s.repo || l, path: a.path }); }
|
|
216
|
+
}
|
|
198
217
|
for (const p of Object.values(by)) { p.lit = !!(p.asks || p.ready); if (isParked(p.repo)) p.parked = true; }
|
|
199
218
|
const projects = Object.values(by).sort((a, b) => (b.lit ? 1 : 0) - (a.lit ? 1 : 0) || (a.parked ? 1 : 0) - (b.parked ? 1 : 0) || (b.running ? 1 : 0) - (a.running ? 1 : 0) || b.ready - a.ready || b.waiting - a.waiting || b.last - a.last || (a.repo < b.repo ? -1 : 1));
|
|
200
219
|
return { running, ready, waiting: waiting.slice(0, 12), waitingCount: waiting.length, canGo: waiting.filter((w) => !w.busy && !w.parked).length, projects: projects.slice(0, 24), projectCount: projects.length };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice
|
|
3
|
+
"version": "0.52.1",
|
|
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": {
|
|
7
7
|
"symbiot": "index.mjs"
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
|
-
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/post.mjs && node test/peek.mjs && node test/mind.mjs && node test/knowledge.mjs && node test/checks.mjs &&node test/lanes.mjs && node test/handback.mjs && node test/reports.mjs && node test/sandbox.mjs && node test/adapt.mjs && node test/work.mjs && node test/mapknn.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
77
|
+
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/post.mjs && node test/peek.mjs && node test/mind.mjs && node test/knowledge.mjs && node test/checks.mjs &&node test/lanes.mjs && node test/handback.mjs && node test/autoallow.mjs && node test/reports.mjs && node test/sandbox.mjs && node test/adapt.mjs && node test/work.mjs && node test/mapknn.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
78
78
|
"lint": "eslint ."
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
package/server.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { createServer } from "node:http";
|
|
|
12
12
|
import { randomBytes } from "node:crypto";
|
|
13
13
|
import { EMBEDDED_UI } from "./ui.mjs";
|
|
14
14
|
import { VERSION, LATEST_VERSION, REGISTRY, semverGt, checkLatest, CONFIG_PATH, loadConfig, saveConfig, loadTasks, hasCmd, chromeBinary } from "./core.mjs";
|
|
15
|
-
import { shSingle, handoffCmd, setHandoffCmd, grantAgent, runHandoff, track, detectHandoffs, connectorsInfo, answerQuestions, skipIdea, agentsList, startWaiting, parkLane, parkedPaths } from "./agents.mjs";
|
|
15
|
+
import { shSingle, handoffCmd, setHandoffCmd, grantAgent, runHandoff, track, detectHandoffs, connectorsInfo, answerQuestions, skipIdea, agentsList, startWaiting, parkLane, parkedPaths, autoAllowSweep } from "./agents.mjs";
|
|
16
16
|
import { PROVIDERS, resolveProvider, connectProvider, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel } from "./ai.mjs";
|
|
17
17
|
import { SCAN, SCAN_TIMEOUT_MS, scanRoots, scanHome, addScanRoot, removeScanRoot, buildMap, nodeDetail, repoPathMap } from "./scan.mjs";
|
|
18
18
|
import { computeDrift } from "./drift.mjs";
|
|
@@ -424,6 +424,9 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
424
424
|
startPhone(); // Watch on your phone: the computer listens if it's switched on, the phone asks if it's paired
|
|
425
425
|
setInterval(() => { try { startWaiting(); } catch {} }, 20000).unref(); // a run that waits for your step starts once the file it names changes
|
|
426
426
|
setInterval(() => { try { lanesTick(); } catch {} }, 20000).unref(); // agents hand work to other lanes, and hear back when it's done
|
|
427
|
+
// an agent's allow list that stays inside your work is turned on by itself, and the agent carries on
|
|
428
|
+
setTimeout(() => { try { autoAllowSweep(); } catch {} }, 4000).unref();
|
|
429
|
+
setInterval(() => { try { autoAllowSweep(); } catch {} }, 20000).unref();
|
|
427
430
|
// knowledge folders: changed files re-read (a stat per file when nothing changed), then checked again for where two files disagree (checks.mjs)
|
|
428
431
|
const knowTick = () => { try { const r = knowledgeTick(); if (r && (r.read || r.removed || !checksState().at)) runChecks(); } catch {} };
|
|
429
432
|
setTimeout(knowTick, 5000).unref(); setInterval(knowTick, 3 * 60 * 1000).unref();
|
package/ui.mjs
CHANGED
|
@@ -681,6 +681,50 @@ body.lq-liquid .sset .cxh{margin-top:0}
|
|
|
681
681
|
/* the Close bar carries the panel's own ground, so what scrolls under it stays clean */
|
|
682
682
|
body.lq-liquid.lq-pooled #lqsinkrow{margin:-14px -14px 10px;padding:10px 10px 8px;background:linear-gradient(180deg,rgba(6,8,11,.97) 70%,rgba(6,8,11,0));border-radius:22px 22px 0 0}
|
|
683
683
|
body.lq-liquid.lq-light.lq-pooled #lqsinkrow{background:linear-gradient(180deg,rgba(246,248,251,.98) 70%,rgba(246,248,251,0))}
|
|
684
|
+
|
|
685
|
+
/* Agents: needs you, at work, finished (folded) */
|
|
686
|
+
.agdone{margin-top:6px}
|
|
687
|
+
.agdone > summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px}
|
|
688
|
+
.agdone > summary::-webkit-details-marker{display:none}
|
|
689
|
+
.agdone > summary::before{content:'';width:5px;height:5px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .2s ease}
|
|
690
|
+
.agdone[open] > summary::before{transform:rotate(45deg)}
|
|
691
|
+
.tgroup.agg{margin-top:16px}
|
|
692
|
+
/* the relay: Agents on the Tasks screen, a line and a blob per project */
|
|
693
|
+
#lqline,#lqrelay{display:none}
|
|
694
|
+
body.lq-work:not(.lq-pooled) #lqline{display:block;position:absolute;left:50%;top:84px;transform:translateX(-50%);z-index:3;max-width:calc(100% - 32px);text-align:center;font:400 13.5px/1.4 var(--sans);color:#A1A8B1;text-shadow:0 1px 10px rgba(0,0,0,.7)}
|
|
695
|
+
#lqline b{color:#F2A541;font-weight:600}
|
|
696
|
+
body.lq-light #lqline{color:#4A5565;text-shadow:0 1px 8px rgba(255,255,255,.8)}body.lq-light #lqline b{color:#9A5200}
|
|
697
|
+
body.lq-work:not(.lq-pooled) #lqrelay:not(:empty){display:flex;flex-direction:column;gap:12px;position:absolute;right:16px;top:116px;bottom:96px;width:min(420px,32vw);overflow-y:auto;scrollbar-width:none;z-index:4;padding:2px 2px 24px;-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 28px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 28px),transparent)}
|
|
698
|
+
#lqrelay::-webkit-scrollbar{display:none}
|
|
699
|
+
@media (max-width:899px){body.lq-work:not(.lq-pooled) #lqrelay:not(:empty){display:none}}
|
|
700
|
+
.rly{flex:none;display:flex;flex-direction:column;gap:8px;padding:14px 16px;border-radius:20px;background:rgba(14,16,19,.86);border:1px solid rgba(255,255,255,.08);backdrop-filter:blur(14px);box-shadow:0 22px 50px -28px rgba(0,0,0,.9);color:#ECE9E4}
|
|
701
|
+
.rly.lit{border-color:rgba(242,165,65,.35)}
|
|
702
|
+
body.lq-light .rly{background:rgba(255,255,255,.92);border-color:rgba(21,26,33,.08);color:#151A21;box-shadow:0 22px 50px -30px rgba(30,45,60,.45)}
|
|
703
|
+
body.lq-light .rly.lit{border-color:rgba(154,82,0,.3)}
|
|
704
|
+
.rlh{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
|
|
705
|
+
.rlh b{font:600 15px var(--sans);letter-spacing:-.01em}
|
|
706
|
+
.rls{font:500 11.5px var(--sans);color:#8A919B;white-space:nowrap}
|
|
707
|
+
.rly.lit .rls{color:#F2A541}body.lq-light .rly.lit .rls{color:#9A5200}
|
|
708
|
+
.rlsum{margin:0;font-size:13.5px;line-height:1.45;color:#C9CDD3;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
709
|
+
body.lq-light .rlsum{color:#2B3644}
|
|
710
|
+
.lqblob.rlask{position:static;display:flex;flex-direction:column;gap:7px;padding:10px 12px;border-radius:14px;background:rgba(242,165,65,.06);border:1px solid rgba(242,165,65,.22)}
|
|
711
|
+
.lqblob.rlask .nhead{font-size:12px;color:#8A919B}.lqblob.rlask .nhead b{font-weight:600;color:inherit}
|
|
712
|
+
.lqblob.rlask .bq{-webkit-line-clamp:4;font-size:13px}
|
|
713
|
+
body.lq-light .lqblob.rlask{background:rgba(154,82,0,.05);border-color:rgba(154,82,0,.2)}
|
|
714
|
+
.rly .rlok{align-self:flex-start}
|
|
715
|
+
.rlk{margin-top:2px;font:500 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;color:#8A919B}
|
|
716
|
+
.rli{display:flex;align-items:flex-start;gap:10px;padding:6px 0;border-top:1px solid rgba(255,255,255,.06);font-size:13px;line-height:1.4;color:#C9CDD3;transition:opacity .3s ease}
|
|
717
|
+
.rli > span:first-child{flex:1;min-width:0}
|
|
718
|
+
.rli.gone{opacity:0}
|
|
719
|
+
body.lq-light .rli{border-top-color:rgba(21,26,33,.07);color:#2B3644}
|
|
720
|
+
.rlia{flex:none;display:flex;gap:2px}
|
|
721
|
+
.rlia button,.rlf button{all:unset;box-sizing:border-box;cursor:pointer;padding:3px 7px;border-radius:8px;font:500 12px var(--sans);color:#8A919B}
|
|
722
|
+
.rlia button:hover,.rlf button:hover{color:#ECE9E4;background:rgba(255,255,255,.06)}
|
|
723
|
+
body.lq-light .rlia button:hover,body.lq-light .rlf button:hover{color:#151A21;background:rgba(21,26,33,.05)}
|
|
724
|
+
.rlia .rladd{color:#ECE9E4}body.lq-light .rlia .rladd{color:#151A21}
|
|
725
|
+
.rlia button:focus-visible,.rlf button:focus-visible{outline:2px solid currentColor;outline-offset:1px}
|
|
726
|
+
.rldone{font-size:12px;color:#8A919B}
|
|
727
|
+
.rlf{display:flex;justify-content:space-between;margin:2px -7px -4px}
|
|
684
728
|
/* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
|
|
685
729
|
.bstream{position:relative;margin-top:10px;border-radius:18px;overflow:hidden;isolation:isolate;background:#08090B;box-shadow:0 0 0 1px rgba(255,255,255,.08)}
|
|
686
730
|
body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
@@ -737,6 +781,8 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
737
781
|
<div id="liquid" aria-label="Symbiot">
|
|
738
782
|
<canvas id="lq" aria-hidden="true"></canvas>
|
|
739
783
|
<div id="lqorbits"></div>
|
|
784
|
+
<div id="lqline" aria-live="polite"></div>
|
|
785
|
+
<div id="lqrelay" aria-label="Your projects: what their agents did, ask and suggest"></div>
|
|
740
786
|
<div id="lqgroups"></div>
|
|
741
787
|
<div id="lqneedt"><span>Needs you</span><small>only you can do these</small></div>
|
|
742
788
|
<div id="lqdrops"></div>
|
|
@@ -1350,9 +1396,12 @@ function answering(){var f=document.activeElement;return !!(f&&f.closest&&f.clos
|
|
|
1350
1396
|
// Handovers (lanes.mjs): what one lane's agent handed to another, and where it stands.
|
|
1351
1397
|
var LANEWORD={started:'working on it',held:'queued: that lane is busy',done:'done, reported back',error:"couldn't hand over"};
|
|
1352
1398
|
function loadLanes(){api('/api/lanes').then(function(d){var el=document.getElementById('laneslist');if(!el||!d)return;var hs=d.handoffs||[];
|
|
1353
|
-
|
|
1399
|
+
var row=function(h){
|
|
1354
1400
|
var back=h.result||h.error,more=(h.full?"<div>"+esc(h.full)+"</div>":"")+(back?"<div class='tres'>"+esc(back)+"</div>":"");
|
|
1355
|
-
return "<div class='task'><span class='t'><b>"+esc(h.from)+"</b> → <b>"+esc(h.to)+"</b>: "+esc(h.text)+(more?"<details class='tfull'><summary>"+(h.full?"in full":"what came back")+"</summary>"+more+"</details>":"")+"</span><span class='rp'"+(h.status==='error'?" style='color:var(--amber)'":"")+">"+esc(LANEWORD[h.status]||h.status)+"</span></div>";}
|
|
1401
|
+
return "<div class='task'><span class='t'><b>"+esc(h.from)+"</b> → <b>"+esc(h.to)+"</b>: "+esc(String(h.text||'').split('**').join(''))+(h.n>1?" <span class='muted'>×"+h.n+"</span>":"")+(more?"<details class='tfull'><summary>"+(h.full?"in full":"what came back")+"</summary>"+more+"</details>":"")+"</span><span class='rp'"+(h.status==='error'?" style='color:var(--amber)'":"")+">"+esc(LANEWORD[h.status]||h.status)+"</span></div>";};
|
|
1402
|
+
var seen={},uniq=[];hs.forEach(function(h){var k=h.from+'|'+h.to+'|'+h.status+'|'+h.text;if(seen[k]){seen[k].n++;return;}var c={};for(var x in h)c[x]=h[x];c.n=1;seen[k]=c;uniq.push(c);});
|
|
1403
|
+
var live=uniq.filter(function(h){return h.status!=='done';}),fin=uniq.filter(function(h){return h.status==='done';});
|
|
1404
|
+
whenFree(el,'lanes',function(){sameHtml(el,(live.length?"<div class='tgroup agg'>Handovers under way <span class='tcount'>"+live.length+"</span></div>"+live.slice(0,12).map(row).join(''):'')+(fin.length?"<details class='agdone'><summary class='tgroup agg'>Finished handovers <span class='tcount'>"+fin.length+"</span></summary>"+fin.slice(0,20).map(row).join('')+"</details>":''));});});}
|
|
1356
1405
|
|
|
1357
1406
|
// ---- an agent's work, live (work.mjs parses its stream) ------------------------------
|
|
1358
1407
|
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'/>"};
|
|
@@ -1381,12 +1430,18 @@ if(todoBox)h+="<div class='wk'>"+todoBox+"</div>";
|
|
|
1381
1430
|
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>";
|
|
1382
1431
|
return h;}
|
|
1383
1432
|
function loadAgents(){loadLanes();loadParked(agentsDraw);}
|
|
1433
|
+
// Agents, in order: what needs you (a question, a step of yours), what's at work, and
|
|
1434
|
+
// what's finished, folded away (open stays open across refreshes).
|
|
1435
|
+
var AGDONEOPEN=false;
|
|
1436
|
+
function agentGroups(list,fn){var needs=[],run=[],done=[];list.forEach(function(a){if(nQs(a)||a.waiting)needs.push(a);else if(a.status==='running')run.push(a);else done.push(a);});
|
|
1437
|
+
var g=function(t,xs){return xs.length?"<div class='tgroup agg'>"+t+" <span class='tcount'>"+xs.length+"</span></div>"+xs.map(fn).join(''):'';};
|
|
1438
|
+
return g('Needs you',needs)+g('At work',run)+(done.length?"<details class='agdone'"+(AGDONEOPEN?' open':'')+"><summary class='tgroup agg'>Finished <span class='tcount'>"+done.length+"</span></summary>"+done.map(fn).join('')+"</details>":'');}
|
|
1384
1439
|
function agentsDraw(){api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
|
|
1385
1440
|
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;}
|
|
1386
1441
|
var anyRunning=list.some(function(a){return a.status==='running';});
|
|
1387
1442
|
if(answering()||selIn(el)){stopAgentsPoll();if(anyRunning&¤t==='agents')agentsTimer=setTimeout(loadAgents,2000);else selWait('agents',loadAgents);return;} // don't re-render under someone typing an answer, or selecting text to copy
|
|
1388
1443
|
saveDrafts(el);AGENTLIST=list;
|
|
1389
|
-
|
|
1444
|
+
var agentBlock=function(a){var cls=a.status==='running'?'run':(a.status==='done'?'ok':'fail');
|
|
1390
1445
|
var st=a.status==='running'?('working · '+fmtE(a.elapsed)):(esc(a.status)+' · '+fmtE(a.elapsed)+(a.exitCode!=null?' · exit '+a.exitCode:''));
|
|
1391
1446
|
if(a.fromHeld)st+=" · started on the tasks held for the last run";
|
|
1392
1447
|
if(a.earlier)st+=" · "+(a.status==='running'?"started outside this window":"ran before Symbiot last started");
|
|
@@ -1413,7 +1468,8 @@ if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
|
1413
1468
|
}
|
|
1414
1469
|
b+=waitHtml(a)+askHtml(a)+factsHtml(a);
|
|
1415
1470
|
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>");
|
|
1416
|
-
return "<div class='agent' data-i='"+list.indexOf(a)+"'>"+b+"</div>";}
|
|
1471
|
+
return "<div class='agent' data-i='"+list.indexOf(a)+"'>"+b+"</div>";};
|
|
1472
|
+
el.innerHTML=agentGroups(list,agentBlock);var dn=el.querySelector('.agdone');if(dn)dn.addEventListener('toggle',function(){AGDONEOPEN=dn.open;});
|
|
1417
1473
|
el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
|
|
1418
1474
|
restoreDrafts(el);wireAsks(el);wireWaits(el);wireFacts(el);lqLight();
|
|
1419
1475
|
el.querySelectorAll('.apark').forEach(function(btn){btn.addEventListener('click',function(){var g=AGENTLIST[+btn.closest('.agent').getAttribute('data-i')];if(!g)return;btn.disabled=true;lqPark('',btn.getAttribute('data-on')==='1',function(err){btn.disabled=false;$('agentsmsg').innerHTML=err?"<div class='note err'>"+esc(err)+"</div>":'';loadAgents();},g.path);});});
|
|
@@ -2063,11 +2119,41 @@ window.addEventListener('focus',function(){heartbeat(true);}); // re-check for u
|
|
|
2063
2119
|
// turn. One Go starts what's waiting. A tap opens the details; nothing else to read.
|
|
2064
2120
|
function lqWork(){lqRemember();LQ.scene='work';LQ.mode='aware';var sc=$('lqscene');if(sc)sc.textContent=LQ.workBy==='project'?'Projects':'Tasks and agents';var b=document.body;if(b&&b.classList){b.classList.add('lq-work');b.classList.toggle('lq-byproj',LQ.workBy==='project');b.classList.remove('lq-pooled');}lqLoadWork();
|
|
2065
2121
|
if(LQ.workTimer)clearInterval(LQ.workTimer);LQ.workTimer=setInterval(function(){if(LQ.scene==='work'&&LQ.mode!=='pool')lqLoadWork();},5000);}
|
|
2066
|
-
function lqHome(){lqRemember();LQ.scene='home';var b=document.body;if(b&&b.classList){b.classList.remove('lq-work');b.classList.remove('lq-byproj');b.classList.remove('lq-pooled');}if(LQ.workTimer){clearInterval(LQ.workTimer);LQ.workTimer=null;}LQ.mode='aware';lqLoad(true);}
|
|
2122
|
+
function lqHome(){lqRemember();LQ.scene='home';LQ.relaySig='';var rl=$('lqrelay');if(rl)rl.innerHTML='';var li=$('lqline');if(li)li.innerHTML='';var b=document.body;if(b&&b.classList){b.classList.remove('lq-work');b.classList.remove('lq-byproj');b.classList.remove('lq-pooled');}if(LQ.workTimer){clearInterval(LQ.workTimer);LQ.workTimer=null;}LQ.mode='aware';lqLoad(true);}
|
|
2123
|
+
|
|
2124
|
+
// ---- the relay: Agents, on the Tasks screen ----------------------------------------
|
|
2125
|
+
// One line for where everything stands, then a blob per project with something to say:
|
|
2126
|
+
// what its agent did or is doing, what it asks you (its answers as buttons, as on
|
|
2127
|
+
// home), Review and approve when it's ready, and the extra tasks it suggests (+ task
|
|
2128
|
+
// or Skip). You needn't open Agents; it's still there for the detail.
|
|
2129
|
+
function lqLineText(w){var nr=(w.running||[]).length,nq=0,ni=0,nk=(w.ready||[]).length,nw=w.waitingCount||0;(w.projects||[]).forEach(function(p){nq+=p.asks||0;ni+=(p.ideas||[]).length;});
|
|
2130
|
+
var bits=[];if(nq)bits.push('<b>'+nq+' question'+(nq>1?'s':'')+' for you</b>');if(nk)bits.push('<b>'+nk+' ready for your OK</b>');if(nr)bits.push(nr+' at work');if(nw)bits.push(nw+' waiting');if(ni)bits.push(ni+' suggested task'+(ni>1?'s':''));
|
|
2131
|
+
return bits.length?bits.join(' · '):'All quiet: nothing at work, nothing asking you.';}
|
|
2132
|
+
function lqRelayList(w){return lqSortLanes((w.projects||[]).filter(function(p){return p.summary||(p.qs&&p.qs.length)||p.ready||(p.ideas&&p.ideas.length);}).slice());}
|
|
2133
|
+
function lqRelayOn(){var S=lqSize();return LQ.scene==='work'&&S.w>=900&&lqRelayList(LQ.workData||{}).length>0;}
|
|
2134
|
+
function lqRelay(){var el=$('lqrelay'),ln=$('lqline'),w=LQ.workData||{};if(ln)ln.innerHTML=LQ.scene==='work'?lqLineText(w):'';if(!el)return;
|
|
2135
|
+
var ps=lqRelayList(w),sig=JSON.stringify(ps.map(function(p){return [p.repo,p.summary,p.state,p.ready,(p.qs||[]).map(function(q){return q.q;}),(p.ideas||[]).map(function(i){return i.full;}),p.parked];}));
|
|
2136
|
+
if(el.contains&&document.activeElement&&el.contains(document.activeElement)&&el.innerHTML)return;if(sig===LQ.relaySig&&el.innerHTML)return;LQ.relaySig=sig;
|
|
2137
|
+
if(!ps.length){el.innerHTML='';return;}
|
|
2138
|
+
el.innerHTML=ps.map(function(p,i){var nm=p.name||p.repo,q=(p.qs||[])[0],st=p.parked?'parked':(p.qs&&p.qs.length)?'asks you':p.ready?'ready for your OK':p.state||'';
|
|
2139
|
+
var h="<section class='rly"+((p.qs&&p.qs.length)||p.ready?' lit':'')+"' data-i='"+i+"'><div class='rlh'><b>"+esc(nm)+"</b>"+(st?"<span class='rls'>"+esc(st)+"</span>":"")+"</div>";
|
|
2140
|
+
if(p.summary)h+="<p class='rlsum' title='"+escQ(p.summary)+"'>"+esc(p.summary)+"</p>";
|
|
2141
|
+
if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
|
|
2142
|
+
if(p.ready)h+="<button type='button' class='act rlok'>Review and approve</button>";
|
|
2143
|
+
if(p.ideas&&p.ideas.length)h+="<div class='rlk'>Extra tasks it suggests</div>"+p.ideas.map(function(d,j){return "<div class='rli' data-j='"+j+"'><span title='"+escQ(d.full)+"'>"+esc(d.text)+"</span><span class='rlia'><button type='button' class='rladd'>+ task</button><button type='button' class='rlskip'>Skip</button></span></div>";}).join('');
|
|
2144
|
+
return h+"<div class='rlf'><button type='button' class='rlopen'>Its tasks ›</button><button type='button' class='rlag'>Details in Agents ›</button></div></section>";}).join('');
|
|
2145
|
+
el.querySelectorAll('.rly').forEach(function(box){var p=ps[+box.getAttribute('data-i')],q=(p.qs||[])[0],nm=p.name||p.repo;
|
|
2146
|
+
var ab=box.querySelector('.rlask');if(ab&&q)wireBlob(ab,{kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path},{done:function(){LQ.relaySig='';lqLoadWork();}});
|
|
2147
|
+
var ok=box.querySelector('.rlok');if(ok)ok.addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo,lit:true});});
|
|
2148
|
+
box.querySelector('.rlopen').addEventListener('click',function(){lqOpen({kind:'proj',repo:p.repo});});
|
|
2149
|
+
box.querySelector('.rlag').addEventListener('click',function(){lqPool('agents');});
|
|
2150
|
+
box.querySelectorAll('.rli').forEach(function(row){var d=p.ideas[+row.getAttribute('data-j')],a=row.querySelector('.rladd'),k=row.querySelector('.rlskip');
|
|
2151
|
+
a.addEventListener('click',function(){a.disabled=k.disabled=true;api('/api/tasks/add',{text:d.full,repo:d.repo}).then(function(r){if(!r||r.error){a.disabled=k.disabled=false;lqSaid(nm+': '+((r&&r.error)||'couldn’t add it'),true);return;}row.querySelector('.rlia').innerHTML="<span class='rldone'>in Tasks</span>";lqSaid('Added to '+nm+'’s tasks.');});});
|
|
2152
|
+
k.addEventListener('click',function(){a.disabled=k.disabled=true;api('/api/agents/skip',{path:d.path,text:d.full}).then(function(r){if(!r||r.error){a.disabled=k.disabled=false;lqSaid(nm+': '+((r&&r.error)||'couldn’t skip it'),true);return;}if(row.classList)row.classList.add('gone');setTimeout(function(){row.remove();},300);});});});});}
|
|
2067
2153
|
function lqLoadWork(){api('/api/work').then(function(w){LQ.workData=w||{};lqRedraw();}).catch(function(){});}
|
|
2068
2154
|
// New data, drawn: unless you're selecting text on the surface, then once you let go
|
|
2069
2155
|
function lqRedraw(){whenFree($('liquid'),'liquid',function(){if(LQ.scene==='work')lqBuildWork();else lqBuild();});}
|
|
2070
|
-
function lqBuildWork(){if(LQ.workBy==='project'){lqBuildProjects();return;}var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;
|
|
2156
|
+
function lqBuildWork(){lqRelay();if(LQ.workBy==='project'){lqBuildProjects();return;}var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;if(lqRelayOn()){var RW=Math.min(420,S.w*0.32)+28;S={w:S.w-RW,h:S.h,s:S.s};cx=S.w/2;}
|
|
2071
2157
|
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);
|
|
2072
2158
|
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;
|
|
2073
2159
|
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':'';
|
|
@@ -2095,7 +2181,7 @@ var LANESORT='need';try{LANESORT=(window.localStorage&&window.localStorage.getIt
|
|
|
2095
2181
|
function lqSortLanes(ps){if(LANESORT==='name')return ps.sort(function(a,b){var x=String(a.name||a.repo).toLowerCase(),y=String(b.name||b.repo).toLowerCase();return x<y?-1:x>y?1:0;});
|
|
2096
2182
|
if(LANESORT==='recent')return ps.sort(function(a,b){return (b.last||0)-(a.last||0)||(b.lit?1:0)-(a.lit?1:0);});return ps;}
|
|
2097
2183
|
function lqSort(s){LANESORT=s;try{if(window.localStorage)window.localStorage.setItem('symbiot-lanesort',s);}catch(e){}var g=$('lqsort');if(g&&g.querySelectorAll)g.querySelectorAll('button').forEach(function(b){b.setAttribute('aria-checked',b.getAttribute('data-sort')===s?'true':'false');});lqAct();if(LQ.scene==='work'&&LQ.workBy==='project')lqBuildProjects();}
|
|
2098
|
-
function lqBuildProjects(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;
|
|
2184
|
+
function lqBuildProjects(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list=[];LQ.bw=S.w;LQ.bh=S.h;if(lqRelayOn()){var RW=Math.min(420,S.w*0.32)+28;S={w:S.w-RW,h:S.h,s:S.s};cx=S.w/2;}
|
|
2099
2185
|
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);
|
|
2100
2186
|
var all=lqSortLanes((w.projects||[]).slice()),cap=S.w<700?5:7,ps=all.slice(0,cap),rest=all.slice(cap),slots=Math.max(3,ps.length+(rest.length?1:0));
|
|
2101
2187
|
ps.forEach(function(p,i){var ang=Math.PI+i*2*Math.PI/slots,load=p.waiting+2*p.ready+(p.running?3:0)+2*Math.min(2,p.asks||0);
|