symbiot 0.55.3 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +13 -0
- package/away.mjs +12 -2
- package/desktop.mjs +68 -1
- package/home.mjs +43 -5
- package/icon.svg +33 -0
- package/index.mjs +46 -4
- package/package.json +8 -4
- package/postinstall.mjs +19 -0
- package/scan.mjs +50 -5
- package/scanworker.mjs +12 -0
- package/server.mjs +27 -6
- package/ui.mjs +117 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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.57.0
|
|
6
|
+
|
|
7
|
+
- **Setup, before anything else.** A new Symbiot opens straight into setup: what it is (it watches, it works, it asks), your AI (your Claude subscription, found by itself), your work (your projects, found while you read), your coding agent (one click), every app you use (each connected, or marked "I don't use it"; you can't go on until they're all decided), and an optional folder of your documents. Then a short "finding your way" and Home. Where you are is kept, so closing it resumes there; Settings → Setup → Run setup again walks it again.
|
|
8
|
+
- **Installing opens it.** `npm install -g symbiot` on a desktop opens Symbiot right away, into setup, and says so in the terminal (npm used to hide that line).
|
|
9
|
+
- **No more blank orb on the first run.** Symbiot searched your folders for projects again on every Home refresh, and built the Map at startup even when you didn't open it: on a big home folder that froze it for a minute. Your projects are now found once, in the background, kept between runs and refreshed every ten minutes or when your folders change; the Map is built when you open it. Anything slow is noted in Symbiot's log.
|
|
10
|
+
- **The icon follows your look.** Ferrofluid, Glass or Pearl: the app menu's icon and the window's change with it.
|
|
11
|
+
- A new install's Home says "Nothing needs you right now" instead of "All handled".
|
|
12
|
+
|
|
13
|
+
## 0.56.0
|
|
14
|
+
|
|
15
|
+
- **Symbiot in your app menu, no terminal.** On Linux, `npm install -g symbiot` puts Symbiot in your app menu with its own icon, the orb. Click it and Symbiot starts in the background and opens its window; click it again and the window comes up. You never need a terminal to run it. Its window shows the orb in your taskbar too, and the menu entry keeps itself pointing at the installed Symbiot (a new Node, a moved install).
|
|
16
|
+
- **`symbiot uninstall`** removes Symbiot and everything it added: the menu entry and icon, start at login, the Away shortcut, and its data (`--keep-data` keeps your settings, tasks and memory). Claude Code and its sign-in stay.
|
|
17
|
+
- `symbiot open` does what the icon does, from a terminal.
|
|
18
|
+
|
|
5
19
|
## 0.55.3
|
|
6
20
|
|
|
7
21
|
- **Your Claude subscription, no API key.** With Claude Code signed in to your Claude account (Pro or Max), Symbiot now answers through it: chats, write-ups, posts, screenshots. Nothing to connect, no separate bill. It comes first when you haven't picked an AI, ahead of an old saved key. `symbiot login` and Settings → Your AI offer it first; if Claude Code isn't signed in yet, they tell you to run `claude` once and sign in. Before, a fresh install sent you to `symbiot login`, which only asked for an API key.
|
package/README.md
CHANGED
|
@@ -43,6 +43,19 @@ npm install -g symbiot # or run without installing: npx symbiot week
|
|
|
43
43
|
With Claude Code signed in to your Claude account, that's all: Symbiot uses your
|
|
44
44
|
subscription, no API key. See [Connect it (once)](#connect-it-once) for the other options.
|
|
45
45
|
|
|
46
|
+
On Linux, installing puts **Symbiot in your app menu**, with its icon, and opens it
|
|
47
|
+
straight into **setup**: what Symbiot is, your AI (your Claude subscription, found by
|
|
48
|
+
itself), where your work is (it finds your projects meanwhile), your coding agent,
|
|
49
|
+
every app you use (connected, or marked as one you don't use) and, if you like, a
|
|
50
|
+
folder of your documents. Home opens once it's done; Settings → Setup runs it again.
|
|
51
|
+
|
|
52
|
+
After that, open Symbiot from your app menu: it starts in the background (no
|
|
53
|
+
terminal) and opens its window, or brings the window up if it's already running.
|
|
54
|
+
`symbiot open` does the same from a terminal. The icon follows the look you pick.
|
|
55
|
+
|
|
56
|
+
To remove it and everything it added (menu entry, icon, start at login, shortcut,
|
|
57
|
+
its data): `symbiot uninstall` (`--keep-data` keeps your settings, tasks and memory).
|
|
58
|
+
|
|
46
59
|
## Prefer a window? `symbiot app`
|
|
47
60
|
|
|
48
61
|
```bash
|
package/away.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Any key or click, or Super+` again, closes every window. Not a lock: Super+Esc
|
|
15
15
|
// locks the computer.
|
|
16
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { CONFIG_DIR, chromeBinary } from "./core.mjs";
|
|
@@ -110,4 +110,14 @@ function installShortcut({ cmd, home = homedir(), desktop = process.env.XDG_CURR
|
|
|
110
110
|
return { ok: true, file: f, replaced: /key:\s*"grave"/.test(had) };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// Uninstalling: our Super+` line comes out of COSMIC's custom shortcuts; the file
|
|
114
|
+
// goes if nothing else was in it. True when there was one.
|
|
115
|
+
function removeShortcut(home = homedir()) {
|
|
116
|
+
const f = COSMIC_CUSTOM(home); let had = ""; try { had = readFileSync(f, "utf8"); } catch { return false; }
|
|
117
|
+
const lines = had.split("\n"), kept = lines.filter((l) => !/Spawn\("[^"]*symbiot[^"]* away"\)/.test(l));
|
|
118
|
+
if (kept.length === lines.length) return false;
|
|
119
|
+
const rest = kept.join("\n");
|
|
120
|
+
try { if (!rest.replace(/[{}\s]/g, "")) unlinkSync(f); else writeFileSync(f, rest); } catch {}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
export { removeShortcut, placeable, grantProfile, parseMonitors, screens, awayQueries, openAway, closeAway, awayOpen, toggleAway, withShortcut, installShortcut, COSMIC_CUSTOM };
|
package/desktop.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// start-at-login launches that server in the background (no window) when you
|
|
5
5
|
// log in, so the write-up still happens on days you never open Symbiot.
|
|
6
6
|
// `symbiot app` then finds that running copy and opens its window.
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { join, dirname } from "node:path";
|
|
@@ -144,6 +145,72 @@ function autostartContent(node, script, platform = OS, path = process.env.PATH |
|
|
|
144
145
|
return ["[Desktop Entry]", "Type=Application", "Name=Symbiot", "Comment=Symbiot in the background: the weekly write-up and its notification",
|
|
145
146
|
`Exec=env SYMBIOT_NO_OPEN=1 ${q("PATH=" + path)} ${q(node)} ${q(script)} app`, "Terminal=false", "NoDisplay=true", "X-GNOME-Autostart-enabled=true", ""].join("\n");
|
|
146
147
|
}
|
|
148
|
+
// ---- the app launcher ---------------------------------------------------------
|
|
149
|
+
// Symbiot in your app menu, with its icon: click it and Symbiot starts in the
|
|
150
|
+
// background (no terminal) and opens its window, or brings the window up if it's
|
|
151
|
+
// running (`symbiot open`). Linux (XDG): an entry in ~/.local/share/applications and
|
|
152
|
+
// the icon in your icon theme. `npm install -g` writes it (postinstall.mjs), and each
|
|
153
|
+
// start of an installed Symbiot writes it again if it's missing or out of date (a
|
|
154
|
+
// new Node, a moved install), so it never points at something gone.
|
|
155
|
+
const launcherFile = (home = homedir()) => join(home, ".local", "share", "applications", "symbiot.desktop");
|
|
156
|
+
const launcherIcon = (home = homedir()) => join(home, ".local", "share", "icons", "hicolor", "scalable", "apps", "symbiot.svg");
|
|
157
|
+
const ICON_SRC = fileURLToPath(new URL("./icon.svg", import.meta.url));
|
|
158
|
+
// The orb in each look: Ferrofluid (icon.svg, dark), Glass (clear, on a cool light
|
|
159
|
+
// ground) and Pearl (white pearl, on a warm light ground). The app menu's icon and
|
|
160
|
+
// the window's follow the look you pick (setLauncherLook).
|
|
161
|
+
const LOOK_COLOURS = {
|
|
162
|
+
glass: { bg: ["#F4F7FB", "#D9E1EC"], orb: ["#FFFFFF", "#C9D6E6", "#7F93AE"], rim: "#5B8DEF", glow: "#5B8DEF" },
|
|
163
|
+
pearl: { bg: ["#F7F4EE", "#E6E0D5"], orb: ["#FFFFFF", "#ECE7DF", "#A99F92"], rim: "#C9A46A", glow: "#F2A541" },
|
|
164
|
+
};
|
|
165
|
+
function iconSvg(look = "ferro") {
|
|
166
|
+
const base = readFileSync(ICON_SRC, "utf8"), c = LOOK_COLOURS[look];
|
|
167
|
+
if (!c) return base;
|
|
168
|
+
return base.replace('stop-color="#1B1D22"', `stop-color="${c.bg[0]}"`).replace('stop-color="#08090B"', `stop-color="${c.bg[1]}"`)
|
|
169
|
+
.replace('stop-color="#3A3D44"', `stop-color="${c.orb[0]}"`).replace('stop-color="#121317"', `stop-color="${c.orb[1]}"`).replace('stop-color="#030304"', `stop-color="${c.orb[2]}"`)
|
|
170
|
+
.replace('stop-color="#B0466E" stop-opacity="0.55"', `stop-color="${c.rim}" stop-opacity="0.45"`)
|
|
171
|
+
.replace(/stop-color="#F2A541" stop-opacity="0\.16"/, `stop-color="${c.glow}" stop-opacity="0.18"`);
|
|
172
|
+
}
|
|
173
|
+
// The look changed: the app menu's icon follows, if the app menu has Symbiot.
|
|
174
|
+
function setLauncherLook(look, home = homedir()) {
|
|
175
|
+
if (!existsSync(launcherFile(home))) return false;
|
|
176
|
+
try { writeFileSync(launcherIcon(home), iconSvg(look), { mode: 0o644 }); return true; } catch { return false; }
|
|
177
|
+
}
|
|
178
|
+
function launcherContent(node, script, path = process.env.PATH || "") {
|
|
179
|
+
const q = (s) => '"' + String(s).replace(/(["`$\\])/g, "\\$1") + '"';
|
|
180
|
+
return ["[Desktop Entry]", "Type=Application", "Name=Symbiot", "GenericName=Assistant",
|
|
181
|
+
"Comment=Your work, your agents and what needs you, in one place",
|
|
182
|
+
`Exec=env ${q("PATH=" + path)} ${q(node)} ${q(script)} open`, "Icon=symbiot", "Terminal=false",
|
|
183
|
+
"Categories=Office;Utility;Development;", "Keywords=assistant;agents;tasks;week;standup;", "StartupNotify=true",
|
|
184
|
+
// the app's window is Chrome's --app window on Symbiot's address: this groups it under this icon
|
|
185
|
+
"StartupWMClass=chrome-127.0.0.1__-Default", ""].join("\n");
|
|
186
|
+
}
|
|
187
|
+
// The PATH the launcher starts it with: yours, without the throwaway folders npm adds
|
|
188
|
+
// while it installs (…/node_modules/.bin), each once, and Node's own folder in it.
|
|
189
|
+
function launcherPath(path, node) {
|
|
190
|
+
const seen = new Set(), out = [];
|
|
191
|
+
for (const d of [...String(path || "").split(":"), dirname(node)]) if (d && !/node_modules[\\/]\.bin|node-gyp-bin/.test(d) && !seen.has(d)) { seen.add(d); out.push(d); }
|
|
192
|
+
return out.join(":");
|
|
193
|
+
}
|
|
194
|
+
// Writes the entry and icon when they're missing or differ. { written, file } or { skipped }.
|
|
195
|
+
function installLauncher({ node = process.execPath, script, home = homedir(), platform = OS, path = process.env.PATH || "" } = {}) {
|
|
196
|
+
if (platform !== "linux" || !script) return { skipped: true };
|
|
197
|
+
const file = launcherFile(home), icon = launcherIcon(home), want = launcherContent(node, script, launcherPath(path, node));
|
|
198
|
+
let wrote = false;
|
|
199
|
+
try {
|
|
200
|
+
let had = ""; try { had = readFileSync(file, "utf8"); } catch {}
|
|
201
|
+
if (had !== want) { mkdirSync(join(file, ".."), { recursive: true }); writeFileSync(file, want, { mode: 0o644 }); wrote = true; }
|
|
202
|
+
let hadIcon = ""; try { hadIcon = readFileSync(icon, "utf8"); } catch {}
|
|
203
|
+
const svg = iconSvg(loadConfig().look || "ferro");
|
|
204
|
+
if (hadIcon !== svg) { mkdirSync(join(icon, ".."), { recursive: true }); writeFileSync(icon, svg, { mode: 0o644 }); wrote = true; }
|
|
205
|
+
} catch (e) { return { error: (e && e.message) || String(e) }; }
|
|
206
|
+
if (wrote) { try { spawn("update-desktop-database", [join(file, "..")], { stdio: "ignore", detached: true }).on("error", () => {}).unref(); } catch {} }
|
|
207
|
+
return { written: wrote, file };
|
|
208
|
+
}
|
|
209
|
+
function removeLauncher(home = homedir()) {
|
|
210
|
+
const gone = [];
|
|
211
|
+
for (const f of [launcherFile(home), launcherIcon(home)]) { try { if (existsSync(f)) { unlinkSync(f); gone.push(f); } } catch {} }
|
|
212
|
+
return gone;
|
|
213
|
+
}
|
|
147
214
|
function autostartState() { const file = autostartFile(); return { on: existsSync(file), file, ...(OS === "android-app" ? { phone: "app" } : OS === "android" ? { phone: "termux" } : {}) }; }
|
|
148
215
|
// `script` is index.mjs's real path. From npx that's a cache folder that goes
|
|
149
216
|
// away, so it has to be installed for this to keep working.
|
|
@@ -159,4 +226,4 @@ function setAutostart(on, script) {
|
|
|
159
226
|
} catch (e) { return { ...autostartState(), error: String((e && e.message) || e) }; }
|
|
160
227
|
}
|
|
161
228
|
|
|
162
|
-
export { weeklyCfg, lastSlot, weeklyDue, setWeekly, weeklyState, latestWeek, runWeekly, startWeekly, notifyCmd, desktopNotify, autostartFile, autostartContent, autostartState, setAutostart };
|
|
229
|
+
export { iconSvg, setLauncherLook, launcherPath, launcherFile, launcherIcon, launcherContent, installLauncher, removeLauncher, weeklyCfg, lastSlot, weeklyDue, setWeekly, weeklyState, latestWeek, runWeekly, startWeekly, notifyCmd, desktopNotify, autostartFile, autostartContent, autostartState, setAutostart };
|
package/home.mjs
CHANGED
|
@@ -14,14 +14,14 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
15
|
import { watchBoard, draftReply } from "./watch.mjs";
|
|
16
16
|
import { pendingReview, pushTasks, addTask } from "./tasks.mjs";
|
|
17
|
-
import { agentsList, runHandoff, runningHandoff, handoffCmd, connectorsInfo, parkedPaths, agentMissing, settleNeeds, pickAgent } from "./agents.mjs";
|
|
17
|
+
import { agentsList, runHandoff, runningHandoff, handoffCmd, connectorsInfo, parkedPaths, agentMissing, settleNeeds, pickAgent, detectHandoffs, linkReach } from "./agents.mjs";
|
|
18
18
|
import { linksState } from "./links.mjs";
|
|
19
19
|
import { knowledgeState } from "./knowledge.mjs";
|
|
20
|
-
import { CONFIG_DIR, loadTasks, saveTasks } from "./core.mjs";
|
|
20
|
+
import { CONFIG_DIR, loadTasks, saveTasks, loadConfig, saveConfig } from "./core.mjs";
|
|
21
21
|
import { lanesState, stuckHandovers, allowHandover, skipHandover } from "./lanes.mjs";
|
|
22
22
|
import { converse, actIn, actNow, taskIn, loadMind } from "./mind.mjs";
|
|
23
|
-
import { repoPathMap, laneMap } from "./scan.mjs";
|
|
24
|
-
import { resolveProvider } from "./ai.mjs";
|
|
23
|
+
import { repoPathMap, laneMap, reposState, scanRoots } from "./scan.mjs";
|
|
24
|
+
import { resolveProvider, PROVIDERS, claudeState } from "./ai.mjs";
|
|
25
25
|
import { reportsNews, listReports } from "./reports.mjs";
|
|
26
26
|
import { awaitingState, inboxSight } from "./handback.mjs";
|
|
27
27
|
import { newClashes } from "./checks.mjs";
|
|
@@ -493,4 +493,42 @@ function workGo({ push = pushTasks, run = runHandoff } = {}) {
|
|
|
493
493
|
return { started, queued, repos, ...(parked.length ? { parked } : {}), ...(r.unresolved && r.unresolved.length ? { unresolved: r.unresolved.map((u) => u.name) } : {}) };
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
|
|
496
|
+
// ---- setup (the first run) --------------------------------------------------------
|
|
497
|
+
// A new Symbiot walks you through setup before anything else: what it is, your AI,
|
|
498
|
+
// where your work is, your agent, every app you use (connected, or "I don't use it"),
|
|
499
|
+
// your documents, then Home. It's in config.onboarding: { pending, step, skipped: [link
|
|
500
|
+
// ids you don't use] }; a Symbiot that ran before this existed isn't sent through it.
|
|
501
|
+
const ONB_STEPS = ["welcome", "ai", "work", "agent", "apps", "docs", "done"];
|
|
502
|
+
function onboarding({ fresh = false } = {}) {
|
|
503
|
+
const cfg = loadConfig(), o = cfg.onboarding || {};
|
|
504
|
+
const tryOr = (f, d) => { try { return f(); } catch { return d; } };
|
|
505
|
+
let st = null; try { st = claudeState(fresh); } catch {}
|
|
506
|
+
const r = tryOr(() => resolveProvider(), null);
|
|
507
|
+
const reps = tryOr(() => reposState(), { searching: false, list: [] });
|
|
508
|
+
const links = tryOr(() => linksState(), { items: [] }), reach = tryOr(() => linkReach(), null), skipped = new Set(o.skipped || []);
|
|
509
|
+
const apps = (links.items || []).map((x) => ({ id: x.id, name: x.name, group: x.group, state: x.state, ...(x.note ? { note: x.note } : {}), skipped: skipped.has(x.id),
|
|
510
|
+
...(reach && reach.sites && reach.sites[x.id] ? { agents: !!reach.sites[x.id].ready, connector: reach.sites[x.id].name } : {}) }));
|
|
511
|
+
const det = tryOr(() => detectHandoffs(), { agents: [] }), cmd = tryOr(() => handoffCmd(), "");
|
|
512
|
+
return {
|
|
513
|
+
pending: !!o.pending, step: ONB_STEPS.includes(o.step) ? o.step : "welcome", steps: ONB_STEPS,
|
|
514
|
+
ai: { connected: !!r, provider: r ? r.provider : "", line: r ? `${PROVIDERS[r.provider].label}${r.model ? " · " + r.model : ""}` : "", claude: st ? { installed: st.installed, signedIn: st.signedIn } : null },
|
|
515
|
+
work: { searching: reps.searching, done: reps.done || 0, total: reps.total || 0, count: reps.list.length, repos: reps.list.slice(0, 60).map((x) => ({ name: x.name, path: x.path })), roots: tryOr(() => scanRoots(), []).map((r) => (r === homedir() ? "~" : r.startsWith(homedir() + "/") ? "~" + r.slice(homedir().length) : r)) },
|
|
516
|
+
agent: { cmd, pick: tryOr(() => pickAgent(), null), agents: (det.agents || []).map((a) => ({ label: a.label, tmpl: a.tmpl })) },
|
|
517
|
+
apps, groups: links.groups || [], decided: apps.every((a) => a.skipped || a.state !== "off"),
|
|
518
|
+
docs: { folders: tryOr(() => (knowledgeState().folders || []).map((f) => f.path), []) },
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// step: where you are; skip / unskip: an app you do or don't use; skipRest: every app not connected.
|
|
522
|
+
function setOnboarding({ step, skip, unskip, skipRest, done, restart } = {}) {
|
|
523
|
+
const cfg = loadConfig(), o = { ...(cfg.onboarding || {}) }, sk = new Set(o.skipped || []);
|
|
524
|
+
if (restart) { o.pending = true; o.step = "welcome"; }
|
|
525
|
+
if (step && ONB_STEPS.includes(step)) o.step = step;
|
|
526
|
+
if (skip) sk.add(String(skip)); if (unskip) sk.delete(String(unskip));
|
|
527
|
+
if (skipRest) { try { for (const x of linksState().items) if (x.state === "off") sk.add(x.id); } catch {} }
|
|
528
|
+
o.skipped = [...sk];
|
|
529
|
+
if (done) { o.pending = false; o.done = Date.now(); o.step = "done"; }
|
|
530
|
+
cfg.onboarding = o; saveConfig(cfg); cached = null;
|
|
531
|
+
return onboarding();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export { marketingState, marketingGo, marketingTask, moveToMarketing, goLane, homeState, homeContext, homeAsk, homeAnswer, homeNext, nextUp, laneNamed, NEXT_FILE, workScene, workGo, displayName, firstSteps, onboarding, setOnboarding, ONB_STEPS };
|
package/icon.svg
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
|
2
|
+
<title>Symbiot</title>
|
|
3
|
+
<defs>
|
|
4
|
+
<radialGradient id="bg" cx="50%" cy="42%" r="70%">
|
|
5
|
+
<stop offset="0" stop-color="#1B1D22"/>
|
|
6
|
+
<stop offset="1" stop-color="#08090B"/>
|
|
7
|
+
</radialGradient>
|
|
8
|
+
<radialGradient id="orb" cx="40%" cy="34%" r="66%">
|
|
9
|
+
<stop offset="0" stop-color="#3A3D44"/>
|
|
10
|
+
<stop offset="0.45" stop-color="#121317"/>
|
|
11
|
+
<stop offset="1" stop-color="#030304"/>
|
|
12
|
+
</radialGradient>
|
|
13
|
+
<radialGradient id="rim" cx="50%" cy="50%" r="50%">
|
|
14
|
+
<stop offset="0.86" stop-color="#000" stop-opacity="0"/>
|
|
15
|
+
<stop offset="0.97" stop-color="#B0466E" stop-opacity="0.55"/>
|
|
16
|
+
<stop offset="1" stop-color="#F2A541" stop-opacity="0"/>
|
|
17
|
+
</radialGradient>
|
|
18
|
+
<radialGradient id="glow" cx="50%" cy="56%" r="50%">
|
|
19
|
+
<stop offset="0.55" stop-color="#F2A541" stop-opacity="0.16"/>
|
|
20
|
+
<stop offset="1" stop-color="#F2A541" stop-opacity="0"/>
|
|
21
|
+
</radialGradient>
|
|
22
|
+
<linearGradient id="shine" x1="0" y1="0" x2="0" y2="1">
|
|
23
|
+
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.85"/>
|
|
24
|
+
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
|
|
25
|
+
</linearGradient>
|
|
26
|
+
</defs>
|
|
27
|
+
<rect width="256" height="256" rx="58" fill="url(#bg)"/>
|
|
28
|
+
<circle cx="128" cy="136" r="96" fill="url(#glow)"/>
|
|
29
|
+
<circle cx="128" cy="128" r="78" fill="url(#orb)"/>
|
|
30
|
+
<circle cx="128" cy="128" r="78" fill="url(#rim)"/>
|
|
31
|
+
<ellipse cx="102" cy="92" rx="30" ry="17" transform="rotate(-28 102 92)" fill="url(#shine)"/>
|
|
32
|
+
<ellipse cx="160" cy="176" rx="22" ry="5" transform="rotate(-30 160 176)" fill="#FFFFFF" opacity="0.12"/>
|
|
33
|
+
</svg>
|
package/index.mjs
CHANGED
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
// tasks.mjs (tasks, review and Approve), agents.mjs (handing work to an agent),
|
|
24
24
|
// drift.mjs, server.mjs (`symbiot app`'s server) and ui.mjs (its page).
|
|
25
25
|
|
|
26
|
-
import { spawn } from "node:child_process";
|
|
26
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
27
27
|
import { homedir } from "node:os";
|
|
28
28
|
import { join, basename } from "node:path";
|
|
29
|
-
import { realpathSync, readFileSync } from "node:fs";
|
|
29
|
+
import { realpathSync, readFileSync, mkdirSync, openSync, rmSync, existsSync } from "node:fs";
|
|
30
30
|
import { createInterface } from "node:readline";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
import { EMBEDDED_UI } from "./ui.mjs";
|
|
33
|
-
import { VERSION, CONFIG_PATH, loadConfig, saveConfig, repoState, semverGt } from "./core.mjs";
|
|
33
|
+
import { VERSION, CONFIG_PATH, CONFIG_DIR, loadConfig, saveConfig, repoState, semverGt } from "./core.mjs";
|
|
34
34
|
import { HANDOFF_PROMPT, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, fillHandoff, runHandoff, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, parseQuestions, agentQuestions } from "./agents.mjs";
|
|
35
35
|
import { AI_UI, PROVIDERS, resolveProvider, validate, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel, claudeState, connectProvider } from "./ai.mjs";
|
|
36
36
|
import { setScanOptions, scanBase, authorship, readmeInfo, repoShape, houseRules, reportFooter, findAllRepos, buildMap } from "./scan.mjs";
|
|
@@ -42,7 +42,7 @@ import { mapPage, wholePage, pressRegion, typeRegion, scrollPage, signIn, isTrus
|
|
|
42
42
|
import { watchState, addWatch, removeWatch, seenWatch, newsSince, markNews, checkWatch, setBrief, draftReply, watchBoard, boardLine, boardChat, boardTalk, clearBoardChat } from "./watch.mjs";
|
|
43
43
|
import { PORT as PHONE_PORT, phoneState, pairComputer, pollComputer, forgetComputer } from "./phone.mjs";
|
|
44
44
|
import { KIND_LABEL as POST_KIND, PATHS as POST_PATHS, draftPosts, postsState, postLog, approvePost, editPost, skipPost, voiceFromLinkedIn, openUrl, addMedia as addPostMedia, removeMedia as removePostMedia, mediaDir as postMediaDir, pictureOfPage, clipOfPage } from "./post.mjs";
|
|
45
|
-
import { startApp, updateCmd, isAppRunningWeekly } from "./server.mjs";
|
|
45
|
+
import { startApp, updateCmd, isAppRunningWeekly, askRunningApp, openApp } from "./server.mjs";
|
|
46
46
|
import { listReports, readReport } from "./reports.mjs";
|
|
47
47
|
import { runSandbox } from "./sandbox.mjs";
|
|
48
48
|
import { knowledgeState, addKnowledgeFolder, removeKnowledgeFolder, indexKnowledge, searchKnowledge, waitingOn, ownerOf, myName, setMyName, itemLine, caseLine } from "./knowledge.mjs";
|
|
@@ -350,6 +350,43 @@ async function cmdAway() {
|
|
|
350
350
|
if (!r) { console.log(c.y("Symbiot's app isn't running.") + c.d(" Start it with symbiot app, then Away works.")); process.exitCode = 1; return; }
|
|
351
351
|
if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; }
|
|
352
352
|
}
|
|
353
|
+
// symbiot open: what the app-menu icon runs. Running: its window comes up. Not
|
|
354
|
+
// running: it starts in the background (no terminal; what it prints goes to
|
|
355
|
+
// ~/.config/symbiot/app.log) and opens its own window.
|
|
356
|
+
async function cmdOpen() {
|
|
357
|
+
const cfg = loadConfig(), port = Number(process.env.SYMBIOT_PORT || cfg.appPort) || 7391;
|
|
358
|
+
const p = cfg.appToken ? await askRunningApp("ping", { port, token: cfg.appToken, ms: 1500 }) : null;
|
|
359
|
+
if (p && p.version) { openApp(`http://127.0.0.1:${port}/?t=${cfg.appToken}`); return; }
|
|
360
|
+
let fd = "ignore"; try { mkdirSync(CONFIG_DIR, { recursive: true }); fd = openSync(join(CONFIG_DIR, "app.log"), "a"); } catch {}
|
|
361
|
+
const env = { ...process.env }; delete env.SYMBIOT_NO_OPEN;
|
|
362
|
+
spawn(process.execPath, [realpathSync(fileURLToPath(import.meta.url)), "app"], { detached: true, stdio: ["ignore", fd, fd], env }).unref();
|
|
363
|
+
}
|
|
364
|
+
// symbiot uninstall: everything Symbiot put on this computer, then the program.
|
|
365
|
+
// --keep-data keeps ~/.config/symbiot (settings, tasks, memory); --yes doesn't ask.
|
|
366
|
+
async function cmdUninstall() {
|
|
367
|
+
const keep = has("keep-data");
|
|
368
|
+
console.log(c.b("Remove Symbiot from this computer") + "\n");
|
|
369
|
+
console.log(" · its app-menu entry and icon, start at login, the Super+` shortcut");
|
|
370
|
+
console.log(keep ? " · " + c.d("keeps your data in " + CONFIG_DIR) : " · everything it knows: settings, tasks, memory, drafts, screenshots (" + CONFIG_DIR + ")");
|
|
371
|
+
if (!has("keep-program")) console.log(" · the program itself (npm uninstall -g symbiot)");
|
|
372
|
+
console.log(c.d(" Claude Code and its sign-in stay. Folders it made inside your projects (.symbiot/) stay too."));
|
|
373
|
+
if (!has("yes")) {
|
|
374
|
+
if (!process.stdin.isTTY) { console.log("\n" + c.y("Run it again with --yes to go ahead.")); process.exitCode = 1; return; }
|
|
375
|
+
if (((await ask("\nType yes to remove it: ")) || "").trim().toLowerCase() !== "yes") { console.log("Nothing removed."); return; }
|
|
376
|
+
}
|
|
377
|
+
const { removeLauncher, setAutostart } = await import("./desktop.mjs");
|
|
378
|
+
const { removeShortcut } = await import("./away.mjs");
|
|
379
|
+
await askRunningApp("quit", { ms: 2000 });
|
|
380
|
+
try { setAutostart(false); } catch {}
|
|
381
|
+
removeLauncher(); removeShortcut();
|
|
382
|
+
try { rmSync(join(homedir(), ".symbiot"), { recursive: true, force: true }); } catch {}
|
|
383
|
+
if (!keep) { try { rmSync(CONFIG_DIR, { recursive: true, force: true }); } catch {} }
|
|
384
|
+
console.log(c.g("✓ ") + "Removed what Symbiot added" + (keep ? ", your data kept." : ", and its data."));
|
|
385
|
+
if (has("keep-program")) return;
|
|
386
|
+
const r = spawnSync("npm", ["uninstall", "-g", "symbiot"], { stdio: "inherit", shell: process.platform === "win32" });
|
|
387
|
+
console.log(r.status === 0 ? c.g("✓ ") + "Symbiot is uninstalled." : c.y("Couldn't remove the program: run npm uninstall -g symbiot"));
|
|
388
|
+
if (existsSync(CONFIG_DIR) && !keep) console.log(c.d("(Something wrote to " + CONFIG_DIR + " meanwhile; delete it if you like.)"));
|
|
389
|
+
}
|
|
353
390
|
async function cmdScreens() {
|
|
354
391
|
const [sub = "list", a1, a2, a3] = argv.slice(1).filter((x, i, all) => !x.startsWith("--") && all[i - 1] !== "--name");
|
|
355
392
|
const out = (x) => { console.log(JSON.stringify(x, null, 2)); if (x && x.error) process.exitCode = 1; };
|
|
@@ -775,6 +812,9 @@ ${c.b("Usage")}
|
|
|
775
812
|
symbiot standup yesterday + today, for standup
|
|
776
813
|
symbiot todo what's still on your plate
|
|
777
814
|
symbiot app open the visual app in your browser
|
|
815
|
+
symbiot open start Symbiot in the background and open its window
|
|
816
|
+
(what its app-menu icon does)
|
|
817
|
+
symbiot uninstall [--keep-data] remove Symbiot and everything it added
|
|
778
818
|
symbiot away [--shortcut] Symbiot full screen on every screen while you're away;
|
|
779
819
|
again closes it. --shortcut: Super+\` does it
|
|
780
820
|
symbiot app --fresh [--keep] try it as someone new: a brand-new Symbiot in
|
|
@@ -833,6 +873,8 @@ async function main() {
|
|
|
833
873
|
if (cmd === "whoami" || cmd === "status") return cmdWhoami();
|
|
834
874
|
if (cmd === "app" || cmd === "ui") return cmdApp();
|
|
835
875
|
if (cmd === "away") return cmdAway();
|
|
876
|
+
if (cmd === "open") return cmdOpen();
|
|
877
|
+
if (cmd === "uninstall") return cmdUninstall();
|
|
836
878
|
if (cmd === "models" || cmd === "hardware") return cmdModels();
|
|
837
879
|
if (cmd === "setup-local" || cmd === "setup-ollama") return cmdSetupLocal();
|
|
838
880
|
if (cmd === "drift") return cmdDrift();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,10 @@
|
|
|
44
44
|
"mapknn.mjs",
|
|
45
45
|
"guard.mjs",
|
|
46
46
|
"away.mjs",
|
|
47
|
-
"marketing.mjs"
|
|
47
|
+
"marketing.mjs",
|
|
48
|
+
"postinstall.mjs",
|
|
49
|
+
"icon.svg",
|
|
50
|
+
"scanworker.mjs"
|
|
48
51
|
],
|
|
49
52
|
"engines": {
|
|
50
53
|
"node": ">=18"
|
|
@@ -77,8 +80,9 @@
|
|
|
77
80
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
78
81
|
},
|
|
79
82
|
"scripts": {
|
|
80
|
-
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/post.mjs && node test/peek.mjs && node test/mind.mjs && node test/knowledge.mjs && node test/checks.mjs &&node test/lanes.mjs && node test/handback.mjs && node test/autoallow.mjs && node test/guard.mjs && node test/reports.mjs && node test/sandbox.mjs && node test/adapt.mjs && node test/work.mjs && node test/mapknn.mjs && node test/away.mjs && node test/subscription.mjs && node test/marketing.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
81
|
-
"lint": "eslint ."
|
|
83
|
+
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/post.mjs && node test/peek.mjs && node test/mind.mjs && node test/knowledge.mjs && node test/checks.mjs &&node test/lanes.mjs && node test/handback.mjs && node test/autoallow.mjs && node test/guard.mjs && node test/reports.mjs && node test/sandbox.mjs && node test/adapt.mjs && node test/work.mjs && node test/mapknn.mjs && node test/away.mjs && node test/subscription.mjs && node test/launcher.mjs && node test/onboarding.mjs && node test/marketing.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
84
|
+
"lint": "eslint .",
|
|
85
|
+
"postinstall": "node postinstall.mjs"
|
|
82
86
|
},
|
|
83
87
|
"devDependencies": {
|
|
84
88
|
"eslint": "^9.39.5",
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// npm install -g symbiot runs this: Symbiot in your app menu, with its icon, so you
|
|
2
|
+
// never need a terminal to start it (desktop.mjs installLauncher), and then it opens,
|
|
3
|
+
// so setup starts right away. Only for a global install on a desktop, and it never
|
|
4
|
+
// fails the install. npm hides what install scripts print, so the line goes straight
|
|
5
|
+
// to your terminal.
|
|
6
|
+
import { realpathSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
const say = (s) => { try { writeFileSync("/dev/tty", s + "\n"); } catch { console.log(s); } };
|
|
10
|
+
try {
|
|
11
|
+
if (process.env.npm_config_global === "true" && !process.env.SYMBIOT_NO_LAUNCHER) {
|
|
12
|
+
const script = realpathSync(fileURLToPath(new URL("./index.mjs", import.meta.url)));
|
|
13
|
+
const { installLauncher } = await import("./desktop.mjs");
|
|
14
|
+
const r = installLauncher({ script });
|
|
15
|
+
const desktop = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY) && !process.env.CI && !process.env.SYMBIOT_NO_OPEN;
|
|
16
|
+
if (desktop) { spawn(process.execPath, [script, "open"], { detached: true, stdio: "ignore" }).unref(); say("\nSymbiot is installed and opening now. Next time, open it from your app menu: no terminal needed.\n"); }
|
|
17
|
+
else if (r.file) say("\nSymbiot is in your app menu: open it from there, no terminal needed.\n");
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
package/scan.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// distros), finding repos and project folders without hanging, the facts each
|
|
3
3
|
// one gives (who you are in it, its commits, open work, README, rules), the
|
|
4
4
|
// Map's graph, and the one repo set every view shares.
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import { join } from "node:path";
|
|
8
|
-
import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
|
|
9
|
+
import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
10
|
import { VERSION, loadConfig, saveConfig, sh, repoState } from "./core.mjs";
|
|
10
11
|
import { detectHandoffs } from "./agents.mjs";
|
|
11
12
|
import { PROVIDERS, resolveProvider } from "./ai.mjs";
|
|
@@ -18,7 +19,7 @@ let LAST_MAP = null; // cached graph so node clicks don't rescan
|
|
|
18
19
|
// How this process scans: dir (--dir, one folder this run instead of the scan
|
|
19
20
|
// folders), quiet (the app: no progress line) and plain (no colour). Set by
|
|
20
21
|
// index.mjs from its flags.
|
|
21
|
-
const OPTS = { dir: "", quiet: false, plain: false };
|
|
22
|
+
const OPTS = { dir: "", quiet: false, plain: false, cache: false };
|
|
22
23
|
function setScanOptions(o) { Object.assign(OPTS, o); }
|
|
23
24
|
const paint = (code) => (s) => OPTS.plain ? s : `\x1b[38;5;${code}m${s}\x1b[0m`;
|
|
24
25
|
const green = paint(42), faint = paint(66), amber = paint(179);
|
|
@@ -209,11 +210,11 @@ function addScanRoot(p) {
|
|
|
209
210
|
const cfg = loadConfig();
|
|
210
211
|
let list = Array.isArray(cfg.scanRoots) && cfg.scanRoots.length ? cfg.scanRoots : scanDefaults(); // keep home when adding the first extra folder
|
|
211
212
|
if (!list.includes(p)) list.push(p);
|
|
212
|
-
cfg.scanRoots = list; saveConfig(cfg); LAST_MAP = null; return { ok: true, roots: cfg.scanRoots };
|
|
213
|
+
cfg.scanRoots = list; saveConfig(cfg); LAST_MAP = null; if (OPTS.cache) refreshRepos(); return { ok: true, roots: cfg.scanRoots };
|
|
213
214
|
}
|
|
214
215
|
function removeScanRoot(p) {
|
|
215
216
|
const cfg = loadConfig(); cfg.scanRoots = (Array.isArray(cfg.scanRoots) ? cfg.scanRoots : []).filter((x) => x !== p);
|
|
216
|
-
if (!cfg.scanRoots.length) delete cfg.scanRoots; saveConfig(cfg); LAST_MAP = null; return { ok: true, roots: cfg.scanRoots || [] };
|
|
217
|
+
if (!cfg.scanRoots.length) delete cfg.scanRoots; saveConfig(cfg); LAST_MAP = null; if (OPTS.cache) refreshRepos(); return { ok: true, roots: cfg.scanRoots || [] };
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
function commits(repos, sinceExpr, mineOnly = true) {
|
|
@@ -277,7 +278,50 @@ const MANIFEST_TOOL = {
|
|
|
277
278
|
"Gemfile": "Ruby", "composer.json": "PHP", "Dockerfile": "Docker", "docker-compose.yml": "Docker",
|
|
278
279
|
"terraform.tf": "Terraform", "kubernetes.yml": "Kubernetes", ".github": "GitHub Actions",
|
|
279
280
|
};
|
|
281
|
+
// ---- your projects, kept (the app) -------------------------------------------------
|
|
282
|
+
// Finding them walks your folders and calls git in each repo: a second here, a minute
|
|
283
|
+
// on a big home folder, and it ran again on every Home refresh, so the app stood still
|
|
284
|
+
// (a blank orb for a minute on a first run). The app (setScanOptions({ cache: true }))
|
|
285
|
+
// keeps the list: from repos.json at once on start, refreshed in a worker thread
|
|
286
|
+
// (scanworker.mjs) on start, every REPO_FRESH, and when your folders change. Until the
|
|
287
|
+
// first search ends there's nothing yet, rather than a wait. The CLI still searches
|
|
288
|
+
// each time.
|
|
289
|
+
const REPO_FRESH = 10 * 60 * 1000;
|
|
290
|
+
const repoFile = () => join(homedir(), ".config", "symbiot", "repos.json");
|
|
291
|
+
let REPO_CACHE = null, REPO_JOB = null;
|
|
292
|
+
const REPO_STATE = { searching: false, done: 0, total: 0, phase: "", at: 0, onChange: null };
|
|
293
|
+
function loadRepoCache() {
|
|
294
|
+
if (REPO_CACHE) return REPO_CACHE;
|
|
295
|
+
try { const j = JSON.parse(readFileSync(repoFile(), "utf8")); if (j && Array.isArray(j.list)) { REPO_CACHE = { at: Number(j.at) || 0, list: j.list }; REPO_STATE.at = REPO_CACHE.at; } } catch {}
|
|
296
|
+
return REPO_CACHE;
|
|
297
|
+
}
|
|
298
|
+
// Find them again in a worker; when it's done, keep and save the list. One at a time.
|
|
299
|
+
function refreshRepos({ worker = true } = {}) {
|
|
300
|
+
if (REPO_JOB) return REPO_JOB;
|
|
301
|
+
REPO_STATE.searching = true; Object.assign(REPO_STATE, { done: 0, total: 0, phase: "" });
|
|
302
|
+
const finish = (list) => {
|
|
303
|
+
REPO_CACHE = { at: Date.now(), list: Array.isArray(list) ? list : [] }; REPO_STATE.at = REPO_CACHE.at; REPO_STATE.searching = false; REPO_JOB = null; LAST_MAP = null;
|
|
304
|
+
try { mkdirSync(join(repoFile(), ".."), { recursive: true }); writeFileSync(repoFile(), JSON.stringify(REPO_CACHE), { mode: 0o600 }); } catch {}
|
|
305
|
+
try { if (REPO_STATE.onChange) REPO_STATE.onChange(REPO_CACHE.list); } catch {}
|
|
306
|
+
return REPO_CACHE.list;
|
|
307
|
+
};
|
|
308
|
+
if (!worker) { const was = OPTS.cache; OPTS.cache = false; let l = []; try { l = findAllRepos(); } finally { OPTS.cache = was; } return Promise.resolve(finish(l)); }
|
|
309
|
+
REPO_JOB = new Promise((resolve) => {
|
|
310
|
+
let w; try { w = new Worker(new URL("./scanworker.mjs", import.meta.url)); } catch { const was = OPTS.cache; OPTS.cache = false; let l = []; try { l = findAllRepos(); } finally { OPTS.cache = was; } resolve(finish(l)); return; }
|
|
311
|
+
let got = null;
|
|
312
|
+
w.on("message", (m) => { if (m && m.progress) Object.assign(REPO_STATE, m.progress); if (m && m.list) got = m.list; });
|
|
313
|
+
w.on("error", () => {}); w.on("exit", () => resolve(finish(got || (REPO_CACHE && REPO_CACHE.list) || [])));
|
|
314
|
+
});
|
|
315
|
+
return REPO_JOB;
|
|
316
|
+
}
|
|
317
|
+
// What the app knows of your projects right now, and whether it's still looking.
|
|
318
|
+
const reposState = () => ({ searching: REPO_STATE.searching, done: REPO_STATE.done, total: REPO_STATE.total, phase: REPO_STATE.phase, at: REPO_STATE.at, list: (REPO_CACHE && REPO_CACHE.list) || [] });
|
|
280
319
|
function findAllRepos(base) {
|
|
320
|
+
if (!base && OPTS.cache) {
|
|
321
|
+
const c = loadRepoCache();
|
|
322
|
+
if (!c || Date.now() - c.at > REPO_FRESH) refreshRepos();
|
|
323
|
+
return c ? c.list : [];
|
|
324
|
+
}
|
|
281
325
|
const own = scanBegin();
|
|
282
326
|
try {
|
|
283
327
|
const roots = base ? [base] : scanRoots();
|
|
@@ -418,6 +462,7 @@ async function buildMapNow() {
|
|
|
418
462
|
}
|
|
419
463
|
async function buildMapScan() {
|
|
420
464
|
const who = me();
|
|
465
|
+
if (OPTS.cache && (REPO_STATE.searching || !loadRepoCache())) await refreshRepos(); // the first search, not an empty Map
|
|
421
466
|
const found = findAllRepos();
|
|
422
467
|
const repos = [];
|
|
423
468
|
scanPhase("reading repo details", found.length);
|
|
@@ -561,4 +606,4 @@ function discoveredRepos() {
|
|
|
561
606
|
return findAllRepos();
|
|
562
607
|
}
|
|
563
608
|
|
|
564
|
-
export { laneMap, SCAN, SCAN_TIMEOUT_MS, setScanOptions, scanBegin, scanPhase, scanTick, scanExpired, scanEnd, me, authorship, authorArgs, readmeInfo, repoShape, houseRules, reportFooter, expandRoot, scanHome, scanBase, storageBlocked, prootDistros, prootHomes, scanRoots, addScanRoot, removeScanRoot, commits, openWork, findAllRepos, detectRepo, findProjectFolders, detectFolder, claudeProjects, buildMap, nodeDetail, repoPathMap, discoveredRepos };
|
|
609
|
+
export { refreshRepos, reposState, REPO_STATE, laneMap, SCAN, SCAN_TIMEOUT_MS, setScanOptions, scanBegin, scanPhase, scanTick, scanExpired, scanEnd, me, authorship, authorArgs, readmeInfo, repoShape, houseRules, reportFooter, expandRoot, scanHome, scanBase, storageBlocked, prootDistros, prootHomes, scanRoots, addScanRoot, removeScanRoot, commits, openWork, findAllRepos, detectRepo, findProjectFolders, detectFolder, claudeProjects, buildMap, nodeDetail, repoPathMap, discoveredRepos };
|
package/scanworker.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// symbiot — finding your projects off the app's main thread (scan.mjs refreshRepos):
|
|
2
|
+
// a walk of your folders and a git call per repo can take a minute on a big home
|
|
3
|
+
// folder, and the app has to keep answering meanwhile. Posts { progress } as it goes
|
|
4
|
+
// and { list } at the end.
|
|
5
|
+
import { parentPort } from "node:worker_threads";
|
|
6
|
+
const scan = await import("./scan.mjs");
|
|
7
|
+
scan.setScanOptions({ quiet: true });
|
|
8
|
+
const tick = setInterval(() => { const s = scan.SCAN; parentPort.postMessage({ progress: { phase: s.phase, done: s.done, total: s.total } }); }, 400);
|
|
9
|
+
let list = [];
|
|
10
|
+
try { list = scan.findAllRepos(); } catch {}
|
|
11
|
+
clearInterval(tick);
|
|
12
|
+
parentPort.postMessage({ list });
|
package/server.mjs
CHANGED
|
@@ -16,13 +16,13 @@ import { EMBEDDED_UI } from "./ui.mjs";
|
|
|
16
16
|
import { VERSION, LATEST_VERSION, REGISTRY, semverGt, checkLatest, CONFIG_PATH, loadConfig, saveConfig, loadTasks, hasCmd, chromeBinary, CONFIG_DIR } from "./core.mjs";
|
|
17
17
|
import { shSingle, handoffCmd, setHandoffCmd, grantAgent, runHandoff, track, detectHandoffs, connectorsInfo, linkReach, answerQuestions, skipIdea, agentsList, startWaiting, parkLane, parkedPaths, autoAllowSweep, trustFull, readLastWords, sandboxState } from "./agents.mjs";
|
|
18
18
|
import { PROVIDERS, resolveProvider, connectProvider, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel } from "./ai.mjs";
|
|
19
|
-
import { SCAN, SCAN_TIMEOUT_MS, scanRoots, scanHome, addScanRoot, removeScanRoot, buildMap, nodeDetail, repoPathMap, laneMap } from "./scan.mjs";
|
|
19
|
+
import { SCAN, SCAN_TIMEOUT_MS, scanRoots, scanHome, addScanRoot, removeScanRoot, buildMap, nodeDetail, repoPathMap, laneMap, setScanOptions, refreshRepos, REPO_STATE } from "./scan.mjs";
|
|
20
20
|
import { computeDrift } from "./drift.mjs";
|
|
21
21
|
import { addTask, toggleTask, removeTask, restoreTask, syncTasks, taskType, pushTasks, pendingReview, workingDiff, learnNpm, withReleases, releaseInput, approveRepo, approveChanges, sendBack, setAutoMerge } from "./tasks.mjs";
|
|
22
22
|
import { repoReview, repoSuggest, folderSuggest, taskChat, clearTaskChat, mailState, setMail, sentMail, produce, releaseNotes } from "./writeups.mjs";
|
|
23
23
|
import { loadScreens, screenImage, captureScreen, splitScreen, listMonitors, allowScreenshots, importScreen, setRegions, renameScreen, removeScreen, blueprint, clickRegion } from "./screens.mjs";
|
|
24
24
|
import { mapPage, wholePage, pressRegion, typeRegion, scrollPage, signIn, keepBrowserOpen, isTrusted, trustedSites, trustSite, untrustSite } from "./headless.mjs";
|
|
25
|
-
import { weeklyState, setWeekly, runWeekly, startWeekly, autostartState, setAutostart } from "./desktop.mjs";
|
|
25
|
+
import { weeklyState, setWeekly, runWeekly, startWeekly, autostartState, setAutostart, installLauncher, iconSvg, setLauncherLook } from "./desktop.mjs";
|
|
26
26
|
import { markNews, newsSince, watchState, addWatch, setEvery, removeWatch, clearNews, seenWatch, checkWatch, startWatches, setBrief, draftReply, openChat, watchBoard, boardChat, clearBoardChat } from "./watch.mjs";
|
|
27
27
|
import { linksState, linkSite, checkLink, unlinkSite } from "./links.mjs";
|
|
28
28
|
import { testWeeks, testInstalls, postsState, draftPosts, approvePost, editPost, skipPost, voiceFromLinkedIn, addMedia, removeMedia, mediaFile, mediaDir, pictureOfPage, clipOfPage, openUrl } from "./post.mjs";
|
|
@@ -30,7 +30,7 @@ import { mindState, forget } from "./mind.mjs";
|
|
|
30
30
|
import { lanesTick, lanesState, partlyDone, orcaRelink } from "./lanes.mjs";
|
|
31
31
|
import { keepFacts, skipFacts, awaitTick, awaitingState, stopWaiting } from "./handback.mjs";
|
|
32
32
|
import { adaptState, noteUse } from "./adapt.mjs";
|
|
33
|
-
import { homeState, homeAsk, homeAnswer, homeNext, workScene, workGo, firstSteps, marketingState, marketingGo, marketingTask, moveToMarketing } from "./home.mjs";
|
|
33
|
+
import { homeState, homeAsk, homeAnswer, homeNext, workScene, workGo, firstSteps, marketingState, marketingGo, marketingTask, moveToMarketing, onboarding, setOnboarding } from "./home.mjs";
|
|
34
34
|
import { MARKETING_DIR } from "./marketing.mjs";
|
|
35
35
|
import { listReports, readReport, markAllRead } from "./reports.mjs";
|
|
36
36
|
import { phoneState, setPhoneLink, newCode, unpairPhone, pairComputer, forgetComputer, pollComputer, startPhone } from "./phone.mjs";
|
|
@@ -186,6 +186,8 @@ const PLAIN_COLOURS = { g: (s) => s, d: (s) => s, b: (s) => s, y: (s) => s };
|
|
|
186
186
|
async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {}) {
|
|
187
187
|
const writeup = (cmd) => produce(cmd, { since, all });
|
|
188
188
|
const SERVER_STARTED = Date.now();
|
|
189
|
+
// a brand-new Symbiot (no config yet) walks you through setup first (home.mjs onboarding)
|
|
190
|
+
const FRESH = !SANDBOX && !existsSync(CONFIG_PATH);
|
|
189
191
|
// Stable token + port so the URL survives a restart — the open tab can
|
|
190
192
|
// reconnect and auto-reload itself instead of you closing and reopening it.
|
|
191
193
|
const cfg0 = loadConfig();
|
|
@@ -204,7 +206,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
204
206
|
if (p && p.version) {
|
|
205
207
|
const url = `http://127.0.0.1:${PORT}/?t=${TOKEN}`; const how = process.env.SYMBIOT_NO_OPEN === "1" ? "" : openApp(url);
|
|
206
208
|
console.log(`\n${c.g("●")} ${c.b("Symbiot")} is already running (v${p.version}) at ${c.b(url)}`);
|
|
207
|
-
console.log(how ? c.d(` Opened the existing window (a ${how}).`) : c.d(" Open that URL in your browser."));
|
|
209
|
+
console.log(how ? c.d(` Opened the existing window (${/^[aeiou]/.test(how) ? "an" : "a"} ${how}).`) : c.d(" Open that URL in your browser."));
|
|
208
210
|
console.log(c.d(" (Not starting a second copy. Set SYMBIOT_FORCE_NEW=1 to force one.)"));
|
|
209
211
|
return;
|
|
210
212
|
}
|
|
@@ -214,13 +216,27 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
214
216
|
keepBrowserOpen(BROWSER_KEEP);
|
|
215
217
|
const json = (res, obj) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
216
218
|
const screenOut =(s) => (s && s.id ? { ...s, blueprint: blueprint(s), ...(s.page ? { trusted: isTrusted(s.page.url) } : {}) } : s && s.screens ? { ...s, screens: s.screens.map(screenOut) } : s);
|
|
219
|
+
// an installed Symbiot keeps its app-menu entry pointing at itself (a new Node, a moved
|
|
220
|
+
// install); not a copy run from a checkout or a test
|
|
221
|
+
if (/[\\/]node_modules[\\/]symbiot[\\/]/.test(bin || "") && !SANDBOX) { try { installLauncher({ script: bin }); } catch {} }
|
|
222
|
+
if (FRESH) { try { const cf = loadConfig(); if (!cf.onboarding) { cf.onboarding = { pending: true, step: "welcome", skipped: [] }; saveConfig(cf); } } catch {} }
|
|
223
|
+
// your projects: kept, and found again off the main thread, so nothing waits on a search
|
|
224
|
+
setScanOptions({ cache: true }); REPO_STATE.onChange = () => { try { homeState({ fresh: true }); } catch {} };
|
|
225
|
+
{ const t0 = Date.now(); refreshRepos().then((l) => console.log(`Found ${l.length} project${l.length === 1 ? "" : "s"} in ${((Date.now() - t0) / 1000).toFixed(1)}s.`)); }
|
|
217
226
|
let UPDATING = null; // an update in flight: { target, attempt, retrying? }
|
|
218
227
|
let NEWEST_WIN = ""; // the Symbiot window opened last: older ones close themselves (one window, not a pile)
|
|
219
228
|
process.on("exit", () => closeAway()); // Away's windows go with the app (a quit, an update's restart)
|
|
220
229
|
const server = createServer(async (req, res) => {
|
|
230
|
+
const t0 = Date.now(); res.on("finish", () => { const ms = Date.now() - t0; if (ms > 1500) console.log(`slow: ${String(req.url || "").split("?")[0]} took ${(ms / 1000).toFixed(1)}s`); });
|
|
221
231
|
const u = new URL(req.url, "http://127.0.0.1");
|
|
222
232
|
if (req.method === "GET" && u.pathname === "/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(EMBEDDED_UI); return; }
|
|
223
233
|
// the app's typeface ships in the package (fonts/), so it's there offline
|
|
234
|
+
// the window's own icon (its taskbar entry), the same orb as the app menu's
|
|
235
|
+
if (req.method === "GET" && (u.pathname === "/favicon.svg" || u.pathname === "/favicon.ico")) {
|
|
236
|
+
try { const b = iconSvg(u.searchParams.get("look") || loadConfig().look || "ferro"); res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "no-cache" }); res.end(b); }
|
|
237
|
+
catch { res.writeHead(404); res.end(); }
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
224
240
|
if (req.method === "GET" && u.pathname === "/fonts/Geist-Variable.woff2") {
|
|
225
241
|
try { const b = readFileSync(fileURLToPath(new URL("./fonts/Geist-Variable.woff2", import.meta.url))); res.writeHead(200, { "content-type": "font/woff2", "cache-control": "public, max-age=31536000, immutable" }); res.end(b); }
|
|
226
242
|
catch { res.writeHead(404); res.end(); }
|
|
@@ -380,6 +396,11 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
380
396
|
// the work scene: what agents are doing, what's waiting, what's ready; Go starts what's waiting
|
|
381
397
|
if (u.pathname === "/api/work") return json(res, workScene());
|
|
382
398
|
if (u.pathname === "/api/work/go" && req.method === "POST") return json(res, workGo());
|
|
399
|
+
if (u.pathname === "/api/onboarding") return json(res, onboarding({ fresh: u.searchParams.get("fresh") === "1" }));
|
|
400
|
+
if (u.pathname === "/api/onboarding/set" && req.method === "POST") { const b = await readBody(req); return json(res, setOnboarding({ step: b.step, skip: b.skip, unskip: b.unskip, skipRest: !!b.skipRest })); }
|
|
401
|
+
if (u.pathname === "/api/onboarding/done" && req.method === "POST") return json(res, setOnboarding({ done: true }));
|
|
402
|
+
if (u.pathname === "/api/onboarding/restart" && req.method === "POST") return json(res, setOnboarding({ restart: true }));
|
|
403
|
+
if (u.pathname === "/api/look" && req.method === "POST") { const b = await readBody(req), look = ["ferro", "glass", "pearl"].includes(b.look) ? b.look : "ferro"; const cf = loadConfig(); if (cf.look !== look) { cf.look = look; saveConfig(cf); } return json(res, { look, launcher: setLauncherLook(look) }); }
|
|
383
404
|
if (u.pathname === "/api/firststeps") return json(res, firstSteps()); // Settings' first steps: what's set up, in order
|
|
384
405
|
if (u.pathname === "/api/home/answer" && req.method === "POST") { const b = await readBody(req); return json(res, homeAnswer(b.id, { pick: b.pick, text: b.text })); }
|
|
385
406
|
if (u.pathname === "/api/away" && req.method === "POST") { const b = await readBody(req); return json(res, toggleAway(`http://127.0.0.1:${server.address().port}/?t=${TOKEN}`, b.open)); }
|
|
@@ -477,7 +498,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
477
498
|
const url = `http://127.0.0.1:${server.address().port}/?t=${TOKEN}`;
|
|
478
499
|
const how = opened || RELAUNCH || process.env.SYMBIOT_NO_OPEN === "1" ? "" : openApp(url); opened = true; // only pop a window the first time (never in tests, never after an update)
|
|
479
500
|
console.log(`\n${c.g("●")} ${c.b("Symbiot")} is running at ${c.b(url)}`);
|
|
480
|
-
console.log(RELAUNCH ? c.d(" Restarted after an update; the open window reloads itself.") : how ? c.d(` Opened in a ${how}.`) : c.d(" Open that URL in your browser."));
|
|
501
|
+
console.log(RELAUNCH ? c.d(" Restarted after an update; the open window reloads itself.") : how ? c.d(` Opened in ${/^[aeiou]/.test(how) ? "an" : "a"} ${how}.`) : c.d(" Open that URL in your browser."));
|
|
481
502
|
console.log(c.d(" Leave this running; press Ctrl+C to stop (or click the X in the window's top corner)."));
|
|
482
503
|
});
|
|
483
504
|
server.on("error", (e) => {
|
|
@@ -508,4 +529,4 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
508
529
|
setInterval(() => { try { awaitTick(); } catch {} }, 60000).unref(); // a reply an agent's email waits on: found, and handed on
|
|
509
530
|
}
|
|
510
531
|
|
|
511
|
-
export { updateCmd, changesSince, whatsNew, tarFile, registryChangelog, BROWSER_KEEP, startApp, askRunningApp, isAppRunningWeekly };
|
|
532
|
+
export { updateCmd, changesSince, whatsNew, tarFile, registryChangelog, BROWSER_KEEP, startApp, askRunningApp, isAppRunningWeekly, openApp };
|
package/ui.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// Kept apart from the CLI/server so UI edits can't break the backend (and vice
|
|
6
6
|
// versa); test/smoke.mjs boots the real page and exercises every handler.
|
|
7
7
|
export const EMBEDDED_UI = `<!doctype html><html><head><meta charset="utf8">
|
|
8
|
-
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" id="themecolor" content="#08090B"
|
|
8
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" id="themecolor" content="#08090B">
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="color-scheme" content="dark light"><title>Symbiot</title>
|
|
9
10
|
<style>
|
|
10
11
|
@font-face{font-family:'Geist';src:url('/fonts/Geist-Variable.woff2') format('woff2');font-weight:100 900;font-style:normal;font-display:swap}
|
|
11
12
|
:root{--ink:#0E1A1F;--ink2:#15262C;--ink3:#1D333A;--line:#24404A;--bone:#F4F1EA;--text:#B7C9C4;--faint:#7E9690;--green:#3DDC97;--amber:#F2A541;--green-dim:#16322D;--sans:'Geist',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
|
@@ -901,6 +902,42 @@ body.lq-away #lqaway{display:block;position:fixed;left:0;top:0;width:440px;text-
|
|
|
901
902
|
#lqaway .awh{margin-top:16px;font-size:11.5px;opacity:.32}
|
|
902
903
|
body.lq-light #lqaway{color:#151A21;text-shadow:0 1px 12px rgba(255,255,255,.8)}
|
|
903
904
|
body.lq-light #lqaway .aws b{color:#9A5200}
|
|
905
|
+
|
|
906
|
+
/* Setup: the first run, over everything, until it's done */
|
|
907
|
+
.onb{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(5,6,8,.74);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}
|
|
908
|
+
.onb.hidden{display:none}
|
|
909
|
+
.onbin{width:min(700px,calc(100vw - 32px));max-height:calc(100vh - 40px);overflow:auto;padding:30px 32px 26px;border-radius:28px;background:rgba(19,21,25,.9);border:1px solid rgba(255,255,255,.08);box-shadow:0 30px 80px -30px rgba(0,0,0,.9);color:var(--text)}
|
|
910
|
+
.onbdots{display:flex;gap:6px;margin-bottom:22px}.onbdots i{display:block;height:4px;flex:1;border-radius:4px;background:rgba(255,255,255,.1)}.onbdots i.on{background:var(--amber)}.onbdots i.past{background:rgba(242,165,65,.45)}
|
|
911
|
+
.onb h2{margin:0 0 8px;font:650 28px/1.15 var(--sans);letter-spacing:-.02em;color:var(--bone)}
|
|
912
|
+
.onbl{margin:0 0 20px;font-size:15px;line-height:1.55;color:var(--faint)}
|
|
913
|
+
.onbcards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:6px 0 20px}
|
|
914
|
+
.onbcard{padding:16px;border-radius:18px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07)}
|
|
915
|
+
.onbcard b{display:block;margin-bottom:6px;font:600 15px var(--sans);color:var(--bone)}.onbcard span{font-size:13.5px;line-height:1.5;color:var(--faint)}
|
|
916
|
+
.onbnote{font-size:13px;color:var(--faint);margin:0 0 18px}
|
|
917
|
+
.onbok{display:flex;gap:10px;align-items:flex-start;padding:14px 16px;border-radius:16px;background:rgba(79,190,140,.08);border:1px solid rgba(79,190,140,.25);margin-bottom:18px;font-size:14.5px;line-height:1.5}
|
|
918
|
+
.onbok i{flex:none;width:20px;height:20px;border-radius:50%;background:#4FBE8C;color:#06120C;font:700 12px/20px var(--sans);text-align:center;font-style:normal}
|
|
919
|
+
.onbbox{padding:16px;border-radius:18px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.07);margin-bottom:12px}
|
|
920
|
+
.onbbox h3{margin:0 0 6px;font:600 15px var(--sans);color:var(--bone)}.onbbox p{margin:0 0 10px;font-size:13.5px;line-height:1.5;color:var(--faint)}
|
|
921
|
+
.onbbox code{font-size:12.5px;padding:1px 6px;border-radius:6px;background:rgba(255,255,255,.08)}
|
|
922
|
+
.onbrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.onbrow input,.onbrow select{flex:1;min-width:160px}
|
|
923
|
+
.onbnav{display:flex;align-items:center;gap:10px;margin-top:22px}.onbnav .sp{flex:1}
|
|
924
|
+
.onbnav .back{background:none;border:0;color:var(--faint);font:500 13.5px var(--sans);cursor:pointer;padding:6px 2px}
|
|
925
|
+
.onbrepos{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 4px}.onbrepos span{padding:5px 10px;border-radius:999px;font-size:12.5px;background:rgba(255,255,255,.06);color:var(--text)}
|
|
926
|
+
.onbspin{display:inline-block;width:12px;height:12px;margin-right:8px;border-radius:50%;border:2px solid rgba(242,165,65,.3);border-top-color:var(--amber);animation:onbspin 1s linear infinite;vertical-align:-1px}
|
|
927
|
+
@keyframes onbspin{to{transform:rotate(360deg)}}
|
|
928
|
+
.onbgrp{margin:16px 0 6px;font:600 11px var(--sans);letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
|
|
929
|
+
.onbapp{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:14px;background:rgba(255,255,255,.03);margin-bottom:6px}
|
|
930
|
+
.onbapp .nm{flex:1;font-size:14px;color:var(--bone)}.onbapp .st{font-size:12.5px;color:var(--faint)}.onbapp .st.ok{color:#4FBE8C}.onbapp .st.wait{color:var(--amber)}
|
|
931
|
+
.onbapp button{padding:5px 12px;font-size:12.5px}
|
|
932
|
+
.onborb{display:block;width:72px;height:72px;margin:0 0 14px;border-radius:18px}
|
|
933
|
+
.onbfoot{position:sticky;bottom:-26px;margin:12px -32px -26px;padding:12px 32px 22px;background:#15171B;border-top:1px solid rgba(255,255,255,.06)}.onbfoot .onbnav{margin-top:6px}.onbfoot .onbnote{margin:0}
|
|
934
|
+
body.lq-light .onbfoot{background:#FFFFFF;border-top-color:rgba(21,26,33,.07)}
|
|
935
|
+
.onbtour{margin:6px 0 0;padding:0 0 0 18px;font-size:14px;line-height:1.7;color:var(--text)}
|
|
936
|
+
body.lq-light .onb{background:rgba(236,239,244,.7)}
|
|
937
|
+
body.lq-light .onbin{background:rgba(255,255,255,.92);border-color:rgba(21,26,33,.08);box-shadow:0 30px 80px -30px rgba(40,52,74,.45)}
|
|
938
|
+
body.lq-light .onbcard,body.lq-light .onbbox,body.lq-light .onbapp{background:rgba(21,26,33,.035);border-color:rgba(21,26,33,.07)}
|
|
939
|
+
body.lq-light .onbdots i{background:rgba(21,26,33,.1)}body.lq-light .onbdots i.on{background:var(--amber)}body.lq-light .onbdots i.past{background:rgba(242,165,65,.5)}body.lq-light .onbrepos span{background:rgba(21,26,33,.06)}body.lq-light .onbbox code{background:rgba(21,26,33,.07)}
|
|
940
|
+
@media(max-width:640px){.onbcards{grid-template-columns:1fr}.onbin{padding:22px 18px}}
|
|
904
941
|
/* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
|
|
905
942
|
.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)}
|
|
906
943
|
body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
@@ -954,6 +991,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
954
991
|
@media(max-width:760px){.maprow{flex-direction:column}.detail{width:auto;max-height:none}}
|
|
955
992
|
@media(max-width:600px){#screenname,#pagesite{flex:1 1 100%!important}.tabs{overflow-x:auto;scrollbar-width:none;padding:0 8px;gap:0}.tabs::-webkit-scrollbar{display:none}.tab{flex:none;padding:9px 11px}header{padding:12px 14px 8px}main{padding:12px}}
|
|
956
993
|
</style></head><body>
|
|
994
|
+
<div id="onb" class="onb hidden" role="dialog" aria-modal="true" aria-labelledby="onbt"><div class="onbin"><div class="onbdots" id="onbdots"></div><div id="onbbody"></div></div></div>
|
|
957
995
|
<div id="liquid" aria-label="Symbiot">
|
|
958
996
|
<canvas id="lq" aria-hidden="true"></canvas>
|
|
959
997
|
<div id="lqorbits"></div>
|
|
@@ -1075,6 +1113,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1075
1113
|
</section>
|
|
1076
1114
|
<section id="panel-settings" class="hidden">
|
|
1077
1115
|
<div id="firststeps" class="sset hidden" aria-live="polite"></div>
|
|
1116
|
+
<div class="sset"><h3 class="ssh">Setup</h3><div class="row"><span class="muted" style="flex:1">Walk through setup again: your AI, your work, your agent and your apps.</span><button class="ghost" id="rerunsetup">Run setup again</button></div></div>
|
|
1078
1117
|
<div class="sset"><h3 class="ssh">Your AI</h3>
|
|
1079
1118
|
<label>Which AI should Symbiot write with?</label>
|
|
1080
1119
|
<select id="provider">
|
|
@@ -2570,7 +2609,7 @@ var LQNAMES={board:'Dashboard',map:'Map',tasks:'Workdesk',agents:'Workdesk',week
|
|
|
2570
2609
|
var LQ_REST=60000,LQ_MAX=11;
|
|
2571
2610
|
function lqMM(q){try{return !!(window.matchMedia&&window.matchMedia(q).matches);}catch(e){return false;}}
|
|
2572
2611
|
function lqLookGet(){var l='';try{l=window.localStorage&&window.localStorage.getItem('symbiot-look')||'';}catch(e){}return l==='glass'||l==='pearl'?l:'ferro';}
|
|
2573
|
-
function lqLook(l){try{if(window.localStorage)window.localStorage.setItem('symbiot-look',l);}catch(e){}lqTheme();var g=$('lqlook');if(g&&g.querySelectorAll)g.querySelectorAll('button').forEach(function(b){b.setAttribute('aria-checked',b.getAttribute('data-look')===l?'true':'false');});LQ.ripple=[0.5,0.47,LQ.t||0];}
|
|
2612
|
+
function lqLook(l){try{if(window.localStorage)window.localStorage.setItem('symbiot-look',l);}catch(e){}lqTheme();lqIcon(l);api('/api/look',{look:l}).catch(function(){});var g=$('lqlook');if(g&&g.querySelectorAll)g.querySelectorAll('button').forEach(function(b){b.setAttribute('aria-checked',b.getAttribute('data-look')===l?'true':'false');});LQ.ripple=[0.5,0.47,LQ.t||0];}
|
|
2574
2613
|
function lqTheme(){var look=lqLookGet();var th={look:look,light:look!=='ferro',contrast:lqMM('(prefers-contrast: more)'),forced:lqMM('(forced-colors: active)'),solid:lqMM('(prefers-reduced-transparency: reduce)'),still:lqMM('(prefers-reduced-motion: reduce)'),wide:lqMM('(color-gamut: p3)'),night:false,accent:null};
|
|
2575
2614
|
var h=new Date().getHours();th.night=h>=22||h<6;
|
|
2576
2615
|
try{if(document.createElement&&typeof getComputedStyle==='function'&&document.body&&document.body.appendChild){var a=document.createElement('span'),b=document.createElement('span');a.style.color='AccentColor';b.style.color='CanvasText';document.body.appendChild(a);document.body.appendChild(b);
|
|
@@ -2680,7 +2719,7 @@ list=list.concat(need.items);var old={};LQ.drops.forEach(function(d){old[d.id]=d
|
|
|
2680
2719
|
// a new blob grows where it belongs, rather than sweeping out of the core across the rest
|
|
2681
2720
|
LQ.drops=list.map(function(d){var o=old[d.id];d.x=o?o.x:d.blob?d.tx:cx;d.y=o?o.y:d.blob?d.ty:cy;d.vx=o?o.vx:0;d.vy=o?o.vy:0;d.cr=o?o.cr:0;return d;});
|
|
2682
2721
|
var n=(h.you||[]).length,w=h.working||0;
|
|
2683
|
-
LQ.coreText=n?(n+(n>1?' things need':' thing needs')+' only you'+(w?' · '+w+' agent'+(w>1?'s':'')+' working':'')):(w?w+' agent'+(w>1?'s':'')+' working · nothing needs you':'
|
|
2722
|
+
LQ.coreText=n?(n+(n>1?' things need':' thing needs')+' only you'+(w?' · '+w+' agent'+(w>1?'s':'')+' working':'')):(w?w+' agent'+(w>1?'s':'')+' working · nothing needs you':'Nothing needs you right now');
|
|
2684
2723
|
var b=document.body;if(b&&b.classList){b.classList.toggle('lq-touch',!!(a.modes&&a.modes.touch)||LQ.touch);b.classList.toggle('lq-keys',!!(a.modes&&a.modes.keyboard));}
|
|
2685
2724
|
LQ.talkWeight=(a.modes&&a.modes.talkWeight)||0.35;
|
|
2686
2725
|
lqLabels();}
|
|
@@ -3037,7 +3076,80 @@ awTick();awStatus();setInterval(awTick,5000);setInterval(awStatus,15000);
|
|
|
3037
3076
|
var bye=function(ev){if(Date.now()-AWAY.shown<1500)return;if(ev&&ev.type==='keydown'&&['Meta','OS','Super','Shift','Control','Alt','Hyper'].indexOf(ev.key)>=0)return;api('/api/away',{open:false}).catch(function(){});};
|
|
3038
3077
|
['keydown','mousedown','touchstart','wheel'].forEach(function(t){document.addEventListener(t,bye,true);});
|
|
3039
3078
|
var raf=window.requestAnimationFrame&&function(f){return window.requestAnimationFrame(f);};if(raf){var loop=function(){awPlace();raf(loop);};raf(loop);}}
|
|
3040
|
-
|
|
3079
|
+
|
|
3080
|
+
// ---- Setup: the first run --------------------------------------------------------
|
|
3081
|
+
// A new Symbiot shows this before anything else (home.mjs onboarding): what it is, your
|
|
3082
|
+
// AI, where your work is, your agent, every app you use (connected, or "I don't use
|
|
3083
|
+
// it"), your documents, then Home. Where you are is kept, so closing it resumes there.
|
|
3084
|
+
var ONB=null,ONB_T=null,ONB_NAMES={welcome:'Welcome',ai:'Your AI',work:'Your work',agent:'Your agent',apps:'Your apps',docs:'Documents',done:'Done'};
|
|
3085
|
+
function onbLoad(fresh){return api('/api/onboarding'+(fresh?'?fresh=1':'')).then(function(o){ONB=o;var el=$('onb');if(!el)return;if(!o||!o.pending){el.classList.add('hidden');document.body.classList.remove('lq-onb');if(ONB_T){clearInterval(ONB_T);ONB_T=null;}return;}
|
|
3086
|
+
el.classList.remove('hidden');document.body.classList.add('lq-onb');onbRender();
|
|
3087
|
+
if(!ONB_T)ONB_T=setInterval(function(){if(ONB&&ONB.pending&&['ai','work','agent','apps'].indexOf(ONB.step)>=0&&!onbBusy())onbLoad(ONB.step==='ai');},2500);}).catch(function(){});}
|
|
3088
|
+
// not while you're typing in it
|
|
3089
|
+
function onbBusy(){var a=document.activeElement;return !!(a&&$('onb').contains(a)&&(a.tagName==='INPUT'||a.tagName==='SELECT'));}
|
|
3090
|
+
function onbGo(step){api('/api/onboarding/set',{step:step}).then(function(o){ONB=o;onbRender();var b=$('onbbody');if(b&&b.parentNode)b.parentNode.scrollTop=0;});}
|
|
3091
|
+
function onbNav(prev,next,label,ok){return "<div class='onbnav'>"+(prev?"<button class='back' data-go='"+prev+"'>Back</button>":"")+"<span class='sp'></span>"+(next?"<button class='act' data-go='"+next+"'"+(ok===false?' disabled':'')+">"+esc(label||'Continue')+"</button>":"")+"</div>";}
|
|
3092
|
+
function onbRender(){var o=ONB,b=$('onbbody'),d=$('onbdots');if(!o||!b)return;var st=o.steps||[],k=st.indexOf(o.step);
|
|
3093
|
+
d.innerHTML=st.map(function(x,i){return "<i class='"+(i===k?'on':i<k?'past':'')+"' title='"+esc(ONB_NAMES[x]||x)+"'></i>";}).join('');
|
|
3094
|
+
var h='';
|
|
3095
|
+
if(o.step==='welcome'){
|
|
3096
|
+
h="<img class='onborb' src='/favicon.svg?look="+encodeURIComponent(lqLookGet()||'ferro')+"' alt=''><h2 id='onbt'>Meet Symbiot</h2><p class='onbl'>It works alongside you, so your time goes on what only you can do.</p><div class='onbcards'>"+
|
|
3097
|
+
[['It watches','What you work on and what comes in: your projects, your mail, chats and code.'],['It works','It hands tasks to your coding agent, which does them on its own and tells you what it did.'],['It asks','Only what only you can do: a sign-in, a decision, your OK before anything goes out.']].map(function(c){return "<div class='onbcard'><b>"+c[0]+"</b><span>"+c[1]+"</span></div>";}).join('')+
|
|
3098
|
+
"</div><p class='onbnote'>Setup takes a few minutes: your AI, your work, your agent and every app you use. Symbiot starts finding your projects meanwhile.</p>"+onbNav('', 'ai', 'Set it up');}
|
|
3099
|
+
else if(o.step==='ai'){var a=o.ai||{},c=a.claude||{};
|
|
3100
|
+
h="<h2 id='onbt'>Your AI</h2><p class='onbl'>Symbiot thinks with an AI: it reads what comes in, talks with you and writes for you.</p>";
|
|
3101
|
+
if(a.connected)h+="<div class='onbok'><i>✓</i><div>Symbiot is using <b>"+esc(a.line)+"</b>"+(a.provider==='claude'?". It’s the Claude account you’re signed in with in Claude Code: no key, nothing extra to pay.":".")+"</div></div>";
|
|
3102
|
+
else h+="<div class='onbbox'><h3>Your Claude subscription (recommended)</h3><p>"+(c.installed?"Claude Code is on this computer but not signed in. Open a terminal, run <code>claude</code> once and sign in with your Claude account.":"Install Claude Code (<code>npm install -g @anthropic-ai/claude-code</code>), run <code>claude</code> once and sign in with your Claude account.")+" No key, and nothing extra to pay.</p><button class='ghost' id='onbrecheck'>I’ve signed in: check again</button></div>"+
|
|
3103
|
+
"<div class='onbbox'><h3>Or an API key, or a free local model</h3><div class='onbrow'><select id='onbprov'><option value='anthropic'>Claude (Anthropic) API key</option><option value='openai'>OpenAI API key</option><option value='gemini'>Gemini API key</option><option value='ollama'>Local model (Ollama), no key</option></select><input id='onbkey' type='password' placeholder='paste your key'><button class='ghost' id='onbconnect'>Connect</button></div><p id='onbaimsg' style='margin:8px 0 0'></p></div>";
|
|
3104
|
+
h+=onbNav('welcome','work','Continue',!!a.connected);}
|
|
3105
|
+
else if(o.step==='work'){var w=o.work||{};
|
|
3106
|
+
h="<h2 id='onbt'>Your work</h2><p class='onbl'>Symbiot finds the projects on this computer, so it knows what you’re working on and where to send each task.</p>";
|
|
3107
|
+
h+=w.searching?"<p><span class='onbspin'></span>Looking through your folders"+(w.total?": "+w.done+" of "+w.total+" checked":"")+"…</p>":"<p>"+(w.count?"Found <b>"+w.count+"</b> project"+(w.count>1?'s':'')+".":"No projects found yet. Add the folder they’re in.")+"</p>";
|
|
3108
|
+
if(w.repos&&w.repos.length)h+="<div class='onbrepos'>"+w.repos.slice(0,40).map(function(r){return "<span title='"+esc(r.path)+"'>"+esc(r.name)+"</span>";}).join('')+(w.count>40?"<span>and "+(w.count-40)+" more</span>":"")+"</div>";
|
|
3109
|
+
h+="<div class='onbbox' style='margin-top:14px'><p>Looking in: "+((w.roots||[]).map(function(r){return "<code>"+esc(r)+"</code>";}).join(' ')||'your home folder')+"</p><div class='onbrow'><input id='onbroot' placeholder='Add a folder, e.g. ~/work'><button class='ghost' id='onbaddroot'>Add</button></div></div>";
|
|
3110
|
+
h+=onbNav('ai','agent');}
|
|
3111
|
+
else if(o.step==='agent'){var g=o.agent||{},cur=(g.agents||[]).find(function(x){return x.tmpl===g.cmd;}),pk=g.pick;
|
|
3112
|
+
h="<h2 id='onbt'>Your agent</h2><p class='onbl'>Your coding agent takes the tasks Symbiot hands it and does them on its own, inside that project. Symbiot stops it from pushing to main, publishing, deleting outside the project or touching your keys.</p>";
|
|
3113
|
+
if(g.cmd)h+="<div class='onbok'><i>✓</i><div>Tasks go to <b>"+esc(cur?cur.label:'your agent')+"</b>.</div></div>";
|
|
3114
|
+
else if(pk)h+="<div class='onbbox'><h3>"+esc(pk.name)+" is on this computer</h3><p>"+esc(pk.label)+"</p><button class='act' id='onbuse' data-cmd='"+esc(pk.tmpl)+"'>Use "+esc(pk.name)+"</button></div>";
|
|
3115
|
+
else h+="<div class='onbbox'><h3>No coding agent found</h3><p>Install Claude Code (<code>npm install -g @anthropic-ai/claude-code</code>) to let Symbiot hand work over. You can carry on and add it later in Settings.</p></div>";
|
|
3116
|
+
h+=onbNav('work','apps');}
|
|
3117
|
+
else if(o.step==='apps'){var ap=o.apps||[],left=ap.filter(function(x){return !x.skipped&&x.state==='off';}).length;
|
|
3118
|
+
h="<h2 id='onbt'>Your apps</h2><p class='onbl'>Connect every app you use, so Symbiot sees what comes in. Each opens in a window to sign in; Symbiot keeps the sign-in on this computer. Mark the ones you don’t use.</p>";
|
|
3119
|
+
(o.groups||[]).forEach(function(gp){var rows=ap.filter(function(x){return x.group===gp;});if(!rows.length)return;h+="<div class='onbgrp'>"+esc(gp)+"</div>";
|
|
3120
|
+
rows.forEach(function(x){var stt=x.skipped?"<span class='st'>Not used</span><button class='ghost' data-unskip='"+esc(x.id)+"'>Undo</button>":
|
|
3121
|
+
x.state==='off'?"<button class='act' data-link='"+esc(x.id)+"'>Connect</button><button class='ghost' data-skip='"+esc(x.id)+"'>I don’t use it</button>":
|
|
3122
|
+
x.state==='ok'?"<span class='st ok'>✓ Connected"+(x.agents?" · your agents can use it too":"")+"</span>":
|
|
3123
|
+
"<span class='st wait'>"+(x.state==='signedout'?'Signed out':'Sign in in the window that opened')+"</span><button class='ghost' data-check='"+esc(x.id)+"'>Check</button><button class='ghost' data-link='"+esc(x.id)+"'>Open again</button>";
|
|
3124
|
+
h+="<div class='onbapp'><span class='nm'>"+esc(x.name)+"</span>"+stt+"</div>";});});
|
|
3125
|
+
h+="<div class='onbfoot'><p class='onbnote'>"+(left?left+" left to decide. ":"All decided. ")+(left?"<button class='ghost' id='onbskiprest'>I don’t use the rest</button>":"")+"</p>"+onbNav('agent','docs','Continue',!!o.decided)+"</div>";}
|
|
3126
|
+
else if(o.step==='docs'){var fs=(o.docs||{}).folders||[];
|
|
3127
|
+
h="<h2 id='onbt'>Your documents</h2><p class='onbl'>Optional. A folder of your company’s documents lets Symbiot quote them when you ask, and spot where they disagree (a deadline on someone’s leave, a price told two ways).</p>";
|
|
3128
|
+
if(fs.length)h+="<div class='onbok'><i>✓</i><div>"+fs.map(function(f){return "<code>"+esc(f)+"</code>";}).join(' ')+"</div></div>";
|
|
3129
|
+
h+="<div class='onbbox'><div class='onbrow'><input id='onbdoc' placeholder='A folder, e.g. ~/Company'><button class='ghost' id='onbadddoc'>Add</button></div><p id='onbdocmsg' style='margin:8px 0 0'></p></div>";
|
|
3130
|
+
h+=onbNav('apps','done',fs.length?'Continue':'Skip for now');}
|
|
3131
|
+
else{var conn=(o.apps||[]).filter(function(x){return x.state!=='off'&&!x.skipped;}).length;
|
|
3132
|
+
h="<h2 id='onbt'>You’re set up</h2><p class='onbl'>Here’s what Symbiot has to work with. You can change any of it in Settings.</p><div class='onbbox'><p>"+
|
|
3133
|
+
[(o.ai&&o.ai.connected?'✓ ':'· ')+'AI: '+esc((o.ai&&o.ai.line)||'not connected'),(((o.work||{}).count)?'✓ ':'· ')+((o.work||{}).count||0)+' project'+(((o.work||{}).count)===1?'':'s'),(o.agent&&o.agent.cmd?'✓ ':'· ')+'Agent: '+(o.agent&&o.agent.cmd?'ready':'none yet'),(conn?'✓ ':'· ')+(conn?conn+' app'+(conn===1?'':'s')+' connected':'no apps connected yet')].join('<br>')+
|
|
3134
|
+
"</p></div><h3 style='margin:18px 0 4px;font:600 15px var(--sans);color:var(--bone)'>Finding your way</h3><ul class='onbtour'><li>Each orb is a place: tap it to open it, right-click to go back.</li><li>Amber means something needs you.</li><li>Talk to Symbiot at the bottom: ask anything, or tell it what to do.</li></ul>"+
|
|
3135
|
+
"<div class='onbnav'><button class='back' data-go='docs'>Back</button><span class='sp'></span><button class='act' id='onbfinish'>Open Symbiot</button></div>";}
|
|
3136
|
+
b.innerHTML=h;
|
|
3137
|
+
b.querySelectorAll('[data-go]').forEach(function(x){x.addEventListener('click',function(){onbGo(x.getAttribute('data-go'));});});
|
|
3138
|
+
var on=function(id,f){var x=$(id);if(x)x.addEventListener('click',f);};
|
|
3139
|
+
on('onbrecheck',function(){onbLoad(true);});
|
|
3140
|
+
on('onbconnect',function(){var pv=$('onbprov').value,m=$('onbaimsg');m.textContent='Checking…';api('/api/connect',{provider:pv,key:$('onbkey').value.trim(),model:''}).then(function(r){m.textContent=r.message||'';onbLoad(true);});});
|
|
3141
|
+
var pvs=$('onbprov');if(pvs)pvs.addEventListener('change',function(){$('onbkey').style.display=pvs.value==='ollama'?'none':'';});
|
|
3142
|
+
on('onbaddroot',function(){var v=$('onbroot').value.trim();if(!v)return;api('/api/scanroots/add',{path:v}).then(function(){onbLoad();});});
|
|
3143
|
+
on('onbuse',function(){var c=$('onbuse').getAttribute('data-cmd');api('/api/agentcmd',{cmd:c}).then(function(){onbLoad();});});
|
|
3144
|
+
on('onbskiprest',function(){api('/api/onboarding/set',{skipRest:true}).then(function(r){ONB=r;onbRender();});});
|
|
3145
|
+
on('onbadddoc',function(){var v=$('onbdoc').value.trim();if(!v)return;api('/api/knowledge/add',{path:v}).then(function(r){$('onbdocmsg').textContent=r&&r.error?r.error:'';onbLoad();});});
|
|
3146
|
+
on('onbfinish',function(){api('/api/onboarding/done',{}).then(function(){onbLoad();LQ.fresh=true;lqLoad(true);});});
|
|
3147
|
+
b.querySelectorAll('[data-link]').forEach(function(x){x.addEventListener('click',function(){x.disabled=true;api('/api/links/link',{id:x.getAttribute('data-link')}).then(function(){onbLoad();});});});
|
|
3148
|
+
b.querySelectorAll('[data-check]').forEach(function(x){x.addEventListener('click',function(){x.disabled=true;api('/api/links/check',{id:x.getAttribute('data-check')}).then(function(){onbLoad();});});});
|
|
3149
|
+
b.querySelectorAll('[data-skip]').forEach(function(x){x.addEventListener('click',function(){api('/api/onboarding/set',{skip:x.getAttribute('data-skip')}).then(function(r){ONB=r;onbRender();});});});
|
|
3150
|
+
b.querySelectorAll('[data-unskip]').forEach(function(x){x.addEventListener('click',function(){api('/api/onboarding/set',{unskip:x.getAttribute('data-unskip')}).then(function(r){ONB=r;onbRender();});});});}
|
|
3151
|
+
function lqIcon(l){var k=document.querySelector('link[rel=icon]');if(k)k.setAttribute('href','/favicon.svg?look='+encodeURIComponent(l||'ferro'));}
|
|
3152
|
+
function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqIcon(lqLookGet());lqTheme();bd.classList.add('lq-liquid');
|
|
3041
3153
|
['(prefers-color-scheme: light)','(prefers-contrast: more)','(forced-colors: active)','(prefers-reduced-transparency: reduce)','(prefers-reduced-motion: reduce)'].forEach(function(q){try{var mq=window.matchMedia&&window.matchMedia(q);if(mq&&mq.addEventListener)mq.addEventListener('change',lqTheme);else if(mq&&mq.addListener)mq.addListener(lqTheme);}catch(e){}});
|
|
3042
3154
|
if(document.addEventListener){document.addEventListener('contextmenu',function(ev){var b=document.body,t=ev&&ev.target;if(!b||!b.classList||!b.classList.contains('lq-liquid'))return;if(t&&t.closest&&t.closest('input,textarea,select,[contenteditable]'))return;var sel=window.getSelection?String(window.getSelection()):'';if(sel)return;ev.preventDefault();lqBack();});
|
|
3043
3155
|
document.addEventListener('mouseup',function(ev){var b=document.body;if(ev&&ev.button===3&&b&&b.classList&&b.classList.contains('lq-liquid')){ev.preventDefault();lqBack();}});}
|
|
@@ -3066,5 +3178,5 @@ var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAn
|
|
|
3066
3178
|
function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=(GLSOFT||GLSTILL)?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0)lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU
|
|
3067
3179
|
raf(frame);}
|
|
3068
3180
|
|
|
3069
|
-
initGraphEvents();syncP();refresh();
|
|
3181
|
+
initGraphEvents();syncP();refresh();firstTab();lqInit();onbLoad();var rr=$('rerunsetup');if(rr)rr.addEventListener('click',function(){api('/api/onboarding/restart',{}).then(function(){lqSink();onbLoad();});});awayInit();lqDeep();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
|
|
3070
3182
|
</script></body></html>`;
|