symbiot 0.54.4 → 0.55.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 CHANGED
@@ -2,6 +2,11 @@
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.55.0
6
+
7
+ - **Away: Super+S.** Symbiot full screen while you're away from the desk: the orb at rest, the time, the agents at work and what each is doing, how many things only you can do, and how much waits on what you watch. Counts only, never anyone's words. Any key or click brings you back. On X11 with more than one screen the orb bounces across all of them; under Wayland (COSMIC, GNOME, KDE) it's one window on your screen, because no app may place windows there. `symbiot away --shortcut` sets up Super+S (on COSMIC it's done for you). Super+Esc still locks.
8
+ - **Approve no longer strands a commit.** It picked a branch name only by what's local, so a name already on GitHub (an earlier PR of that name, merged) had its push refused, and the approved work sat in a commit with no PR. Now it picks a name free on GitHub too, and if a push is still refused, it moves the commit to a fresh name and pushes that.
9
+
5
10
  ## 0.54.4 — 2026-10-07
6
11
 
7
12
  - Removed the "Add a task" row from Tasks, since you add tasks through the bottom chat bar, and moved Send to repos beside Close.
package/README.md CHANGED
@@ -643,6 +643,23 @@ of this: it adapts to you.
643
643
  Everything it learns stays on this computer: `adapt.json` and `mind.json` in
644
644
  Symbiot's config folder, readable by you only.
645
645
 
646
+ ### Away: Super+S
647
+
648
+ Stepping away? **Super+S** (or `symbiot away`) puts Symbiot full screen: the orb at
649
+ rest, the time, and what's going on. That means the agents at work and what each is
650
+ doing, how many things only you can do, and how much waits on what you watch. It shows
651
+ counts only, never anyone's words, so it's fine on a screen across the room. Any key
652
+ or click (or Super+S again) brings you back.
653
+
654
+ On one screen the orb rests in the middle. On X11 desktops with more screens it
655
+ bounces across all of them, one window per screen, with the orb crossing from one to
656
+ the next. Under Wayland (COSMIC, GNOME, KDE) no app may place a window on a screen, so
657
+ it's one window on the screen you're on.
658
+
659
+ `symbiot away --shortcut` sets up Super+S. On COSMIC that's done for you (it replaces
660
+ COSMIC's own Super+S, which stacks windows); elsewhere it tells you what to bind.
661
+ Away isn't a lock: **Super+Esc** still locks the computer.
662
+
646
663
  ## Link your work
647
664
 
648
665
  **Connections** (on the Dashboard, and in Settings) has one entry per standard work site: Gmail,
