symbiot 0.53.3 → 0.54.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 +16 -0
- package/README.md +4 -1
- package/ai.mjs +15 -13
- package/home.mjs +8 -4
- package/mind.mjs +2 -2
- package/package.json +1 -1
- package/server.mjs +32 -6
- package/ui.mjs +47 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.54.0
|
|
6
|
+
|
|
7
|
+
- **Show Symbiot a screenshot.** Paste one into "Talk to Symbiot" (Ctrl+V) or drop it on the bar: it shows as a thumbnail you can remove, goes with what you say, and the AI you connected sees it (Claude, OpenAI, Gemini, or a local model that reads images). If Symbiot hands the work to an agent, the agent gets the screenshot too. Up to 4 at a time; they're kept a week, readable by you only.
|
|
8
|
+
- **A pass over every screen, in every look.**
|
|
9
|
+
- Scrollbars match the look everywhere: thin, no arrows, no white track.
|
|
10
|
+
- On the Dashboard, emails that land minutes apart fan out along their current instead of melting into one blob.
|
|
11
|
+
- The **Go** button no longer peeks out under the top of an open panel.
|
|
12
|
+
- A project's page no longer shows the same Approve twice ("Needs you here" and "Awaiting your review").
|
|
13
|
+
- The Agents and Reports **Refresh** buttons are round icons, like the Dashboard's.
|
|
14
|
+
- In Pearl, the main buttons lose the old chrome stripe for a soft pearl sheen.
|
|
15
|
+
- In the light looks, the Tasks screen's status line is crisp instead of washed out.
|
|
16
|
+
|
|
17
|
+
## 0.53.4
|
|
18
|
+
|
|
19
|
+
- **Updates wait for npm instead of giving up.** npm lists a new version a little before its download is there, so an update straight after a release could fail and tell you to run `npm install` in a terminal. Now Symbiot tries again every 30 seconds (up to 5 times) and the bar says it's waiting on npm; there's nothing for you to do.
|
|
20
|
+
|
|
5
21
|
## 0.53.3
|
|
6
22
|
|
|
7
23
|
- **The window's top bar matches the look.** Symbiot now tells the browser its colour, so the bar the app window opens with is the look's own (warm grey in Pearl, near-black in Ferrofluid, cool grey in Glass) instead of your desktop's accent colour.
|
package/README.md
CHANGED
|
@@ -599,7 +599,10 @@ time; **Close** or Esc closes a part. In a text box, or over text you've selecte
|
|
|
599
599
|
right-click still gives the usual menu. Whatever needs you glows **amber**, here and
|
|
600
600
|
on the Dashboard. Talk to it in the bar at the bottom: "open tasks" opens Tasks,
|
|
601
601
|
"go" in the work view starts what's waiting, anything else goes to the same Symbiot
|
|
602
|
-
as every chat.
|
|
602
|
+
as every chat. Paste a screenshot into the bar (Ctrl+V) or drop one on it, and
|
|
603
|
+
Symbiot sees it with what you say (up to 4, with any AI you've connected that reads
|
|
604
|
+
images); if it hands the work to an agent, the agent gets the screenshot too.
|
|
605
|
+
Leave it alone and it rests as one orb. There are no settings for any
|
|
603
606
|
of this: it adapts to you.
|
|
604
607
|
|
|
605
608
|
- **Where things sit: nearest neighbours.** Inside a group, the parts you move
|
package/ai.mjs
CHANGED
|
@@ -48,9 +48,11 @@ function resolveProvider() {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// ---- model calls (one per provider, same in/out) --------------------------
|
|
51
|
-
|
|
51
|
+
// Screenshots you dropped into the talk: [{ mime, data (base64) }], in each model's own shape.
|
|
52
|
+
async function callAnthropic(r, system, prompt, images = []) {
|
|
52
53
|
const client = new Anthropic(r.key ? { apiKey: r.key } : {});
|
|
53
|
-
const
|
|
54
|
+
const content = images.length ? [...images.map((i) => ({ type: "image", source: { type: "base64", media_type: i.mime, data: i.data } })), { type: "text", text: prompt }] : prompt;
|
|
55
|
+
const base = { model: r.model, max_tokens: MAX_TOKENS, system, messages: [{ role: "user", content }] };
|
|
54
56
|
let res;
|
|
55
57
|
try { res = await client.messages.create({ ...base, output_config: { effort: "low" } }); }
|
|
56
58
|
catch (e) {
|
|
@@ -61,8 +63,8 @@ async function callAnthropic(r, system, prompt) {
|
|
|
61
63
|
if (res.stop_reason === "refusal") return "(the model declined this one — odd for a work summary; try again)";
|
|
62
64
|
return res.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
63
65
|
}
|
|
64
|
-
async function callOpenAI(r, system, prompt) {
|
|
65
|
-
const messages = [{ role: "system", content: system }, { role: "user", content: prompt }];
|
|
66
|
+
async function callOpenAI(r, system, prompt, images = []) {
|
|
67
|
+
const messages = [{ role: "system", content: system }, { role: "user", content: images.length ? [{ type: "text", text: prompt }, ...images.map((i) => ({ type: "image_url", image_url: { url: `data:${i.mime};base64,${i.data}` } }))] : prompt }];
|
|
66
68
|
// Newer models want max_completion_tokens instead of max_tokens; try both.
|
|
67
69
|
for (const tokKey of ["max_tokens", "max_completion_tokens"]) {
|
|
68
70
|
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
@@ -77,13 +79,13 @@ async function callOpenAI(r, system, prompt) {
|
|
|
77
79
|
}
|
|
78
80
|
return "";
|
|
79
81
|
}
|
|
80
|
-
async function callGemini(r, system, prompt) {
|
|
82
|
+
async function callGemini(r, system, prompt, images = []) {
|
|
81
83
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(r.model)}:generateContent?key=${encodeURIComponent(r.key)}`;
|
|
82
84
|
const res = await fetch(url, {
|
|
83
85
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
84
86
|
body: JSON.stringify({
|
|
85
87
|
systemInstruction: { parts: [{ text: system }] },
|
|
86
|
-
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
88
|
+
contents: [{ role: "user", parts: [{ text: prompt }, ...images.map((i) => ({ inline_data: { mime_type: i.mime, data: i.data } }))] }],
|
|
87
89
|
generationConfig: { maxOutputTokens: MAX_TOKENS },
|
|
88
90
|
}),
|
|
89
91
|
});
|
|
@@ -92,10 +94,10 @@ async function callGemini(r, system, prompt) {
|
|
|
92
94
|
const parts = JSON.parse(text).candidates?.[0]?.content?.parts || [];
|
|
93
95
|
return parts.map((p) => p.text || "").join("").trim();
|
|
94
96
|
}
|
|
95
|
-
async function callOllama(r, system, prompt) {
|
|
97
|
+
async function callOllama(r, system, prompt, images = []) {
|
|
96
98
|
const res = await fetch(`${r.baseUrl}/api/chat`, {
|
|
97
99
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
98
|
-
body: JSON.stringify({ model: r.model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
100
|
+
body: JSON.stringify({ model: r.model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt, ...(images.length ? { images: images.map((i) => i.data) } : {}) }] }),
|
|
99
101
|
});
|
|
100
102
|
const text = await res.text();
|
|
101
103
|
if (!res.ok) {
|
|
@@ -112,15 +114,15 @@ const AI_UI = {
|
|
|
112
114
|
notConnected: () => console.log("Symbiot needs an AI to write your updates. Connect one with: symbiot login"),
|
|
113
115
|
rejected: (label) => console.log(`Your ${label} credentials were rejected. Reconnect with: symbiot login --force`),
|
|
114
116
|
};
|
|
115
|
-
async function write(system, prompt) {
|
|
117
|
+
async function write(system, prompt, { images = [] } = {}) {
|
|
116
118
|
const r = resolveProvider();
|
|
117
119
|
if (!r) { AI_UI.notConnected(); return null; }
|
|
118
120
|
const stop = AI_UI.spinner("thinking…");
|
|
119
121
|
try {
|
|
120
|
-
if (r.provider === "anthropic") return await callAnthropic(r, system, prompt);
|
|
121
|
-
if (r.provider === "openai") return await callOpenAI(r, system, prompt);
|
|
122
|
-
if (r.provider === "gemini") return await callGemini(r, system, prompt);
|
|
123
|
-
if (r.provider === "ollama") return await callOllama(r, system, prompt);
|
|
122
|
+
if (r.provider === "anthropic") return await callAnthropic(r, system, prompt, images);
|
|
123
|
+
if (r.provider === "openai") return await callOpenAI(r, system, prompt, images);
|
|
124
|
+
if (r.provider === "gemini") return await callGemini(r, system, prompt, images);
|
|
125
|
+
if (r.provider === "ollama") return await callOllama(r, system, prompt, images);
|
|
124
126
|
return null;
|
|
125
127
|
} catch (err) {
|
|
126
128
|
if (/\b401\b|\b403\b|invalid|authentication|api key|unauthor/i.test(err?.message || "")) {
|
package/home.mjs
CHANGED
|
@@ -143,17 +143,21 @@ function homeContext(h) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Home's talk: the same Symbiot, acting in the right lane (mind.mjs actIn/actNow).
|
|
146
|
-
async function homeAsk(question, { ask, now = Date.now(), state } = {}) {
|
|
146
|
+
async function homeAsk(question, { ask, now = Date.now(), state, images = [] } = {}) {
|
|
147
147
|
question = String(question || "").trim().slice(0, 2000);
|
|
148
|
+
if (!question && images.length) question = "Take a look at this screenshot.";
|
|
148
149
|
if (!question) return { error: "empty" };
|
|
150
|
+
// the screenshots go to the model, and their files with any work it hands an agent
|
|
151
|
+
let pics = []; try { pics = images.map((p) => ({ path: p, mime: /\.png$/i.test(p) ? "image/png" : /\.webp$/i.test(p) ? "image/webp" : /\.gif$/i.test(p) ? "image/gif" : "image/jpeg", data: readFileSync(p).toString("base64") })); } catch {}
|
|
152
|
+
const shotNote = pics.length ? `\n\nScreenshots the user attached (open them with your Read tool): ${pics.map((i) => i.path).join(", ")}` : "";
|
|
149
153
|
if (!ask && !resolveProvider()) return { error: "not-connected" };
|
|
150
154
|
const h = state || homeState({ now });
|
|
151
155
|
let map = {}; try { map = repoPathMap(); } catch {}
|
|
152
156
|
const role = "Here they're on Symbiot's home: one liquid surface that shows what only they can do, what's new on what they watch, and the lanes. Answer from what it shows, and act on what they ask.";
|
|
153
|
-
const r = await converse({ where: "Home", role, context: homeContext(h), question, map, now, ...(ask ? { ask } : {}),
|
|
157
|
+
const r = await converse({ where: "Home", role, context: homeContext(h), question: question + (pics.length ? `\n(The user attached ${pics.length} screenshot${pics.length > 1 ? "s" : ""}: you can see ${pics.length > 1 ? "them" : "it"}.)` : ""), map, now, images: pics.map(({ mime, data }) => ({ mime, data })), ...(ask ? { ask } : {}),
|
|
154
158
|
act: {
|
|
155
|
-
agent: (req, known, repo) => (repo ? actIn(req, repo, { map, known, title: "Home" }) : actNow(req, { title: "Home", known })),
|
|
156
|
-
task: (text, repo) => taskIn(text, repo, { map }),
|
|
159
|
+
agent: (req, known, repo) => (repo ? actIn(req + shotNote, repo, { map, known, title: "Home" }) : actNow(req + shotNote, { title: "Home", known })),
|
|
160
|
+
task: (text, repo) => taskIn(text + shotNote, repo, { map }),
|
|
157
161
|
} });
|
|
158
162
|
return { answer: r.reply, ...(r.did ? { did: r.did } : {}), steps: r.steps || [] };
|
|
159
163
|
}
|
package/mind.mjs
CHANGED
|
@@ -191,7 +191,7 @@ function parseReply(raw) {
|
|
|
191
191
|
// what this page is for; context: what the page shows; history: this chat's
|
|
192
192
|
// own last turns. act: { agent(request) -> result, task(text, repo) -> result },
|
|
193
193
|
// what "do" runs here. Gives { reply, did?, remembered }.
|
|
194
|
-
async function converse({ where, role = "", context = "", history = "", question, act = {}, ask = write, map = null, now = Date.now() }) {
|
|
194
|
+
async function converse({ where, role = "", context = "", history = "", question, act = {}, ask = write, map = null, now = Date.now(), images = [] }) {
|
|
195
195
|
const d = loadMind(), hits = recall(question, now, d), known = recallText(hits), elsewhere = lately(where, d);
|
|
196
196
|
// what it did to answer, to show under the reply (as the app shows an agent's work)
|
|
197
197
|
const steps = [];
|
|
@@ -207,7 +207,7 @@ async function converse({ where, role = "", context = "", history = "", question
|
|
|
207
207
|
const system = `${IDENTITY} ${role}\n\n${rulesFor(Object.keys(lanes), selfLane(lanes))}${voice ? "\n" + voice : ""}`;
|
|
208
208
|
const prompt = (known ? `What you know (from across the app):\n${known}\n\n` : "") + (kn.text ? kn.text + "\n\n" : "") + (elsewhere ? `Lately, elsewhere in the app:\n${elsewhere}\n\n` : "") +
|
|
209
209
|
(context ? context + "\n\n" : "") + (history ? `This chat so far:\n${history}\n\n` : "") + `They say (on ${where}): ${question}`;
|
|
210
|
-
const raw = await ask(system, prompt);
|
|
210
|
+
const raw = await ask(system, prompt, images.length ? { images } : undefined);
|
|
211
211
|
if (!raw || /^\(?couldn't reach the model/i.test(String(raw))) return { reply: "(couldn't reach the model)", remembered: 0 };
|
|
212
212
|
const j = parseReply(raw);
|
|
213
213
|
let reply = String(j.reply || "").trim().slice(0, 4000) || "(no answer)", did = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.mjs
CHANGED
|
@@ -7,11 +7,11 @@ import { homedir } from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { gunzipSync } from "node:zlib";
|
|
10
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
12
|
import { randomBytes } from "node:crypto";
|
|
13
13
|
import { EMBEDDED_UI } from "./ui.mjs";
|
|
14
|
-
import { VERSION, LATEST_VERSION, REGISTRY, semverGt, checkLatest, CONFIG_PATH, loadConfig, saveConfig, loadTasks, hasCmd, chromeBinary } from "./core.mjs";
|
|
14
|
+
import { VERSION, LATEST_VERSION, REGISTRY, semverGt, checkLatest, CONFIG_PATH, loadConfig, saveConfig, loadTasks, hasCmd, chromeBinary, CONFIG_DIR } from "./core.mjs";
|
|
15
15
|
import { shSingle, handoffCmd, setHandoffCmd, grantAgent, runHandoff, track, detectHandoffs, connectorsInfo, answerQuestions, skipIdea, agentsList, startWaiting, parkLane, parkedPaths, autoAllowSweep, trustFull } from "./agents.mjs";
|
|
16
16
|
import { PROVIDERS, resolveProvider, connectProvider, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel } from "./ai.mjs";
|
|
17
17
|
import { SCAN, SCAN_TIMEOUT_MS, scanRoots, scanHome, addScanRoot, removeScanRoot, buildMap, nodeDetail, repoPathMap } from "./scan.mjs";
|
|
@@ -98,6 +98,21 @@ async function registryChangelog(version, registry = REGISTRY) {
|
|
|
98
98
|
// Self-contained HTML served at / — no backticks or ${} inside (it lives in a
|
|
99
99
|
// template literal). Talks to the local API with the per-launch token.
|
|
100
100
|
|
|
101
|
+
// Screenshots dropped into the talk: data: URLs of PNG, JPEG, WebP or GIF, up to 4 of
|
|
102
|
+
// 8 MB each, saved under Symbiot's config (uploads/, readable by you only, kept a week), so the
|
|
103
|
+
// model can see them and an agent can open them. Returns their paths.
|
|
104
|
+
const UPLOADS = join(CONFIG_DIR, "uploads");
|
|
105
|
+
function saveShots(list) {
|
|
106
|
+
const out = [];
|
|
107
|
+
try { for (const f of readdirSync(UPLOADS)) { const fp = join(UPLOADS, f); if (Date.now() - statSync(fp).mtimeMs > 7 * 86400000) unlinkSync(fp); } } catch {} // kept a week
|
|
108
|
+
for (const it of (Array.isArray(list) ? list : []).slice(0, 4)) {
|
|
109
|
+
const m = /^data:image\/(png|jpeg|jpg|webp|gif);base64,([A-Za-z0-9+/=]+)$/.exec(String((it && it.data) || ""));
|
|
110
|
+
if (!m) continue;
|
|
111
|
+
const buf = Buffer.from(m[2], "base64"); if (!buf.length || buf.length > 8 * 1024 * 1024) continue;
|
|
112
|
+
try { mkdirSync(UPLOADS, { recursive: true, mode: 0o700 }); const f = join(UPLOADS, `${Date.now()}-${randomBytes(3).toString("hex")}.${m[1] === "jpeg" ? "jpg" : m[1]}`); writeFileSync(f, buf, { mode: 0o600 }); out.push(f); } catch {}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
101
116
|
function readBody(req) {
|
|
102
117
|
return new Promise((resolve) => {
|
|
103
118
|
let d = ""; req.on("data", (ch) => (d += ch));
|
|
@@ -186,6 +201,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
186
201
|
keepBrowserOpen(BROWSER_KEEP);
|
|
187
202
|
const json = (res, obj) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
188
203
|
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);
|
|
204
|
+
let UPDATING = null; // an update in flight: { target, attempt, retrying? }
|
|
189
205
|
const server = createServer(async (req, res) => {
|
|
190
206
|
const u = new URL(req.url, "http://127.0.0.1");
|
|
191
207
|
if (req.method === "GET" && u.pathname === "/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(EMBEDDED_UI); return; }
|
|
@@ -327,7 +343,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
327
343
|
if (u.pathname === "/api/work") return json(res, workScene());
|
|
328
344
|
if (u.pathname === "/api/work/go" && req.method === "POST") return json(res, workGo());
|
|
329
345
|
if (u.pathname === "/api/firststeps") return json(res, firstSteps()); // Settings' first steps: what's set up, in order
|
|
330
|
-
if (u.pathname === "/api/home/ask" && req.method === "POST") { const b = await readBody(req); return json(res, await homeAsk(b.question)); }
|
|
346
|
+
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) })); }
|
|
331
347
|
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" } : {}) }));
|
|
332
348
|
if (u.pathname === "/api/adapt/use" && req.method === "POST") { const b = await readBody(req); return json(res, noteUse(b)); }
|
|
333
349
|
// Lanes (lanes.mjs): work agents handed to each other, and where it stands.
|
|
@@ -371,7 +387,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
371
387
|
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)); }
|
|
372
388
|
if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
|
|
373
389
|
// a sandbox (symbiot app --fresh) never offers an update: it would replace your real install
|
|
374
|
-
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), ...(IN_TERMUX ? { termux: true } : {}), ...(SANDBOX ? { sandbox: true } : {}) }); }
|
|
390
|
+
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 } : {}) }); }
|
|
375
391
|
// What's new: after an update, since the version you last saw (until you click Got it);
|
|
376
392
|
// ?latest=1, what the update on offer brings, from its package on npm
|
|
377
393
|
if (u.pathname === "/api/whatsnew") {
|
|
@@ -392,12 +408,22 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
392
408
|
if (SANDBOX) return json(res, { error: "This is a sandbox (symbiot app --fresh): update your real Symbiot instead." });
|
|
393
409
|
const { target, cmd } = updateCmd(LATEST_VERSION, VERSION);
|
|
394
410
|
const inst = process.env.SYMBIOT_UPDATE_CMD || cmd;
|
|
395
|
-
|
|
396
|
-
|
|
411
|
+
// npm lists a new version a little before its download is there (a 404 for the
|
|
412
|
+
// .tgz): then it tries again every 30s, up to 5 times, and says so, instead of
|
|
413
|
+
// giving up and sending you to a terminal.
|
|
414
|
+
const attempt = (n) => track("symbiot update", inst, homedir(), (code) => {
|
|
415
|
+
if (code !== 0) {
|
|
416
|
+
let tail = ""; try { tail = readFileSync(join(homedir(), ".symbiot", "agent.log"), "utf8").slice(-3000); } catch {}
|
|
417
|
+
if (n < 5 && /E404|ETARGET|notarget|No matching version|is not in this registry/i.test(tail)) { UPDATING = { target, retrying: true, attempt: n + 1, at: Date.now() }; setTimeout(() => attempt(n + 1), 30000).unref(); }
|
|
418
|
+
else UPDATING = null;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
397
421
|
server.close(); if (server.closeAllConnections) server.closeAllConnections();
|
|
398
422
|
try { const ch = spawn(process.execPath, process.argv.slice(1), { detached: true, stdio: "ignore", env: { ...process.env, SYMBIOT_RELAUNCH: "1" } }); ch.unref(); } catch {}
|
|
399
423
|
setTimeout(() => process.exit(0), 1200);
|
|
400
424
|
});
|
|
425
|
+
UPDATING = { target, attempt: 1, at: Date.now() };
|
|
426
|
+
const e = attempt(1);
|
|
401
427
|
return json(res, { started: true, id: e ? e.id : "", target });
|
|
402
428
|
}
|
|
403
429
|
if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
|
package/ui.mjs
CHANGED
|
@@ -64,7 +64,7 @@ body.lq-light #lqform.lqglass{background:rgba(255,255,255,.62);border-color:rgba
|
|
|
64
64
|
body.lq-light #lqask::placeholder{color:#6A7480}
|
|
65
65
|
#lqsend{flex:none;width:40px;height:40px;padding:0;display:grid;place-items:center;border-radius:50%;border:0;background:#F3F0EA;color:#0A0B0D;cursor:pointer}
|
|
66
66
|
body.lq-light #lqsend{background:#151A21;color:#FFFFFF}
|
|
67
|
-
body.lq-look-pearl #lqsend{background:linear-gradient(180deg,#FFFFFF 0%,#
|
|
67
|
+
body.lq-look-pearl #lqsend{background:linear-gradient(180deg,#FFFFFF 0%,#F1F2F5 60%,#E6E9EE 100%);color:#1B1E23;box-shadow:0 1px 2px rgba(21,26,33,.10),0 8px 18px -10px rgba(40,52,74,.45),inset 0 0 0 1px rgba(21,26,33,.10)}
|
|
68
68
|
#lqtalk{position:absolute;left:50%;bottom:88px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;flex-direction:column;gap:16px;z-index:3;max-height:min(34vh,320px);overflow-x:hidden;overflow-y:auto;scrollbar-width:none;padding-top:90px;scroll-behavior:smooth;-webkit-mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.12) 22%,rgba(0,0,0,.6) 48%,#000 72%);mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.12) 22%,rgba(0,0,0,.6) 48%,#000 72%);transition:max-height .3s ease}
|
|
69
69
|
#lqtalk:hover,#lqtalk:focus-within,#lqtalk.back{max-height:min(56vh,560px);-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 60px);mask-image:linear-gradient(to bottom,transparent 0,#000 60px)}
|
|
70
70
|
#lqtalk .lqmsg{transition:opacity .4s ease}
|
|
@@ -124,7 +124,7 @@ body.lq-light #quit{border-color:rgba(20,30,45,.16);background:rgba(255,255,255,
|
|
|
124
124
|
body.lq-pooled #lqtalk,body.lq-pooled #lqmore,body.lq-pooled #lqcore{display:none}
|
|
125
125
|
/* the work view says where things stand in its line under the heading, not in the core */
|
|
126
126
|
body.lq-work #lqcore{display:none}
|
|
127
|
-
body.lq-pooled #lqgo,body.lq-pooled #lqgo.on{display:none}
|
|
127
|
+
body.lq-pooled #lqgo,body.lq-pooled.lq-work #lqgo.on{display:none}
|
|
128
128
|
body.lq-talking .lqd{opacity:.55}
|
|
129
129
|
@media (prefers-reduced-motion: reduce){.lqd,#lqcore{transition:none}}
|
|
130
130
|
/* ---- the Symbiot look on every part: glass over the liquid, chrome, a silver edge ---- */
|
|
@@ -136,7 +136,7 @@ body.lq-liquid.lq-light .task,body.lq-liquid.lq-light .agent,body.lq-liquid.lq-l
|
|
|
136
136
|
body.lq-liquid button.act{background:#F3F0EA;color:#0A0B0D;border:0;border-radius:999px;box-shadow:none;font-weight:600}
|
|
137
137
|
body.lq-liquid button.act:hover{background:#FFFFFF}
|
|
138
138
|
body.lq-liquid.lq-light button.act{background:#151A21;color:#FFFFFF}
|
|
139
|
-
body.lq-liquid.lq-look-pearl button.act,body.lq-look-pearl #lqgo.on,body.lq-liquid.lq-look-pearl .updatebar.show{background:linear-gradient(180deg,#FFFFFF 0%,#
|
|
139
|
+
body.lq-liquid.lq-look-pearl button.act,body.lq-look-pearl #lqgo.on,body.lq-liquid.lq-look-pearl .updatebar.show{background:linear-gradient(180deg,#FFFFFF 0%,#F1F2F5 60%,#E6E9EE 100%);color:#1B1E23;box-shadow:0 1px 2px rgba(21,26,33,.10),0 8px 18px -10px rgba(40,52,74,.45),inset 0 0 0 1px rgba(21,26,33,.10)}
|
|
140
140
|
body.lq-liquid button.ghost{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.14);border-radius:999px;color:var(--bone);backdrop-filter:blur(6px)}
|
|
141
141
|
body.lq-liquid.lq-light button.ghost{background:rgba(255,255,255,.7);border-color:rgba(20,30,45,.18)}
|
|
142
142
|
body.lq-liquid input,body.lq-liquid select,body.lq-liquid textarea{border-radius:12px;border:1px solid rgba(220,228,240,.2);background:rgba(255,255,255,.04);color:var(--bone)}
|
|
@@ -185,7 +185,7 @@ body.lq-work.lq-byproj #lqsort{display:inline-flex;position:absolute;left:112px;
|
|
|
185
185
|
body.lq-light #lqsort{background:rgba(21,26,33,.05);border-color:rgba(21,26,33,.1)}
|
|
186
186
|
body.lq-light #lqsort button{color:#5A6470}
|
|
187
187
|
body.lq-light #lqsort button[aria-checked='true']{background:#151A21;color:#FFFFFF}
|
|
188
|
-
body.lq-look-pearl #lqsort button[aria-checked='true']{background:linear-gradient(180deg,#FFFFFF 0%,#
|
|
188
|
+
body.lq-look-pearl #lqsort button[aria-checked='true']{background:linear-gradient(180deg,#FFFFFF 0%,#F1F2F5 60%,#E6E9EE 100%);color:#1B1E23}
|
|
189
189
|
body.lq-touch #lqsort button{min-height:44px}
|
|
190
190
|
body.lq-pooled #lqsort{display:none!important}
|
|
191
191
|
@media (max-width:700px){body.lq-work.lq-byproj #lqsort{left:16px;top:104px}}
|
|
@@ -225,7 +225,7 @@ body.lq-light .lqblob .nopt.rec{background:#151A21;color:#FFFFFF;border-color:#1
|
|
|
225
225
|
body.lq-light .lqblob .bfx input{background:#FFFFFF;border-color:rgba(20,30,45,.18)}
|
|
226
226
|
body.lq-light .lqblob .bfx button{background:#151A21;color:#FFFFFF}
|
|
227
227
|
body.lq-light .lqblob .bfree,body.lq-light .lqd.lqblob .lt i,body.lq-light .lqblob .bmore{color:#5A6470}
|
|
228
|
-
body.lq-look-pearl .lqblob .nopt.rec,body.lq-look-pearl .lqblob .bfx button{background:linear-gradient(180deg,#FFFFFF 0%,#
|
|
228
|
+
body.lq-look-pearl .lqblob .nopt.rec,body.lq-look-pearl .lqblob .bfx button{background:linear-gradient(180deg,#FFFFFF 0%,#F1F2F5 60%,#E6E9EE 100%);color:#1B1E23;border-color:rgba(0,0,0,.08)}
|
|
229
229
|
body.lq-contrast .lqblob .nopt{border:2px solid currentColor}
|
|
230
230
|
body.lq-touch .lqblob .nopt,body.lq-touch .lqblob .bfx input{min-height:44px}
|
|
231
231
|
body.lq-talking .lqd.lqblob{opacity:.55}
|
|
@@ -696,7 +696,7 @@ body.lq-liquid.lq-light.lq-pooled #lqsinkrow{background:linear-gradient(180deg,r
|
|
|
696
696
|
#lqline,#lqrelay{display:none}
|
|
697
697
|
body.lq-work:not(.lq-pooled) #lqline{display:block;position:absolute;left:50%;top:84px;transform:translateX(-50%);z-index:3;max-width:calc(100% - 32px);text-align:center;font:400 13.5px/1.4 var(--sans);color:#A1A8B1;text-shadow:0 1px 10px rgba(0,0,0,.7)}
|
|
698
698
|
#lqline b{color:#F2A541;font-weight:600}
|
|
699
|
-
body.lq-light #lqline{color:#
|
|
699
|
+
body.lq-light #lqline{color:#2E3743;text-shadow:none}body.lq-light #lqline b{color:#9A5200}
|
|
700
700
|
body.lq-work:not(.lq-pooled) #lqrelay:not(:empty){display:flex;flex-direction:column;gap:12px;position:absolute;right:16px;top:116px;bottom:96px;width:min(420px,32vw);overflow-y:auto;scrollbar-width:none;z-index:4;padding:2px 2px 24px;-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 28px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 28px),transparent)}
|
|
701
701
|
#lqrelay::-webkit-scrollbar{display:none}
|
|
702
702
|
@media (max-width:899px){body.lq-work:not(.lq-pooled) #lqrelay:not(:empty){display:none}}
|
|
@@ -775,6 +775,25 @@ body.lq-liquid .rcard .task{margin-top:6px;padding:9px 12px;border-radius:12px}
|
|
|
775
775
|
.rwork i{flex:none;width:9px;height:9px;border-radius:50%;background:#F2A541;box-shadow:0 0 0 0 rgba(242,165,65,.5);animation:rwork 1.8s ease-out infinite}
|
|
776
776
|
@keyframes rwork{0%{box-shadow:0 0 0 0 rgba(242,165,65,.5)}100%{box-shadow:0 0 0 9px rgba(242,165,65,0)}}
|
|
777
777
|
@media (prefers-reduced-motion: reduce){.rwork i{animation:none}}
|
|
778
|
+
|
|
779
|
+
/* scrollbars, everywhere: thin, no arrows, in the look's ink */
|
|
780
|
+
body.lq-liquid,body.lq-liquid *{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.16) transparent}
|
|
781
|
+
body.lq-liquid.lq-light,body.lq-liquid.lq-light *{scrollbar-color:rgba(21,26,33,.18) transparent}
|
|
782
|
+
body.lq-liquid ::-webkit-scrollbar{width:8px;height:8px}
|
|
783
|
+
body.lq-liquid ::-webkit-scrollbar-button{display:none}
|
|
784
|
+
body.lq-liquid ::-webkit-scrollbar-track{background:transparent}
|
|
785
|
+
body.lq-liquid ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:999px}
|
|
786
|
+
body.lq-liquid.lq-light ::-webkit-scrollbar-thumb{background:rgba(21,26,33,.16)}
|
|
787
|
+
|
|
788
|
+
/* screenshots in the talk */
|
|
789
|
+
#lqatts{position:absolute;left:50%;bottom:78px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;gap:8px;z-index:3;pointer-events:none}
|
|
790
|
+
#lqatts:empty{display:none}
|
|
791
|
+
.lqatt{position:relative;pointer-events:auto}
|
|
792
|
+
.lqatt img{display:block;width:64px;height:48px;object-fit:cover;border-radius:10px;border:1px solid rgba(255,255,255,.2);box-shadow:0 8px 20px -10px rgba(0,0,0,.8)}
|
|
793
|
+
.lqatt button{position:absolute;top:-7px;right:-7px;width:20px;height:20px;border-radius:50%;border:0;background:#F3F0EA;color:#0A0B0D;font:600 13px/20px var(--sans);text-align:center;cursor:pointer;padding:0}
|
|
794
|
+
body.lq-light .lqatt img{border-color:rgba(21,26,33,.15)}body.lq-light .lqatt button{background:#151A21;color:#FFFFFF}
|
|
795
|
+
#lqform.drop{outline:2px dashed rgba(242,165,65,.7);outline-offset:4px}
|
|
796
|
+
.lqmimgs{display:flex;gap:6px;margin-bottom:6px}.lqmimgs img{width:120px;max-height:90px;object-fit:cover;border-radius:8px}
|
|
778
797
|
/* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
|
|
779
798
|
.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)}
|
|
780
799
|
body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
|
|
@@ -843,7 +862,8 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
843
862
|
<button type="button" id="lqcore" title="talk to Symbiot"></button>
|
|
844
863
|
<div id="lqmore"></div>
|
|
845
864
|
<div id="lqtalk" aria-live="polite"></div>
|
|
846
|
-
<
|
|
865
|
+
<div id="lqatts" aria-live="polite"></div>
|
|
866
|
+
<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><input id="lqask" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do (paste a screenshot to show it)"><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>
|
|
847
867
|
</div>
|
|
848
868
|
|
|
849
869
|
<div id="updatebar" class="updatebar"></div>
|
|
@@ -921,13 +941,13 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
921
941
|
<div id="tasklist"></div>
|
|
922
942
|
</section>
|
|
923
943
|
<section id="panel-agents" class="hidden">
|
|
924
|
-
<div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output. Questions, options and ideas an agent leaves for you show up on its block, whichever model it runs.</span><button class="
|
|
944
|
+
<div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output. Questions, options and ideas an agent leaves for you show up on its block, whichever model it runs.</span><button class="iconbtn" id="agentsrefresh" style="margin-left:auto" title="Refresh" aria-label="Refresh"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M13 8a5 5 0 1 1-1.5-3.6M13 2.5V5h-2.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
|
925
945
|
<div id="agentsmsg"></div>
|
|
926
946
|
<div id="laneslist"></div>
|
|
927
947
|
<div id="agentslist"></div>
|
|
928
948
|
</section>
|
|
929
949
|
<section id="panel-reports" class="hidden">
|
|
930
|
-
<div class="row"><span class="muted" style="flex:1">What agents wrote up for you (findings, audits, plans), newest first. They leave each one in a project's .symbiot folder; it shows here as soon as it's written.</span><button class="
|
|
950
|
+
<div class="row"><span class="muted" style="flex:1">What agents wrote up for you (findings, audits, plans), newest first. They leave each one in a project's .symbiot folder; it shows here as soon as it's written.</span><button class="iconbtn" id="reportsrefresh" title="Refresh" aria-label="Refresh"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M13 8a5 5 0 1 1-1.5-3.6M13 2.5V5h-2.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
|
931
951
|
<div id="reportslist"></div>
|
|
932
952
|
<div id="reportview"></div>
|
|
933
953
|
</section>
|
|
@@ -1944,6 +1964,7 @@ cs.forEach(function(c,i){(c.items||[]).forEach(function(n,j){var age=Math.max(0,
|
|
|
1944
1964
|
BS.beads.push({key:key,i:i,j:j,n:n,need:!!n.need&&!!(n.mail||n.chat),tx:g.x0+(1-age/H)*(g.x1-g.x0),ty:laneY(i),tr:n.need&&(n.mail||n.chat)?13:n.read?6:9,x:o?o.x:g.x1,y:o?o.y:laneY(i),r:o?o.r:0});});
|
|
1945
1965
|
(c.past||[]).forEach(function(n,j){var age=Math.max(0,now-(n.ts||now));if(age>H)return;var key=c.id+':p:'+(n.id||j),o=old[key];BS.beads.push({key:key,i:i,j:-1-j,n:n,past:true,need:false,tx:g.x0+(1-age/H)*(g.x1-g.x0),ty:laneY(i),tr:4.5,x:o?o.x:g.x1,y:o?o.y:laneY(i),r:o?o.r:0});});});
|
|
1946
1966
|
html+=BS.beads.map(function(x,k){return "<button type='button' class='bbead' data-k='"+k+"' aria-label='"+esc(x.n.text||'')+"'></button>";}).join('');
|
|
1967
|
+
var byLane={};BS.beads.forEach(function(x){(byLane[x.i]=byLane[x.i]||[]).push(x);});Object.keys(byLane).forEach(function(k){var L=byLane[k].slice().sort(function(a,b){return a.tx-b.tx;}),run=0;for(var q=1;q<L.length;q++){if(L[q].tx-L[q-1].tx<26){run++;L[q].ty+=(run%2?-1:1)*Math.ceil(run/2)*20;}else run=0;}});
|
|
1947
1968
|
var ml=bsMailLane();BS.waits=[];if(ml>=0)(AWAITS||[]).forEach(function(w,k){if(w.status!=='waiting'&&!(w.reply&&now-w.reply.ts<H))return;var sent=w.sent||now,age=Math.max(0,now-sent),x=g.x0+(1-Math.min(age,H)/H)*(g.x1-g.x0),y=laneY(ml)+(k%2?14:-14);BS.waits.push({w:w,x:x,y:y});
|
|
1948
1969
|
html+=(w.status==='waiting'?"<div class='bwthread' style='left:"+Math.round(x)+"px;width:"+Math.max(0,Math.round(g.x1-x))+"px;top:"+Math.round(y)+"px'></div>":"")+"<button type='button' class='bwait"+(w.status==='waiting'?'':' in')+(age>H?' early':'')+"' data-w='"+(BS.waits.length-1)+"' style='left:"+Math.round(x)+"px;top:"+Math.round(y)+"px' aria-label='"+escQ((w.status==='waiting'?'waiting on a reply from ':'reply in from ')+(w.to||[]).join(', '))+"'></button>";});
|
|
1949
1970
|
html+="<div class='btip' role='dialog' aria-live='polite'></div>";ov.innerHTML=html;
|
|
@@ -2145,6 +2166,9 @@ function heartbeat(fresh){
|
|
|
2145
2166
|
var tried=null;try{tried=localStorage.getItem('symbiot_update_tried');}catch(e){}
|
|
2146
2167
|
if(tried){
|
|
2147
2168
|
if(p.version!==tried){try{localStorage.removeItem('symbiot_update_tried');}catch(e){} } // advanced → success, clear
|
|
2169
|
+
else if(p.retrying){ // npm hasn't finished publishing it: the app is trying again by itself
|
|
2170
|
+
b.className='updatebar show';b.innerHTML="Updating to "+esc(p.retrying.target)+": npm is still publishing it, so Symbiot tries again every 30 seconds (try "+p.retrying.attempt+" of 5). Nothing to do.";return;
|
|
2171
|
+
}
|
|
2148
2172
|
else if(p.latest&&p.newer){ // still on the same version → the install didn't advance
|
|
2149
2173
|
b.className='updatebar reconnect show';
|
|
2150
2174
|
b.innerHTML="Auto-update to "+esc(p.latest)+" didn't take (still on "+esc(p.version)+"). Run <code>npm install -g symbiot@latest</code> in a terminal, then restart.";
|
|
@@ -2498,7 +2522,7 @@ api('/api/home?fresh=1').then(function(h){if(h&&h.you)LQ.home=h;renderNeeds();})
|
|
|
2498
2522
|
// Needs you here: what only you can do in the project the Tasks list is filtered
|
|
2499
2523
|
// to, at its top, each answerable where it is; its held handovers under them.
|
|
2500
2524
|
function renderNeeds(){var el=$('needsbox');if(!el)return;var repo=TFILTER.repo;if(!repo||TARCH){el.innerHTML='';return;}
|
|
2501
|
-
var ys=((LQ.home&&LQ.home.you)||[]).filter(function(y){return y.repo===repo&&
|
|
2525
|
+
var ys=((LQ.home&&LQ.home.you)||[]).filter(function(y){return y.repo===repo&&y.kind==='ask';});
|
|
2502
2526
|
var paint=function(held){if(TFILTER.repo!==repo)return;if(!ys.length&&!held.length){el.innerHTML='';lqLight();return;}
|
|
2503
2527
|
el.innerHTML="<div class='tgroup' style='color:var(--amber)'>Needs you here <span class='tcount'>"+(ys.length+held.length)+"</span></div>"+
|
|
2504
2528
|
ys.map(function(y,i){return "<div class='task nitem lqblob' data-k='"+i+"'>"+blobBody(y,false)+"</div>";}).join('')+
|
|
@@ -2532,17 +2556,23 @@ function lqPool(shape){if(shape==='map'&&LM.items&&LM.items.length)setTimeout(fu
|
|
|
2532
2556
|
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);}
|
|
2533
2557
|
function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
|
|
2534
2558
|
function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
|
|
2535
|
-
function lqTalkShow(wait){var el=$('lqtalk');if(!el)return;var n=LQ.talk.length;el.innerHTML=LQ.talk.map(function(m,i){var age=n-1-i,op=age<2?1:age===2?0.7:age===3?0.45:0.25;return "<div class='lqmsg"+(m.me?' me':'')+"' style='opacity:"+op+"'>"+esc(m.text)+stepsHtml(m.steps)+"</div>";}).join('')+(wait?"<div class='lqmsg thinking'>Thinking: recalling what it knows, reading what's here…</div>":'');if(el.scrollHeight)el.scrollTop=el.scrollHeight;
|
|
2559
|
+
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;
|
|
2536
2560
|
// scrolled up to read back: the whole history shows, clear; back at the bottom it rolls on again
|
|
2537
2561
|
if(!el.wired&&el.addEventListener){el.wired=true;el.addEventListener('scroll',function(){var up=el.scrollHeight-el.scrollTop-el.clientHeight>24;if(el.classList)el.classList.toggle('back',up);});}}
|
|
2538
|
-
|
|
2562
|
+
// Screenshots in the talk: paste one (Ctrl+V) or drop it on the bar; it shows as a
|
|
2563
|
+
// thumbnail above it, goes with what you say, and the model sees it.
|
|
2564
|
+
LQ.atts=[];
|
|
2565
|
+
function lqAttsShow(){var el=$('lqatts');if(!el)return;el.innerHTML=LQ.atts.map(function(a,i){return "<span class='lqatt'><img src='"+a.data+"' alt='screenshot "+(i+1)+"'><button type='button' data-i='"+i+"' aria-label='remove screenshot "+(i+1)+"'>×</button></span>";}).join('');
|
|
2566
|
+
el.querySelectorAll('button').forEach(function(b){b.addEventListener('click',function(){LQ.atts.splice(+b.getAttribute('data-i'),1);lqAttsShow();});});}
|
|
2567
|
+
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);}
|
|
2568
|
+
function lqSay(){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='';LQ.atts=[];lqAttsShow();lqAct();
|
|
2539
2569
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
2540
2570
|
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;});
|
|
2541
2571
|
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');lqGo(hit);return;}
|
|
2542
2572
|
if(/^(go|start|go for it|start them|start it)[.! ]*$/.test(low)){lqTalkMode(true);lqGoWork();return;}
|
|
2543
2573
|
if(/^(home|back)[.! ]*$/.test(low)&&LQ.scene==='work'){lqHome();return;}
|
|
2544
|
-
LQ.talk.push({me:true,text:q});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
2545
|
-
api('/api/home/ask',{question:q}).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);});}
|
|
2574
|
+
LQ.talk.push({me:true,text:q,imgs:shots.map(function(a){return a.data;})});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
2575
|
+
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);});}
|
|
2546
2576
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
2547
2577
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
2548
2578
|
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;
|
|
@@ -2637,7 +2667,9 @@ if(document.addEventListener){document.addEventListener('contextmenu',function(e
|
|
|
2637
2667
|
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();}});}
|
|
2638
2668
|
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();});}
|
|
2639
2669
|
var f=$('lqform');if(f&&f.addEventListener)f.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();lqSay();});
|
|
2640
|
-
var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('
|
|
2670
|
+
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();});
|
|
2671
|
+
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]);});}
|
|
2672
|
+
ia.addEventListener('focus',function(){lqTalkMode(true);lqAct();});ia.addEventListener('blur',function(){if(!ia.value&&!LQ.talk.length)lqTalkMode(false);});ia.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape'){if(LQ.mode==='pool'){lqSink();return;}if(LQ.scene==='work'&&!ia.value){lqHome();return;}ia.value='';LQ.talk=[];lqTalkShow(false);lqTalkMode(false);if(ia.blur)ia.blur();}});}
|
|
2641
2673
|
var co=$('lqcore');if(co&&co.addEventListener)co.addEventListener('click',function(){var i=$('lqask');if(i&&i.focus)i.focus();});
|
|
2642
2674
|
var sk=$('lqsink');if(sk&&sk.addEventListener)sk.addEventListener('click',lqSink);
|
|
2643
2675
|
var bk=$('lqback');if(bk&&bk.addEventListener)bk.addEventListener('click',lqHome);
|