symbiot 0.54.4 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +17 -0
- package/away.mjs +113 -0
- package/index.mjs +17 -0
- package/package.json +5 -4
- package/server.mjs +9 -2
- package/tasks.mjs +11 -2
- package/ui.mjs +72 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.1
|
|
6
|
+
|
|
7
|
+
- **No more stray Symbiot windows.** 0.55.0's smoke test called Away like every other endpoint, so each full test run (yours, or an agent's in the symbiot repo) opened Away windows on your screen. Away now opens only when asked to by name, and never while windows are turned off (as in tests).
|
|
8
|
+
- **One Symbiot window, not a pile.** Opening Symbiot again (its icon, `symbiot app`) used to add a window each time. Now the newest window wins and older ones close themselves; a tab that can't close itself says Symbiot is open in a newer window, with **Use this one**. An update's restart that can't get its port bows out instead of lingering unseen.
|
|
9
|
+
- **Away is Super+`** (Super and the key above Tab). Super+S stacks windows on COSMIC, so it was the wrong key; `symbiot away --shortcut` sets up Super+` and clears the old one.
|
|
10
|
+
|
|
11
|
+
## 0.55.0
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
5
16
|
## 0.54.4 — 2026-10-07
|
|
6
17
|
|
|
7
18
|
- 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+`
|
|
647
|
+
|
|
648
|
+
Stepping away? **Super+`** (Super and the key above Tab), 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+` 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+`. On COSMIC that's done for you; elsewhere
|
|
660
|
+
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,113 @@
|
|
|
1
|
+
// symbiot — Away: Symbiot full screen on every screen while you're away from the
|
|
2
|
+
// desk (Super+`, 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+` 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
|
+
if (process.env.SYMBIOT_NO_OPEN === "1") return { open: false, note: "Windows are off here (SYMBIOT_NO_OPEN): tests, and runs that mustn't open any." };
|
|
69
|
+
closeAway();
|
|
70
|
+
const qs = awayQueries(scr.list, now);
|
|
71
|
+
const origin = (String(url).match(/^https?:\/\/[^/?#]+/) || [""])[0];
|
|
72
|
+
qs.forEach((q, i) => {
|
|
73
|
+
const s = scr.list[i], dir = join(CONFIG_DIR, "away", String(i)); grantProfile(dir, origin);
|
|
74
|
+
const args = ["--user-data-dir=" + dir, "--no-first-run", "--no-default-browser-check", "--disable-session-crashed-bubble", "--hide-crash-restore-bubble", "--noerrdialogs", "--kiosk"];
|
|
75
|
+
if (s) args.push(`--window-position=${s.x},${s.y}`, `--window-size=${s.w},${s.h}`); // X places it on its screen
|
|
76
|
+
args.push(url + (url.includes("?") ? "&" : "?") + q + `&si=${i}`);
|
|
77
|
+
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 {}
|
|
78
|
+
});
|
|
79
|
+
return { open: KIDS.length > 0, screens: Math.max(1, scr.list.length) };
|
|
80
|
+
}
|
|
81
|
+
// open: true opens, false closes, "toggle" goes the other way; anything else only
|
|
82
|
+
// says whether it's open (a call that names nothing never opens windows: the smoke
|
|
83
|
+
// test calls every endpoint, and it opened Away on the owner's screen every run).
|
|
84
|
+
function toggleAway(url, open, deps) {
|
|
85
|
+
if (open !== true && open !== false && open !== "toggle") return { open: awayOpen() };
|
|
86
|
+
const want = open === "toggle" ? !awayOpen() : open;
|
|
87
|
+
if (!want) { closeAway(); return { open: false }; }
|
|
88
|
+
return openAway(url, deps);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- the shortcut -------------------------------------------------------------
|
|
92
|
+
// Super+` (the key above Tab) runs `symbiot away`: free on COSMIC, where Super+S
|
|
93
|
+
// stacks windows. COSMIC: a custom shortcut in its config (it reloads it as it
|
|
94
|
+
// changes). Elsewhere it says how to add one.
|
|
95
|
+
const COSMIC_CUSTOM = (home = homedir()) => join(home, ".config", "cosmic", "com.system76.CosmicSettings.Shortcuts", "v1", "custom");
|
|
96
|
+
const KEY = "grave";
|
|
97
|
+
function shortcutLine(cmd) { return ` (modifiers: [Super], key: "${KEY}"): Spawn(${JSON.stringify(cmd)}),`; }
|
|
98
|
+
// The new custom file: whatever was there, minus any Super+` and any earlier one of
|
|
99
|
+
// ours (on another key), plus ours.
|
|
100
|
+
function withShortcut(text, cmd) {
|
|
101
|
+
const body = String(text || "").trim().replace(/^\{/, "").replace(/\}\s*$/, "");
|
|
102
|
+
const kept = body.split("\n").filter((l) => l.trim() && !/\(modifiers:\s*\[\s*Super\s*,?\s*\],\s*key:\s*"grave"\s*\)/.test(l) && !/Spawn\("[^"]*symbiot[^"]* away"\)/.test(l));
|
|
103
|
+
return "{\n" + [...kept.map((l) => l.replace(/\s*$/, "")), shortcutLine(cmd)].join("\n") + "\n}\n";
|
|
104
|
+
}
|
|
105
|
+
function installShortcut({ cmd, home = homedir(), desktop = process.env.XDG_CURRENT_DESKTOP || "" } = {}) {
|
|
106
|
+
if (!/cosmic/i.test(desktop)) return { manual: true, note: `Add a keyboard shortcut in your desktop's settings: Super+\` runs ${cmd}` };
|
|
107
|
+
const f = COSMIC_CUSTOM(home);
|
|
108
|
+
let had = ""; try { had = existsSync(f) ? readFileSync(f, "utf8") : ""; } catch {}
|
|
109
|
+
try { mkdirSync(join(f, ".."), { recursive: true }); writeFileSync(f, withShortcut(had, cmd)); } catch (e) { return { error: "Couldn't write " + f + ": " + ((e && e.message) || e) }; }
|
|
110
|
+
return { ok: true, file: f, replaced: /key:\s*"grave"/.test(had) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
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+` 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+\` (the key above Tab) now opens Away. ${c.d(r.file)}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const r = await viaApp("/api/away", { open: "toggle" });
|
|
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+\` 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.
|
|
4
|
-
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice
|
|
3
|
+
"version": "0.55.1",
|
|
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,8 @@ 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
|
+
let NEWEST_WIN = ""; // the Symbiot window opened last: older ones close themselves (one window, not a pile)
|
|
218
|
+
process.on("exit", () => closeAway()); // Away's windows go with the app (a quit, an update's restart)
|
|
216
219
|
const server = createServer(async (req, res) => {
|
|
217
220
|
const u = new URL(req.url, "http://127.0.0.1");
|
|
218
221
|
if (req.method === "GET" && u.pathname === "/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(EMBEDDED_UI); return; }
|
|
@@ -371,6 +374,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
371
374
|
if (u.pathname === "/api/work/go" && req.method === "POST") return json(res, workGo());
|
|
372
375
|
if (u.pathname === "/api/firststeps") return json(res, firstSteps()); // Settings' first steps: what's set up, in order
|
|
373
376
|
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 })); }
|
|
377
|
+
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
378
|
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
379
|
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
380
|
if (u.pathname === "/api/adapt/use" && req.method === "POST") { const b = await readBody(req); return json(res, noteUse(b)); }
|
|
@@ -415,7 +419,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
415
419
|
if (u.pathname === "/api/run" && req.method === "POST") { const b = await readBody(req); const cmd = ["week", "standup", "todo"].includes(b.cmd) ? b.cmd : "week"; return json(res, cmd === "week" ? await runWeekly(writeup, { notify: false }) : await writeup(cmd)); }
|
|
416
420
|
if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
|
|
417
421
|
// a sandbox (symbiot app --fresh) never offers an update: it would replace your real install
|
|
418
|
-
if (u.pathname === "/api/ping") { if (u.searchParams.get("fresh") === "1" && !SANDBOX) await checkLatest(); return json(res, { version: VERSION, started: SERVER_STARTED, latest: LATEST_VERSION, newer: !SANDBOX && semverGt(LATEST_VERSION, VERSION), ...(UPDATING && UPDATING.retrying ? { retrying: UPDATING } : {}), ...(IN_TERMUX ? { termux: true } : {}), ...(SANDBOX ? { sandbox: true } : {}) }); }
|
|
422
|
+
if (u.pathname === "/api/ping") { if (u.searchParams.get("fresh") === "1" && !SANDBOX) await checkLatest(); const w = u.searchParams.get("w") || ""; if (/^[a-z0-9]{6,20}$/.test(w) && u.searchParams.get("new") === "1") NEWEST_WIN = w; return json(res, { ...(NEWEST_WIN ? { window: NEWEST_WIN } : {}), version: VERSION, started: SERVER_STARTED, latest: LATEST_VERSION, newer: !SANDBOX && semverGt(LATEST_VERSION, VERSION), ...(UPDATING && UPDATING.retrying ? { retrying: UPDATING } : {}), ...(IN_TERMUX ? { termux: true } : {}), ...(SANDBOX ? { sandbox: true } : {}) }); }
|
|
419
423
|
// What's new: after an update, since the version you last saw (until you click Got it);
|
|
420
424
|
// ?latest=1, what the update on offer brings, from its package on npm
|
|
421
425
|
if (u.pathname === "/api/whatsnew") {
|
|
@@ -470,7 +474,10 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
470
474
|
server.on("error", (e) => {
|
|
471
475
|
// Stable port busy (an older instance still exiting during an update, or a
|
|
472
476
|
// second app): retry briefly, then fall back to a random port.
|
|
473
|
-
if (e && e.code === "EADDRINUSE" && tries < 8) { tries++; setTimeout(() => { try { server.listen(PORT, "127.0.0.1"); } catch {} }, 500); }
|
|
477
|
+
if (e && e.code === "EADDRINUSE" && tries < (RELAUNCH ? 40 : 8)) { tries++; setTimeout(() => { try { server.listen(PORT, "127.0.0.1"); } catch {} }, 500); }
|
|
478
|
+
// after an update, the window only ever looks for the stable port: a copy on another
|
|
479
|
+
// port would serve nobody and linger (one did, for hours), so it bows out
|
|
480
|
+
else if (RELAUNCH) process.exit(0);
|
|
474
481
|
else { try { server.listen(0, "127.0.0.1"); } catch {} }
|
|
475
482
|
});
|
|
476
483
|
server.listen(PORT, "127.0.0.1");
|
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
|
-
|
|
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
|
-
|
|
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 and the key above Tab): 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)}
|
|
@@ -2264,8 +2280,17 @@ document.getElementById('autostart').addEventListener('change',saveAutostart);
|
|
|
2264
2280
|
(function(){var h='';for(var i=0;i<24;i++)h+="<option value='"+i+"'>"+(i<10?'0':'')+i+":00</option>";document.getElementById('weeklyhour').innerHTML=h;})();
|
|
2265
2281
|
var SRV_STARTED=null,srvDown=false,updBusy=false;
|
|
2266
2282
|
function ubar(){return document.getElementById('updatebar');}
|
|
2283
|
+
// One Symbiot window, not a pile: each window has an id, says so the first time it
|
|
2284
|
+
// asks, and the server remembers the newest. An older one closes itself; where the
|
|
2285
|
+
// browser won't let it (a tab you opened), it says so and can take over. Away's
|
|
2286
|
+
// windows don't count.
|
|
2287
|
+
var WIN_ID=/[?&]away=1(&|$)/.test(location.search||'')?'':Math.random().toString(36).slice(2,12),WIN_NEW=true; // read from the address: AWAY is set further down
|
|
2288
|
+
function winTakeOver(){WIN_NEW=true;var o=$('winold');if(o)o.remove();heartbeat(false);}
|
|
2289
|
+
function winOld(){try{window.close();}catch(e){}setTimeout(function(){if($('winold')||!document.body)return;var o=document.createElement('div');o.id='winold';o.className='updatebar reconnect show';o.innerHTML="Symbiot is open in a newer window. <button class='ghost' id='wintake'>Use this one</button>";document.body.appendChild(o);var t=$('wintake');if(t)t.onclick=winTakeOver;},400);}
|
|
2267
2290
|
function heartbeat(fresh){
|
|
2268
|
-
|
|
2291
|
+
var q=[];if(fresh)q.push('fresh=1');if(WIN_ID){q.push('w='+WIN_ID);if(WIN_NEW)q.push('new=1');}
|
|
2292
|
+
api('/api/ping'+(q.length?'?'+q.join('&'):'')).then(function(p){
|
|
2293
|
+
if(WIN_ID){WIN_NEW=false;if(p&&p.window&&p.window!==WIN_ID){if(!$('winold'))winOld();return;}}
|
|
2269
2294
|
var b=ubar();
|
|
2270
2295
|
var ve=document.getElementById('ver');if(ve&&p.version){ve.textContent='v'+p.version+(p.sandbox?' · sandbox':'');if(p.sandbox){ve.title='A brand-new Symbiot to try first run in (symbiot app --fresh): its own empty home, none of your data, accounts or memory. It is deleted when you quit.';ve.style.color='var(--amber)';}}
|
|
2271
2296
|
if(SRV_STARTED===null){SRV_STARTED=p.started;appBar(p);}
|
|
@@ -2677,7 +2702,7 @@ LQ.back=true;try{if(s.scene==='work'&&(cur.scene!=='work'||cur.by!==s.by)){LQ.wo
|
|
|
2677
2702
|
if(s.pool){if(s.arch)lqArchive();else lqPool(s.pool);}else if(LQ.mode==='pool')lqSink();}finally{LQ.back=false;}}
|
|
2678
2703
|
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
2704
|
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);}}
|
|
2705
|
+
function lqAct(){if(AWAY)return;LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
|
|
2681
2706
|
function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
|
|
2682
2707
|
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…</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;
|
|
2683
2708
|
// scrolled up to read back: the whole history shows, clear; back at the bottom it rolls on again
|
|
@@ -2699,6 +2724,7 @@ api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me
|
|
|
2699
2724
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
2700
2725
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
2701
2726
|
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;
|
|
2727
|
+
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
2728
|
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
2729
|
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
2730
|
var D=LQ.drops,N=D.length;
|
|
@@ -2799,6 +2825,49 @@ if(slow){var sig=W+'|'+H+'|'+th.look+th.night+th.contrast;for(var q=0;q<o.length
|
|
|
2799
2825
|
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
2826
|
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
2827
|
gl.drawArrays(gl.TRIANGLES,0,3);}
|
|
2828
|
+
|
|
2829
|
+
// ---- Away (Super and the key above Tab) ------------------------------------------------------------
|
|
2830
|
+
// ?away=1 from "symbiot away": this window is one of the screens. One screen: the
|
|
2831
|
+
// orb rests in the middle. More: it bounces across their shared area (gw x gh) at
|
|
2832
|
+
// the same pace in every window from the same start (t0), each drawing it while it's
|
|
2833
|
+
// over its own part (ax, ay, aw x ah), so it crosses from screen to screen. Under it,
|
|
2834
|
+
// the time and what's going on, counts only. Any key or click closes every window.
|
|
2835
|
+
var AWAY=null;
|
|
2836
|
+
(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;};
|
|
2837
|
+
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){}})();
|
|
2838
|
+
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;};
|
|
2839
|
+
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};
|
|
2840
|
+
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;
|
|
2841
|
+
var gx=R+pingpong(v*t*0.83+L/2,L),gy=R+pingpong(v*t*0.56+M/2,M);
|
|
2842
|
+
return {x:(gx-AWAY.ax)*k,y:(gy-AWAY.ay)*k};}
|
|
2843
|
+
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();}
|
|
2844
|
+
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=[];
|
|
2845
|
+
var runs=(w.running||[]).slice(0,4),nr=(w.running||[]).length||h.working||0;
|
|
2846
|
+
out.push(nr?'<b>'+nr+' agent'+(nr===1?'':'s')+' at work</b>':'No agent at work');
|
|
2847
|
+
runs.forEach(function(x){out.push("<span class='awr'>"+esc(x.lane||x.name||'')+': '+esc(x.doing||'working on it')+'</span>');});
|
|
2848
|
+
var y=h.youCount||0;out.push(y?'<b>'+y+' thing'+(y===1?'':'s')+' only you can do</b>':'Nothing needs you');
|
|
2849
|
+
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 '));
|
|
2850
|
+
el.innerHTML=out.join('<br>');}).catch(function(){});}
|
|
2851
|
+
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());
|
|
2852
|
+
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());}
|
|
2853
|
+
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;
|
|
2854
|
+
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';}
|
|
2855
|
+
// Its own screen, full: Chrome lists the screens (its profile lets this address see
|
|
2856
|
+
// them) and puts this window full screen on the si-th, left to right; their places
|
|
2857
|
+
// come from there too (scaled, as the page sees them). No list: full screen where it is.
|
|
2858
|
+
function awayScreen(){var si=0;try{si=Number(new URLSearchParams(location.search).get('si'))||0;}catch(e){}
|
|
2859
|
+
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){}};
|
|
2860
|
+
if(!AWAY||AWAY.n<2||!window.getScreenDetails){fs(null);return;} // one window: full screen where it is
|
|
2861
|
+
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;}
|
|
2862
|
+
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;});
|
|
2863
|
+
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;
|
|
2864
|
+
fs(t);}).catch(function(){fs(null);});}
|
|
2865
|
+
function awayInit(){if(!AWAY||!document.body)return;document.body.classList.add('lq-away');LQ.mode='rest';awayScreen();
|
|
2866
|
+
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);
|
|
2867
|
+
awTick();awStatus();setInterval(awTick,5000);setInterval(awStatus,15000);
|
|
2868
|
+
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(){});};
|
|
2869
|
+
['keydown','mousedown','touchstart','wheel'].forEach(function(t){document.addEventListener(t,bye,true);});
|
|
2870
|
+
var raf=window.requestAnimationFrame&&function(f){return window.requestAnimationFrame(f);};if(raf){var loop=function(){awPlace();raf(loop);};raf(loop);}}
|
|
2802
2871
|
function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
|
|
2803
2872
|
['(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
2873
|
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 +2891,5 @@ var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAn
|
|
|
2822
2891
|
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
2892
|
raf(frame);}
|
|
2824
2893
|
|
|
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();
|
|
2894
|
+
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
2895
|
</script></body></html>`;
|