symbiot 0.49.2 → 0.50.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 +13 -0
- package/README.md +20 -13
- package/agents.mjs +18 -1
- package/mapknn.mjs +71 -0
- package/package.json +4 -3
- package/scan.mjs +20 -3
- package/ui.mjs +330 -67
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
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.50.1
|
|
6
|
+
|
|
7
|
+
- **A permission you pick is given.** When an agent asks for one ("let agents read ~/.config/symbiot/screens") and you choose it in the app, Symbiot grants it then and there (as Settings' grant boxes do) and the agent carries on, instead of leaving it as a step for you to do by hand. Steps only you can do, like pasting your own message, are still yours.
|
|
8
|
+
- **The chat on home rolls.** Newest at the bottom by the input; older lines fade as they rise and dissolve before they reach the droplets. Point at it or scroll back to read the whole conversation clearly.
|
|
9
|
+
- **No asterisks in names.** An agent's name on home no longer shows its markdown (`**What's needed:**`).
|
|
10
|
+
|
|
11
|
+
## 0.50.0
|
|
12
|
+
|
|
13
|
+
- **The Map, by nearest neighbours.** Your repos are droplets in the liquid, each beside the repos most like it: what they're built with, the weeks you work on them, and what they're about (from their READMEs). The closest merge into one shape, the next are joined by a thread of liquid; each cluster is named over its region; what you haven't touched in a while is smaller and sinks; "you are here" sits where your recent work is. Point at a repo to see its neighbours and why; click for its details, now with **Most like it** and your last 12 weeks. Zoom and fit buttons, and the languages and tools you build with underneath instead of a coloured legend. Screens folds away.
|
|
14
|
+
- **Every panel in the look.** Week, Standup and Todo are a page with a title and dates, the write-up set as a document. Tasks are calm rows with round check beads; Ask and Remove are icons. Agents' questions get a calm card with bead radio buttons. Drift has a status bead per repo, switches for its options, and its issues listed under each. Settings is a column of titled cards.
|
|
15
|
+
- **Icons, not emoji.** Chat, person, agent, idea, question, archive and plug are drawn line icons in the look's ink.
|
|
16
|
+
- **Close stays clean.** Content scrolls under the Close bar instead of colliding with it.
|
|
17
|
+
|
|
5
18
|
## 0.49.2
|
|
6
19
|
|
|
7
20
|
- **Projects shows your projects.** Click **Projects** on the liquid and each repo with work on it is a sphere: what's going on there under its name (an agent at work, ready for your OK, or how many tasks are waiting), spheres orbiting while an agent works, amber when its work waits for you. Click one to open just that project's tasks; right-click comes back.
|
package/README.md
CHANGED
|
@@ -43,18 +43,24 @@ no install — it's the same CLI. Press Ctrl+C (or click Quit) to stop.
|
|
|
43
43
|
|
|
44
44
|
It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of these parts opens from it:
|
|
45
45
|
|
|
46
|
-
- **Map** —
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
**
|
|
46
|
+
- **Map** — your repos and project folders as droplets in the liquid, placed by
|
|
47
|
+
their **nearest neighbours**: each repo's fingerprint is what it's built with
|
|
48
|
+
(languages and tools), the weeks you work on it (your commits per week over the
|
|
49
|
+
last 12), and what it's about (the words of its name, README and package
|
|
50
|
+
description, TF-IDF weighted), and it sits by the 3 repos most like it
|
|
51
|
+
(`mapknn.mjs`). The closest merge into one shape, the next are joined by a thin
|
|
52
|
+
thread of liquid, the rest by a faint line; each cluster is named over its region
|
|
53
|
+
("JavaScript · Node"); what you haven't touched in a while is smaller and sinks;
|
|
54
|
+
**you are here** marks where your recent work is. **Point at a repo** to light up
|
|
55
|
+
its neighbours and see why ("shares JavaScript, Node; you worked on both the same
|
|
56
|
+
weeks"). **Click** for its details: branch, commits, uncommitted work, your last
|
|
57
|
+
12 weeks, **Most like it**, a **Suggest next steps** button, and an **AI review**
|
|
58
|
+
(what the project does, whether it needs new work at all, and ideas you can tick
|
|
59
|
+
into **Tasks**). Scroll to zoom, drag to pan. Under the map: the languages and
|
|
60
|
+
tools you build with. The Map needs **no AI key**; reviews and suggestions use
|
|
61
|
+
your chosen model. Project folders that **aren't git repos** show up too (see
|
|
62
|
+
[Where it looks](#where-it-looks)), as rings. The [experimental](#screens-blueprints-for-screen-automation)
|
|
63
|
+
**Screens** view folds away under the map.
|
|
58
64
|
- **Dashboard** — everything you [watch](#screens-blueprints-for-screen-automation),
|
|
59
65
|
as a stream: time runs left to right, one current per feed (your inbox, GitHub,
|
|
60
66
|
WhatsApp, any other page), "now" on the right where each feed pools. Every message
|
|
@@ -75,7 +81,8 @@ It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of
|
|
|
75
81
|
agent can go over what's new with you too.
|
|
76
82
|
- **Drift** — the [`symbiot drift`](#whats-out-of-sync-symbiot-drift) report, with
|
|
77
83
|
a "fetch latest" toggle (and an [experimental](#experimental) "check CI").
|
|
78
|
-
- **Week / Standup / Todo** — the write-ups (these use your chosen AI)
|
|
84
|
+
- **Week / Standup / Todo** — the write-ups (these use your chosen AI), set as a
|
|
85
|
+
readable page with its dates; **Copy** copies the text as written.
|
|
79
86
|
- **Tasks** — a checklist, grouped by kind (Fixes, Tests & CI, Docs, …). Filter by
|
|
80
87
|
type or repo, then **Send to repos** to hand just those to your agent (see
|
|
81
88
|
[`symbiot push`](#hand-tasks-to-your-coding-agent-symbiot-push)). What your agent
|
package/agents.mjs
CHANGED
|
@@ -573,6 +573,20 @@ function agentQuestions(path, repo) {
|
|
|
573
573
|
// An answer that picks a "👤 You:" option is a step for the user: they're told
|
|
574
574
|
// it's still theirs (`yours`), and the next run waits for it (waiting.json).
|
|
575
575
|
// A folder a run started in before a restart can be answered too (runs.json).
|
|
576
|
+
// A permission picked in the app is given then and there: the click is the user's
|
|
577
|
+
// say-so. "let agents read ~/x" / "allow /x" becomes --add-dir, "let agents run
|
|
578
|
+
// \`tool\`" a grant for that tool (never sudo/rm), as Settings' grant boxes do. What's
|
|
579
|
+
// granted isn't a step left for the user. Returns what was granted, or null.
|
|
580
|
+
function grantFromStep(step) {
|
|
581
|
+
const t = String(step || "");
|
|
582
|
+
if (!/only you: a permission|\b(let|allow) (the )?agents?\b|\ballow (access|reading)\b/i.test(t)) return null;
|
|
583
|
+
const got = [];
|
|
584
|
+
const dir = (t.match(/(~\/[^\s,;)`'"]+|\/(?:home|Users|opt|srv|mnt|media|tmp)\/[^\s,;)`'"]+)/) || [])[1];
|
|
585
|
+
if (dir) { const d = dir.replace(/^~(?=\/)/, homedir()).replace(/[.]$/, ""); const r = grantAgent({ dir: d }); if (r && r.ok) got.push(d); }
|
|
586
|
+
const tool = (t.match(/\b(?:run|use)\s+`([A-Za-z0-9._+-]+)`/) || [])[1];
|
|
587
|
+
if (tool && !/^(sudo|rm|dd|mkfs)$/.test(tool)) { const r = grantAgent({ tool }); if (r && r.ok) got.push(tool); }
|
|
588
|
+
return got.length ? got : null;
|
|
589
|
+
}
|
|
576
590
|
const stepText = (a) => String(a).split("🤖")[0].replace(/^\s*👤\s*(You:)?\s*/, "").replace(/\s*\(recommended\)\s*$/i, "").trim();
|
|
577
591
|
function answerQuestions(path, answers, opts = {}) {
|
|
578
592
|
path = String(path || "");
|
|
@@ -594,7 +608,9 @@ function answerQuestions(path, answers, opts = {}) {
|
|
|
594
608
|
const youOpts = (q) => ((asked.get(qKey(q)) || {}).options || []).filter(yourStep).map((o) => o.split("🤖")[0]);
|
|
595
609
|
const steps = rows.filter((x) => yourStep(x.a) || holdAnswer(x.a)).map((x) => yourStep(x.a) ? { step: stepText(x.a), named: x.a.split("🤖")[0] }
|
|
596
610
|
: { step: (youOpts(x.q).map(stepText)[0] || x.a).trim(), named: [x.a, ...youOpts(x.q)].join(" ") });
|
|
597
|
-
const
|
|
611
|
+
const granted = [];
|
|
612
|
+
const yours = steps.map((s) => s.step).filter(Boolean).filter((st) => { const g = grantFromStep(st); if (g) granted.push(...g); return !g; });
|
|
613
|
+
if (granted.length) out.granted = granted;
|
|
598
614
|
if (yours.length) {
|
|
599
615
|
const step = yours.join(" "), files = namedFiles(steps.map((s) => s.named).join(" "), path);
|
|
600
616
|
try { writeFileSync(join(path, ".symbiot", WAITING), JSON.stringify({ step, files: files.map((f) => ({ ...f, sig: fileSig(f.path) })), cmd: runCmd(path), at: Date.now(), rerun: !!opts.rerun })); } catch {}
|
|
@@ -604,6 +620,7 @@ function answerQuestions(path, answers, opts = {}) {
|
|
|
604
620
|
return out;
|
|
605
621
|
}
|
|
606
622
|
if (opts.rerun) { const e = runHandoff(path); if (e && e.busy) out.note = "Answers saved. An agent is still running in that folder, so another wasn't started. Send them again once it finishes."; else if (e && e.blocked) out.note = "Answers saved. " + e.note; else if (e) out.rerun = e.id; else out.note = "Answers saved. Set an agent command in Settings to have the agent pick them up automatically."; }
|
|
623
|
+
if (out.granted) out.note = "Allowed " + out.granted.join(", ") + " for your agents" + (opts.rerun ? ", and carried on. " : ". ") + (out.note || "");
|
|
607
624
|
return out;
|
|
608
625
|
}
|
|
609
626
|
// The open tasks' titles in a held brief (TASKS.next.md), or null if nothing is held.
|
package/mapknn.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The Map's nearest neighbours: which of your repos belong together, and why.
|
|
2
|
+
//
|
|
3
|
+
// Each repo (and project folder) gets a fingerprint from three things the scan
|
|
4
|
+
// already reads, none of which needs an AI:
|
|
5
|
+
// - its stack: the languages and tools it uses (Jaccard overlap);
|
|
6
|
+
// - when you work on it: your commits per week over the last 12 weeks (cosine),
|
|
7
|
+
// so two repos you worked on in the same weeks count as related;
|
|
8
|
+
// - what it's about: the words of its name, README and package description
|
|
9
|
+
// (TF-IDF cosine, so words every repo has count for little).
|
|
10
|
+
// Similarity is a weighted sum of the three. Each repo keeps its k nearest above a
|
|
11
|
+
// floor (k-NN), with the reasons; repos joined by strong links form clusters, named
|
|
12
|
+
// after what they share most. Vitality (how recently you worked on it) lets the Map
|
|
13
|
+
// size and sink repos by how alive they are.
|
|
14
|
+
|
|
15
|
+
const W = { stack: 0.4, weeks: 0.35, words: 0.25 };
|
|
16
|
+
const K = 3, FLOOR = 0.12, CLUSTER = 0.3, HALF_LIFE_DAYS = 21;
|
|
17
|
+
const STOP = new Set("a an and are as at be by can for from has have how if in into is it its of on or our so that the this to was we what when which will with you your use used using get set run runs make made new not but all any each one two more most also only than then there these those they them their just like via app apps code project projects repo repository readme install npm yarn pnpm license mit see docs file files src test tests build version".split(" "));
|
|
18
|
+
|
|
19
|
+
function words(text = "") {
|
|
20
|
+
const tf = new Map();
|
|
21
|
+
for (const w of String(text).toLowerCase().match(/[a-z][a-z0-9]{2,}/g) || []) if (!STOP.has(w)) tf.set(w, (tf.get(w) || 0) + 1);
|
|
22
|
+
return tf;
|
|
23
|
+
}
|
|
24
|
+
function jaccard(a, b) { if (!a.size && !b.size) return 0; let i = 0; for (const x of a) if (b.has(x)) i++; return i / (a.size + b.size - i); }
|
|
25
|
+
function cosine(a, b) { let d = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { d += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } return na && nb ? d / Math.sqrt(na * nb) : 0; }
|
|
26
|
+
function cosMap(a, b) { let d = 0, na = 0, nb = 0; for (const [k, v] of a) { na += v * v; if (b.has(k)) d += v * b.get(k); } for (const v of b.values()) nb += v * v; return na && nb ? d / Math.sqrt(na * nb) : 0; }
|
|
27
|
+
|
|
28
|
+
// repos: [{ id, name, langs, tools, weeks: [12 numbers, oldest first], text, last: "YYYY-MM-DD" }]
|
|
29
|
+
function fingerprints(repos) {
|
|
30
|
+
const docs = repos.map((r) => words(`${r.name || ""} ${r.name || ""} ${r.text || ""}`));
|
|
31
|
+
const df = new Map(); for (const d of docs) for (const k of d.keys()) df.set(k, (df.get(k) || 0) + 1);
|
|
32
|
+
const N = repos.length;
|
|
33
|
+
return repos.map((r, i) => {
|
|
34
|
+
const tfidf = new Map(); for (const [k, v] of docs[i]) tfidf.set(k, (1 + Math.log(v)) * (Math.log((N + 1) / (df.get(k) + 1)) + 1));
|
|
35
|
+
return { id: r.id, stack: new Set([...(r.langs || []), ...(r.tools || [])]), weeks: (r.weeks || []).slice(-12), words: tfidf };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function similarity(a, b) {
|
|
39
|
+
const stack = jaccard(a.stack, b.stack), weeks = a.weeks.length && b.weeks.length ? cosine(a.weeks, b.weeks) : 0, wd = cosMap(a.words, b.words);
|
|
40
|
+
const why = [];
|
|
41
|
+
const shared = [...a.stack].filter((x) => b.stack.has(x)).slice(0, 3);
|
|
42
|
+
if (shared.length) why.push("shares " + shared.join(", "));
|
|
43
|
+
if (weeks >= 0.5) why.push("you worked on both the same weeks");
|
|
44
|
+
if (wd >= 0.15) { const top = [...a.words.keys()].filter((k) => b.words.has(k)).sort((x, y) => a.words.get(y) * b.words.get(y) - a.words.get(x) * b.words.get(x)).slice(0, 2); if (top.length) why.push("both about " + top.join(" and ")); }
|
|
45
|
+
return { sim: +(W.stack * stack + W.weeks * weeks + W.words * wd).toFixed(3), why };
|
|
46
|
+
}
|
|
47
|
+
function vitality(last, now = Date.now()) { const t = Date.parse(last || ""); if (!t) return 0; const days = Math.max(0, (now - t) / 86400000); return +Math.pow(0.5, days / HALF_LIFE_DAYS).toFixed(3); }
|
|
48
|
+
|
|
49
|
+
function mapKnn(repos, { k = K, now = Date.now() } = {}) {
|
|
50
|
+
const fp = fingerprints(repos), byId = {};
|
|
51
|
+
const neighbours = {}, edges = [], seen = new Set();
|
|
52
|
+
fp.forEach((a, i) => {
|
|
53
|
+
const near = fp.map((b, j) => (i === j ? null : { id: b.id, ...similarity(a, b) })).filter((x) => x && x.sim >= FLOOR).sort((x, y) => y.sim - x.sim || (x.id < y.id ? -1 : 1)).slice(0, k);
|
|
54
|
+
neighbours[a.id] = near;
|
|
55
|
+
for (const n of near) { const key = [a.id, n.id].sort().join("|"); if (!seen.has(key)) { seen.add(key); edges.push({ a: a.id, b: n.id, sim: n.sim }); } }
|
|
56
|
+
});
|
|
57
|
+
// clusters: strong links, joined (union-find); named after the stack they share most
|
|
58
|
+
const par = {}; const root = (x) => (par[x] === x ? x : (par[x] = root(par[x])));
|
|
59
|
+
for (const r of repos) par[r.id] = r.id;
|
|
60
|
+
for (const e of edges) if (e.sim >= CLUSTER) par[root(e.a)] = root(e.b);
|
|
61
|
+
const groups = {}; for (const r of repos) (groups[root(r.id)] = groups[root(r.id)] || []).push(r);
|
|
62
|
+
const clusters = Object.values(groups).filter((g) => g.length > 1).map((g) => {
|
|
63
|
+
const count = new Map(); for (const r of g) for (const s of new Set([...(r.langs || []), ...(r.tools || [])])) count.set(s, (count.get(s) || 0) + 1);
|
|
64
|
+
const label = [...count].filter(([, c]) => c >= 2).sort((x, y) => y[1] - x[1] || (x[0] < y[0] ? -1 : 1)).slice(0, 2).map(([s]) => s).join(" · ");
|
|
65
|
+
return { ids: g.map((r) => r.id), label: label || g.map((r) => r.name).slice(0, 2).join(" & ") };
|
|
66
|
+
}).sort((x, y) => y.ids.length - x.ids.length);
|
|
67
|
+
for (const r of repos) byId[r.id] = vitality(r.last, now);
|
|
68
|
+
return { neighbours, edges, clusters, vitality: byId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { mapKnn, similarity, fingerprints, vitality, words, W as KNN_WEIGHTS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.1",
|
|
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": {
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"README.md",
|
|
35
35
|
"CHANGELOG.md",
|
|
36
36
|
"LICENSE",
|
|
37
|
-
"fonts"
|
|
37
|
+
"fonts",
|
|
38
|
+
"mapknn.mjs"
|
|
38
39
|
],
|
|
39
40
|
"engines": {
|
|
40
41
|
"node": ">=18"
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
68
69
|
},
|
|
69
70
|
"scripts": {
|
|
70
|
-
"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",
|
|
71
|
+
"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/mapknn.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
71
72
|
"lint": "eslint ."
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
package/scan.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
|
|
|
9
9
|
import { VERSION, loadConfig, saveConfig, sh, repoState } from "./core.mjs";
|
|
10
10
|
import { detectHandoffs } from "./agents.mjs";
|
|
11
11
|
import { PROVIDERS, resolveProvider } from "./ai.mjs";
|
|
12
|
+
import { mapKnn } from "./mapknn.mjs";
|
|
12
13
|
|
|
13
14
|
const MAX_COMMITS = 140;
|
|
14
15
|
|
|
@@ -304,6 +305,14 @@ function findAllRepos(base) {
|
|
|
304
305
|
return Object.values(byCommon).sort((a, b) => b.recency - a.recency).slice(0, 60);
|
|
305
306
|
} finally { scanEnd(own); }
|
|
306
307
|
}
|
|
308
|
+
// What a project is about, in its own words: its package description and keywords
|
|
309
|
+
// and the start of its README (the Map's neighbours read the words, nothing else).
|
|
310
|
+
function aboutText(path) {
|
|
311
|
+
let t = "";
|
|
312
|
+
try { const p = JSON.parse(readFileSync(join(path, "package.json"), "utf8")); t += " " + (p.description || "") + " " + (Array.isArray(p.keywords) ? p.keywords.join(" ") : ""); } catch {}
|
|
313
|
+
for (const f of ["README.md", "readme.md", "README"]) { try { t += " " + readFileSync(join(path, f), "utf8").slice(0, 3000); break; } catch {} }
|
|
314
|
+
return t.slice(0, 4000);
|
|
315
|
+
}
|
|
307
316
|
function detectRepo(r) {
|
|
308
317
|
const files = sh(`git -C ${JSON.stringify(r.path)} ls-files 2>/dev/null | head -3000`).split("\n").filter(Boolean);
|
|
309
318
|
const count = {}; const tools = new Set();
|
|
@@ -318,7 +327,10 @@ function detectRepo(r) {
|
|
|
318
327
|
const mine = Number(sh(`git -C ${JSON.stringify(r.path)} log ${email ? `--author=${JSON.stringify(email)}` : ""} --oneline 2>/dev/null | wc -l`).trim()) || 0;
|
|
319
328
|
const branch = sh(`git -C ${JSON.stringify(r.path)} rev-parse --abbrev-ref HEAD 2>/dev/null`).trim();
|
|
320
329
|
const last = sh(`git -C ${JSON.stringify(r.path)} log -1 --format=%cd --date=short 2>/dev/null`).trim();
|
|
321
|
-
|
|
330
|
+
// for the Map's neighbours (mapknn.mjs): your commits per week, 12 weeks, oldest first; and what it's about
|
|
331
|
+
const weeks = Array(12).fill(0), now = Date.now();
|
|
332
|
+
for (const t of sh(`git -C ${JSON.stringify(r.path)} log ${email ? `--author=${JSON.stringify(email)}` : ""} --since=84.days --format=%ct 2>/dev/null`).split("\n").filter(Boolean)) { const w = Math.floor((now - Number(t) * 1000) / (7 * 86400000)); if (w >= 0 && w < 12) weeks[11 - w]++; }
|
|
333
|
+
return { ...r, langs, tools: [...tools], mine, files: files.length, branch, last, weeks, text: aboutText(r.path) };
|
|
322
334
|
}
|
|
323
335
|
// Non-git PROJECT folders inside your scan roots (you added them = consent):
|
|
324
336
|
// a directory with a manifest but no .git — "not everything is a repo".
|
|
@@ -419,7 +431,7 @@ async function buildMapScan() {
|
|
|
419
431
|
for (const r of repos) {
|
|
420
432
|
const rid = "repo:" + r.path;
|
|
421
433
|
add({ id: rid, type: "repo", label: r.name, weight: Math.min(9 + Math.log2(1 + r.mine) * 3, 26),
|
|
422
|
-
meta: { commits: r.mine, langs: r.langs.slice(0, 3), tools: r.tools, files: r.files, branch: r.branch, last: r.last, path: r.path } });
|
|
434
|
+
meta: { commits: r.mine, langs: r.langs.slice(0, 3), tools: r.tools, files: r.files, branch: r.branch, last: r.last, path: r.path, weeks: r.weeks } });
|
|
423
435
|
edges.push({ source: "me", target: rid });
|
|
424
436
|
for (const L of r.langs.slice(0, 3)) { const id = "lang:" + L; add({ id, type: "lang", label: L, weight: 15 }); edges.push({ source: rid, target: id }); }
|
|
425
437
|
for (const T of r.tools) { const id = "tool:" + T; add({ id, type: "tool", label: T, weight: 12 }); edges.push({ source: rid, target: id }); }
|
|
@@ -474,7 +486,12 @@ async function buildMapScan() {
|
|
|
474
486
|
edges.push({ source: "me", target: id }); extra++;
|
|
475
487
|
}
|
|
476
488
|
} catch {}
|
|
477
|
-
|
|
489
|
+
// the Map's nearest neighbours (mapknn.mjs): repos and project folders, by stack, weeks and words
|
|
490
|
+
let knn = null;
|
|
491
|
+
try { const byPath = new Map(repos.map((r) => [r.path, r]));
|
|
492
|
+
knn = mapKnn(nodes.filter((n) => n.type === "repo" || n.type === "folder").map((n) => { const m = n.meta || {}, r = byPath.get(m.path) || {};
|
|
493
|
+
return { id: n.id, name: n.label, langs: m.langs || [], tools: m.tools || [], weeks: r.weeks || [], text: r.text != null ? r.text : (m.path ? aboutText(m.path) : ""), last: m.last || r.last || "" }; })); } catch {}
|
|
494
|
+
const out = { nodes, edges, ...(knn ? { knn } : {}), stats: {
|
|
478
495
|
repos: repos.length,
|
|
479
496
|
folders: nodes.filter((n) => n.type === "folder").length,
|
|
480
497
|
languages: nodes.filter((n) => n.type === "lang").length,
|
package/ui.mjs
CHANGED
|
@@ -65,7 +65,10 @@ body.lq-light #lqask::placeholder{color:#6A7480}
|
|
|
65
65
|
#lqsend{flex:none;width:40px;height:40px;padding:0;display:grid;place-items:center;border-radius:50%;border:0;background:#F3F0EA;color:#0A0B0D;cursor:pointer}
|
|
66
66
|
body.lq-light #lqsend{background:#151A21;color:#FFFFFF}
|
|
67
67
|
body.lq-look-pearl #lqsend{background:linear-gradient(180deg,#FFFFFF 0%,#D9DDE2 45%,#9AA1AA 55%,#E8EAED 100%);color:#1B1E23;box-shadow:0 2px 6px rgba(0,0,0,.18),inset 0 0 0 1px rgba(0,0,0,.08)}
|
|
68
|
-
#lqtalk{position:absolute;left:50%;bottom:88px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;flex-direction:column;gap:16px;z-index:3;max-height:
|
|
68
|
+
#lqtalk{position:absolute;left:50%;bottom:88px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;flex-direction:column;gap:16px;z-index:3;max-height:min(34vh,320px);overflow-x:hidden;overflow-y:auto;scrollbar-width:none;padding-top:90px;scroll-behavior:smooth;-webkit-mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.12) 22%,rgba(0,0,0,.6) 48%,#000 72%);mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.12) 22%,rgba(0,0,0,.6) 48%,#000 72%);transition:max-height .3s ease}
|
|
69
|
+
#lqtalk:hover,#lqtalk:focus-within,#lqtalk.back{max-height:min(56vh,560px);-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 60px);mask-image:linear-gradient(to bottom,transparent 0,#000 60px)}
|
|
70
|
+
#lqtalk .lqmsg{transition:opacity .4s ease}
|
|
71
|
+
#lqtalk:hover .lqmsg,#lqtalk.back .lqmsg{opacity:1 !important}
|
|
69
72
|
#lqtalk::-webkit-scrollbar{display:none}
|
|
70
73
|
.lqmsg{align-self:flex-start;max-width:92%;padding:0;border:0;background:none;color:#ECE9E4;font-size:15px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere;text-shadow:0 1px 12px rgba(0,0,0,.7)}
|
|
71
74
|
.lqmsg.me{align-self:flex-end;max-width:80%;padding:8px 14px;border-radius:18px;background:rgba(255,255,255,.09);color:#C9CDD3;font-size:14.5px;text-shadow:none}
|
|
@@ -383,7 +386,175 @@ body.lq-liquid .board .bcard.has{border-color:rgba(242,165,65,.35)}
|
|
|
383
386
|
body.lq-liquid .bcard .bc{font:300 30px/1.1 var(--sans);letter-spacing:-.02em;color:var(--faint)}
|
|
384
387
|
body.lq-liquid .bcard.has .bc{color:var(--amber)}
|
|
385
388
|
body.lq-liquid .bcard .bbrief{border-left:2px solid var(--line);background:transparent;padding:2px 0 2px 12px;color:var(--text)}
|
|
386
|
-
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
/* the Map, in the liquid */
|
|
392
|
+
#lmap,#lmtools{display:none}
|
|
393
|
+
body.lq-liquid #graph,body.lq-liquid #mapbar,body.lq-liquid .legend .lg,body.lq-liquid .legend > .muted{display:none}
|
|
394
|
+
body.lq-liquid #lmap{display:block;position:relative;flex:1;min-width:0;height:64vh;min-height:420px;border-radius:18px;overflow:hidden;isolation:isolate;background:#08090B;box-shadow:0 0 0 1px rgba(255,255,255,.08);touch-action:none;cursor:grab}
|
|
395
|
+
body.lq-liquid #lmap:active{cursor:grabbing}
|
|
396
|
+
body.lq-liquid.lq-light #lmap{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
397
|
+
body.lq-look-pearl #lmap{background:#EDECE9}
|
|
398
|
+
#lmc{position:absolute;inset:0;width:100%;height:100%;display:block}
|
|
399
|
+
#lmlines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
|
|
400
|
+
#lmlines line{stroke:rgba(255,255,255,.12);stroke-width:1;stroke-dasharray:2 4}
|
|
401
|
+
#lmlines line.hot{stroke:rgba(255,255,255,.4)}
|
|
402
|
+
body.lq-light #lmlines line{stroke:rgba(21,26,33,.14)}body.lq-light #lmlines line.hot{stroke:rgba(21,26,33,.45)}
|
|
403
|
+
#lmov{position:absolute;inset:0;pointer-events:none}
|
|
404
|
+
.lml{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;gap:0;padding:2px 6px;border:0;border-radius:8px;background:transparent;color:#ECE9E4;font:inherit;text-align:center;white-space:nowrap;pointer-events:auto;cursor:pointer;transition:opacity .25s ease;will-change:transform;text-shadow:0 1px 10px rgba(0,0,0,.7)}
|
|
405
|
+
.lml b{font:600 13px/1.2 var(--sans)}
|
|
406
|
+
.lml span{font:400 11.5px/1.3 var(--sans);color:#8A919B}
|
|
407
|
+
.lml.dormant{opacity:.55}.lml.far{opacity:.3}.lml.near b{color:#F2A541}
|
|
408
|
+
.lmme{position:absolute;left:0;top:0;display:flex;align-items:center;gap:7px;pointer-events:none;will-change:transform;font:500 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;color:#ECE9E4}
|
|
409
|
+
.lmme i{width:14px;height:14px;border-radius:50%;background:#F3F0EA;box-shadow:0 0 0 4px rgba(243,240,234,.18),0 0 18px rgba(243,240,234,.45)}
|
|
410
|
+
body.lq-light .lmme{color:#151A21}body.lq-light .lmme i{background:#151A21;box-shadow:0 0 0 4px rgba(21,26,33,.15)}
|
|
411
|
+
.lml:focus-visible{outline:2px solid currentColor;outline-offset:2px}
|
|
412
|
+
body.lq-light .lml{color:#151A21;text-shadow:0 1px 8px rgba(255,255,255,.8)}body.lq-light .lml span{color:#5A6470}body.lq-light .lml.near b{color:#A85F0A}
|
|
413
|
+
.lmc{position:absolute;left:0;top:0;font:500 11px var(--sans);letter-spacing:.12em;text-transform:uppercase;color:#8A919B;white-space:nowrap;pointer-events:none;will-change:transform}
|
|
414
|
+
body.lq-light .lmc{color:#5A6470}
|
|
415
|
+
.lmring{position:absolute;left:0;top:0;border-radius:50%;box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.35);pointer-events:none}
|
|
416
|
+
body.lq-light .lmring{box-shadow:inset 0 0 0 1.5px rgba(21,26,33,.35)}
|
|
417
|
+
.lmtip{position:absolute;left:0;top:0;width:300px;max-width:calc(100% - 20px);padding:12px 14px;border-radius:16px;background:rgba(16,18,21,.94);border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 50px -20px rgba(0,0,0,.8);color:#ECE9E4;opacity:0;pointer-events:none;transition:opacity .15s ease;z-index:3}
|
|
418
|
+
.lmtip.on{opacity:1}
|
|
419
|
+
.lmtip b{font:600 13.5px var(--sans)}
|
|
420
|
+
.lmtip .lmts{display:block;font-size:12px;color:#8A919B;margin-top:1px}
|
|
421
|
+
.lmtip .lmtn{margin-top:8px;display:flex;flex-direction:column;gap:4px;font-size:12.5px;color:#C9CDD3}
|
|
422
|
+
.lmtip .lmtn b{font-size:12.5px;color:#F2A541}
|
|
423
|
+
body.lq-light .lmtip{background:rgba(255,255,255,.96);border-color:rgba(21,26,33,.1);color:#151A21;box-shadow:0 24px 50px -24px rgba(30,45,60,.45)}
|
|
424
|
+
body.lq-light .lmtip .lmtn{color:#2B3644}body.lq-light .lmtip .lmtn b{color:#A85F0A}
|
|
425
|
+
.lmzoom{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;z-index:2}
|
|
426
|
+
.lmzoom .iconbtn{width:32px;height:32px;font:500 16px var(--sans)}
|
|
427
|
+
body.lq-liquid #lmtools{display:flex;flex-direction:column;gap:8px;margin-top:14px}
|
|
428
|
+
.lmrow{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}
|
|
429
|
+
.lmk{font:500 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-right:6px;min-width:110px}
|
|
430
|
+
.lmt{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:var(--text);font:500 13px var(--sans);padding:4px 8px;border-radius:8px;cursor:pointer}
|
|
431
|
+
.lmt:hover{background:rgba(255,255,255,.05);color:var(--bone)}body.lq-light .lmt:hover{background:rgba(21,26,33,.05)}
|
|
432
|
+
.lmt small{font-size:11px;color:var(--faint)}
|
|
433
|
+
.lmnear{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-direction:column;gap:2px}
|
|
434
|
+
.lmnear button{all:unset;box-sizing:border-box;display:flex;flex-direction:column;width:100%;padding:6px 8px;margin-left:-8px;border-radius:10px;cursor:pointer}
|
|
435
|
+
.lmnear button:hover{background:rgba(255,255,255,.05)}body.lq-light .lmnear button:hover{background:rgba(21,26,33,.05)}
|
|
436
|
+
.lmnear button:focus-visible{outline:2px solid currentColor}
|
|
437
|
+
.lmnear b{font:600 13px var(--sans);color:var(--bone)}.lmnear span{font-size:12px;color:var(--faint)}
|
|
438
|
+
.lmweeks{display:flex;align-items:flex-end;gap:3px;height:30px;margin-top:6px}
|
|
439
|
+
.lmweeks i{flex:1;min-width:4px;border-radius:2px;background:var(--faint);opacity:.6}
|
|
440
|
+
.lmweeks i:last-child{background:var(--bone);opacity:1}
|
|
441
|
+
body.lq-liquid .detail{width:310px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:16px;max-height:64vh}
|
|
442
|
+
body.lq-liquid.lq-light .detail{background:rgba(255,255,255,.6);border-color:rgba(21,26,33,.08)}
|
|
443
|
+
body.lq-liquid .detail h3{font:600 17px var(--sans);letter-spacing:-.01em}
|
|
444
|
+
body.lq-liquid .detail .k{font:500 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;margin-top:14px}
|
|
445
|
+
body.lq-liquid .detail .tag{border:0;background:rgba(255,255,255,.06);border-radius:999px;padding:3px 9px;font-size:12px;color:var(--text)}
|
|
446
|
+
body.lq-liquid.lq-light .detail .tag{background:rgba(21,26,33,.05)}
|
|
447
|
+
body.lq-liquid .profile{font-size:14px;color:var(--text);margin-bottom:10px}
|
|
448
|
+
.screensbox{margin-top:22px;border-top:1px solid var(--line);padding-top:12px}
|
|
449
|
+
.screensbox > summary{cursor:pointer;list-style:none;display:flex;align-items:baseline;gap:8px}
|
|
450
|
+
.screensbox > summary::-webkit-details-marker{display:none}
|
|
451
|
+
.screensbox > summary::before{content:'';width:5px;height:5px;border-right:1.5px solid var(--faint);border-bottom:1.5px solid var(--faint);transform:rotate(-45deg);transition:transform .2s ease;align-self:center}
|
|
452
|
+
.screensbox[open] > summary::before{transform:rotate(45deg)}
|
|
453
|
+
.screensbox .note{max-width:80ch}
|
|
454
|
+
@media (max-width:720px){body.lq-liquid .maprow{flex-direction:column}body.lq-liquid .detail{width:auto;max-height:none}.lmk{min-width:0;width:100%}}
|
|
455
|
+
i[class^=ic-]{display:inline-block;width:1.05em;height:1.05em;vertical-align:-.17em;background:currentColor;-webkit-mask:var(--ic) center/contain no-repeat;mask:var(--ic) center/contain no-repeat;font-style:normal}
|
|
456
|
+
.ic-chat{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h16v11H9l-5 4z'/%3E%3C/svg%3E")}
|
|
457
|
+
.ic-plug{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 2v6M15 2v6M6 8h12v3a6 6 0 0 1-12 0zM12 17v5'/%3E%3C/svg%3E")}
|
|
458
|
+
.ic-person{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M4 21a8 8 0 0 1 16 0'/%3E%3C/svg%3E")}
|
|
459
|
+
.ic-bot{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='8' width='16' height='12' rx='3'/%3E%3Cpath d='M12 3v5M9 14h.01M15 14h.01'/%3E%3C/svg%3E")}
|
|
460
|
+
.ic-archive{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='5' rx='1'/%3E%3Cpath d='M5 9v11h14V9M10 13h4'/%3E%3C/svg%3E")}
|
|
461
|
+
.ic-idea{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18h6M10 21h4M12 3a6 6 0 0 0-4 10.5c.8.9 1 1.5 1 2.5h6c0-1 .2-1.6 1-2.5A6 6 0 0 0 12 3z'/%3E%3C/svg%3E")}
|
|
462
|
+
.ic-ask{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.7.4-1 1-1 1.7M12 17h.01'/%3E%3C/svg%3E")}
|
|
463
|
+
.ic-pause{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 5v14M15 5v14'/%3E%3C/svg%3E")}
|
|
464
|
+
.ic-warn{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l10 18H2zM12 10v4M12 18h.01'/%3E%3C/svg%3E")}
|
|
465
|
+
.ic-x{--ic:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E")}
|
|
466
|
+
|
|
467
|
+
/* ---- every panel, in the look ---- */
|
|
468
|
+
body.lq-liquid input[type=checkbox],body.lq-liquid input[type=radio]{-webkit-appearance:none;appearance:none;flex:none;width:18px;height:18px;margin:0;border-radius:50%;border:1.5px solid rgba(255,255,255,.3);background:transparent;cursor:pointer;display:inline-grid;place-items:center;transition:background-color .15s ease,border-color .15s ease;vertical-align:-4px}
|
|
469
|
+
body.lq-liquid input[type=checkbox]:checked{border-color:transparent;background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#C9CFD9 20%,#5B626D 58%,#121418 100%)}
|
|
470
|
+
body.lq-liquid input[type=radio]:checked{border-color:#F3F0EA;background:radial-gradient(circle,#F3F0EA 0 4px,transparent 4.5px)}
|
|
471
|
+
body.lq-liquid.lq-light input[type=checkbox],body.lq-liquid.lq-light input[type=radio]{border-color:rgba(21,26,33,.3)}
|
|
472
|
+
body.lq-liquid.lq-light input[type=checkbox]:checked{background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#E3E6EA 22%,#9BA2AC 55%,#3A3F46 100%)}
|
|
473
|
+
body.lq-liquid.lq-light input[type=radio]:checked{border-color:#151A21;background:radial-gradient(circle,#151A21 0 4px,transparent 4.5px)}
|
|
474
|
+
body.lq-liquid input[type=checkbox]:focus-visible,body.lq-liquid input[type=radio]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
|
|
475
|
+
body.lq-liquid .switch input[type=checkbox],body.lq-liquid input[type=checkbox].sw{width:34px;height:20px;border-radius:999px;background:rgba(255,255,255,.1);border:0;position:relative}
|
|
476
|
+
body.lq-liquid .switch input[type=checkbox]::after,body.lq-liquid input[type=checkbox].sw::after{content:'';position:absolute;left:3px;top:3px;width:14px;height:14px;border-radius:50%;background:#8A919B;transition:transform .18s ease,background-color .18s ease}
|
|
477
|
+
body.lq-liquid .switch input[type=checkbox]:checked,body.lq-liquid input[type=checkbox].sw:checked{background:#F3F0EA}
|
|
478
|
+
body.lq-liquid .switch input[type=checkbox]:checked::after,body.lq-liquid input[type=checkbox].sw:checked::after{transform:translateX(14px);background:#0A0B0D}
|
|
479
|
+
body.lq-liquid.lq-light .switch input[type=checkbox],body.lq-liquid.lq-light input[type=checkbox].sw{background:rgba(21,26,33,.12)}
|
|
480
|
+
body.lq-liquid.lq-light .switch input[type=checkbox]:checked,body.lq-liquid.lq-light input[type=checkbox].sw:checked{background:#151A21}
|
|
481
|
+
body.lq-liquid.lq-light .switch input[type=checkbox]:checked::after,body.lq-liquid.lq-light input[type=checkbox].sw:checked::after{background:#FFFFFF}
|
|
482
|
+
body.lq-liquid .fchip{border:1px solid rgba(255,255,255,.1);background:transparent;color:var(--text);font:500 12.5px var(--sans);padding:5px 11px}
|
|
483
|
+
body.lq-liquid .fchip:hover{border-color:rgba(255,255,255,.22);color:var(--bone)}
|
|
484
|
+
body.lq-liquid.lq-light .fchip{border-color:rgba(21,26,33,.12)}
|
|
485
|
+
body.lq-liquid .tag{background:rgba(255,255,255,.06);border:0;color:var(--text)}
|
|
486
|
+
body.lq-liquid.lq-light .tag{background:rgba(21,26,33,.05)}
|
|
487
|
+
body.lq-liquid .note{border-radius:12px}
|
|
488
|
+
/* write-ups */
|
|
489
|
+
.wuhead{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:12px;max-width:680px;margin:4px auto 18px}
|
|
490
|
+
.wut{margin:0;font:600 26px/1.15 var(--sans);letter-spacing:-.02em;color:var(--bone)}
|
|
491
|
+
.wus{font-size:13px;color:var(--faint);margin-top:4px}
|
|
492
|
+
.wua{display:flex;align-items:center;gap:8px}
|
|
493
|
+
#copy.done{color:#F2A541}
|
|
494
|
+
.swl{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px}.swl small{color:var(--faint)}
|
|
495
|
+
body.lq-liquid .out{max-width:680px;margin:0 auto;min-height:0;padding:0;background:transparent;border:0;border-radius:0;white-space:normal;font-size:15.5px;line-height:1.7;color:var(--text)}
|
|
496
|
+
body.lq-liquid .out.muted{padding:48px 0;text-align:center;color:var(--faint)}
|
|
497
|
+
body.lq-liquid .out h3,body.lq-liquid .out h4{margin:22px 0 6px;font:600 15px var(--sans);letter-spacing:.01em;color:var(--bone)}
|
|
498
|
+
body.lq-liquid .out h3{font-size:17px}
|
|
499
|
+
body.lq-liquid .out p{margin:0 0 10px}
|
|
500
|
+
body.lq-liquid .out ul{margin:4px 0 12px;padding-left:20px}
|
|
501
|
+
body.lq-liquid .out li{margin:3px 0}
|
|
502
|
+
body.lq-liquid .out li::marker{color:var(--faint)}
|
|
503
|
+
body.lq-liquid .out b{color:var(--bone);font-weight:600}
|
|
504
|
+
body.lq-liquid .rfoot{max-width:680px;margin:18px auto 0;font-family:var(--sans);border-top:1px solid var(--line)}
|
|
505
|
+
/* tasks */
|
|
506
|
+
body.lq-liquid .task{gap:12px;padding:11px 14px;border-radius:14px;border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03);margin-top:6px;transition:background-color .15s ease}
|
|
507
|
+
body.lq-liquid .task:hover{background:rgba(255,255,255,.055)}
|
|
508
|
+
body.lq-liquid.lq-light .task{border-color:rgba(21,26,33,.07);background:rgba(255,255,255,.55)}
|
|
509
|
+
body.lq-liquid.lq-light .task:hover{background:rgba(255,255,255,.85)}
|
|
510
|
+
body.lq-liquid .task .t{font-size:14px;line-height:1.5;color:var(--text)}
|
|
511
|
+
body.lq-liquid .task .rp{background:transparent;border:0;padding:0;font-size:12px;color:var(--faint);white-space:nowrap}
|
|
512
|
+
body.lq-liquid .task .ask,body.lq-liquid .task .rm{display:inline-flex;align-items:center;justify-content:center;gap:4px;white-space:nowrap;min-width:28px;padding:0 6px;height:28px;border-radius:8px;font-size:15px;color:var(--faint)}
|
|
513
|
+
body.lq-liquid .task .rm{opacity:0;transition:opacity .15s ease}
|
|
514
|
+
body.lq-liquid .task:hover .rm,body.lq-liquid .task .rm:focus-visible,body.lq-touch .task .rm{opacity:1}
|
|
515
|
+
body.lq-liquid .task .ask:hover,body.lq-liquid .task .rm:hover{background:rgba(255,255,255,.07);color:var(--bone)}
|
|
516
|
+
body.lq-liquid .task .ask.has,body.lq-liquid .task .ask.on{color:var(--bone)}
|
|
517
|
+
body.lq-liquid #panel-tasks > .note,body.lq-liquid #panel-tasks .tnote{font-size:12.5px}
|
|
518
|
+
/* agents' questions */
|
|
519
|
+
body.lq-liquid .aq{border:1px solid rgba(255,255,255,.08);border-left:2px solid var(--amber);border-radius:14px;background:rgba(255,255,255,.03);padding:12px 16px}
|
|
520
|
+
body.lq-liquid.lq-light .aq{border-color:rgba(21,26,33,.08);border-left-color:var(--amber);background:rgba(255,255,255,.55)}
|
|
521
|
+
body.lq-liquid .aq h4{font:600 11px var(--sans);letter-spacing:.1em;display:flex;align-items:center;gap:6px}
|
|
522
|
+
body.lq-liquid .aq .q{border-bottom-color:var(--line)}
|
|
523
|
+
body.lq-liquid .aq label.opt{gap:10px;padding:4px 0}
|
|
524
|
+
body.lq-liquid .aq .opt input{margin-top:1px}
|
|
525
|
+
body.lq-liquid .who{display:inline-flex;align-items:center;gap:4px;padding:1px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:12px;font-weight:600;color:var(--bone);white-space:nowrap}
|
|
526
|
+
body.lq-liquid.lq-light .who{background:rgba(21,26,33,.06)}
|
|
527
|
+
/* drift */
|
|
528
|
+
body.lq-liquid .drift{border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03);border-radius:14px;padding:14px 16px}
|
|
529
|
+
body.lq-liquid.lq-light .drift{border-color:rgba(21,26,33,.07);background:rgba(255,255,255,.55)}
|
|
530
|
+
.dbead{flex:none;width:12px;height:12px;border-radius:50%;background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#C9CFD9 20%,#5B626D 58%,#121418 100%)}
|
|
531
|
+
body.lq-light .dbead{background:radial-gradient(circle at 34% 28%,#FFFFFF 0,#E3E6EA 22%,#9BA2AC 55%,#3A3F46 100%)}
|
|
532
|
+
.drift.risk .dbead{background:radial-gradient(circle at 34% 28%,#FFF4E2 0,#F2A541 45%,#7A4A10 100%);box-shadow:0 0 10px rgba(242,165,65,.4)}
|
|
533
|
+
body.lq-liquid .drift .dn{font-size:14.5px}
|
|
534
|
+
body.lq-liquid .drift ul{margin:8px 0 0 20px}
|
|
535
|
+
body.lq-liquid .drift li{position:relative;padding:3px 0 3px 14px;font-size:13.5px;color:var(--text)}
|
|
536
|
+
body.lq-liquid .drift li::before{content:'';position:absolute;left:0;top:11px;width:5px;height:5px;border-radius:50%;background:var(--faint)}
|
|
537
|
+
body.lq-liquid .drift li.warn{color:var(--bone)}
|
|
538
|
+
body.lq-liquid .drift li.warn::before{background:var(--amber);box-shadow:0 0 6px rgba(242,165,65,.6)}
|
|
539
|
+
body.lq-liquid .drift .ev{font-family:var(--sans);font-size:11.5px}
|
|
540
|
+
|
|
541
|
+
/* links in the look's ink */
|
|
542
|
+
body.lq-liquid main a{color:var(--bone);text-decoration:underline;text-decoration-color:rgba(255,255,255,.3);text-underline-offset:3px;cursor:pointer}
|
|
543
|
+
body.lq-liquid.lq-light main a{text-decoration-color:rgba(21,26,33,.3)}
|
|
544
|
+
/* Settings: one column of titled cards */
|
|
545
|
+
body.lq-liquid #panel-settings{max-width:780px;margin:0 auto;display:flex;flex-direction:column;gap:14px;padding-bottom:20px}
|
|
546
|
+
body.lq-liquid .sset{border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.03);border-radius:18px;padding:18px 20px}
|
|
547
|
+
body.lq-liquid.lq-light .sset{border-color:rgba(21,26,33,.07);background:rgba(255,255,255,.55)}
|
|
548
|
+
body.lq-liquid .sset.hidden{display:none}
|
|
549
|
+
.ssh{margin:0 0 10px;font:600 16px var(--sans);letter-spacing:-.01em;color:var(--bone);display:flex;align-items:baseline;gap:8px}
|
|
550
|
+
.ssh .muted{font-weight:400;font-size:12.5px}
|
|
551
|
+
body.lq-liquid .sset > label:not(.check):not(.swl){display:block;font:500 12.5px var(--sans);color:var(--faint);margin:12px 0 6px}
|
|
552
|
+
body.lq-liquid .sset label.check{display:flex;align-items:center;gap:10px;margin:8px 0;color:var(--text);font-size:13.5px}
|
|
553
|
+
body.lq-liquid .sset .note{font-size:12.5px;max-width:70ch}
|
|
554
|
+
body.lq-liquid .sset .cxh{margin-top:0}
|
|
555
|
+
/* the Close bar carries the panel's own ground, so what scrolls under it stays clean */
|
|
556
|
+
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}
|
|
557
|
+
body.lq-liquid.lq-light.lq-pooled #lqsinkrow{background:linear-gradient(180deg,rgba(246,248,251,.98) 70%,rgba(246,248,251,0))}
|
|
387
558
|
/* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
|
|
388
559
|
.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)}
|
|
389
560
|
body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
@@ -470,9 +641,11 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
470
641
|
<section id="panel-map">
|
|
471
642
|
<div class="profile muted" id="profile">Mapping your work…</div>
|
|
472
643
|
<div class="maprow">
|
|
644
|
+
<div id="lmap" class="lmap"><canvas id="lmc" aria-hidden="true"></canvas><svg id="lmlines" aria-hidden="true"></svg><div id="lmov"></div><div id="lmtip" class="lmtip" role="status" aria-live="polite"></div><div class="lmzoom"><button type="button" class="iconbtn" id="lmin" aria-label="Zoom in">+</button><button type="button" class="iconbtn" id="lmout" aria-label="Zoom out">−</button><button type="button" class="iconbtn" id="lmfit" aria-label="Fit the map" title="Fit the map"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M1 5V1h4M13 5V1H9M1 9v4h4M13 9v4H9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button></div></div>
|
|
473
645
|
<svg id="graph" viewBox="0 0 960 620" preserveAspectRatio="xMidYMid meet"></svg>
|
|
474
646
|
<aside id="detail" class="detail hidden"></aside>
|
|
475
647
|
</div>
|
|
648
|
+
<div id="lmtools" class="lmtools"></div>
|
|
476
649
|
<div id="mapbar" class="mapbar">Hover a node for a summary · click for details · drag to move</div>
|
|
477
650
|
<div class="legend">
|
|
478
651
|
<span class="lg"><i style="background:var(--green)"></i>you</span>
|
|
@@ -486,8 +659,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
486
659
|
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
487
660
|
</div>
|
|
488
661
|
<div id="review" class="review hidden"></div>
|
|
489
|
-
<
|
|
490
|
-
<div class="tgroup">Screens <span class="tcount">experimental · blueprints for screen automation</span></div>
|
|
662
|
+
<details id="screensbox" class="screensbox"><summary><span class="tgroup">Screens</span> <span class="tcount">experimental · blueprints for screen automation</span></summary>
|
|
491
663
|
<div class="note muted" style="margin-top:2px">Capture a screen, then drag a box over each part that matters (a button, a field, a menu) and name it. Each region keeps its pixel coordinates and its centre, ready for automation to aim at. With more than one display, pick which one to capture, or one screen per display. <b>Click here</b> on a region clicks its centre on your real screen, after you confirm. Or let Symbiot do it all for a web page: <b>Map page</b> opens it in a hidden browser, takes its screenshot and marks every button, link and field by itself, and <b>Press</b> on one of them follows it there and maps the next page; <b>Type</b> on a field types into it there. A map only has what fits in the window, so on a longer page <b>Scroll down</b> maps the next part, or <b>Whole page</b> maps all of it in one tall screenshot (where a list scrolls inside the page, like Gmail's mail, that list opened out: a whole inbox on one screen). Only a mapped page is typed into: nothing types on your real screen. <b>Watch</b> on a mapped page (your inbox, say) has Symbiot read it again every few minutes while it runs and tell you what's new there.</div>
|
|
492
664
|
<div class="row" style="margin-top:10px"><input id="pagesite" placeholder="a web page to map by itself: gmail, github.com/pulls or a web address" style="flex:1"><button class="act" id="mappage">Map page</button><button class="ghost" id="pagesignin" title="open this site in Symbiot's own browser as a window, to sign in once; close it when you're done">Sign in</button></div>
|
|
493
665
|
<div class="row" style="margin-top:10px"><input id="screenname" placeholder="name the screen, e.g. GitHub PR page" style="flex:1"><select id="screenwhich" class="hidden" title="which display to capture" style="flex:0 0 auto;width:auto"></select><select id="screendelay" title="wait first, so you can bring the right window to the front" style="flex:0 0 auto;width:auto"><option value="0">now</option><option value="3">in 3s</option><option value="5">in 5s</option><option value="10">in 10s</option></select><button class="ghost" id="capture">Capture screen</button><button class="ghost" id="screenload" title="use a PNG screenshot you already have">Load image</button><input type="file" id="screenfile" accept="image/png" class="hidden"></div>
|
|
@@ -495,7 +667,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
495
667
|
<div id="watchbox"></div>
|
|
496
668
|
<div id="screenlist" class="taskfilter"></div>
|
|
497
669
|
<div id="screenview"></div>
|
|
498
|
-
</
|
|
670
|
+
</details>
|
|
499
671
|
</section>
|
|
500
672
|
<section id="panel-board" class="hidden">
|
|
501
673
|
<div class="bhead"><div class="bsumw"><div id="boardsum" class="bsum">Everything you watch, side by side.</div><div id="boardsub" class="bsub"></div></div><div class="seg" id="boardwin" role="radiogroup" aria-label="How far back"><button type="button" role="radio" data-h="24" aria-checked="true">24 hours</button><button type="button" role="radio" data-h="72" aria-checked="false">3 days</button><button type="button" role="radio" data-h="168" aria-checked="false">7 days</button></div><button type="button" class="iconbtn" id="boardrefresh" title="Refresh" aria-label="Refresh"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M13 8a5 5 0 1 1-1.5-3.6M13 2.5V5h-2.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
|
@@ -506,15 +678,13 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
506
678
|
<div id="boardlinks"></div>
|
|
507
679
|
</section>
|
|
508
680
|
<section id="panel-run" class="hidden">
|
|
509
|
-
<div class="
|
|
510
|
-
<
|
|
511
|
-
<span class="muted">Reads your local git and writes it up.</span></div>
|
|
512
|
-
<div class="out muted" id="out">Nothing yet - hit the button.</div>
|
|
681
|
+
<div class="wuhead"><div><h2 class="wut" id="wutitle">Your week</h2><div class="wus" id="wusub">The last 7 days, from your local git</div></div><div class="wua"><button class="iconbtn hidden" id="copy" title="Copy" aria-label="Copy"><svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3 11V3a1 1 0 0 1 1-1h7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button><button class="act" id="write">Write my <span id="what">week</span></button></div></div>
|
|
682
|
+
<div class="out muted" id="out">Nothing written yet. Symbiot reads your local git and writes it up for you.</div>
|
|
513
683
|
<div class="rfoot" id="outfoot" style="display:none"></div>
|
|
514
684
|
</section>
|
|
515
685
|
<section id="panel-tasks" class="hidden">
|
|
516
686
|
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><select id="newtaskrepo" title="Which repo this task is for (needed to send it to an agent)" style="flex:0 0 auto;max-width:180px"><option value="">repo…</option></select><button class="act" id="addtask">Add</button><button class="ghost" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></div>
|
|
517
|
-
<div class="note muted" style="margin-top:2px">To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>↶</b> sends it back. Ticking a task yourself marks it done (it auto-archives). Click <b
|
|
687
|
+
<div class="note muted" style="margin-top:2px">To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>↶</b> sends it back. Ticking a task yourself marks it done (it auto-archives). Click <b><i class=ic-chat></i></b> on a task to ask questions about it.</div>
|
|
518
688
|
<div id="pushout"></div>
|
|
519
689
|
<div id="reviewout"></div>
|
|
520
690
|
<div id="reviewlist"></div>
|
|
@@ -529,12 +699,13 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
529
699
|
</section>
|
|
530
700
|
<section id="panel-drift" class="hidden">
|
|
531
701
|
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
532
|
-
<label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch">
|
|
533
|
-
<label class="muted"><input type="checkbox" id="driftci">
|
|
702
|
+
<label class="muted swl" style="margin-left:auto"><input type="checkbox" class="sw" id="driftfetch"> Fetch latest</label>
|
|
703
|
+
<label class="muted swl"><input type="checkbox" class="sw" id="driftci"> Check CI <small>(needs gh, experimental)</small></label>
|
|
534
704
|
<button class="ghost" id="driftrun">Rescan</button></div>
|
|
535
705
|
<div id="driftout"></div>
|
|
536
706
|
</section>
|
|
537
707
|
<section id="panel-settings" class="hidden">
|
|
708
|
+
<div class="sset"><h3 class="ssh">Your AI</h3>
|
|
538
709
|
<label>Which AI should Symbiot write with?</label>
|
|
539
710
|
<select id="provider">
|
|
540
711
|
<option value="anthropic">Claude (Anthropic)</option>
|
|
@@ -550,19 +721,20 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
550
721
|
<input id="model" type="text" placeholder="(blank = default)">
|
|
551
722
|
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
552
723
|
<span class="note" id="saveMsg"></span></div>
|
|
553
|
-
|
|
554
|
-
<
|
|
724
|
+
</div>
|
|
725
|
+
<div class="sset">
|
|
726
|
+
<h3 class="ssh">Folders to scan for repos</h3>
|
|
555
727
|
<div id="scanroots"></div>
|
|
556
728
|
<div class="row" style="margin-top:6px"><input id="newroot" placeholder="/path/to/folder (or ~/work) — where your projects live" style="flex:1"><button class="ghost" id="addroot">Add folder</button></div>
|
|
557
729
|
<div class="note muted" id="scanrootnote">Point Symbiot at where your work lives — inside or outside your home folder. Defaults to your home folder.</div>
|
|
558
730
|
</div>
|
|
559
|
-
<div id="phonebox" class="hidden
|
|
560
|
-
<
|
|
731
|
+
<div id="phonebox" class="hidden sset">
|
|
732
|
+
<h3 class="ssh">Projects in Termux</h3>
|
|
561
733
|
<div class="note muted" id="phonenote"></div>
|
|
562
734
|
<div class="row" style="margin-top:6px"><button class="ghost" id="phonebtn"></button></div>
|
|
563
735
|
</div>
|
|
564
|
-
<div
|
|
565
|
-
<
|
|
736
|
+
<div class="sset">
|
|
737
|
+
<h3 class="ssh">Hand off to your agent when you "Send to repos"</h3>
|
|
566
738
|
<input id="agentcmd" type="text" placeholder="e.g. aider --message "{prompt}" --yes · code {dir} · leave blank to just write the file">
|
|
567
739
|
<div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor — it's your command.</div>
|
|
568
740
|
<div id="agentpresets" style="margin-top:8px"></div>
|
|
@@ -574,26 +746,24 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
574
746
|
<div class="note" id="grantnote"></div>
|
|
575
747
|
</div>
|
|
576
748
|
</div>
|
|
577
|
-
<div
|
|
578
|
-
<label>Link your work</label>
|
|
579
|
-
<div class="note muted" style="margin-top:2px">One click per site you work in: it opens the site in Symbiot's own browser, where you sign in the usual way (its own login, SSO and 2FA; Symbiot never sees your password). Symbiot then trusts the site and watches its inbox or notifications, so what arrives there shows on the Dashboard and in Week and Standup. Close the window when you're signed in.</div>
|
|
749
|
+
<div class="sset">
|
|
580
750
|
<div id="links"></div>
|
|
581
751
|
<div id="linksmsg"></div>
|
|
582
752
|
<div class="note muted" id="linksnote">For a whole team, one <b>links.json</b> in Symbiot's config folder adds your company's own sites and hides the ones you don't use, so everyone gets the same buttons.</div>
|
|
583
753
|
</div>
|
|
584
|
-
<div
|
|
585
|
-
<
|
|
754
|
+
<div class="sset">
|
|
755
|
+
<h3 class="ssh">What Symbiot remembers</h3>
|
|
586
756
|
<div class="note muted" style="margin-top:2px">Every chat in the app is the same Symbiot. What's worth knowing on another page (who someone is, which account is what, what you decided) is kept here, on this computer only, and each chat gets just the parts its question touches.</div>
|
|
587
757
|
<div id="mindlist"></div>
|
|
588
758
|
</div>
|
|
589
|
-
<div
|
|
590
|
-
<
|
|
759
|
+
<div class="sset">
|
|
760
|
+
<h3 class="ssh">Trusted sites for Screens <span class="muted">(experimental)</span></h3>
|
|
591
761
|
<div id="trustedsites"></div>
|
|
592
762
|
<div class="row" style="margin-top:6px"><input id="newtrusted" placeholder="a site, e.g. mail.google.com or github.com" style="flex:1"><button class="ghost" id="addtrusted">Trust site</button></div>
|
|
593
763
|
<div class="note muted" id="trustednote">On a mapped page from one of these sites, <b>Press</b> and <b>Type</b> go ahead without asking, for you and for agents (<b>symbiot screens press</b> / <b>type</b>), signed in as you. A site covers its subdomains: google.com covers mail.google.com. Everywhere else, each one asks first. Only you add sites, here: Symbiot gives agents no command for it.</div>
|
|
594
764
|
</div>
|
|
595
|
-
<div
|
|
596
|
-
<
|
|
765
|
+
<div class="sset">
|
|
766
|
+
<h3 class="ssh">Email — add what you sent to Week and Standup <span class="muted">(experimental)</span></h3>
|
|
597
767
|
<label class="check"><input type="checkbox" id="mailon"> Use my sent email (subjects & recipients only)</label>
|
|
598
768
|
<div id="mailsources"></div>
|
|
599
769
|
<div class="row" style="margin-top:6px"><input id="newmail" placeholder="add a mail folder or .mbox file (e.g. a Google Takeout export)" style="flex:1"><button class="ghost" id="addmail">Add</button><button class="ghost" id="mailpreview">Preview</button></div>
|
|
@@ -601,19 +771,19 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
601
771
|
<div class="note muted" id="mailnote">No API, no OAuth, no password: Symbiot reads the mail your desktop mail app (Thunderbird, Apple Mail, Evolution, mutt…) already keeps on this computer, or an exported .mbox — so anyone can link theirs. Headers only, never a message body; off until you tick it. Experimental: tested with mbox and Maildir, not yet on real Apple Mail, Evolution or KMail stores.</div>
|
|
602
772
|
<div id="mailout"></div>
|
|
603
773
|
</div>
|
|
604
|
-
<div
|
|
605
|
-
<
|
|
774
|
+
<div class="sset">
|
|
775
|
+
<h3 class="ssh">Weekly write-up and start at login</h3>
|
|
606
776
|
<label class="check"><input type="checkbox" id="weeklyon"> Write my week and send me a desktop notification every</label>
|
|
607
777
|
<div class="row" style="margin-top:2px"><select id="weeklyday" style="width:auto"><option value="1">Monday</option><option value="2">Tuesday</option><option value="3">Wednesday</option><option value="4">Thursday</option><option value="5">Friday</option><option value="6">Saturday</option><option value="0">Sunday</option></select><span class="muted">at</span><select id="weeklyhour" style="width:auto"></select><button class="ghost" id="weeklynow" title="write it now and send the notification, to check it works">Write it now</button></div>
|
|
608
778
|
<label class="check"><input type="checkbox" id="autostart"> <span id="autostartlbl">Start Symbiot in the background when I log in (no window)</span></label>
|
|
609
779
|
<div class="note muted" id="desktopnote"></div>
|
|
610
780
|
</div>
|
|
611
|
-
<div
|
|
612
|
-
<
|
|
781
|
+
<div class="sset">
|
|
782
|
+
<h3 class="ssh">Watch on your phone <span class="muted">(experimental)</span></h3>
|
|
613
783
|
<div id="phonelink"></div>
|
|
614
784
|
</div>
|
|
615
|
-
<div
|
|
616
|
-
<
|
|
785
|
+
<div class="sset">
|
|
786
|
+
<h3 class="ssh">Local models <span class="muted">(experimental)</span></h3>
|
|
617
787
|
<button class="ghost" id="recbtn">Recommend models for my machine</button>
|
|
618
788
|
<button class="ghost" id="setuplocal" style="margin-left:8px">Set up a free local model</button>
|
|
619
789
|
<div id="setupout" class="note muted" style="margin-top:10px"></div>
|
|
@@ -640,7 +810,7 @@ $('panel-settings').classList.toggle('hidden',!isSet);
|
|
|
640
810
|
$('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
641
811
|
$('panel-drift').classList.toggle('hidden',!isDrift);
|
|
642
812
|
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
643
|
-
if(isRun){$('what').textContent=tab;$('out').textContent=
|
|
813
|
+
if(isRun){$('what').textContent=tab;wuHead();WUTEXT='';$('out').textContent=WUEMPTY;$('out').classList.add('muted');$('copy').classList.add('hidden');$('outfoot').style.display='none';if(tab==='week')showLatestWeek();}
|
|
644
814
|
if(isMap&&!mapLoaded)loadMap();
|
|
645
815
|
if(isTasks){fillTaskRepos();loadTasks();}
|
|
646
816
|
if(isDrift&&!driftLoaded)loadDrift();
|
|
@@ -652,12 +822,13 @@ function firstTab(){api('/api/watch/board').then(function(b){if(b&&b.cards&&b.ca
|
|
|
652
822
|
function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
|
|
653
823
|
// the status opens Settings: on a phone its tab is scrolled out of sight
|
|
654
824
|
$('status').style.cursor='pointer';$('status').addEventListener('click',function(){setTab('settings');var t=document.querySelector('.tab[data-tab=settings]');if(t&&t.scrollIntoView)t.scrollIntoView({block:'nearest',inline:'nearest'});});
|
|
655
|
-
|
|
825
|
+
var WUEMPTY='Nothing written yet. Symbiot reads your local git and writes it up for you.',WUTEXT='';
|
|
826
|
+
$('write').addEventListener('click',function(){$('out').textContent='Writing it up from your local git…';$('out').classList.add('muted');$('copy').classList.add('hidden');
|
|
656
827
|
api('/api/run',{cmd:current}).then(function(r){var f=$('outfoot');if(r.error==='not-connected'){$('out').textContent='Not connected yet - open Settings and pick an AI.';f.style.display='none';return;}
|
|
657
828
|
// a week is saved to weeks/ as well (r.file), like Settings' Write it now
|
|
658
829
|
var foot=[r.footer,r.file?'Saved to '+r.file:'',r.error&&r.text&&r.error!==r.text?r.error:''].filter(Boolean).join(' · ');
|
|
659
|
-
|
|
660
|
-
$('copy').addEventListener('click',function(){navigator.clipboard.writeText(
|
|
830
|
+
WUTEXT=r.text||'';$('out').innerHTML=r.text?mdLite(r.text):'(no output)';$('out').classList.remove('muted');$('copy').classList.remove('hidden');if(foot){f.textContent=foot;f.style.display='block';}else{f.style.display='none';}});});
|
|
831
|
+
$('copy').addEventListener('click',function(){var c=$('copy');try{navigator.clipboard.writeText(WUTEXT||$('out').textContent);}catch(e){}c.setAttribute('title','Copied');c.classList.add('done');setTimeout(function(){c.setAttribute('title','Copy');c.classList.remove('done');},1400);});
|
|
661
832
|
function syncP(){var p=$('provider').value;var local=p==='ollama';$('keyWrap').classList.toggle('hidden',local);$('baseWrap').classList.toggle('hidden',!local);
|
|
662
833
|
$('getkey').textContent=local?'About Ollama':'Where do I get a key?';$('modelHint').textContent='(default '+DEFMODEL[p]+')';$('model').placeholder='(blank = '+DEFMODEL[p]+')';}
|
|
663
834
|
$('provider').addEventListener('change',syncP);
|
|
@@ -670,7 +841,7 @@ var GRAPH=null,sel=null,view={k:1,x:0,y:0},GW=960,GH=620;
|
|
|
670
841
|
function esc(s){return String(s).replace(/[&<>]/g,function(ch){return ch==='&'?'&':ch==='<'?'<':'>';});}
|
|
671
842
|
// An agent's option, with its who-acts markers as badges: 👤 You (you do this
|
|
672
843
|
// step) and 🤖 Agent (picking it is enough; the next run does it). Escaped first.
|
|
673
|
-
function whoHtml(o){return esc(o).replace(/👤[ ]*(You:|You(?=[ ]))?/g,"<span class='who you'
|
|
844
|
+
function whoHtml(o){return esc(o).replace(/👤[ ]*(You:|You(?=[ ]))?/g,"<span class='who you'><i class=ic-person></i> You</span> ").replace(/🤖[ ]*(Agent:|Agent(?=[ ]))?/g,"<span class='who agent'><i class=ic-bot></i> Agent</span> ");}
|
|
674
845
|
function layout(nodes,edges){var idx={};nodes.forEach(function(n){n.x=GW/2+(Math.random()-0.5)*GW*0.8;n.y=GH/2+(Math.random()-0.5)*GH*0.8;n.vx=0;n.vy=0;idx[n.id]=n;});
|
|
675
846
|
for(var it=0;it<340;it++){for(var i=0;i<nodes.length;i++)for(var j=i+1;j<nodes.length;j++){var a=nodes[i],b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y;var d2=dx*dx+dy*dy+0.01;var d=Math.sqrt(d2);var f=4600/d2;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;}
|
|
676
847
|
edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y;var d=Math.sqrt(dx*dx+dy*dy)+0.01;var f=(d-115)*0.03;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;});
|
|
@@ -683,7 +854,97 @@ GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=
|
|
|
683
854
|
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>";
|
|
684
855
|
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>";});
|
|
685
856
|
document.getElementById("graph").innerHTML=s+"</g>";}
|
|
686
|
-
|
|
857
|
+
|
|
858
|
+
// ---- the Map, in the liquid -------------------------------------------------------
|
|
859
|
+
// Your repos and project folders as droplets in the look, placed by their nearest
|
|
860
|
+
// neighbours (mapknn.mjs: what they're built with, the weeks you work on them, what
|
|
861
|
+
// they're about). The closest merge into one shape, the next are joined by a tendril,
|
|
862
|
+
// the rest by a faint line; each cluster is named over its region; what you haven't
|
|
863
|
+
// touched in a while is smaller and sinks; you sit where your recent work is.
|
|
864
|
+
// Point at a droplet: its neighbours light up and say why. Click: its details.
|
|
865
|
+
var LM={G:null,on:false,items:[],edges:[],clusters:[],view:{k:1,x:0,y:0},hot:null,pan:null,built:false};
|
|
866
|
+
function lmTilde(p){p=String(p||'');var parts=p.split('/');return (parts[1]==='home'||parts[1]==='Users')&&parts.length>2?'~'+(parts.length>3?'/'+parts.slice(3).join('/'):''):p;}
|
|
867
|
+
function lmName(id){var n=GRAPH&&nodeById(id);return n?n.label:String(id).split('/').pop();}
|
|
868
|
+
function lmNearHtml(id){var k=GRAPH&&GRAPH.knn,ns=k&&k.neighbours&&k.neighbours[id];if(!ns||!ns.length)return '';
|
|
869
|
+
return "<div class='k'>Most like it</div><ul class='lmnear'>"+ns.map(function(n){return "<li><button type='button' data-id='"+escQ(n.id)+"'><b>"+esc(lmName(n.id))+"</b><span>"+esc(n.why.join('; ')||'a little alike')+"</span></button></li>";}).join('')+"</ul>";}
|
|
870
|
+
function lmNearWire(el){if(el&&el.querySelectorAll)el.querySelectorAll('.lmnear button').forEach(function(b){b.addEventListener('click',function(){selectNode(b.getAttribute('data-id'));});});}
|
|
871
|
+
function lmWeeks(d){var n=GRAPH&&nodeById('repo:'+d.path),w=n&&n.meta&&n.meta.weeks;if(!w||!w.length)return '';var mx=Math.max.apply(null,w.concat([1]));
|
|
872
|
+
return "<div class='k'>Your last 12 weeks</div><div class='lmweeks' role='img' aria-label='"+w.join(', ')+" commits a week'>"+w.map(function(x){return "<i style='height:"+Math.max(2,Math.round(28*x/mx))+"px' title='"+x+" commits'></i>";}).join('')+"</div>";}
|
|
873
|
+
function lmBuild(g){var el=$('lmap');if(!el||!g)return;LM.g=g;var K=g.knn||{neighbours:{},edges:[],clusters:[],vitality:{}};
|
|
874
|
+
var items=g.nodes.filter(function(n){return n.type==='repo'||n.type==='folder';}),mx=1;items.forEach(function(n){var c=(n.meta&&n.meta.commits)||0;if(c>mx)mx=c;});
|
|
875
|
+
var byId={};LM.items=items.map(function(n){var v=K.vitality[n.id]||0,c=(n.meta&&n.meta.commits)||0,fold=n.type==='folder';
|
|
876
|
+
var it={id:n.id,n:n,v:v,fold:fold,r:fold?10:(14+30*Math.sqrt(c/mx))*(0.7+0.3*v),x:0,y:0};byId[n.id]=it;return it;});
|
|
877
|
+
LM.edges=(K.edges||[]).filter(function(e){return byId[e.a]&&byId[e.b];}).map(function(e){return {a:byId[e.a],b:byId[e.b],sim:e.sim};});
|
|
878
|
+
// seed by cluster: each cluster (and each repo on its own) gets its own arc
|
|
879
|
+
var groups=(K.clusters||[]).map(function(c){return c.ids.filter(function(id){return byId[id];}).map(function(id){return byId[id];});}).filter(function(m){return m.length;}),inG={};
|
|
880
|
+
groups.forEach(function(m){m.forEach(function(it){inG[it.id]=1;});});LM.items.forEach(function(it){if(!inG[it.id])groups.push([it]);});
|
|
881
|
+
var tot=0;groups.forEach(function(m){tot+=m.length+0.6;});var acc=-Math.PI/2;
|
|
882
|
+
groups.forEach(function(m){var span=(m.length+0.6)/tot*2*Math.PI,mid=acc+span/2;acc+=span;var R=180+40*m.length;m.forEach(function(it,i){var a=mid+(i-(m.length-1)/2)*Math.min(0.5,span/m.length);it.x=Math.cos(a)*R*(i%2?1.12:0.92);it.y=Math.sin(a)*R*0.8*(i%2?1.12:0.92);});});
|
|
883
|
+
var link={};LM.edges.forEach(function(e){link[e.a.id+'|'+e.b.id]=link[e.b.id+'|'+e.a.id]=e.sim;});
|
|
884
|
+
var L=LM.items;for(var it=0;it<700;it++){
|
|
885
|
+
LM.edges.forEach(function(e){var dx=e.b.x-e.a.x,dy=e.b.y-e.a.y,d=Math.sqrt(dx*dx+dy*dy)||1,rest=e.sim>=0.45?(e.a.r+e.b.r)*1.15:e.a.r+e.b.r+40+150*(1-e.sim),f=(d-rest)*0.05*Math.min(1,e.sim*2.2)/d;e.a.x+=dx*f;e.a.y+=dy*f;e.b.x-=dx*f;e.b.y-=dy*f;});
|
|
886
|
+
for(var i=0;i<L.length;i++){var a=L[i];for(var j=i+1;j<L.length;j++){var b=L[j],dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)||1,sm=link[a.id+'|'+b.id],mn=sm>=0.45?(a.r+b.r)*1.08:a.r+b.r+(sm?30:70);if(d<mn){var f=(mn-d)*0.3/d;a.x-=dx*f;a.y-=dy*f;b.x+=dx*f;b.y+=dy*f;}}
|
|
887
|
+
a.x*=0.996;a.y=a.y*0.996+(1-a.v)*0.5;}}
|
|
888
|
+
// you: where your recent work is
|
|
889
|
+
var sx=0,sy=0,sw=0;L.forEach(function(a){var w=a.fold?0:a.v*(1+((a.n.meta&&a.n.meta.commits)||0));sx+=a.x*w;sy+=a.y*w;sw+=w;});
|
|
890
|
+
LM.me={id:'me',x:sw?sx/sw:0,y:sw?sy/sw-10:-10,r:14};for(var q=0;q<200;q++)L.forEach(function(a){var dx=LM.me.x-a.x,dy=LM.me.y-a.y,d=Math.sqrt(dx*dx+dy*dy)||1,mn=a.r+LM.me.r+28;if(d<mn){LM.me.x+=dx/d*(mn-d)*0.5;LM.me.y+=dy/d*(mn-d)*0.5;}});
|
|
891
|
+
LM.clusters=(K.clusters||[]).map(function(c){return {label:c.label,m:c.ids.map(function(id){return byId[id];}).filter(Boolean)};}).filter(function(c){return c.m.length>1;});
|
|
892
|
+
// labels
|
|
893
|
+
var ov=$('lmov');if(ov){ov.innerHTML=L.map(function(a,i){var m=a.n.meta||{},sub=a.fold?'folder, no git':(m.commits||0)+((m.commits||0)===1?' commit':' commits')+(m.last?' · '+lmAgo(m.last):'');return "<button type='button' class='lml"+(a.v<0.25&&!a.fold?' dormant':'')+(a.fold?' fold':'')+"' data-i='"+i+"'><b>"+esc(a.n.label)+"</b><span>"+sub+"</span></button>";}).join('')+
|
|
894
|
+
"<div class='lmme' id='lmme' aria-hidden='true'><i></i><b>you are here</b></div>"+LM.clusters.map(function(c,i){return "<div class='lmc' data-c='"+i+"'>"+esc(c.label)+"</div>";}).join('')+L.filter(function(a){return a.fold;}).map(function(a){return "<i class='lmring' data-id='"+escQ(a.id)+"'></i>";}).join('');
|
|
895
|
+
LM.labs=[].slice.call(ov.querySelectorAll('.lml[data-i]'));LM.cls=[].slice.call(ov.querySelectorAll('.lmc'));LM.rings=[].slice.call(ov.querySelectorAll('.lmring'));
|
|
896
|
+
LM.labs.forEach(function(b){var a=L[+b.getAttribute('data-i')];b.addEventListener('click',function(){selectNode(a.id);});b.addEventListener('pointerenter',function(){lmHot(a);});b.addEventListener('focus',function(){lmHot(a);});b.addEventListener('pointerleave',function(){lmHot(null);});b.addEventListener('blur',function(){lmHot(null);});});}
|
|
897
|
+
// the rest of what you build with, as a quiet row under the map
|
|
898
|
+
var tl=$('lmtools');if(tl){var langs={};L.forEach(function(a){((a.n.meta&&a.n.meta.langs)||[]).forEach(function(x){langs[x]=(langs[x]||0)+1;});});
|
|
899
|
+
var lg=Object.keys(langs).sort(function(x,y){return langs[y]-langs[x];});var tools=g.nodes.filter(function(n){return n.type==='tool'||n.type==='agent'||n.type==='ai';});
|
|
900
|
+
tl.innerHTML=(lg.length?"<div class='lmrow'><span class='lmk'>Languages</span>"+lg.map(function(x){return "<button type='button' class='lmt' data-id='lang:"+escQ(x)+"'>"+esc(x)+"<small>"+langs[x]+"</small></button>";}).join('')+"</div>":"")+
|
|
901
|
+
(tools.length?"<div class='lmrow'><span class='lmk'>You build with</span>"+tools.map(function(n){return "<button type='button' class='lmt' data-id='"+escQ(n.id)+"'>"+esc(n.label)+"</button>";}).join('')+"</div>":"");
|
|
902
|
+
tl.querySelectorAll('.lmt').forEach(function(b){b.addEventListener('click',function(){selectNode(b.getAttribute('data-id'));});});}
|
|
903
|
+
lmFit();if(!LM.built){LM.built=true;lmWire();}lmStart();}
|
|
904
|
+
function lmAgo(day){var t=Date.parse(day);if(!t)return day;var d=Math.round((Date.now()-t)/86400000);return d<=0?'today':d===1?'yesterday':d<14?d+' days ago':d<60?Math.round(d/7)+' weeks ago':Math.round(d/30)+' months ago';}
|
|
905
|
+
function lmFit(){var el=$('lmap'),L=LM.items;if(!el||!L.length)return;var x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;L.concat([LM.me]).forEach(function(a){x0=Math.min(x0,a.x-a.r);y0=Math.min(y0,a.y-a.r);x1=Math.max(x1,a.x+a.r);y1=Math.max(y1,a.y+a.r+30);});
|
|
906
|
+
var w=el.clientWidth||900,h=el.clientHeight||520,k=Math.min(1.6,(w-160)/Math.max(1,x1-x0),(h-150)/Math.max(1,y1-y0));LM.view={k:k,x:w/2-(x0+x1)/2*k,y:h/2-(y0+y1)/2*k+20};}
|
|
907
|
+
function lmHot(a){LM.hot=a;var tip=$('lmtip');var near={};if(a&&GRAPH&&GRAPH.knn)(GRAPH.knn.neighbours[a.id]||[]).forEach(function(n){near[n.id]=n;});
|
|
908
|
+
(LM.labs||[]).forEach(function(b){var it=LM.items[+b.getAttribute('data-i')];if(b.classList){b.classList.toggle('near',!!(a&&near[it.id]));b.classList.toggle('far',!!(a&&it!==a&&!near[it.id]));}});
|
|
909
|
+
if(!tip)return;if(!a){tip.className='lmtip';return;}var m=a.n.meta||{},ns=(GRAPH&&GRAPH.knn&&GRAPH.knn.neighbours[a.id])||[];
|
|
910
|
+
tip.innerHTML="<b>"+esc(a.n.label)+"</b><span class='lmts'>"+(a.fold?'a project folder, not a git repo':(m.commits||0)+((m.commits||0)===1?' commit':' commits')+(m.last?' · last '+lmAgo(m.last):'')+(m.branch?' · on '+esc(m.branch):''))+"</span>"+(ns.length?"<div class='lmtn'>"+ns.map(function(n){return "<div><b>"+esc(lmName(n.id))+"</b> "+esc(n.why.join('; ')||'a little alike')+"</div>";}).join('')+"</div>":"<div class='lmtn'>Nothing else is much like it.</div>");tip.className='lmtip on';}
|
|
911
|
+
function lmS(p){return {x:p.x*LM.view.k+LM.view.x,y:p.y*LM.view.k+LM.view.y};}
|
|
912
|
+
function lmWire(){var el=$('lmap');if(!el||!el.addEventListener)return;
|
|
913
|
+
el.addEventListener('wheel',function(e){if(!e)return;e.preventDefault();var r=el.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top,k0=LM.view.k,k=Math.max(0.3,Math.min(4,k0*Math.exp(-e.deltaY*0.0015)));LM.view.x=mx-(mx-LM.view.x)*k/k0;LM.view.y=my-(my-LM.view.y)*k/k0;LM.view.k=k;},{passive:false});
|
|
914
|
+
el.addEventListener('pointerdown',function(e){if(e.target&&e.target.closest&&e.target.closest('button,.lmtip'))return;LM.pan={x:e.clientX,y:e.clientY,vx:LM.view.x,vy:LM.view.y,moved:false};if(el.setPointerCapture)el.setPointerCapture(e.pointerId);});
|
|
915
|
+
el.addEventListener('pointermove',function(e){if(LM.pan){var dx=e.clientX-LM.pan.x,dy=e.clientY-LM.pan.y;if(Math.abs(dx)+Math.abs(dy)>3)LM.pan.moved=true;LM.view.x=LM.pan.vx+dx;LM.view.y=LM.pan.vy+dy;return;}
|
|
916
|
+
var r=el.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top,hit=null;LM.items.forEach(function(a){var p=lmS(a);if(!hit&&Math.hypot(mx-p.x,my-p.y)<=a.r*LM.view.k+6)hit=a;});if(hit!==LM.hot&&!(e.target&&e.target.closest&&e.target.closest('.lml')))lmHot(hit);});
|
|
917
|
+
el.addEventListener('pointerup',function(e){var p=LM.pan;LM.pan=null;if(p&&!p.moved&&LM.hot&&!(e.target&&e.target.closest&&e.target.closest('button')))selectNode(LM.hot.id);});
|
|
918
|
+
el.addEventListener('pointerleave',function(){if(!LM.pan)lmHot(null);});
|
|
919
|
+
var zi=$('lmin'),zo=$('lmout'),zf=$('lmfit');function z(f){var w=el.clientWidth/2,h=el.clientHeight/2,k0=LM.view.k,k=Math.max(0.3,Math.min(4,k0*f));LM.view.x=w-(w-LM.view.x)*k/k0;LM.view.y=h-(h-LM.view.y)*k/k0;LM.view.k=k;}
|
|
920
|
+
if(zi)zi.addEventListener('click',function(){z(1.25);});if(zo)zo.addEventListener('click',function(){z(0.8);});if(zf)zf.addEventListener('click',lmFit);}
|
|
921
|
+
function lmStart(){if(LM.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;LM.on=true;
|
|
922
|
+
var loop=function(){var p=$('panel-map'),el=$('lmap');if(!p||!el||!el.offsetParent){LM.on=false;return;}lmFrame();raf(loop);};raf(loop);}
|
|
923
|
+
function lmFrame(){var el=$('lmap'),cv=$('lmc');if(!el||!cv||!LM.items.length)return;if(!LM.G)LM.G=lqGL(cv,64);var w=el.clientWidth,h=el.clientHeight,t=LM.t=(LM.t||0)+0.016,still=LQ.theme&&LQ.theme.still,k=LM.view.k;
|
|
924
|
+
var P=LM.items.map(function(a,i){var p=lmS(a);if(!still){p.x+=Math.sin(t*0.4+i*1.7)*2;p.y+=Math.cos(t*0.35+i*2.3)*2;}return p;}),ME=lmS(LM.me);
|
|
925
|
+
// labels, rings, cluster names, tip
|
|
926
|
+
(LM.labs||[]).forEach(function(b){var i=+b.getAttribute('data-i'),a=LM.items[i],p=P[i];if(!b.style)return;b.style.transform='translate('+Math.round(p.x-(b.offsetWidth||0)/2)+'px,'+Math.round(p.y+a.r*k+4)+'px)';});
|
|
927
|
+
var me=$('lmme');if(me&&me.style)me.style.transform='translate('+Math.round(ME.x-(me.offsetWidth||0)/2)+'px,'+Math.round(ME.y-7)+'px)';
|
|
928
|
+
(LM.rings||[]).forEach(function(rg){var i=-1;LM.items.forEach(function(a,j){if(a.id===rg.getAttribute('data-id'))i=j;});if(i<0||!rg.style)return;var R=Math.round(LM.items[i].r*k);rg.style.width=rg.style.height=(2*R)+'px';rg.style.transform='translate('+Math.round(P[i].x-R)+'px,'+Math.round(P[i].y-R)+'px)';});
|
|
929
|
+
(LM.cls||[]).forEach(function(c){var cl=LM.clusters[+c.getAttribute('data-c')],sx=0,top=1e9;cl.m.forEach(function(a){var i=LM.items.indexOf(a);sx+=P[i].x;top=Math.min(top,P[i].y-a.r*k);});if(c.style)c.style.transform='translate('+Math.round(sx/cl.m.length-(c.offsetWidth||0)/2)+'px,'+Math.round(top-30)+'px)';});
|
|
930
|
+
var tip=$('lmtip');if(tip&&LM.hot){var hi=LM.items.indexOf(LM.hot),hp=P[hi],tw=tip.offsetWidth||280,th=tip.offsetHeight||100,tx=Math.max(10,Math.min(w-tw-10,hp.x+LM.hot.r*k+16)),ty=Math.max(10,Math.min(h-th-10,hp.y-th/2));if(tx<hp.x&&hp.x+LM.hot.r*k+16+tw>w)tx=Math.max(10,hp.x-LM.hot.r*k-16-tw);tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
|
|
931
|
+
// faint lines for the weak links
|
|
932
|
+
var sv=$('lmlines');if(sv){var ln='';LM.edges.forEach(function(e){if(e.sim>=0.3)return;var a=P[LM.items.indexOf(e.a)],b=P[LM.items.indexOf(e.b)],hot=LM.hot&&(LM.hot===e.a||LM.hot===e.b);ln+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' class='"+(hot?'hot':'')+"'/>";});sv.innerHTML=ln;}
|
|
933
|
+
var G=LM.G;if(!G)return;var gl=G.gl,d=Math.min(window.devicePixelRatio||1,2),W=Math.max(1,Math.floor(w*d)),H=Math.max(1,Math.floor(h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
|
|
934
|
+
var o=G.out;for(var z=0;z<o.length;z++)o[z]=0;var n=0,put=function(x,y,r,a){if(n>=64||r<=0)return;o[n*4]=x/w;o[n*4+1]=y/h;o[n*4+2]=r;o[n*4+3]=a||0;n++;};
|
|
935
|
+
LM.items.forEach(function(a,i){if(!a.fold)put(P[i].x,P[i].y,a.r*k*(LM.hot===a?1.08:1),0);});
|
|
936
|
+
// tendrils: a chain of small drops, close enough to always bridge, for the middling links
|
|
937
|
+
LM.edges.forEach(function(e){if(e.sim<0.3||e.a.fold||e.b.fold)return;var A=P[LM.items.indexOf(e.a)],B=P[LM.items.indexOf(e.b)],dx=B.x-A.x,dy=B.y-A.y,D=Math.sqrt(dx*dx+dy*dy)||1,gap=D-(e.a.r+e.b.r)*k;if(gap<=4)return;var m=Math.min(20,Math.ceil(gap/8)),sp=gap/(m+1),rr=Math.max(1.8,sp*0.4);for(var c=1;c<=m;c++){var f=(e.a.r*k+sp*c)/D;put(A.x+dx*f,A.y+dy*f,rr,0);}});
|
|
938
|
+
var th=LQ.theme||{},U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,0,0,-10);gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);gl.drawArrays(gl.TRIANGLES,0,3);}
|
|
939
|
+
|
|
940
|
+
// The write-ups as a document: # headings, - bullets, **bold**; everything escaped first.
|
|
941
|
+
function mdLite(t){var out=[],list=false;String(t||'').split(String.fromCharCode(10)).forEach(function(line){var l=line.trim(),b=function(x){var p=esc(x).split('**');return p.map(function(s,i){return i%2?'<b>'+s+'</b>':s;}).join('');};
|
|
942
|
+
if(l.indexOf('- ')===0||l.indexOf('* ')===0||l.indexOf('• ')===0){if(!list){out.push('<ul>');list=true;}out.push('<li>'+b(l.slice(2))+'</li>');return;}
|
|
943
|
+
if(list){out.push('</ul>');list=false;}if(!l){return;}var h=0;while(l.charAt(h)==='#')h++;if(h&&l.charAt(h)===' '){out.push('<h'+Math.min(4,h+2)+'>'+b(l.slice(h+1))+'</h'+Math.min(4,h+2)+'>');return;}
|
|
944
|
+
out.push('<p>'+b(l)+'</p>');});if(list)out.push('</ul>');return out.join('');}
|
|
945
|
+
function wuHead(){var t=$('wutitle'),u=$('wusub');if(!t||!u)return;var c=typeof current==='string'?current:'week',d=new Date(),f=function(x){return x.toLocaleDateString(undefined,{day:'numeric',month:'short'});};
|
|
946
|
+
if(c==='standup'){t.textContent='Your standup';u.textContent='Yesterday and today, from your local git';}else if(c==='todo'){t.textContent='Still on your plate';u.textContent='What your repos say is unfinished';}else{var a=new Date(d.getTime()-6*86400000);t.textContent='Your week';u.textContent=f(a)+' to '+f(d)+', from your local git';}}
|
|
947
|
+
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=lmTilde(g.stats.base)||"your home folder";
|
|
687
948
|
if(g.stats.noStorage)return "Symbiot can't see your phone's files yet, so it can't find your projects. Android calls this <b>All files access</b>."+(window.SymbiotAndroid?" <button class='act' id='allowfiles'>Allow file access</button>":"");
|
|
688
949
|
if(!repos.length)return "No git repositories found under "+esc(base)+" yet."+(g.stats.android?" Projects in Termux's home folder (<code>~</code>) are private to Termux, so no other app can see them."+(phoneTermux().installed?" Symbiot running in Termux can, and this app can show it: <button class='act' id='usetermux'>Open Termux</button> copies the command that starts it. Paste it there, and Termux opens it here. Or keep":" Keep")+" your projects in shared storage: in Termux, run <code>termux-setup-storage</code> and work under <code>~/storage/shared</code>.":"");var langs=g.nodes.filter(function(n){return n.type==="lang";}).map(function(n){return n.label;});var top=repos.slice().sort(function(a,b){return (b.meta.commits||0)-(a.meta.commits||0);})[0];var more=(g.stats.roots||[]).filter(function(r){return r!==base;}).length;var s="Scanned <b>"+esc(base)+"</b>"+(more?" and "+more+" more folder"+(more>1?"s":""):"")+" ·<b>"+g.stats.repos+"</b> repos · <b>"+g.stats.commits+"</b> of your commits · <b>"+(g.stats.files||0)+"</b> files · ";s+=langs.length?("mostly <b>"+esc(langs.slice(0,3).join(", "))+"</b>"):"no languages detected";if(top)s+=" · most active: <b>"+esc(top.label)+"</b>";return s;}
|
|
689
950
|
function nodeById(id){for(var i=0;i<GRAPH.nodes.length;i++)if(GRAPH.nodes[i].id===id)return GRAPH.nodes[i];return null;}
|
|
@@ -695,7 +956,7 @@ function agentBadge(d){if(!d.agents||!d.agents.length)return'';return d.agents.m
|
|
|
695
956
|
function showDetail(d){var el=document.getElementById('detail');el.classList.remove('hidden');
|
|
696
957
|
if(d.error){el.innerHTML="<h3>—</h3><div class='k'>"+esc(d.error)+"</div>";return;}
|
|
697
958
|
if(d.type==='repo'){var chips="";['branch: '+(d.branch||'?'),d.commits+' commits',(d.dirty?d.dirty+' uncommitted':'clean'),(d.last?'last '+d.last:'')].concat(d.langs||[]).concat(d.tools||[]).forEach(function(x){if(x)chips+="<span class='tag'>"+esc(x)+"</span>";});
|
|
698
|
-
el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div>"+agentBadge(d)+"<button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";
|
|
959
|
+
el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div>"+lmWeeks(d)+lmNearHtml('repo:'+d.path)+agentBadge(d)+"<button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";lmNearWire(el);
|
|
699
960
|
document.getElementById('suggest').addEventListener('click',function(){var o=document.getElementById('sugout');o.innerHTML="<div class='out2'>Thinking...</div>";api('/api/suggest',{path:d.path}).then(function(r){if(r.error==='not-connected'){o.innerHTML="<div class='out2'>Connect a model in Settings to get suggestions - Ollama is free and runs locally.</div>";return;}o.innerHTML="<div class='out2'>"+esc(r.text||'(no output)')+"</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");});});return;}
|
|
700
961
|
if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){return "<li>"+esc(r)+"</li>";}).join("");el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Used in "+((d.repos||[]).length)+" repos</div><ul>"+lis+"</ul>";return;}
|
|
701
962
|
if(d.type==='agent'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+(m.kind==='agent'?'AI coding agent you have installed':'editor you have installed')+"</div>"+(m.cmd?"<div class='out2' style='font-size:11px'>"+esc(m.cmd)+"</div>":"");return;}
|
|
@@ -734,7 +995,7 @@ var rm=row.querySelector('.rm');if(rm&&rm.classList.contains('restore'))rm.addEv
|
|
|
734
995
|
else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){var nx=row.nextElementSibling;if(nx&&nx.classList.contains('tchat'))nx.remove();row.remove();});});});}
|
|
735
996
|
// Per-task Q&A: the button opens a thread under the row; answers come from the
|
|
736
997
|
// connected model, grounded in the task's repo (and its pending changes).
|
|
737
|
-
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")+"'
|
|
998
|
+
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")+"'><i class=ic-chat></i>"+(n?" "+Math.ceil(n/2):"")+"</button>";}
|
|
738
999
|
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;}
|
|
739
1000
|
function stepsHtml(st){return st&&st.length?"<details class='steps'><summary>"+st.length+(st.length>1?' steps':' step')+" · "+esc(String(st[0]).charAt(0).toLowerCase()+String(st[0]).slice(1))+"</summary><ul>"+st.map(function(x){return "<li>"+esc(x)+"</li>";}).join('')+"</ul></details>":"";}
|
|
740
1001
|
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("");}
|
|
@@ -751,9 +1012,9 @@ msgs.insertAdjacentHTML('beforeend',chatMsgs([{role:'user',text:q}])+"<div class
|
|
|
751
1012
|
api('/api/tasks/chat',{id:id,question:q}).then(function(r){send.disabled=false;var th=msgs.querySelector('.thinking');if(th)th.remove();
|
|
752
1013
|
if(r.error==='not-connected'){msgs.insertAdjacentHTML('beforeend',"<div class='msg a'>Connect a model in Settings to ask questions - Ollama is free and runs locally.</div>");return;}
|
|
753
1014
|
if(r.error){msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(r.error)+"</div>");return;}
|
|
754
|
-
t.chat=r.chat||[];msgs.innerHTML=chatMsgs(t.chat);btn.className='ask on has';btn.innerHTML="
|
|
1015
|
+
t.chat=r.chat||[];msgs.innerHTML=chatMsgs(t.chat);btn.className='ask on has';btn.innerHTML="<i class=ic-chat></i> "+Math.ceil(t.chat.length/2);}).catch(function(e){send.disabled=false;var th=msgs.querySelector('.thinking');if(th)th.remove();msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(String((e&&e.message)||e))+"</div>");});}
|
|
755
1016
|
send.addEventListener('click',ask);inp.addEventListener('keydown',function(e){if(e.key==='Enter')ask();});
|
|
756
|
-
box.querySelector('.askclear').addEventListener('click',function(){api('/api/tasks/chat/clear',{id:id}).then(function(){t.chat=[];msgs.innerHTML=CHATHINT;btn.className='ask on';btn.innerHTML='
|
|
1017
|
+
box.querySelector('.askclear').addEventListener('click',function(){api('/api/tasks/chat/clear',{id:id}).then(function(){t.chat=[];msgs.innerHTML=CHATHINT;btn.className='ask on';btn.innerHTML='<i class=ic-chat></i>';});});
|
|
757
1018
|
inp.focus();}
|
|
758
1019
|
function fchip(dim,val,label){var on=(TFILTER[dim]||'')===val;return "<button class='fchip"+(on?' on':'')+"' data-dim='"+dim+"' data-val='"+esc(val)+"'>"+esc(label)+"</button>";}
|
|
759
1020
|
function renderFilter(){var box=document.getElementById('taskfilter');
|
|
@@ -761,7 +1022,7 @@ if(TARCH){box.innerHTML="<button class='fchip on' id='archtoggle'>↩ back t
|
|
|
761
1022
|
var types={},repos={};ALLTASKS.forEach(function(t){types[t.type||'Features & other']=1;if(t.repo)repos[t.repo]=1;});
|
|
762
1023
|
var fb="<span class='fl'>Type</span>"+fchip('type','','All')+Object.keys(types).sort(function(a,b){return TORDER.indexOf(a)-TORDER.indexOf(b);}).map(function(t){return fchip('type',t,t);}).join('');
|
|
763
1024
|
fb+="<span class='fl'>Repo</span>"+fchip('repo','','All')+Object.keys(repos).sort().map(function(r){return fchip('repo',r,r);}).join('');
|
|
764
|
-
fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'
|
|
1025
|
+
fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'><i class=ic-archive></i> archived</button>";
|
|
765
1026
|
box.innerHTML=fb;
|
|
766
1027
|
box.querySelectorAll('.fchip[data-dim]').forEach(function(c){c.addEventListener('click',function(){TFILTER[c.getAttribute('data-dim')]=c.getAttribute('data-val');renderTasks();});});
|
|
767
1028
|
document.getElementById('archtoggle').addEventListener('click',loadArchived);}
|
|
@@ -845,12 +1106,12 @@ function relHtml(rl){return "<div class='qrel"+(rl.waiting?" wait":"")+"'>Needs
|
|
|
845
1106
|
(rl.waiting==='npm'?" · not published yet, so this can't be done yet":rl.waiting==='install'?" · "+(rl.name==='symbiot'?"update Symbiot first":"install it first"):"")+"</div>";}
|
|
846
1107
|
function askHtml(a){var k=a.ask;if(!k)return '';var qs=k.questions||[],ss=k.suggestions||[];if(!qs.length&&!ss.length)return '';
|
|
847
1108
|
var h="<div class='aq' data-id='"+esc(a.id)+"'>";
|
|
848
|
-
if(qs.length){h+="<h4
|
|
1109
|
+
if(qs.length){h+="<h4><i class=ic-ask></i> "+qs.length+" question"+(qs.length>1?"s":"")+" for you</h4>";
|
|
849
1110
|
qs.forEach(function(q,i){var rl=q.release;h+="<div class='q' data-i='"+i+"'><div class='qt'>"+esc(q.q)+"</div>"+(q.context?"<div class='qc'>"+esc(q.context)+"</div>":"")+(rl?relHtml(rl):"");
|
|
850
1111
|
(q.options||[]).forEach(function(o,j){var off=rl&&rl.waiting&&DONEOPT.test(o);h+="<label class='opt"+(off?" off":"")+"'><input type='radio' name='q_"+esc(a.id)+"_"+i+"' value='"+j+"'"+(off?" disabled":"")+"><span>"+whoHtml(o)+(off?" <i>(once "+esc(rl.name+" "+rl.needs)+" is installed)</i>":"")+"</span></label>";});
|
|
851
1112
|
h+="<input class='qother' placeholder='"+((q.options&&q.options.length)?"or answer in your own words":"your answer")+"'></div>";});
|
|
852
1113
|
h+="<div class='row'><button class='act qsend' title='save the answers and hand the repo back to your agent'>Send answers & continue</button><button class='ghost qsave' title='save the answers for the next run'>Save only</button></div>";}
|
|
853
|
-
if(ss.length){var shown=IDEASOPEN[a.path]?ss.length:(k.ideasShown||ss.length);h+="<h4
|
|
1114
|
+
if(ss.length){var shown=IDEASOPEN[a.path]?ss.length:(k.ideasShown||ss.length);h+="<h4><i class=ic-idea></i> Ideas from the agent</h4>";ss.forEach(function(s,i){if(i>=shown)return;h+="<div class='idea'><span style='flex:1'>"+esc(s.text)+(s.other?" <span class='tag' title='this idea is for another project, so + task adds it to that one'>for "+esc(s.repo)+"</span>":"")+"</span>"+(s.added?"<span class='tag'>in Tasks</span>":"<button class='ghost qidea' data-i='"+i+"' style='padding:3px 9px;font-size:12px'>+ task</button><button class='ghost qskip' data-i='"+i+"' title='turn this idea down: the next one moves up, and the agent is told not to suggest it again' style='padding:3px 9px;font-size:12px'>Skip</button>")+"</div>";});
|
|
854
1115
|
if(ss.length>shown)h+="<div class='row'><button class='ghost qmore' title='the agent ranks its ideas best first: weigh these two before adding more'>"+(ss.length-shown)+" more idea"+(ss.length-shown>1?"s":"")+", ranked lower</button></div>";}
|
|
855
1116
|
return h+"</div>";}
|
|
856
1117
|
var IDEASOPEN={};
|
|
@@ -865,7 +1126,7 @@ if(!ans.length){msg.innerHTML="<div class='note err'>Pick an option or type an a
|
|
|
865
1126
|
box.querySelectorAll('button').forEach(function(b){b.disabled=true;});
|
|
866
1127
|
api('/api/agents/answer',{path:a.path,answers:ans,rerun:rerun}).then(function(r){
|
|
867
1128
|
if(!r||r.error){box.querySelectorAll('button').forEach(function(b){b.disabled=false;});msg.innerHTML="<div class='note err'>"+esc((r&&r.error)||'failed')+"</div>";return;}
|
|
868
|
-
msg.innerHTML="<div class='note ok'>✓ Saved "+r.saved+" answer"+(r.saved>1?"s":"")+" for <b>"+esc(a.name)+"</b> in .symbiot/ANSWERS.md"+(r.rerun?" · your agent is picking them up now.":".")+(r.yours&&r.yours.length?"<div class='err'
|
|
1129
|
+
msg.innerHTML="<div class='note ok'>✓ Saved "+r.saved+" answer"+(r.saved>1?"s":"")+" for <b>"+esc(a.name)+"</b> in .symbiot/ANSWERS.md"+(r.rerun?" · your agent is picking them up now.":".")+(r.yours&&r.yours.length?"<div class='err'><i class=ic-person></i> Still yours to do: "+esc(r.yours.join(' '))+"</div>":"")+(r.note?"<div class='muted'>"+esc(r.note)+"</div>":"")+"</div>";loadAgents();});}
|
|
869
1130
|
var s1=box.querySelector('.qsend'),s2=box.querySelector('.qsave');
|
|
870
1131
|
if(s1)s1.addEventListener('click',function(){send(true);});if(s2)s2.addEventListener('click',function(){send(false);});
|
|
871
1132
|
var more=box.querySelector('.qmore');if(more)more.addEventListener('click',function(){IDEASOPEN[a.path]=true;loadAgents();});
|
|
@@ -876,7 +1137,7 @@ box.querySelectorAll('.qskip').forEach(function(btn){btn.addEventListener('click
|
|
|
876
1137
|
api('/api/agents/skip',{path:a.path,text:s.text}).then(function(r){if(!r||r.error){btn.disabled=false;document.getElementById('agentsmsg').innerHTML="<div class='note err'>"+esc((r&&r.error)||'failed')+"</div>";return;}loadAgents();});});});});}
|
|
877
1138
|
// A step of yours an answer picked (👤 You): the next run waits for it (agents.mjs waitingFor)
|
|
878
1139
|
function waitHtml(a){var w=a.waiting;if(!w)return '';var fs=(w.files||[]).map(function(f){return "<code>"+esc(f)+"</code>";}).join(" or ");if(w.cmd)fs=(fs?fs+" or ":"")+"the agent command in Settings → Handoff";
|
|
879
|
-
return "<div class='aq'><h4
|
|
1140
|
+
return "<div class='aq'><h4><i class=ic-pause></i> Waiting on your step</h4><div class='qt'><i class=ic-person></i> "+esc(w.step)+"</div><div class='qc'>"+(fs?"Your agent "+(w.rerun?"starts by itself":"can start")+" once "+fs+" changes"+(w.rerun?", while Symbiot runs":"")+". Done it some other way?":"Your agent waits for it, so it doesn't stop on the same questions again.")+" Start it now once it's done.</div><div class='row'><button class='act wstart' data-id='"+esc(a.id)+"'>Start it now</button></div></div>";}
|
|
880
1141
|
function wireWaits(el){el.querySelectorAll('.wstart').forEach(function(btn){btn.addEventListener('click',function(){var a=agentById(btn.getAttribute('data-id'));if(!a)return;btn.disabled=true;
|
|
881
1142
|
api('/api/open',{path:a.path,force:true}).then(function(x){var msg=document.getElementById('agentsmsg');msg.innerHTML=x&&x.opened?"<div class='note ok'>✓ Started your agent in <b>"+esc(a.name)+"</b>.</div>":"<div class='note err'>"+esc(a.name)+": "+(x&&x.busy?"an agent is already running there.":"it didn't start. Check the command in Settings.")+"</div>";loadAgents();}).catch(function(e){btn.disabled=false;document.getElementById('agentsmsg').innerHTML="<div class='note err'>"+esc(String((e&&e.message)||e))+"</div>";});});});}
|
|
882
1143
|
function answering(){var f=document.activeElement;return !!(f&&f.closest&&f.closest('.aq'));}
|
|
@@ -925,8 +1186,8 @@ if(a.waiting)st+=" · <span style='color:var(--amber)'>waiting on your ste
|
|
|
925
1186
|
if(nQs(a))st+=" · <span style='color:var(--amber)'>needs your answers</span>";
|
|
926
1187
|
if(a.held!=null){var hn=a.held.length,ht=a.held.map(function(t){return "• "+esc(t).replace(/'/g,''');}).join(' ');
|
|
927
1188
|
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.");
|
|
928
|
-
st+=" · <span style='color:var(--amber);cursor:help' title='"+ht+"'
|
|
929
|
-
var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
|
|
1189
|
+
st+=" · <span style='color:var(--amber);cursor:help' title='"+ht+"'><i class=ic-pause></i> "+(hn?hn+" task"+(hn>1?"s":"")+" held":"tasks held")+"</span>";}
|
|
1190
|
+
var b="<div class='dh'><span class='orb "+cls+"'></span><span class='dn'>"+esc(String(a.name||'').split('**').join(''))+"</span><span class='dd'>"+st+"</span></div>";
|
|
930
1191
|
b+=workHtml(a);
|
|
931
1192
|
var ch=a.changed;
|
|
932
1193
|
if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
@@ -949,8 +1210,8 @@ var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getEle
|
|
|
949
1210
|
api('/api/drift?ci='+ci+'&fetch='+ft).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
|
|
950
1211
|
var h="<div class='k' style='margin:10px 0'><b>"+repos.length+"</b> repos · <b>"+risky.length+"</b> with risks"+(d.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"")+"</div>";
|
|
951
1212
|
repos.forEach(function(r){if(!r.flags.length)return;var warn=r.flags.some(function(f){return f.level==='warn';});
|
|
952
|
-
h+="<div class='drift'><div class='dh'><span class='
|
|
953
|
-
r.flags.forEach(function(f){h+="<li class='"+esc(f.level)+"'>"+
|
|
1213
|
+
h+="<div class='drift"+(warn?' risk':'')+"'><div class='dh'><span class='dbead' aria-hidden='true'></span><span class='dn'>"+esc(r.name)+"</span><span class='dd'>"+esc(r.def)+(r.fetchAgeDays!=null&&r.fetchAgeDays>3?" · fetch "+r.fetchAgeDays+"d old":"")+"</span></div><ul>";
|
|
1214
|
+
r.flags.forEach(function(f){h+="<li class='"+esc(f.level)+"'>"+esc(f.text)+(f.evidence?" <span class='ev'>["+esc(f.evidence)+"]</span>":"")+"</li>";});
|
|
954
1215
|
h+="</ul></div>";});
|
|
955
1216
|
var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){return r.name;});
|
|
956
1217
|
if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
|
|
@@ -963,9 +1224,9 @@ o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' styl
|
|
|
963
1224
|
api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){
|
|
964
1225
|
if(r.empty){btn.disabled=false;o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tick ideas in a repo review, or add tasks above.</div>";return;}
|
|
965
1226
|
var n=(r.written||[]).length;var opens=[];var h="<div class='drift' style='margin-top:10px'><div class='dh'><span class='dot-c'></span><span class='dn'>Done — wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
|
|
966
|
-
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span>"+(w.held?" <span class='ev'
|
|
967
|
-
if(r.unresolved&&r.unresolved.length){var names=r.unresolved.map(function(u){return u.name;}).join(", ");var hasNoRepo=r.unresolved.some(function(u){return u.name==='(no repo)';});h+="<div class='dd' style='margin-top:6px'
|
|
968
|
-
if(r.handoff&&r.written&&r.written.length){opens=r.written.map(function(w){return api('/api/open',{path:w.path}).then(function(x){if(x&&x.blocked){var d=document.createElement('div');d.className='dd';d.innerHTML="
|
|
1227
|
+
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span>"+(w.held?" <span class='ev'><i class=ic-pause></i> held: an agent is still running there, so these land when it finishes</span>":"")+"</li>";});h+="</ul>";}
|
|
1228
|
+
if(r.unresolved&&r.unresolved.length){var names=r.unresolved.map(function(u){return u.name;}).join(", ");var hasNoRepo=r.unresolved.some(function(u){return u.name==='(no repo)';});h+="<div class='dd' style='margin-top:6px'><i class=ic-warn></i> not sent: "+esc(names)+". "+(hasNoRepo?"Pick a repo in the dropdown next to <b>Add</b> so the task has somewhere to go.":"That repo isn't in the map — add its folder in Settings.")+"</div>";}
|
|
1229
|
+
if(r.handoff&&r.written&&r.written.length){opens=r.written.map(function(w){return api('/api/open',{path:w.path}).then(function(x){if(x&&x.blocked){var d=document.createElement('div');d.className='dd';d.innerHTML="<i class=ic-pause></i> <b>"+esc(w.name)+"</b>: "+esc(x.note)+" <button class='ghost' style='padding:2px 8px;font-size:12px'>Start it anyway</button>";d.querySelector('button').addEventListener('click',function(){this.disabled=true;api('/api/open',{path:w.path,force:true}).then(function(y){d.innerHTML=y&&y.opened?"✓ Started an agent in <b>"+esc(w.name)+"</b>.":"<i class=ic-warn></i> <b>"+esc(w.name)+"</b>: "+(y&&y.busy?"an agent is already running there.":"it didn't start. Check the command in Settings.");});});o.appendChild(d);}if(x&&x.busy){var d=document.createElement('div');d.className='dd';d.innerHTML="<i class=ic-warn></i> <b>"+esc(w.name)+"</b> already has an agent running, so another wasn't started. "+(x.auto?"Its new tasks are held, and an agent starts on them when it finishes.":"Its new tasks are held until it finishes. After that, an agent starts on them the next time this tab checks the repo, or send again.");o.appendChild(d);}}).catch(function(){});});h+="<div class='dd' style='margin-top:8px'><i class=ic-bot></i> Handed "+n+" repo(s) to your agent — opening the <b>Agents</b> tab to watch it work…</div>";setTimeout(function(){setTab('agents');},500);}
|
|
969
1230
|
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send (and watch it in the Agents tab). For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
970
1231
|
h+="</div>";
|
|
971
1232
|
o.innerHTML=h;
|
|
@@ -978,9 +1239,9 @@ function linkGap(){var off=LINKCONN.filter(function(x){return !x.ready;});if(!of
|
|
|
978
1239
|
return "✉️ "+n+(one?" is":" are")+" linked in Symbiot, but not "+(off.some(function(x){return x.connector;})?"ready ":"")+"as a Claude connector, so your agent's runs have no tools for "+(one?"it":"them")+": linking a site here signs Symbiot's browser in, not Claude. Connect "+n+" in claude.ai → Settings → Connectors, and the next run gets "+(one?"it":"them")+" by itself. Until then a run sees only what's new there (Watch).";}
|
|
979
1240
|
function renderConnectors(){var el=document.getElementById('agentconnectors'),gap=linkGap();if(!CONNECTORS.length){el.innerHTML=/^\\s*claude\\b/.test(document.getElementById('agentcmd').value)?gap:'';return;}
|
|
980
1241
|
var nm=function(c){return "<b>"+esc(c.name.replace(/^claude\\.ai\\s+/i,''))+"</b>";},ready=CONNECTORS.filter(function(c){return c.ready;}),wait=CONNECTORS.filter(function(c){return !c.ready;});
|
|
981
|
-
var cmd=document.getElementById('agentcmd').value;if(/orca-ide/.test(cmd)&&/claude/.test(cmd)){el.innerHTML="
|
|
982
|
-
if(!/^\\s*claude\\b/.test(cmd)){el.innerHTML="
|
|
983
|
-
el.innerHTML=(ready.length?"
|
|
1242
|
+
var cmd=document.getElementById('agentcmd').value;if(/orca-ide/.test(cmd)&&/claude/.test(cmd)){el.innerHTML="<i class=ic-plug></i> Claude in Orca's tab asks you before it uses your connectors ("+CONNECTORS.map(nm).join(", ")+").";return;}
|
|
1243
|
+
if(!/^\\s*claude\\b/.test(cmd)){el.innerHTML="<i class=ic-plug></i> Your Claude connectors ("+CONNECTORS.map(nm).join(", ")+") reach Claude Code runs only. This command isn't Claude, so its runs can't use them.";return;}
|
|
1244
|
+
el.innerHTML=(ready.length?"<i class=ic-plug></i> Your agent's runs can use "+ready.map(nm).join(", ")+": linked to Claude, so each run allows their tools.":"")+(wait.length?(ready.length?" ":"<i class=ic-plug></i> ")+wait.map(nm).join(", ")+(wait.length===1?" needs":" need")+" authorizing in your claude.ai connector settings first.":"")+(gap?" "+gap:"");}
|
|
984
1245
|
function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
|
|
985
1246
|
CONNECTORS=(d.connectors&&d.connectors.list)||[];LINKCONN=(d.connectors&&d.connectors.links)||[];renderConnectors();
|
|
986
1247
|
var chips=[];(d.agents||[]).forEach(function(a){chips.push(a);});(d.editors||[]).forEach(function(e){chips.push(e);});
|
|
@@ -1110,7 +1371,7 @@ function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+
|
|
|
1110
1371
|
function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
|
|
1111
1372
|
function poll(){if(done)return;api('/api/scan').then(function(s){if(done)return;if(s&&s.active&&s.phase)p.textContent=scanLine(s);setTimeout(poll,600);}).catch(function(){});}
|
|
1112
1373
|
setTimeout(poll,400);
|
|
1113
|
-
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;fillTaskRepos();layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");var af=$('allowfiles');if(af)af.addEventListener('click',function(){SymbiotAndroid.storage();});var ut=$('usetermux');if(ut)ut.addEventListener('click',function(){SymbiotAndroid.openTermux();});render();});}
|
|
1374
|
+
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;fillTaskRepos();layout(g.nodes,g.edges);lmBuild(g);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");var af=$('allowfiles');if(af)af.addEventListener('click',function(){SymbiotAndroid.storage();});var ut=$('usetermux');if(ut)ut.addEventListener('click',function(){SymbiotAndroid.openTermux();});render();});}
|
|
1114
1375
|
// The Android app calls this when you come back having allowed file access, so
|
|
1115
1376
|
// the map rescans instead of staying empty.
|
|
1116
1377
|
window.symbiotStorageGranted=function(){if(current==='map')loadMap();else mapLoaded=false;};
|
|
@@ -1284,7 +1545,7 @@ var tab=$('boardtab');if(tab)tab.textContent='Dashboard'+(b.total?' · '+b.total
|
|
|
1284
1545
|
var bst=$('bstream');if(bst&&bst.classList)bst.classList.toggle('none',!cs.length);var bl=$('blist');if(bl&&!cs.length)bl.open=true;
|
|
1285
1546
|
if(!cs.length){$('boardsum').textContent='Everything you watch, side by side.';
|
|
1286
1547
|
el.innerHTML="<div style='grid-column:1/-1'><div class='note muted' style='margin-top:0'>Nothing watched yet. Map your inbox, a chat (web.whatsapp.com) or any page under <b>Screens</b> on the Map tab and click <b>Watch</b> on it, or click <b>Watch GitHub</b> there. Each one gets a card here with what's new on it.</div><div class='row' style='margin-top:8px'><button class='ghost' id='boardgo'>Go to Screens</button></div></div>";
|
|
1287
|
-
$('boardgo').addEventListener('click',function(){setTab('map');var w=$('watchbox');if(w&&w.scrollIntoView)w.scrollIntoView({block:'center'});});return;}
|
|
1548
|
+
$('boardgo').addEventListener('click',function(){setTab('map');var sbx=$('screensbox');if(sbx)sbx.open=true;var w=$('watchbox');if(w&&w.scrollIntoView)w.scrollIntoView({block:'center'});});return;}
|
|
1288
1549
|
var when=b.hours===24?'since yesterday':b.hours===72?'in the last 3 days':'in the last 7 days';
|
|
1289
1550
|
var lastRead=0;cs.forEach(function(c){if(c.checked>lastRead)lastRead=c.checked;});var sub=$('boardsub');if(sub)sub.textContent=(lastRead?'Read '+agoTxt(lastRead)+' \u00b7 ':'')+cs.length+(cs.length===1?' feed':' feeds');
|
|
1290
1551
|
var waiting=cs.filter(function(c){return c.count;}).map(function(c){return c.label+(c.source==='page'?' on '+c.name:'');});
|
|
@@ -1311,9 +1572,9 @@ function done(){BTALKBUSY=Math.max(0,BTALKBUSY-1);send.disabled=false;var th=msg
|
|
|
1311
1572
|
api('/api/watch/chat',{id:c.id,question:q}).then(function(r){done();
|
|
1312
1573
|
if(r.error==='not-connected'){msgs.insertAdjacentHTML('beforeend',"<div class='msg a'>Connect a model in Settings to talk it over - Ollama is free and runs locally.</div>");return;}
|
|
1313
1574
|
if(r.error){msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(r.error)+"</div>");return;}
|
|
1314
|
-
c.chat=r.chat||[];msgs.innerHTML=chatMsgs(c.chat);var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='
|
|
1575
|
+
c.chat=r.chat||[];msgs.innerHTML=chatMsgs(c.chat);var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='<i class=ic-chat></i> '+Math.ceil(c.chat.length/2);}).catch(function(e){done();msgs.insertAdjacentHTML('beforeend',"<div class='msg a err'>"+esc(String((e&&e.message)||e))+"</div>");});}
|
|
1315
1576
|
send.addEventListener('click',ask);inp.addEventListener('keydown',function(e){if(e.key==='Enter')ask();});
|
|
1316
|
-
box.querySelector('.talkclear').addEventListener('click',function(){api('/api/watch/chat/clear',{id:c.id}).then(function(){c.chat=[];msgs.innerHTML=TALKHINT;var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='
|
|
1577
|
+
box.querySelector('.talkclear').addEventListener('click',function(){api('/api/watch/chat/clear',{id:c.id}).then(function(){c.chat=[];msgs.innerHTML=TALKHINT;var tb=box.closest('.bcard').querySelector('.btalkbtn');if(tb)tb.innerHTML='<i class=ic-chat></i>';});});});
|
|
1317
1578
|
el.querySelectorAll('.bopenwa').forEach(function(btn){btn.addEventListener('click',function(){openChatUI(item(btn),btn,boardMsg);});});
|
|
1318
1579
|
function item(btn){return card(btn).items[+btn.closest('.bi').getAttribute('data-j')];}
|
|
1319
1580
|
el.querySelectorAll('.bcheck').forEach(function(btn){btn.addEventListener('click',function(){var c=card(btn);btn.disabled=true;btn.textContent='Reading…';
|
|
@@ -1387,8 +1648,8 @@ function saveWeekly(){api('/api/desktop/weekly',{on:$('weeklyon').checked,day:$(
|
|
|
1387
1648
|
function saveAutostart(){api('/api/desktop/autostart',{on:$('autostart').checked}).then(function(a){loadDesktop(a&&a.error);});}
|
|
1388
1649
|
function weeklyNow(){var b=$('weeklynow');b.disabled=true;b.textContent='Writing...';
|
|
1389
1650
|
api('/api/desktop/weekly/run',{}).then(function(r){b.disabled=false;b.textContent='Write it now';loadDesktop(r&&r.error?(r.error==='not-connected'?'Connect an AI above first.':r.error):'');});}
|
|
1390
|
-
function showLatestWeek(){api('/api/desktop').then(function(d){var l=d&&d.weekly&&d.weekly.latest;if(!l||current!=='week'||$('out').textContent!==
|
|
1391
|
-
|
|
1651
|
+
function showLatestWeek(){api('/api/desktop').then(function(d){var l=d&&d.weekly&&d.weekly.latest;if(!l||current!=='week'||$('out').textContent!==WUEMPTY)return;
|
|
1652
|
+
WUTEXT=l.text;$('out').innerHTML=mdLite(l.text);$('out').classList.remove('muted');$('copy').classList.remove('hidden');var f=$('outfoot');f.textContent='Written automatically '+new Date(l.at).toLocaleString()+' · '+l.file+(l.footer?' · '+l.footer:'');f.style.display='block';});}
|
|
1392
1653
|
function fitBadge(m){return m.fits?"<span class='tag'>fits your RAM</span>":"<span class='tag' style='background:#3a2a12;border-color:#6b4a1f;color:#F2A541'>needs more RAM</span>";}
|
|
1393
1654
|
function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
|
|
1394
1655
|
api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
|
|
@@ -1630,7 +1891,7 @@ 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
|
|
|
1630
1891
|
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;}}
|
|
1631
1892
|
if(!bad)return true;L.forEach(function(x){x.r*=0.9;});coreR*=0.9;}return false;}
|
|
1632
1893
|
function lqLabels(){var el=$('lqdrops');if(!el)return;
|
|
1633
|
-
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>"+(d.kind==='proj'&&d.sub?"<span class='lm'>"+esc(d.sub)+"</span>":"")+"</button>"+(d.sub?"<small>"+esc(d.sub)+"</small>":"")+"<button type='button' class='lgo'>Open ›</button></div>";}).join('');
|
|
1894
|
+
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(String(d.title||'').split('**').join(''))+"</span>"+(d.kind==='proj'&&d.sub?"<span class='lm'>"+esc(d.sub)+"</span>":"")+"</button>"+(d.sub?"<small>"+esc(d.sub)+"</small>":"")+"<button type='button' class='lgo'>Open ›</button></div>";}).join('');
|
|
1634
1895
|
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);
|
|
1635
1896
|
if(lt)lt.addEventListener('click',function(ev){lqOpen(d,ev);});
|
|
1636
1897
|
if(go)go.addEventListener('click',function(ev){LQ.openTag=null;lqOpen(d,ev);});});
|
|
@@ -1661,11 +1922,13 @@ function lqRemember(){if(LQ.back)return;LQ.hist=LQ.hist||[];var s=lqState(),top=
|
|
|
1661
1922
|
function lqBack(){var h=LQ.hist||[],s=h.pop(),cur=lqState();if(!s){if(cur.pool)s={scene:cur.scene,pool:null};else if(cur.scene==='work')s={scene:'home',pool:null};else return;}
|
|
1662
1923
|
LQ.back=true;try{if(s.scene==='work'&&(cur.scene!=='work'||cur.by!==s.by)){LQ.workBy=s.by||'task';lqWork();}else if(s.scene==='home'&&cur.scene==='work')lqHome();
|
|
1663
1924
|
if(s.pool){if(s.arch)lqArchive();else lqPool(s.pool);}else if(LQ.mode==='pool')lqSink();}finally{LQ.back=false;}}
|
|
1664
|
-
function lqPool(shape){lqRemember();LQ.poolShape=shape;LQ.arch=false;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();}
|
|
1925
|
+
function lqPool(shape){if(shape==='map'&&LM.items&&LM.items.length)setTimeout(function(){lmFit();lmStart();},60);lqRemember();LQ.poolShape=shape;LQ.arch=false;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();}
|
|
1665
1926
|
function lqSink(){lqRemember();var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
|
|
1666
1927
|
function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
|
|
1667
1928
|
function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
|
|
1668
|
-
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;
|
|
1929
|
+
function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;var n=LQ.talk.length;el.innerHTML=LQ.talk.map(function(m,i){var age=n-1-i,op=age<2?1:age===2?0.7:age===3?0.45:0.25;return "<div class='lqmsg"+(m.me?' me':'')+"' style='opacity:"+op+"'>"+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;
|
|
1930
|
+
// scrolled up to read back: the whole history shows, clear; back at the bottom it rolls on again
|
|
1931
|
+
if(!el.wired&&el.addEventListener){el.wired=true;el.addEventListener('scroll',function(){var up=el.scrollHeight-el.scrollTop-el.clientHeight>24;if(el.classList)el.classList.toggle('back',up);});}}
|
|
1669
1932
|
function lqSay(){var i=$('lqask');var q=((i&&i.value)||'').trim();if(!q)return;i.value='';lqAct();
|
|
1670
1933
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
1671
1934
|
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;});
|