package/away.mjs ADDED
@@ -0,0 +1,107 @@
1
+ // symbiot — Away: Symbiot full screen on every screen while you're away from the
2
+ // desk (Super+S, or `symbiot away`): the orb at rest, the time, and what's going on
3
+ // (agents at work and what they're doing, how much waits on you), counts only, no
4
+ // one's words. One screen: the orb in the middle. More: it bounces across all of
5
+ // them, one kiosk window per screen, each working out where the orb is from the
6
+ // same clock over the screens' shared area and drawing it while it's on that
7
+ // screen, so it crosses from one to the next with nothing passed between them.
8
+ // That needs windows placed on screens, which only X allows: under Wayland (COSMIC,
9
+ // GNOME, KDE) no app can put a window on a screen, and COSMIC ignored every way tried
10
+ // (X moves, Chrome's position flags, fullscreen on a given screen, a keypress for its
11
+ // own move-to-screen shortcut). So under Wayland it's one window, full screen where
12
+ // you are, the orb in the middle. Each window's profile lets Symbiot's address see the
13
+ // screens and go full screen without a click, so nothing asks.
14
+ // Any key or click, or Super+S again, closes every window. Not a lock: Super+Esc
15
+ // locks the computer.
16
+ import { spawn, spawnSync } from "node:child_process";
17
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { homedir } from "node:os";
20
+ import { CONFIG_DIR, chromeBinary } from "./core.mjs";
21
+
22
+ // The screens, from `xrandr --listmonitors` (X, and XWayland under COSMIC, GNOME and
23
+ // KDE): [{ x, y, w, h }], left to right. [] when it can't tell (then one window).
24
+ function parseMonitors(text) {
25
+ const out = [];
26
+ for (const m of String(text || "").matchAll(/^\s*\d+:\s+\S+\s+(\d+)\/\d+x(\d+)\/\d+\+(-?\d+)\+(-?\d+)/gm)) out.push({ w: +m[1], h: +m[2], x: +m[3], y: +m[4] });
27
+ return out.sort((a, b) => a.x - b.x || a.y - b.y);
28
+ }
29
+ function screens() {
30
+ for (const d of [process.env.DISPLAY, ":0", ":1"].filter(Boolean)) {
31
+ const r = spawnSync("xrandr", ["--listmonitors"], { encoding: "utf8", timeout: 3000, env: { ...process.env, DISPLAY: d } });
32
+ const s = r.status === 0 ? parseMonitors(r.stdout) : [];
33
+ if (s.length) return { list: s, display: d };
34
+ }
35
+ return { list: [], display: "" };
36
+ }
37
+ // Each window's place in the shared area: { x, y, w, h } plus the area's size, as
38
+ // the page's query (?away=1&ax=…&ay=…&gw=…&gh=…&n=…&t0=…).
39
+ function awayQueries(list, t0 = Date.now()) {
40
+ if (!list.length) return [`away=1&n=1&t0=${t0}`];
41
+ const x0 = Math.min(...list.map((s) => s.x)), y0 = Math.min(...list.map((s) => s.y));
42
+ const gw = Math.max(...list.map((s) => s.x + s.w)) - x0, gh = Math.max(...list.map((s) => s.y + s.h)) - y0;
43
+ return list.map((s) => `away=1&n=${list.length}&ax=${s.x - x0}&ay=${s.y - y0}&aw=${s.w}&ah=${s.h}&gw=${gw}&gh=${gh}&t0=${t0}`);
44
+ }
45
+
46
+ let KIDS = [];
47
+ const awayOpen = () => KIDS.some((k) => k.exitCode === null && !k.killed);
48
+ function closeAway() {
49
+ for (const k of KIDS) { try { process.kill(-k.pid, "SIGTERM"); } catch { try { k.kill("SIGTERM"); } catch {} } }
50
+ const n = KIDS.length; KIDS = []; return n;
51
+ }
52
+ // The two permissions an Away window needs, for the app's address only, in the
53
+ // window's own profile: the screens (window_placement) and fullscreen without a
54
+ // click (automatic_fullscreen). Chrome keeps them when it writes the file back.
55
+ function grantProfile(dir, origin) {
56
+ const f = join(dir, "Default", "Preferences"); let p = {}; try { p = JSON.parse(readFileSync(f, "utf8")) || {}; } catch {}
57
+ const ex = (((p.profile ||= {}).content_settings ||= {}).exceptions ||= {});
58
+ for (const k of ["window_placement", "automatic_fullscreen"]) (ex[k] ||= {})[`${origin},*`] = { setting: 1 };
59
+ try { mkdirSync(join(dir, "Default"), { recursive: true }); writeFileSync(f, JSON.stringify(p)); } catch {}
60
+ }
61
+ // Opens a window per screen on the app's url (http://127.0.0.1:port/?t=token), the
62
+ // i-th told it's for screen i (si). Each has a profile of its own under Symbiot's
63
+ // config, so its flags take (a window for a profile already open would join that
64
+ // browser and ignore them).
65
+ const placeable = (env = process.env) => (env.XDG_SESSION_TYPE || "").toLowerCase() === "x11" || (!env.WAYLAND_DISPLAY && !!env.DISPLAY);
66
+ function openAway(url, { chrome = chromeBinary(), scr = placeable() ? screens() : { list: [], display: "" }, now = Date.now() } = {}) {
67
+ if (!chrome) return { error: "Away needs Chrome or Chromium on this computer." };
68
+ closeAway();
69
+ const qs = awayQueries(scr.list, now);
70
+ const origin = (String(url).match(/^https?:\/\/[^/?#]+/) || [""])[0];
71
+ qs.forEach((q, i) => {
72
+ const s = scr.list[i], dir = join(CONFIG_DIR, "away", String(i)); grantProfile(dir, origin);
73
+ const args = ["--user-data-dir=" + dir, "--no-first-run", "--no-default-browser-check", "--disable-session-crashed-bubble", "--hide-crash-restore-bubble", "--noerrdialogs", "--kiosk"];
74
+ if (s) args.push(`--window-position=${s.x},${s.y}`, `--window-size=${s.w},${s.h}`); // X places it on its screen
75
+ args.push(url + (url.includes("?") ? "&" : "?") + q + `&si=${i}`);
76
+ try { const k = spawn(chrome, args, { detached: true, stdio: "ignore", env: { ...process.env, ...(s && scr.display ? { DISPLAY: scr.display } : {}) } }); k.on("exit", () => {}); k.unref(); KIDS.push(k); } catch {}
77
+ });
78
+ return { open: KIDS.length > 0, screens: Math.max(1, scr.list.length) };
79
+ }
80
+ // Open, close (open: false) or, with neither said, the other way round.
81
+ function toggleAway(url, open, deps) {
82
+ const want = open === undefined || open === null ? !awayOpen() : !!open;
83
+ if (!want) { closeAway(); return { open: false }; }
84
+ return openAway(url, deps);
85
+ }
86
+
87
+ // ---- the shortcut -------------------------------------------------------------
88
+ // Super+S runs `symbiot away`. COSMIC: a custom shortcut in its config (it reloads
89
+ // it as it changes; one for Super+S takes over the default there, which stacks
90
+ // windows). Elsewhere it says how to add one.
91
+ const COSMIC_CUSTOM = (home = homedir()) => join(home, ".config", "cosmic", "com.system76.CosmicSettings.Shortcuts", "v1", "custom");
92
+ function shortcutLine(cmd) { return ` (modifiers: [Super], key: "s"): Spawn(${JSON.stringify(cmd)}),`; }
93
+ // The new custom file: whatever was there, minus any Super+S, plus ours.
94
+ function withShortcut(text, cmd) {
95
+ const body = String(text || "").trim().replace(/^\{/, "").replace(/\}\s*$/, "");
96
+ const kept = body.split("\n").filter((l) => l.trim() && !/\(modifiers:\s*\[\s*Super\s*,?\s*\],\s*key:\s*"s"\s*\)/.test(l));
97
+ return "{\n" + [...kept.map((l) => l.replace(/\s*$/, "")), shortcutLine(cmd)].join("\n") + "\n}\n";
98
+ }
99
+ function installShortcut({ cmd, home = homedir(), desktop = process.env.XDG_CURRENT_DESKTOP || "" } = {}) {
100
+ if (!/cosmic/i.test(desktop)) return { manual: true, note: `Add a keyboard shortcut in your desktop's settings: Super+S runs ${cmd}` };
101
+ const f = COSMIC_CUSTOM(home);
102
+ let had = ""; try { had = existsSync(f) ? readFileSync(f, "utf8") : ""; } catch {}
103
+ try { mkdirSync(join(f, ".."), { recursive: true }); writeFileSync(f, withShortcut(had, cmd)); } catch (e) { return { error: "Couldn't write " + f + ": " + ((e && e.message) || e) }; }
104
+ return { ok: true, file: f, replaced: /key:\s*"s"/.test(had) };
105
+ }
106
+
107
+ export { placeable, grantProfile, parseMonitors, screens, awayQueries, openAway, closeAway, awayOpen, toggleAway, withShortcut, installShortcut, COSMIC_CUSTOM };
package/index.mjs CHANGED
@@ -328,6 +328,20 @@ async function viaApp(path, body) {
328
328
  return r.ok ? await r.json() : { error: `Symbiot's app answered ${r.status}.` };
329
329
  } catch (e) { return { error: "Lost Symbiot's app mid-way: " + ((e && e.message) || e) }; }
330
330
  }
331
+ // symbiot away: Symbiot full screen on every screen (again: closes it). --shortcut:
332
+ // Super+S runs it (on COSMIC it's set up; elsewhere it says how).
333
+ async function cmdAway() {
334
+ if (has("shortcut")) {
335
+ const { installShortcut } = await import("./away.mjs");
336
+ const r = installShortcut({ cmd: `${process.execPath} ${process.argv[1]} away` });
337
+ if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; return; }
338
+ console.log(r.manual ? r.note : `${c.g("✓")} Super+S now opens Away${r.replaced ? " (it replaces what Super+S did before)" : ""}. ${c.d(r.file)}`);
339
+ return;
340
+ }
341
+ const r = await viaApp("/api/away", {});
342
+ 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; }
343
+ if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; }
344
+ }
331
345
  async function cmdScreens() {
332
346
  const [sub = "list", a1, a2, a3] = argv.slice(1).filter((x, i, all) => !x.startsWith("--") && all[i - 1] !== "--name");
333
347
  const out = (x) => { console.log(JSON.stringify(x, null, 2)); if (x && x.error) process.exitCode = 1; };
@@ -753,6 +767,8 @@ ${c.b("Usage")}
753
767
  symbiot standup yesterday + today, for standup
754
768
  symbiot todo what's still on your plate
755
769
  symbiot app open the visual app in your browser
770
+ symbiot away [--shortcut] Symbiot full screen on every screen while you're away;
771
+ again closes it. --shortcut: Super+S does it
756
772
  symbiot app --fresh [--keep] try it as someone new: a brand-new Symbiot in
757
773
  a throwaway home, none of your data or accounts
758
774
  symbiot drift [--fetch] what's out of sync / at risk across repos
@@ -808,6 +824,7 @@ async function main() {
808
824
  if (cmd === "logout") return cmdLogout();
809
825
  if (cmd === "whoami" || cmd === "status") return cmdWhoami();
810
826
  if (cmd === "app" || cmd === "ui") return cmdApp();
827
+ if (cmd === "away") return cmdAway();
811
828
  if (cmd === "models" || cmd === "hardware") return cmdModels();
812
829
  if (cmd === "setup-local" || cmd === "setup-ollama") return cmdSetupLocal();
813
830
  if (cmd === "drift") return cmdDrift();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.54.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.",
3
+ "version": "0.55.0",
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": {
7
7
  "symbiot": "index.mjs"
@@ -42,7 +42,8 @@
42
42
  "LICENSE",
43
43
  "fonts",
44
44
  "mapknn.mjs",
45
- "guard.mjs"
45
+ "guard.mjs",
46
+ "away.mjs"
46
47
  ],
47
48
  "engines": {
48
49
  "node": ">=18"
@@ -75,7 +76,7 @@
75
76
  "@anthropic-ai/sdk": "^0.128.0"
76
77
  },
77
78
  "scripts": {
78
- "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/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
79
+ "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/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
79
80
  "lint": "eslint ."
80
81
  },
81
82
  "devDependencies": {
package/server.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  // work, on 127.0.0.1 only and behind the per-install token. (Watch on your phone
4
4
  // listens on your network separately, only for that: phone.mjs.)
5
5
  import { spawn, spawnSync } from "node:child_process";
6
+ import { toggleAway, closeAway } from "./away.mjs";
6
7
  import { homedir } from "node:os";
7
8
  import { join } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
@@ -213,6 +214,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
213
214
  const json = (res, obj) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
214
215
  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);
215
216
  let UPDATING = null; // an update in flight: { target, attempt, retrying? }
217
+ process.on("exit", () => closeAway()); // Away's windows go with the app (a quit, an update's restart)
216
218
  const server = createServer(async (req, res) => {
217
219
  const u = new URL(req.url, "http://127.0.0.1");
218
220
  if (req.method === "GET" && u.pathname === "/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(EMBEDDED_UI); return; }
@@ -371,6 +373,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
371
373
  if (u.pathname === "/api/work/go" && req.method === "POST") return json(res, workGo());
372
374
  if (u.pathname === "/api/firststeps") return json(res, firstSteps()); // Settings' first steps: what's set up, in order
373
375
  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 })); }
