symbiot 0.57.9 → 0.58.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 +10 -0
- package/README.md +1 -1
- package/ai.mjs +7 -2
- package/claudesetup.mjs +56 -0
- package/desktop.mjs +7 -1
- package/home.mjs +2 -1
- package/index.mjs +3 -1
- package/marketing.mjs +2 -1
- package/package.json +4 -3
- package/server.mjs +12 -4
- package/ui.mjs +55 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.58.1
|
|
6
|
+
|
|
7
|
+
- **No more "couldn't reach the model" after an update.** An update restarted Symbiot in whatever folder had started it, and when that folder was later deleted, Claude Code refused to run there: Symbiot thought you'd signed out of Claude and fell back to an old API key that no longer worked. Symbiot now always runs, and asks Claude Code, from your home folder.
|
|
8
|
+
- **Setup installs Claude Code and signs you in.** If Claude Code isn't on your computer, setup's "Your AI" step has an **Install Claude Code** button (Anthropic's own installer, for Windows, Mac or Linux) and then **Sign in with Claude**, which opens the sign-in page and ticks itself once you've signed in. No terminal at any point. If the page shows a code instead, setup has a box to paste it in. Symbiot also finds Claude Code in `~/.local/bin`, where that installer puts it.
|
|
9
|
+
- **Ready for the desktop app.** Running inside Symbiot's installed app, Symbiot doesn't offer npm updates (the app updates itself) or make shortcuts of its own (the installer made them), and "Start at login" starts the app.
|
|
10
|
+
|
|
11
|
+
## 0.58.0
|
|
12
|
+
|
|
13
|
+
- **Talk to Symbiot instead of typing.** A mic on the talk bar: click it and speak. Your words appear as you say them, and a pause sends them, as Enter would. When you spoke, Symbiot reads its answer out too, in the most natural voice your computer has (typed questions stay quiet), and talking again cuts it off. It uses the speech recognition built into Chrome and Edge: no key, nothing to pay. If the microphone is blocked or missing, it says what to do.
|
|
14
|
+
|
|
5
15
|
## 0.57.9
|
|
6
16
|
|
|
7
17
|
- **On Windows, Symbiot is in the Start menu and on your desktop.** Installing puts a Symbiot shortcut with the orb in both (your real desktop, even when it's in OneDrive), and opens Symbiot right away, into setup. The shortcuts start it through a small hidden script, so no black console window flashes up; `symbiot uninstall` takes them away again. Installing on a Windows desktop now opens Symbiot too, and says so in the terminal.
|
package/README.md
CHANGED
|
@@ -678,7 +678,7 @@ time; **Close** or Esc closes a part. In a text box, or over text you've selecte
|
|
|
678
678
|
right-click still gives the usual menu. Whatever needs you glows **amber**, here and
|
|
679
679
|
on the Dashboard. Talk to it in the bar at the bottom: "open tasks" opens Tasks,
|
|
680
680
|
"go" in the work view starts what's waiting, anything else goes to the same Symbiot
|
|
681
|
-
as every chat. Paste a screenshot into the bar (Ctrl+V) or drop one on it, and
|
|
681
|
+
as every chat. **Or just say it:** click the mic on the bar and speak; your words appear as you talk, and a pause sends them. When you spoke, Symbiot reads its answer out too (typed, it stays quiet), and talking again cuts it off. It uses the speech recognition built into Chrome and Edge, so there is no key and nothing to pay. Paste a screenshot into the bar (Ctrl+V) or drop one on it, and
|
|
682
682
|
Symbiot sees it with what you say (up to 4, with any AI you've connected that reads
|
|
683
683
|
images); if it hands the work to an agent, the agent gets the screenshot too.
|
|
684
684
|
Once you've said something, the chat has its own bar over the input: **Minimize**
|
package/ai.mjs
CHANGED
|
@@ -26,6 +26,10 @@ const PROVIDERS = {
|
|
|
26
26
|
// agents. Whether it's there and signed in: `claude auth status` (a quarter of a
|
|
27
27
|
// second), kept 5 minutes. SYMBIOT_CLAUDE_CMD replaces the command (the tests' stand-in).
|
|
28
28
|
const CLAUDE_BIN = () => process.env.SYMBIOT_CLAUDE_CMD || "claude";
|
|
29
|
+
// Claude Code's own installer puts it in ~/.local/bin, which a desktop session (and
|
|
30
|
+
// Windows) often doesn't have on PATH yet: Symbiot, and the agents it starts, look there too.
|
|
31
|
+
{ const dir = join(homedir(), ".local", "bin"), sep = process.platform === "win32" ? ";" : ":";
|
|
32
|
+
if (existsSync(dir) && !String(process.env.PATH || "").split(sep).includes(dir)) process.env.PATH = dir + sep + (process.env.PATH || ""); }
|
|
29
33
|
// How to run it. Windows: npm installs `claude` as claude.cmd, which Node can only
|
|
30
34
|
// start through cmd.exe (a .exe, from Claude's own installer, it starts directly), so
|
|
31
35
|
// it's found with `where` and its arguments are quoted for cmd.exe.
|
|
@@ -52,7 +56,8 @@ function claudeState(fresh = false) {
|
|
|
52
56
|
let installed = !!process.env.SYMBIOT_CLAUDE_CMD || hasCmd("claude"), signedIn = false;
|
|
53
57
|
if (installed) {
|
|
54
58
|
try {
|
|
55
|
-
|
|
59
|
+
// from home: Claude Code refuses to run in a folder that has since been deleted
|
|
60
|
+
const r = claudeSpawn(["auth", "status"], { cwd: homedir(), encoding: "utf8", timeout: 8000, env: process.env }, true);
|
|
56
61
|
if (r.error && r.error.code === "ENOENT") installed = false;
|
|
57
62
|
else { const j = JSON.parse(String(r.stdout || "").trim() || "{}"); signedIn = !!j.loggedIn && j.authMethod !== "apiKey"; }
|
|
58
63
|
} catch {}
|
|
@@ -308,4 +313,4 @@ async function connectProvider(b) {
|
|
|
308
313
|
return saveConfig(cfg) ? { ok: true, message: `Connected: ${PROVIDERS[provider].label} · ${model}` } : { ok: false, message: "Couldn't write the config file." };
|
|
309
314
|
}
|
|
310
315
|
|
|
311
|
-
export { PROVIDERS, AI_UI, resolveProvider, write, validate, connectProvider, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel , claudeState, claudeReady, claudeHelp , claudeCommand };
|
|
316
|
+
export { PROVIDERS, AI_UI, resolveProvider, write, validate, connectProvider, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel , claudeState, claudeReady, claudeHelp , claudeCommand , claudeSpawn };
|
package/claudesetup.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// symbiot — Claude Code, set up from Symbiot's own setup, for someone who has never
|
|
2
|
+
// opened a terminal: Anthropic's installer (Windows: install.ps1 in PowerShell; Mac
|
|
3
|
+
// and Linux: install.sh), then `claude auth login --claudeai`, which opens the
|
|
4
|
+
// sign-in page in the browser and finishes by itself once they've signed in. If the
|
|
5
|
+
// page shows a code instead (the browser couldn't hand it back), setup has a box for
|
|
6
|
+
// it, which goes to the sign-in that's waiting. SYMBIOT_CLAUDE_INSTALL_CMD replaces
|
|
7
|
+
// the installer (tests).
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { claudeState, claudeSpawn } from "./ai.mjs";
|
|
11
|
+
|
|
12
|
+
const S = { installing: false, installError: "", signingIn: false, signinError: "", log: "", child: null };
|
|
13
|
+
const tail = (s) => String(s || "").replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").slice(-1500);
|
|
14
|
+
|
|
15
|
+
function installCmd(platform = process.platform) {
|
|
16
|
+
if (process.env.SYMBIOT_CLAUDE_INSTALL_CMD) return { cmd: process.env.SYMBIOT_CLAUDE_INSTALL_CMD, args: [], shell: true };
|
|
17
|
+
if (platform === "win32") return { cmd: "powershell", args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "irm https://claude.ai/install.ps1 | iex"], shell: false };
|
|
18
|
+
return { cmd: "bash", args: ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], shell: false };
|
|
19
|
+
}
|
|
20
|
+
function installClaude() {
|
|
21
|
+
if (S.installing) return claudeSetup();
|
|
22
|
+
S.installing = true; S.installError = ""; S.log = "";
|
|
23
|
+
const c = installCmd();
|
|
24
|
+
let ch; try { ch = spawn(c.cmd, c.args, { cwd: homedir(), shell: c.shell, windowsHide: true, env: process.env }); } catch (e) { S.installing = false; S.installError = String((e && e.message) || e); return claudeSetup(); }
|
|
25
|
+
ch.stdout.on("data", (d) => { S.log = tail(S.log + d); }); ch.stderr.on("data", (d) => { S.log = tail(S.log + d); });
|
|
26
|
+
ch.on("error", (e) => { S.installing = false; S.installError = String((e && e.message) || e); });
|
|
27
|
+
ch.on("close", (code) => {
|
|
28
|
+
S.installing = false;
|
|
29
|
+
const st = claudeState(true);
|
|
30
|
+
if (code !== 0 || !st.installed) S.installError = `The installer didn't finish (${code === 0 ? "Claude Code wasn't found after it" : "exit " + code}). ${S.log.trim().split("\n").pop() || ""}`.trim();
|
|
31
|
+
});
|
|
32
|
+
return claudeSetup();
|
|
33
|
+
}
|
|
34
|
+
function signInClaude() {
|
|
35
|
+
if (S.signingIn) return claudeSetup();
|
|
36
|
+
S.signingIn = true; S.signinError = "";
|
|
37
|
+
let ch; try { ch = claudeSpawn(["auth", "login", "--claudeai"], { cwd: homedir(), stdio: ["pipe", "pipe", "pipe"], env: process.env }); } catch (e) { S.signingIn = false; S.signinError = String((e && e.message) || e); return claudeSetup(); }
|
|
38
|
+
S.child = ch;
|
|
39
|
+
let out = ""; ch.stdout.on("data", (d) => { out = tail(out + d); }); ch.stderr.on("data", (d) => { out = tail(out + d); });
|
|
40
|
+
ch.on("error", (e) => { S.signingIn = false; S.child = null; S.signinError = String((e && e.message) || e); });
|
|
41
|
+
ch.on("close", () => { S.signingIn = false; S.child = null; if (!claudeState(true).signedIn) S.signinError = "Signing in didn't finish. Try again, and sign in on the page that opens."; });
|
|
42
|
+
return claudeSetup();
|
|
43
|
+
}
|
|
44
|
+
// the code the sign-in page shows, when the browser couldn't hand it back
|
|
45
|
+
function sendClaudeCode(code) {
|
|
46
|
+
const c = String(code || "").trim();
|
|
47
|
+
if (!c || !S.child || !S.child.stdin) return { error: "There's no sign-in waiting for a code. Click Sign in with Claude first." };
|
|
48
|
+
try { S.child.stdin.write(c + "\n"); } catch (e) { return { error: String((e && e.message) || e) }; }
|
|
49
|
+
return claudeSetup();
|
|
50
|
+
}
|
|
51
|
+
function claudeSetup(fresh = false) {
|
|
52
|
+
const st = claudeState(fresh);
|
|
53
|
+
return { installed: st.installed, signedIn: st.signedIn, installing: S.installing, signingIn: S.signingIn, ...(S.installError ? { installError: S.installError } : {}), ...(S.signinError ? { signinError: S.signinError } : {}) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { installCmd, installClaude, signInClaude, sendClaudeCode, claudeSetup };
|
package/desktop.mjs
CHANGED
|
@@ -119,7 +119,13 @@ function autostartFile(platform = OS, home = homedir()) {
|
|
|
119
119
|
if (platform === "android") return join(home, ".termux", "boot", "symbiot");
|
|
120
120
|
return join(home, ".config", "autostart", "symbiot.desktop");
|
|
121
121
|
}
|
|
122
|
-
function autostartContent(node, script, platform = OS, path = process.env.PATH || "") {
|
|
122
|
+
function autostartContent(node, script, platform = OS, path = process.env.PATH || "", exe = process.env.SYMBIOT_DESKTOP_EXE || "") {
|
|
123
|
+
if (exe) { // the installed app: it starts its tray and Symbiot itself
|
|
124
|
+
const q = (s) => '"' + String(s).replace(/(["`$\\])/g, "\\$1") + '"';
|
|
125
|
+
if (platform === "win32") return ["@echo off", `start "" "${exe}"`, ""].join("\r\n");
|
|
126
|
+
if (platform === "darwin") return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>co.symbiot.app</string><key>ProgramArguments</key><array><string>${String(exe).replace(/&/g, "&").replace(/</g, "<")}</string></array><key>RunAtLoad</key><true/></dict></plist>\n`;
|
|
127
|
+
return ["[Desktop Entry]", "Type=Application", "Name=Symbiot", `Exec=${q(exe)}`, "Terminal=false", "NoDisplay=true", "X-GNOME-Autostart-enabled=true", ""].join("\n");
|
|
128
|
+
}
|
|
123
129
|
if (platform === "android-app") return "While this file exists, Symbiot's Android app starts itself when the phone starts.\n";
|
|
124
130
|
if (platform === "darwin") {
|
|
125
131
|
const x = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
package/home.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// The app's sections (Map, Tasks, Agents, Week…) are shaped by adapt.mjs from
|
|
10
10
|
// how you use them; this is the live data around them. Home's talk goes to the
|
|
11
11
|
// same Symbiot as every chat (mind.mjs converse), told what home shows.
|
|
12
|
+
import { claudeSetup } from "./claudesetup.mjs";
|
|
12
13
|
import { join } from "node:path";
|
|
13
14
|
import { homedir } from "node:os";
|
|
14
15
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
@@ -734,7 +735,7 @@ function onboarding({ fresh = false } = {}) {
|
|
|
734
735
|
const det = tryOr(() => detectHandoffs(), { agents: [] }), cmd = tryOr(() => handoffCmd(), "");
|
|
735
736
|
return {
|
|
736
737
|
pending: !!o.pending, step: ONB_STEPS.includes(o.step) ? o.step : "welcome", steps: ONB_STEPS,
|
|
737
|
-
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 },
|
|
738
|
+
ai: { connected: !!r, provider: r ? r.provider : "", line: r ? `${PROVIDERS[r.provider].label}${r.model ? " · " + r.model : ""}` : "", claude: tryOr(() => claudeSetup(), st ? { installed: st.installed, signedIn: st.signedIn } : null) },
|
|
738
739
|
work: { searching: reps.searching, at: reps.at || 0, 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)) },
|
|
739
740
|
agent: { cmd, pick: tryOr(() => pickAgent(), null), agents: (det.agents || []).map((a) => ({ label: a.label, tmpl: a.tmpl })) },
|
|
740
741
|
apps, groups: links.groups || [], decided: apps.every((a) => a.skipped || a.state !== "off"),
|
package/index.mjs
CHANGED
|
@@ -870,7 +870,9 @@ async function main() {
|
|
|
870
870
|
if (cmd === "login" || cmd === "auth") return cmdLogin();
|
|
871
871
|
if (cmd === "logout") return cmdLogout();
|
|
872
872
|
if (cmd === "whoami" || cmd === "status") return cmdWhoami();
|
|
873
|
-
|
|
873
|
+
// the app runs from home: whatever folder started it may be deleted later (a worktree,
|
|
874
|
+
// an old copy npm replaced), and Claude Code won't run from a deleted folder
|
|
875
|
+
if (cmd === "app" || cmd === "ui") { try { process.chdir(homedir()); } catch {} return cmdApp(); }
|
|
874
876
|
if (cmd === "away") return cmdAway();
|
|
875
877
|
if (cmd === "open") return cmdOpen();
|
|
876
878
|
if (cmd === "uninstall") return cmdUninstall();
|
package/marketing.mjs
CHANGED
|
@@ -113,7 +113,8 @@ function draftFiles(dir = MARKETING_DIR, names = [], { max = 30 } = {}) {
|
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
walk(root, 0);
|
|
116
|
-
|
|
116
|
+
// newest first; two written in the same moment, by name, so the order never depends on timing
|
|
117
|
+
return out.sort((a, b) => b.at - a.at || String(a.rel || a.file || "").localeCompare(String(b.rel || b.file || ""))).slice(0, max);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// ---- a draft as the post it will be ---------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.1",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"icon.svg",
|
|
50
50
|
"scanworker.mjs",
|
|
51
51
|
"estimate.mjs",
|
|
52
|
-
"icon.ico"
|
|
52
|
+
"icon.ico",
|
|
53
|
+
"claudesetup.mjs"
|
|
53
54
|
],
|
|
54
55
|
"engines": {
|
|
55
56
|
"node": ">=18"
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
83
84
|
},
|
|
84
85
|
"scripts": {
|
|
85
|
-
"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/urgent.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/windows.mjs && node test/marketing.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
86
|
+
"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/urgent.mjs && node test/work.mjs && node test/mapknn.mjs && node test/away.mjs && node test/subscription.mjs && node test/claudesetup.mjs && node test/launcher.mjs && node test/onboarding.mjs && node test/windows.mjs && node test/marketing.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
86
87
|
"lint": "eslint .",
|
|
87
88
|
"postinstall": "node postinstall.mjs"
|
|
88
89
|
},
|
package/server.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (ui.mjs) and its /api routes, each a thin call into the module that does the
|
|
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
|
+
import { claudeSetup, installClaude, signInClaude, sendClaudeCode } from "./claudesetup.mjs";
|
|
5
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
7
|
import { toggleAway, closeAway } from "./away.mjs";
|
|
7
8
|
import { homedir } from "node:os";
|
|
@@ -146,7 +147,9 @@ function readBytes(req, max) {
|
|
|
146
147
|
function appLink(url) { return url.replace(/^http:\/\//, "symbiot://"); }
|
|
147
148
|
// Symbiot running in Termux (not the Android app's own)
|
|
148
149
|
const IN_TERMUX = process.platform === "android" && process.env.SYMBIOT_ANDROID_APP !== "1";
|
|
149
|
-
const SANDBOX = !!process.env.SYMBIOT_SANDBOX;
|
|
150
|
+
const SANDBOX = !!process.env.SYMBIOT_SANDBOX;
|
|
151
|
+
// the installed app (symbiot-desktop): it updates itself and made its own shortcuts
|
|
152
|
+
const DESKTOP = process.env.SYMBIOT_DESKTOP === "1"; // symbiot app --fresh (sandbox.mjs)
|
|
150
153
|
const FIRST_RUN = !existsSync(CONFIG_PATH); // no config yet when Symbiot started: a brand-new install
|
|
151
154
|
function openApp(url) {
|
|
152
155
|
try {
|
|
@@ -228,7 +231,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
228
231
|
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);
|
|
229
232
|
// an installed Symbiot keeps its app-menu entry pointing at itself (a new Node, a moved
|
|
230
233
|
// install); not a copy run from a checkout or a test
|
|
231
|
-
if (/[\\/]node_modules[\\/]symbiot[\\/]/.test(bin || "") && !SANDBOX) { try { installLauncher({ script: bin }); } catch {} }
|
|
234
|
+
if (/[\\/]node_modules[\\/]symbiot[\\/]/.test(bin || "") && !SANDBOX && !process.env.SYMBIOT_NO_LAUNCHER) { try { installLauncher({ script: bin }); } catch {} }
|
|
232
235
|
if (FRESH) { try { const cf = loadConfig(); if (!cf.onboarding) { cf.onboarding = { pending: true, step: "welcome", skipped: [] }; saveConfig(cf); } } catch {} }
|
|
233
236
|
// your projects: kept, and found again off the main thread, so nothing waits on a search
|
|
234
237
|
setScanOptions({ cache: true }); REPO_STATE.onChange = () => { try { homeState({ fresh: true }); } catch {} };
|
|
@@ -473,7 +476,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
473
476
|
// a sandbox (symbiot app --fresh) never offers an update: it would replace your real install.
|
|
474
477
|
// A headless browser (an agent's screenshot of Home) never becomes the newest window:
|
|
475
478
|
// that closed the user's real one, again and again (2026-10-08, "symbiot keeps crashing").
|
|
476
|
-
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" && !headlessAgent(req.headers["user-agent"])) 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 } : {}) }); }
|
|
479
|
+
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" && !headlessAgent(req.headers["user-agent"])) NEWEST_WIN = w; return json(res, { ...(NEWEST_WIN ? { window: NEWEST_WIN } : {}), version: VERSION, started: SERVER_STARTED, latest: LATEST_VERSION, newer: !SANDBOX && !DESKTOP && semverGt(LATEST_VERSION, VERSION), ...(DESKTOP ? { desktop: true } : {}), ...(UPDATING && UPDATING.retrying ? { retrying: UPDATING } : {}), ...(IN_TERMUX ? { termux: true } : {}), ...(SANDBOX ? { sandbox: true } : {}) }); }
|
|
477
480
|
// What's new: after an update, since the version you last saw (until you click Got it);
|
|
478
481
|
// ?latest=1, what the update on offer brings, from its package on npm
|
|
479
482
|
if (u.pathname === "/api/whatsnew") {
|
|
@@ -485,6 +488,11 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
485
488
|
return json(res, { version: to, changes: changesSince(md, VERSION, to), ...(md ? {} : { error: `Couldn't read ${to}'s changelog from npm.` }) });
|
|
486
489
|
}
|
|
487
490
|
if (u.pathname === "/api/whatsnew/seen" && req.method === "POST") { const cfg = loadConfig(); cfg.seenVersion = VERSION; return json(res, saveConfig(cfg) ? { ok: true } : { error: "Couldn't write the config file." }); }
|
|
491
|
+
if (u.pathname === "/api/claude/setup") return json(res, claudeSetup(u.searchParams.get("fresh") === "1"));
|
|
492
|
+
if (u.pathname === "/api/claude/install" && req.method === "POST") return json(res, installClaude());
|
|
493
|
+
if (u.pathname === "/api/claude/signin" && req.method === "POST") return json(res, signInClaude());
|
|
494
|
+
if (u.pathname === "/api/claude/code" && req.method === "POST") { const b = await readBody(req); return json(res, sendClaudeCode(b.code)); }
|
|
495
|
+
if (u.pathname === "/api/update" && req.method === "POST" && DESKTOP) return json(res, { error: "This Symbiot updates with its app." });
|
|
488
496
|
if (u.pathname === "/api/update" && req.method === "POST") {
|
|
489
497
|
// Install the exact newest version (see updateCmd), then relaunch this
|
|
490
498
|
// same app (same port+token => same URL) and exit. The page's heartbeat
|
|
@@ -505,7 +513,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
505
513
|
return;
|
|
506
514
|
}
|
|
507
515
|
server.close(); if (server.closeAllConnections) server.closeAllConnections();
|
|
508
|
-
try { const ch = spawn(process.execPath, process.argv.slice(1), { detached: true, stdio: "ignore", env: { ...process.env, SYMBIOT_RELAUNCH: "1" } }); ch.unref(); } catch {}
|
|
516
|
+
try { const ch = spawn(process.execPath, process.argv.slice(1), { detached: true, stdio: "ignore", env: { ...process.env, SYMBIOT_RELAUNCH: "1" }, cwd: homedir() }); ch.unref(); } catch {}
|
|
509
517
|
setTimeout(() => process.exit(0), 1200);
|
|
510
518
|
});
|
|
511
519
|
UPDATING = { target, attempt: 1, at: Date.now() };
|
package/ui.mjs
CHANGED
|
@@ -966,6 +966,15 @@ body.lq-light .onbin{background:rgba(255,255,255,.92);border-color:rgba(21,26,33
|
|
|
966
966
|
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)}
|
|
967
967
|
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)}
|
|
968
968
|
@media(max-width:640px){.onbcards{grid-template-columns:1fr}.onbin{padding:22px 18px}}
|
|
969
|
+
|
|
970
|
+
/* Talking to Symbiot: the mic on the talk bar, amber while it listens */
|
|
971
|
+
#lqmic{flex:none;width:40px;height:40px;padding:0;display:grid;place-items:center;border-radius:50%;border:0;background:transparent;color:inherit;cursor:pointer;opacity:.72;transition:opacity .15s,background .15s}
|
|
972
|
+
#lqmic:hover,#lqmic:focus-visible{opacity:1;background:rgba(255,255,255,.07)}
|
|
973
|
+
body.lq-light #lqmic:hover,body.lq-light #lqmic:focus-visible{background:rgba(21,26,33,.06)}
|
|
974
|
+
#lqmic.on{opacity:1;background:#F2A541;color:#0A0B0D;animation:lqmicpulse 1.4s ease-in-out infinite}
|
|
975
|
+
@keyframes lqmicpulse{0%,100%{box-shadow:0 0 0 4px rgba(242,165,65,.28)}50%{box-shadow:0 0 0 11px rgba(242,165,65,.05)}}
|
|
976
|
+
@media (prefers-reduced-motion: reduce){#lqmic.on{animation:none;box-shadow:0 0 0 4px rgba(242,165,65,.28)}}
|
|
977
|
+
#lqmic.hidden{display:none}
|
|
969
978
|
/* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
|
|
970
979
|
.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)}
|
|
971
980
|
body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
@@ -1038,7 +1047,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1038
1047
|
<div id="lqtalk" aria-live="polite"></div>
|
|
1039
1048
|
<div id="lqtalkbar"></div>
|
|
1040
1049
|
<div id="lqatts" aria-live="polite"></div>
|
|
1041
|
-
<form id="lqform" class="lqglass"><label for="lqask" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Talk to Symbiot</label><textarea id="lqask" rows="1" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do (paste a screenshot to show it)"></textarea><button type="submit" id="lqsend" aria-label="Send"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form>
|
|
1050
|
+
<form id="lqform" class="lqglass"><label for="lqask" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Talk to Symbiot</label><textarea id="lqask" rows="1" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do (paste a screenshot to show it)"></textarea><button type="button" id="lqmic" class="hidden" aria-label="Talk to Symbiot" aria-pressed="false" title="Talk: click, then speak"><svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg></button><button type="submit" id="lqsend" aria-label="Send"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form>
|
|
1042
1051
|
</div>
|
|
1043
1052
|
|
|
1044
1053
|
<div id="updatebar" class="updatebar"></div>
|
|
@@ -3004,14 +3013,47 @@ el.querySelectorAll('button').forEach(function(b){b.addEventListener('click',fun
|
|
|
3004
3013
|
function lqAttach(file){if(!file||['image/png','image/jpeg','image/webp','image/gif'].indexOf(file.type)<0||file.size>8*1024*1024||LQ.atts.length>=4)return;var r=new FileReader();r.onload=function(){LQ.atts.push({data:String(r.result)});lqAttsShow();};r.readAsDataURL(file);}
|
|
3005
3014
|
// The talk box wraps and grows with what's typed, to five lines, then scrolls.
|
|
3006
3015
|
function lqGrowAsk(){var i=$('lqask');if(!i||!i.style)return;i.style.height='auto';var h=Math.min(126,Math.max(44,i.scrollHeight||44));i.style.height=h+'px';if(i.classList)i.classList.toggle('full',(i.scrollHeight||0)>126);var f=$('lqform'),b=document.body;if(f&&f.offsetHeight&&b&&b.style&&b.style.setProperty)b.style.setProperty('--lqfh',f.offsetHeight+'px');}
|
|
3007
|
-
|
|
3016
|
+
|
|
3017
|
+
// ---- Talking to Symbiot ------------------------------------------------------------
|
|
3018
|
+
// The mic on the talk bar: click, speak, and what you say shows in the bar as you say
|
|
3019
|
+
// it; a pause sends it, as Enter would. Speech becomes text in the browser itself
|
|
3020
|
+
// (Chrome and Edge, no key); where it can't, there's no mic. When you spoke, the
|
|
3021
|
+
// answer is read out too (typed, it stays quiet), and talking again cuts it off.
|
|
3022
|
+
var VOICE={rec:null,on:false,spoke:false,final:'',base:'',err:'',ph:''};
|
|
3023
|
+
function voiceOk(){return !!(window.SpeechRecognition||window.webkitSpeechRecognition);}
|
|
3024
|
+
function voiceMic(on){var m=$('lqmic');if(!m)return;m.classList.toggle('on',on);m.setAttribute('aria-pressed',on?'true':'false');m.setAttribute('aria-label',on?'Stop listening':'Talk to Symbiot');var a=$('lqask');if(a){if(on){VOICE.ph=a.getAttribute('placeholder')||'';a.setAttribute('placeholder','Listening… speak, then pause to send');}else if(VOICE.ph)a.setAttribute('placeholder',VOICE.ph);}}
|
|
3025
|
+
function voiceSay(t){LQ.talkMin=false;lqTalkMode(true);LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);}
|
|
3026
|
+
var VOICE_WHY={'not-allowed':'Symbiot couldn’t use your microphone. Allow it for this window (the icon at the top of the window, or your system’s privacy settings), then click the mic again.','service-not-allowed':'Symbiot couldn’t use your microphone. Allow it for this window, then click the mic again.','audio-capture':'No microphone was found. Plug one in, or check it’s switched on, then try again.','network':'Talking needs an internet connection: your browser turns speech into text online.'};
|
|
3027
|
+
function voiceToggle(){
|
|
3028
|
+
if(VOICE.on){try{VOICE.rec.stop();}catch(e){}return;}
|
|
3029
|
+
try{if(window.speechSynthesis)window.speechSynthesis.cancel();}catch(e){}
|
|
3030
|
+
var R=window.SpeechRecognition||window.webkitSpeechRecognition;if(!R)return;
|
|
3031
|
+
var r;try{r=new R();}catch(e){return;}
|
|
3032
|
+
var ask=$('lqask');VOICE.rec=r;VOICE.final='';VOICE.err='';VOICE.base=((ask&&ask.value)||'').trim();
|
|
3033
|
+
r.lang=navigator.language||'en-US';r.interimResults=true;r.continuous=false;r.maxAlternatives=1;
|
|
3034
|
+
r.onstart=function(){VOICE.on=true;voiceMic(true);lqAct();};
|
|
3035
|
+
r.onresult=function(ev){var interim='';for(var i=ev.resultIndex;i<ev.results.length;i++){var t=ev.results[i][0].transcript;if(ev.results[i].isFinal)VOICE.final+=t;else interim+=t;}
|
|
3036
|
+
if(ask){ask.value=((VOICE.base?VOICE.base+' ':'')+(VOICE.final+interim)).replace(/ +/g,' ').trim();lqGrowAsk();}};
|
|
3037
|
+
r.onerror=function(ev){VOICE.err=(ev&&ev.error)||'';};
|
|
3038
|
+
r.onend=function(){VOICE.on=false;voiceMic(false);VOICE.rec=null;var said=((ask&&ask.value)||'').trim(),why=VOICE_WHY[VOICE.err];
|
|
3039
|
+
if(why){voiceSay(why);return;}
|
|
3040
|
+
if(VOICE.final.trim()&&said){VOICE.spoke=true;lqSay();}};
|
|
3041
|
+
try{r.start();}catch(e){VOICE.on=false;voiceMic(false);}}
|
|
3042
|
+
// read out: plain words (no markdown marks), up to a few sentences; a voice in your language, a natural one if there is
|
|
3043
|
+
function voiceSpeak(t){if(!window.speechSynthesis||!t)return;
|
|
3044
|
+
var clean=String(t).replace(/[*_#>|]+/g,' ').replace(new RegExp(String.fromCharCode(96),'g'),'').replace(/https?:[^ ]+/g,'a link').replace(/ +/g,' ').trim();
|
|
3045
|
+
if(clean.length>700){var cut=clean.slice(0,700),at=Math.max(cut.lastIndexOf('. '),cut.lastIndexOf('? '),cut.lastIndexOf('! '));clean=(at>200?cut.slice(0,at+1):cut)+' The rest is on screen.';}
|
|
3046
|
+
try{var u=new SpeechSynthesisUtterance(clean),lang=(navigator.language||'en').slice(0,2).toLowerCase(),vs=window.speechSynthesis.getVoices().filter(function(v){return (v.lang||'').toLowerCase().indexOf(lang)===0;});
|
|
3047
|
+
var best=vs.filter(function(v){return /natural|neural|google|online/i.test(v.name);})[0]||vs[0];if(best)u.voice=best;u.lang=navigator.language||'en-US';u.rate=1.02;
|
|
3048
|
+
window.speechSynthesis.cancel();window.speechSynthesis.speak(u);}catch(e){}}
|
|
3049
|
+
function lqSay(){var spoke=VOICE.spoke;VOICE.spoke=false;var i=$('lqask');var q=((i&&i.value)||'').trim(),shots=LQ.atts.slice();if(!q&&!shots.length)return;if(!q)q='Take a look at this screenshot.';i.value='';lqGrowAsk();LQ.atts=[];lqAttsShow();lqAct();
|
|
3008
3050
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
3009
3051
|
Object.keys(LQNAMES).forEach(function(k){var nm=LQNAMES[k].toLowerCase();if(hit)return;['open ','show ','go to ','take me to '].forEach(function(v){if(low.indexOf(v+nm)===0||low.indexOf(v+'my '+nm)===0)hit=k;});if(low===nm)hit=k;});
|
|
3010
3052
|
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');lqGo(hit);return;}
|
|
3011
3053
|
if(/^(go|start|go for it|start them|start it)[.! ]*$/.test(low)){lqTalkMode(true);lqGoWork();return;}
|
|
3012
3054
|
if(/^(home|back)[.! ]*$/.test(low)&&LQ.scene==='work'){lqHome();return;}
|
|
3013
3055
|
LQ.talkMin=false;lqTalkMode(true);LQ.talk.push({me:true,text:q,imgs:shots.map(function(a){return a.data;})});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
3014
|
-
api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
3056
|
+
api('/api/home/ask',{question:q,images:shots}).then(function(r){LQ.talk.push({me:false,steps:r.steps||[],text:r.error==='not-connected'?'Connect an AI in Settings to talk to me. Say "open settings".':(r.answer||r.error||'(no answer)')});if(spoke)voiceSpeak(LQ.talk[LQ.talk.length-1].text);LQ.talk=LQ.talk.slice(-8);lqTalkShow(false);LQ.ripple=[0.5,0.47,LQ.t];lqLoad(false);}).catch(function(e){LQ.talk.push({me:false,text:String((e&&e.message)||e)});lqTalkShow(false);});}
|
|
3015
3057
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
3016
3058
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
3017
3059
|
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;
|
|
@@ -3189,7 +3231,12 @@ h="<img class='onborb' src='/favicon.svg?look="+encodeURIComponent(lqLookGet()||
|
|
|
3189
3231
|
else if(o.step==='ai'){var a=o.ai||{},c=a.claude||{};
|
|
3190
3232
|
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>";
|
|
3191
3233
|
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>";
|
|
3192
|
-
else h+="<div class='onbbox'><h3>Your Claude subscription (recommended)</h3><p>
|
|
3234
|
+
else h+="<div class='onbbox'><h3>Your Claude subscription (recommended)</h3><p>Symbiot thinks through Claude Code, Anthropic’s own app, with your Claude account: no key, and nothing extra to pay.</p>"+
|
|
3235
|
+
(c.installing?"<p><span class='onbspin'></span>Installing Claude Code… this takes about a minute.</p>":
|
|
3236
|
+
!c.installed?"<button class='act' id='onbccinst'>Install Claude Code</button>"+(c.installError?"<p class='onbnote' style='margin-top:10px'>"+esc(c.installError)+"</p>":""):
|
|
3237
|
+
c.signingIn?"<p><span class='onbspin'></span>Sign in on the page that opened in your browser. This ticks itself when you’re done.</p><div class='onbrow'><input id='onbcccode' placeholder='If the page shows a code, paste it here'><button class='ghost' id='onbccsend'>Send</button></div>":
|
|
3238
|
+
"<p>Claude Code is installed. Now sign in with your Claude account.</p><button class='act' id='onbccsign'>Sign in with Claude</button>"+(c.signinError?"<p class='onbnote' style='margin-top:10px'>"+esc(c.signinError)+"</p>":""))+
|
|
3239
|
+
"<p class='onbnote' style='margin:12px 0 0'>Already done it yourself? <button class='back' id='onbrecheck' style='padding:0'>Check again</button></p></div>"+
|
|
3193
3240
|
"<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>";
|
|
3194
3241
|
h+=onbNav('welcome','work','Continue',!!a.connected);}
|
|
3195
3242
|
else if(o.step==='work'){var w=o.work||{};
|
|
@@ -3227,6 +3274,9 @@ b.innerHTML=h;
|
|
|
3227
3274
|
b.querySelectorAll('[data-go]').forEach(function(x){x.addEventListener('click',function(){onbGo(x.getAttribute('data-go'));});});
|
|
3228
3275
|
var on=function(id,f){var x=$(id);if(x)x.addEventListener('click',f);};
|
|
3229
3276
|
on('onbrecheck',function(){onbLoad(true);});
|
|
3277
|
+
on('onbccinst',function(){api('/api/claude/install',{}).then(function(){onbLoad(true);});});
|
|
3278
|
+
on('onbccsign',function(){api('/api/claude/signin',{}).then(function(){onbLoad(true);});});
|
|
3279
|
+
on('onbccsend',function(){var v=$('onbcccode').value.trim();if(!v)return;api('/api/claude/code',{code:v}).then(function(){onbLoad(true);});});
|
|
3230
3280
|
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);});});
|
|
3231
3281
|
var pvs=$('onbprov');if(pvs)pvs.addEventListener('change',function(){$('onbkey').style.display=pvs.value==='ollama'?'none':'';});
|
|
3232
3282
|
on('onbaddroot',function(){var v=$('onbroot').value.trim();if(!v)return;api('/api/scanroots/add',{path:v}).then(function(){onbLoad();});});
|
|
@@ -3244,6 +3294,7 @@ function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqIcon(lqLoo
|
|
|
3244
3294
|
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();});
|
|
3245
3295
|
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();}});}
|
|
3246
3296
|
var L=$('liquid');if(L&&L.addEventListener){L.addEventListener('click',function(ev){var t=ev&&ev.target;if(t&&t.closest&&t.closest('button,input,form,.lqd'))return;var x=ev.clientX,y=ev.clientY,hit=null;LQ.drops.forEach(function(d){if(!hit&&Math.hypot(x-d.x,y-d.y)<=d.cr+8)hit=d;});if(hit)lqOpen(hit,ev);else if(LQ.openTag)lqTag(LQ.openTag);});L.addEventListener('pointermove',function(ev){LQ.pointer=[ev.clientX,ev.clientY];if(ev.pointerType==='touch')LQ.touch=true;lqAct();});L.addEventListener('pointerdown',function(ev){if(ev.pointerType==='touch')LQ.touch=true;lqAct();});}
|
|
3297
|
+
var mic=$('lqmic');if(mic&&voiceOk()){mic.classList.remove('hidden');mic.addEventListener('click',function(){voiceToggle();});try{if(window.speechSynthesis)window.speechSynthesis.getVoices();}catch(e){}}
|
|
3247
3298
|
var f=$('lqform');if(f&&f.addEventListener)f.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();lqSay();});
|
|
3248
3299
|
var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('paste',function(ev){var it=(ev.clipboardData&&ev.clipboardData.items)||[],got=false;for(var k=0;k<it.length;k++){if(it[k].kind==='file'&&String(it[k].type).indexOf('image/')===0){lqAttach(it[k].getAsFile());got=true;}}if(got)ev.preventDefault();});
|
|
3249
3300
|
var fm=$('lqform');if(fm&&fm.addEventListener){fm.addEventListener('dragover',function(ev){ev.preventDefault();if(fm.classList)fm.classList.add('drop');});fm.addEventListener('dragleave',function(){if(fm.classList)fm.classList.remove('drop');});fm.addEventListener('drop',function(ev){ev.preventDefault();if(fm.classList)fm.classList.remove('drop');var fs=(ev.dataTransfer&&ev.dataTransfer.files)||[];for(var k=0;k<fs.length;k++)lqAttach(fs[k]);});}
|