376
+ 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)); }
374
377
  if (u.pathname === "/api/home/ask" && req.method === "POST") { const b = await readBody(req); return json(res, await homeAsk(b.question, { images: saveShots(b.images) })); }
375
378
  if (u.pathname === "/api/adapt") return json(res, adaptState({ from: String(u.searchParams.get("from") || ""), commit: u.searchParams.get("commit") === "1", ...(u.searchParams.has("touch") ? { touch: u.searchParams.get("touch") === "1" } : {}) }));
376
379
  if (u.pathname === "/api/adapt/use" && req.method === "POST") { const b = await readBody(req); return json(res, noteUse(b)); }
package/tasks.mjs CHANGED
@@ -433,7 +433,10 @@ function shipChanges(path, texts, opts = {}) {
433
433
  const base = gitDefaultBranch(path), day = new Date().toISOString().slice(0, 10); let branch = ch.branch, fresh = "";
434
434
  const newBranch = () => {
435
435
  const stem = "symbiot/" + branchSlug(!texts.length ? `changes-${day}` : texts.length === 1 ? texts[0] : `${texts.length}-tasks-${day}`);
436
- let b = stem; for (let i = 2; git(path, ["rev-parse", "--verify", "-q", "refs/heads/" + b]).ok; i++) b = `${stem}-${i}`;
436
+ // a name free here and on origin: an earlier day's "7 tasks" may be merged and gone
437
+ // locally but still there, and pushing onto it is refused
438
+ const taken = (b) => git(path, ["rev-parse", "--verify", "-q", "refs/heads/" + b]).ok || git(path, ["rev-parse", "--verify", "-q", "refs/remotes/origin/" + b]).ok;
439
+ let b = stem; for (let i = 2; taken(b); i++) b = `${stem}-${i}`;
437
440
  return b;
438
441
  };
439
442
  if (branch === base) {
@@ -462,7 +465,13 @@ function shipChanges(path, texts, opts = {}) {
462
465
  const noted = (note) => ({ ...out, note: (fresh ? fresh + " " : "") + note });
463
466
  if (opts.push === false) return out;
464
467
  if (!git(path, ["remote", "get-url", "origin"]).ok) return noted("No origin remote — committed locally.");
465
- const ps = git(path, ["push", "-u", "origin", branch], 120000);
468
+ let ps = git(path, ["push", "-u", "origin", branch], 120000);
469
+ // refused because origin's branch of that name went another way (an earlier PR of
470
+ // that name, merged): the commit moves to a fresh name and goes up on that
471
+ if (!ps.ok && /rejected|non-fast-forward|fetch first/i.test(ps.err)) {
472
+ const next = newBranch();
473
+ if (git(path, ["branch", "-m", branch, next]).ok) { branch = next; out.branch = next; ps = git(path, ["push", "-u", "origin", branch], 120000); }
474
+ }
466
475
  if (!ps.ok) return noted("Committed, but the push failed: " + (ps.err.split("\n").filter(Boolean).pop() || "unknown error"));
467
476
  out.pushed = true;
468
477
  if (opts.pr === false) return out;
package/ui.mjs CHANGED
@@ -817,6 +817,22 @@ body.lq-liquid.lq-light ::-webkit-scrollbar-thumb{background:rgba(21,26,33,.16)}
817
817
  body.lq-light .lqatt img{border-color:rgba(21,26,33,.15)}body.lq-light .lqatt button{background:#151A21;color:#FFFFFF}
818
818
  #lqform.drop{outline:2px dashed rgba(242,165,65,.7);outline-offset:4px}
819
819
  .lqmimgs{display:flex;gap:6px;margin-bottom:6px}.lqmimgs img{width:120px;max-height:90px;object-fit:cover;border-radius:8px}
820
+
821
+ /* Away (Super+S): the orb, the time and what's going on, on every screen; nothing else */
822
+ #lqaway{display:none}
823
+ body.lq-away{cursor:none;overflow:hidden}
824
+ body.lq-away #lq{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;display:block!important}
825
+ body.lq-away>*:not(#liquid):not(#lqaway):not(script),body.lq-away #liquid>*:not(#lq){display:none!important}
826
+ body.lq-away #liquid{display:block!important;position:fixed!important;inset:0!important}
827
+ body.lq-away #lqaway{display:block;position:fixed;left:0;top:0;width:440px;text-align:center;pointer-events:none;z-index:5;color:#F3F0EA;text-shadow:0 1px 14px rgba(0,0,0,.65);will-change:transform;transition:opacity .6s}
828
+ #lqaway .awt{font:300 68px/1 var(--sans);letter-spacing:-.03em;font-variant-numeric:tabular-nums}
829
+ #lqaway .awd{margin-top:10px;font:500 12px var(--sans);letter-spacing:.16em;text-transform:uppercase;opacity:.55}
830
+ #lqaway .aws{margin-top:20px;font:400 15px/1.6 var(--sans);opacity:.9}
831
+ #lqaway .aws b{color:var(--amber);font-weight:600}
832
+ #lqaway .awr{display:block;font-size:13.5px;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
833
+ #lqaway .awh{margin-top:16px;font-size:11.5px;opacity:.32}
834
+ body.lq-light #lqaway{color:#151A21;text-shadow:0 1px 12px rgba(255,255,255,.8)}
835
+ body.lq-light #lqaway .aws b{color:#9A5200}
820
836
  /* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
821
837
  .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)}
822
838
  body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
@@ -2677,7 +2693,7 @@ LQ.back=true;try{if(s.scene==='work'&&(cur.scene!=='work'||cur.by!==s.by)){LQ.wo
2677
2693
  if(s.pool){if(s.arch)lqArchive();else lqPool(s.pool);}else if(LQ.mode==='pool')lqSink();}finally{LQ.back=false;}}
2678
2694
  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.scrollTop=0;if(m.focus)m.focus();}}
2679
2695
  function lqSink(){lqRemember();LQ.focus=null;var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
2680
- function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
2696
+ function lqAct(){if(AWAY)return;LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
2681
2697
  function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
2682
2698
  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+"'>"+(m.imgs&&m.imgs.length?"<span class='lqmimgs'>"+m.imgs.map(function(d){return "<img src='"+d+"' alt='your screenshot'>";}).join('')+"</span>":"")+esc(m.text)+stepsHtml(m.steps)+"</div>";}).join('')+(wait?"<div class='lqmsg thinking'>Thinking: recalling what it knows, reading what's here&hellip;</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;
2683
2699
  // scrolled up to read back: the whole history shows, clear; back at the bottom it rolls on again
@@ -2699,6 +2715,7 @@ api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me
2699
2715
  // The physics: each droplet a critically damped spring to its place (no wobble,
2700
2716
  // no overshoot), pushed off its neighbours and the core where they'd overlap.
2701
2717
  function lqStep(){var S0=lqSize();if(S0.w!==LQ.bw||S0.h!==LQ.bh){if(LQ.scene==='work'&&LQ.workData)lqBuildWork();else if(LQ.adapt)lqBuild();}var S=S0,cx=S.w/2,cy=(LQ.scene!=='work'&&LQ.cy)||S.h*0.47,k=0.022,c=2*Math.sqrt(k),th=LQ.theme,still=th.still,rest=LQ.mode==='rest',pool=LQ.mode==='pool',talk=LQ.talking&&!pool;
2718
+ if(AWAY){LQ.mode='rest';rest=true;pool=false;talk=false;var ap=lqAwayPos(S);cx=ap.x;cy=ap.y;} // away: the orb at rest, where the bounce has it
2702
2719
  var core=LQ.core,ctx=cx,cty=talk?S.h-44:cy,ctr=(rest?140:pool?50:talk?54+30*(LQ.talkWeight||0.35):92)*S.s;
2703
2720
  if(!core.x){core.x=cx;core.y=cy;}core.x+=(ctx-core.x)*(still?1:0.06);core.y+=(cty-core.y)*(still?1:0.06);core.cr+=(ctr-core.cr)*(still?1:0.05);
2704
2721
  var D=LQ.drops,N=D.length;
@@ -2799,6 +2816,49 @@ if(slow){var sig=W+'|'+H+'|'+th.look+th.night+th.contrast;for(var q=0;q<o.length
2799
2816
  var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,slow?0:t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
2800
2817
  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);
2801
2818
  gl.drawArrays(gl.TRIANGLES,0,3);}
2819
+
2820
+ // ---- Away (Super+S) ------------------------------------------------------------
2821
+ // ?away=1 from "symbiot away": this window is one of the screens. One screen: the
2822
+ // orb rests in the middle. More: it bounces across their shared area (gw x gh) at
2823
+ // the same pace in every window from the same start (t0), each drawing it while it's
2824
+ // over its own part (ax, ay, aw x ah), so it crosses from screen to screen. Under it,
2825
+ // the time and what's going on, counts only. Any key or click closes every window.
2826
+ var AWAY=null;
2827
+ (function(){try{var q=new URLSearchParams(location.search);if(q.get('away')!=='1')return;var n=function(k,d){var v=Number(q.get(k));return isFinite(v)&&q.get(k)!==null?v:d;};
2828
+ AWAY={n:n('n',1),ax:n('ax',0),ay:n('ay',0),aw:n('aw',0),ah:n('ah',0),gw:n('gw',0),gh:n('gh',0),t0:n('t0',Date.now()),since:Date.now(),shown:Date.now()};}catch(e){}})();
2829
+ var pingpong=function(d,L){if(L<=0)return 0;var m=d%(2*L);if(m<0)m+=2*L;return m<=L?m:2*L-m;};
2830
+ function lqAwayPos(S){var cx=S.w/2,cy=S.h*0.42;if(!AWAY||AWAY.n<2||!AWAY.gw||!AWAY.aw)return {x:cx,y:cy};
2831
+ var k=S.w/AWAY.aw,R=220,L=AWAY.gw-2*R,M=AWAY.gh-2*R,t=(Date.now()-AWAY.t0)/1000,v=95;
2832
+ var gx=R+pingpong(v*t*0.83+L/2,L),gy=R+pingpong(v*t*0.56+M/2,M);
2833
+ return {x:(gx-AWAY.ax)*k,y:(gy-AWAY.ay)*k};}
2834
+ function awShort(t){t=String(t||'');var cut=[' - ',' \u00b7 ',' | '];cut.forEach(function(c){var i=t.indexOf(c);if(i>0)t=t.slice(0,i);});return t.trim();}
2835
+ function awStatus(){var el=$('awstat');if(!el)return;Promise.all([api('/api/home').catch(function(){return {};}),api('/api/work').catch(function(){return {};})]).then(function(r){var h=r[0]||{},w=r[1]||{},out=[];
2836
+ var runs=(w.running||[]).slice(0,4),nr=(w.running||[]).length||h.working||0;
2837
+ out.push(nr?'<b>'+nr+' agent'+(nr===1?'':'s')+' at work</b>':'No agent at work');
2838
+ runs.forEach(function(x){out.push("<span class='awr'>"+esc(x.lane||x.name||'')+': '+esc(x.doing||'working on it')+'</span>');});
2839
+ var y=h.youCount||0;out.push(y?'<b>'+y+' thing'+(y===1?'':'s')+' only you can do</b>':'Nothing needs you');
2840
+ var fd=(h.feeds||[]).filter(function(f){return f.count;}).slice(0,4).map(function(f){return esc(awShort(f.title))+' '+f.count;});if(fd.length)out.push('Waiting: '+fd.join(' \u00b7 '));
2841
+ el.innerHTML=out.join('<br>');}).catch(function(){});}
2842
+ function awTick(){var tm=$('awtime'),dt=$('awdate');if(!tm)return;var d=new Date(),p=function(x){return (x<10?'0':'')+x;};tm.textContent=p(d.getHours())+':'+p(d.getMinutes());
2843
+ dt.textContent=d.toLocaleDateString(undefined,{weekday:'long',day:'numeric',month:'long'})+' \u00b7 away since '+p(new Date(AWAY.since).getHours())+':'+p(new Date(AWAY.since).getMinutes());}
2844
+ function awPlace(){var o=$('lqaway');if(!o||!LQ.core)return;var S=lqSize(),c=LQ.core,h=o.offsetHeight||220,x=c.x-220,below=c.y+c.cr+30,y=below+h<S.h-10?below:c.y-c.cr-30-h;
2845
+ o.style.transform='translate('+Math.round(x)+'px,'+Math.round(y)+'px)';o.style.opacity=(c.x+220<0||c.x-220>S.w)?'0':'1';}
2846
+ // Its own screen, full: Chrome lists the screens (its profile lets this address see
2847
+ // them) and puts this window full screen on the si-th, left to right; their places
2848
+ // come from there too (scaled, as the page sees them). No list: full screen where it is.
2849
+ function awayScreen(){var si=0;try{si=Number(new URLSearchParams(location.search).get('si'))||0;}catch(e){}
2850
+ var fs=function(scr){var el=document.documentElement;try{var p=scr?el.requestFullscreen({screen:scr}):el.requestFullscreen();if(p&&p.catch)p.catch(function(){});}catch(e){}};
2851
+ if(!AWAY||AWAY.n<2||!window.getScreenDetails){fs(null);return;} // one window: full screen where it is
2852
+ window.getScreenDetails().then(function(d){var L=((d&&d.screens)||[]).slice().sort(function(a,b){return a.left-b.left||a.top-b.top;}),t=L[si]||L[0];if(!t){fs(null);return;}
2853
+ var mn=function(f){return Math.min.apply(null,L.map(f));},mx=function(f){return Math.max.apply(null,L.map(f));},x0=mn(function(s){return s.left;}),y0=mn(function(s){return s.top;});
2854
+ AWAY.n=L.length;AWAY.ax=t.left-x0;AWAY.ay=t.top-y0;AWAY.aw=t.width;AWAY.ah=t.height;AWAY.gw=mx(function(s){return s.left+s.width;})-x0;AWAY.gh=mx(function(s){return s.top+s.height;})-y0;
2855
+ fs(t);}).catch(function(){fs(null);});}
2856
+ function awayInit(){if(!AWAY||!document.body)return;document.body.classList.add('lq-away');LQ.mode='rest';awayScreen();
2857
+ var o=document.createElement('div');o.id='lqaway';o.innerHTML="<div class='awt' id='awtime'></div><div class='awd' id='awdate'></div><div class='aws' id='awstat'></div><div class='awh'>Any key or click to come back</div>";document.body.appendChild(o);
2858
+ awTick();awStatus();setInterval(awTick,5000);setInterval(awStatus,15000);
2859
+ 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(){});};
2860
+ ['keydown','mousedown','touchstart','wheel'].forEach(function(t){document.addEventListener(t,bye,true);});
2861
+ var raf=window.requestAnimationFrame&&function(f){return window.requestAnimationFrame(f);};if(raf){var loop=function(){awPlace();raf(loop);};raf(loop);}}
2802
2862
  function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
2803
2863
  ['(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){}});
2804
2864
  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();});
@@ -2822,5 +2882,5 @@ var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAn
2822
2882
  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
2823
2883
  raf(frame);}
2824
2884
 
2825
- initGraphEvents();syncP();refresh();loadMap();firstTab();lqInit();lqDeep();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
2885
+ initGraphEvents();syncP();refresh();loadMap();firstTab();lqInit();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();
2826
2886
  </script></body></html>`;