symbiot 0.53.4 → 0.54.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 +16 -0
- package/README.md +4 -1
- package/agents.mjs +7 -6
- package/ai.mjs +15 -13
- package/home.mjs +8 -4
- package/lanes.mjs +2 -2
- package/mind.mjs +2 -2
- package/package.json +1 -1
- package/server.mjs +20 -4
- package/tasks.mjs +2 -1
- package/ui.mjs +44 -15
- package/work.mjs +26 -1
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.1
|
|
6
|
+
|
|
7
|
+
- **No more lag from a big agent log.** An agent's log keeps every run and everything it reads, screenshots included; one reached 144 MB, and Symbiot reread all of it on almost every refresh, so the app stalled for 5–28 seconds at a time and ate a core. Now it reads only the newest run, from the end of the file, and keeps it until the file changes. A log past 8 MB starts afresh at the next run, with the old one kept as `agent.log.old`.
|
|
8
|
+
|
|
9
|
+
## 0.54.0
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- **A pass over every screen, in every look.**
|
|
13
|
+
- Scrollbars match the look everywhere: thin, no arrows, no white track.
|
|
14
|
+
- On the Dashboard, emails that land minutes apart fan out along their current instead of melting into one blob.
|
|
15
|
+
- The **Go** button no longer peeks out under the top of an open panel.
|
|
16
|
+
- A project's page no longer shows the same Approve twice ("Needs you here" and "Awaiting your review").
|
|
17
|
+
- The Agents and Reports **Refresh** buttons are round icons, like the Dashboard's.
|
|
18
|
+
- In Pearl, the main buttons lose the old chrome stripe for a soft pearl sheen.
|
|
19
|
+
- In the light looks, the Tasks screen's status line is crisp instead of washed out.
|
|
20
|
+
|
|
5
21
|
## 0.53.4
|
|
6
22
|
|
|
7
23
|
- **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.
|
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/agents.mjs
CHANGED
|
@@ -5,10 +5,10 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join, dirname, resolve, relative } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync, renameSync } from "node:fs";
|
|
9
9
|
import { randomBytes, createHash } from "node:crypto";
|
|
10
10
|
import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
|
|
11
|
-
import { parseRun, lastRunText } from "./work.mjs";
|
|
11
|
+
import { parseRun, lastRunText, readRunLog } from "./work.mjs";
|
|
12
12
|
import { parseFacts } from "./handover.mjs";
|
|
13
13
|
import { scanRoots, repoPathMap } from "./scan.mjs";
|
|
14
14
|
import { knowledgeFolders } from "./knowledge.mjs";
|
|
@@ -273,7 +273,7 @@ function earlierRuns() {
|
|
|
273
273
|
const busy = runningHandoff(r.path), ask = agentQuestions(r.path, r.name), wait = waitingFor(r.path), facts = busy ? [] : factsOf(r.path);
|
|
274
274
|
if (!busy && !ask.questions.length && !wait && !facts.length) continue;
|
|
275
275
|
const log = join(r.path, ".symbiot", "agent.log"), startedAt = (busy && busy.startedAt) || Number(r.startedAt) || Date.now();
|
|
276
|
-
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path,
|
|
276
|
+
let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path, readRunLog(log))); if (!busy) end = statSync(log).mtimeMs; } catch {}
|
|
277
277
|
out.push({ id: "earlier-" + digest(r.path).slice(0, 8), name: String(r.name || r.path.split("/").pop()), path: r.path, status: busy ? "running" : "done", earlier: true, elapsed: Math.max(0, end - startedAt), exitCode: null, tail, work, progress, changed: agentChanges(r.path, startedAt), ask, held: heldTasks(r.path), waiting: wait, remember: facts.length ? facts : null, fromHeld: false });
|
|
278
278
|
}
|
|
279
279
|
return out;
|
|
@@ -576,10 +576,12 @@ function _findOrcaCli() {
|
|
|
576
576
|
// ---- background jobs --------------------------------------------------------
|
|
577
577
|
// Run a shell command as a tracked, logged background job that shows up live in
|
|
578
578
|
// the Agents tab. Shared by the agent handoff and the local-model setup.
|
|
579
|
+
const LOG_KEEP = 8 * 1024 * 1024;
|
|
579
580
|
function track(name, cmd, cwd, onExit, env) {
|
|
580
581
|
try {
|
|
581
582
|
const dir = join(cwd, ".symbiot"); mkdirSync(dir, { recursive: true });
|
|
582
583
|
const logp = join(dir, "agent.log");
|
|
584
|
+
try { if (statSync(logp).size > LOG_KEEP) renameSync(logp, logp + ".old"); } catch {} // past 8 MB: kept as agent.log.old, and a fresh one starts
|
|
583
585
|
let fd = "ignore"; try { fd = openSync(logp, "a"); writeSync(fd, `\n=== ${name} ${new Date().toISOString()} ===\n$ ${cmd}\n`); } catch {}
|
|
584
586
|
const entry = { id: randomBytes(4).toString("hex"), name, path: cwd, log: logp, startedAt: Date.now(), status: "running", exitCode: null, endedAt: null };
|
|
585
587
|
const child = spawn(cmd, { shell: true, cwd, detached: true, stdio: ["ignore", fd === "ignore" ? "ignore" : fd, fd === "ignore" ? "ignore" : fd], ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
@@ -623,7 +625,7 @@ const QUESTIONS_MAX = 5;
|
|
|
623
625
|
// the next moves up. The rest are a click away.
|
|
624
626
|
const OPTIONS_SHOWN = 2, IDEAS_SHOWN = 2;
|
|
625
627
|
const qKey = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
626
|
-
const readSymbiot = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
628
|
+
const readSymbiot = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
627
629
|
// "## Questions" → "### question", context lines, "- option" bullets;
|
|
628
630
|
// "## Suggestions" (or Ideas / Follow-ups) → "- idea" bullets. Forgiving: a bare
|
|
629
631
|
// bullet under Questions is a question with no options.
|
|
@@ -880,8 +882,7 @@ function workOf(path, log) {
|
|
|
880
882
|
function agentsList() {
|
|
881
883
|
const seen = new Set();
|
|
882
884
|
return HANDOFFS.map((e) => {
|
|
883
|
-
|
|
884
|
-
const { tail, work, progress } = workOf(e.path, log);
|
|
885
|
+
const { tail, work, progress } = workOf(e.path, readRunLog(e.log));
|
|
885
886
|
const first = !seen.has(e.path); seen.add(e.path);
|
|
886
887
|
const facts = first && e.status !== "running" ? factsOf(e.path) : [];
|
|
887
888
|
return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, work, progress, changed: agentChanges(e.path, e.startedAt), ask: first ? agentQuestions(e.path, e.name) : null, held: first ? heldTasks(e.path) : null, waiting: first && e.status !== "running" ? waitingFor(e.path) : null, remember: facts.length ? facts : null, fromHeld: !!e.fromHeld };
|
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/lanes.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { runHandoff, runningHandoff, waitingFor, agentQuestions } from "./agents
|
|
|
23
23
|
import { addTask, pushTasks } from "./tasks.mjs";
|
|
24
24
|
import { actNow } from "./mind.mjs";
|
|
25
25
|
import { repoPathMap } from "./scan.mjs";
|
|
26
|
-
import { parseRun, lastRunText } from "./work.mjs";
|
|
26
|
+
import { parseRun, lastRunText, readRunLog } from "./work.mjs";
|
|
27
27
|
import { OPS, parseHandoffs } from "./handover.mjs";
|
|
28
28
|
|
|
29
29
|
const LEDGER = join(CONFIG_DIR, "lanes.json");
|
|
@@ -33,7 +33,7 @@ const MAX_KEEP = 200, MAX_CHAIN = 4, TAIL = 4000;
|
|
|
33
33
|
function loadLedger() { try { const d = JSON.parse(readFileSync(LEDGER, "utf8")); return { handoffs: Array.isArray(d.handoffs) ? d.handoffs : [] }; } catch { return { handoffs: [] }; } }
|
|
34
34
|
// It can name accounts and what's in them: yours only (0600).
|
|
35
35
|
function saveLedger(d) { try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(LEDGER, JSON.stringify({ handoffs: d.handoffs.slice(-MAX_KEEP) }, null, 1), { mode: 0o600 }); try { chmodSync(LEDGER, 0o600); } catch {} return true; } catch { return false; } }
|
|
36
|
-
const readSym = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
36
|
+
const readSym = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
|
|
37
37
|
const firstLine = (t) => String(t || "").split("\n").find((l) => l.trim()) || "";
|
|
38
38
|
|
|
39
39
|
// A folder's lane: the repo it is, or ops for a run of its own.
|
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.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": {
|
package/server.mjs
CHANGED
|
@@ -7,11 +7,12 @@ 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 {
|
|
10
|
+
import { readRunLog } from "./work.mjs";
|
|
11
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
11
12
|
import { createServer } from "node:http";
|
|
12
13
|
import { randomBytes } from "node:crypto";
|
|
13
14
|
import { EMBEDDED_UI } from "./ui.mjs";
|
|
14
|
-
import { VERSION, LATEST_VERSION, REGISTRY, semverGt, checkLatest, CONFIG_PATH, loadConfig, saveConfig, loadTasks, hasCmd, chromeBinary } from "./core.mjs";
|
|
15
|
+
import { VERSION, LATEST_VERSION, REGISTRY, semverGt, checkLatest, CONFIG_PATH, loadConfig, saveConfig, loadTasks, hasCmd, chromeBinary, CONFIG_DIR } from "./core.mjs";
|
|
15
16
|
import { shSingle, handoffCmd, setHandoffCmd, grantAgent, runHandoff, track, detectHandoffs, connectorsInfo, answerQuestions, skipIdea, agentsList, startWaiting, parkLane, parkedPaths, autoAllowSweep, trustFull } from "./agents.mjs";
|
|
16
17
|
import { PROVIDERS, resolveProvider, connectProvider, detectHardware, recommendModels, hasOllama, ollamaInstall, ensureOllama, useOllamaModel } from "./ai.mjs";
|
|
17
18
|
import { SCAN, SCAN_TIMEOUT_MS, scanRoots, scanHome, addScanRoot, removeScanRoot, buildMap, nodeDetail, repoPathMap } from "./scan.mjs";
|
|
@@ -98,6 +99,21 @@ async function registryChangelog(version, registry = REGISTRY) {
|
|
|
98
99
|
// Self-contained HTML served at / — no backticks or ${} inside (it lives in a
|
|
99
100
|
// template literal). Talks to the local API with the per-launch token.
|
|
100
101
|
|
|
102
|
+
// Screenshots dropped into the talk: data: URLs of PNG, JPEG, WebP or GIF, up to 4 of
|
|
103
|
+
// 8 MB each, saved under Symbiot's config (uploads/, readable by you only, kept a week), so the
|
|
104
|
+
// model can see them and an agent can open them. Returns their paths.
|
|
105
|
+
const UPLOADS = join(CONFIG_DIR, "uploads");
|
|
106
|
+
function saveShots(list) {
|
|
107
|
+
const out = [];
|
|
108
|
+
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
|
|
109
|
+
for (const it of (Array.isArray(list) ? list : []).slice(0, 4)) {
|
|
110
|
+
const m = /^data:image\/(png|jpeg|jpg|webp|gif);base64,([A-Za-z0-9+/=]+)$/.exec(String((it && it.data) || ""));
|
|
111
|
+
if (!m) continue;
|
|
112
|
+
const buf = Buffer.from(m[2], "base64"); if (!buf.length || buf.length > 8 * 1024 * 1024) continue;
|
|
113
|
+
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 {}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
101
117
|
function readBody(req) {
|
|
102
118
|
return new Promise((resolve) => {
|
|
103
119
|
let d = ""; req.on("data", (ch) => (d += ch));
|
|
@@ -328,7 +344,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
328
344
|
if (u.pathname === "/api/work") return json(res, workScene());
|
|
329
345
|
if (u.pathname === "/api/work/go" && req.method === "POST") return json(res, workGo());
|
|
330
346
|
if (u.pathname === "/api/firststeps") return json(res, firstSteps()); // Settings' first steps: what's set up, in order
|
|
331
|
-
if (u.pathname === "/api/home/ask" && req.method === "POST") { const b = await readBody(req); return json(res, await homeAsk(b.question)); }
|
|
347
|
+
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) })); }
|
|
332
348
|
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" } : {}) }));
|
|
333
349
|
if (u.pathname === "/api/adapt/use" && req.method === "POST") { const b = await readBody(req); return json(res, noteUse(b)); }
|
|
334
350
|
// Lanes (lanes.mjs): work agents handed to each other, and where it stands.
|
|
@@ -398,7 +414,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
398
414
|
// giving up and sending you to a terminal.
|
|
399
415
|
const attempt = (n) => track("symbiot update", inst, homedir(), (code) => {
|
|
400
416
|
if (code !== 0) {
|
|
401
|
-
|
|
417
|
+
const tail = readRunLog(join(homedir(), ".symbiot", "agent.log")).slice(-3000);
|
|
402
418
|
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(); }
|
|
403
419
|
else UPDATING = null;
|
|
404
420
|
return;
|
package/tasks.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { handoverRules, ONLY_YOU, HANDBACK } from "./handover.mjs";
|
|
|
11
11
|
import { userStyleLine } from "./adapt.mjs";
|
|
12
12
|
import { QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, handoffCmd, runningHandoff, writeTasks, droppedTasks, startHeldTasks, connectorsLine } from "./agents.mjs";
|
|
13
13
|
import { gitDefaultBranch } from "./drift.mjs";
|
|
14
|
+
import { readRunLog } from "./work.mjs";
|
|
14
15
|
import { repoPathMap, openWork, detectRepo } from "./scan.mjs";
|
|
15
16
|
|
|
16
17
|
// ---- tasks: a persistent checklist (stored by core.mjs) -------------------
|
|
@@ -342,7 +343,7 @@ function shipWithBump(path, texts, opts) {
|
|
|
342
343
|
// What the last run in a folder said when it finished: agent.log after the last
|
|
343
344
|
// run's "=== name time ===" and "$ command" lines.
|
|
344
345
|
function runSummary(path) {
|
|
345
|
-
|
|
346
|
+
const log = readRunLog(join(path, ".symbiot", "agent.log")); if (!log) return "";
|
|
346
347
|
const at = log.lastIndexOf("\n=== "), rest = at < 0 ? log : log.slice(log.indexOf("\n", at + 1) + 1);
|
|
347
348
|
return rest.replace(/^\$ .*\n?/, "").trim().slice(-8000);
|
|
348
349
|
}
|
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;
|
|
@@ -2501,7 +2522,7 @@ api('/api/home?fresh=1').then(function(h){if(h&&h.you)LQ.home=h;renderNeeds();})
|
|
|
2501
2522
|
// Needs you here: what only you can do in the project the Tasks list is filtered
|
|
2502
2523
|
// to, at its top, each answerable where it is; its held handovers under them.
|
|
2503
2524
|
function renderNeeds(){var el=$('needsbox');if(!el)return;var repo=TFILTER.repo;if(!repo||TARCH){el.innerHTML='';return;}
|
|
2504
|
-
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';});
|
|
2505
2526
|
var paint=function(held){if(TFILTER.repo!==repo)return;if(!ys.length&&!held.length){el.innerHTML='';lqLight();return;}
|
|
2506
2527
|
el.innerHTML="<div class='tgroup' style='color:var(--amber)'>Needs you here <span class='tcount'>"+(ys.length+held.length)+"</span></div>"+
|
|
2507
2528
|
ys.map(function(y,i){return "<div class='task nitem lqblob' data-k='"+i+"'>"+blobBody(y,false)+"</div>";}).join('')+
|
|
@@ -2535,17 +2556,23 @@ function lqPool(shape){if(shape==='map'&&LM.items&&LM.items.length)setTimeout(fu
|
|
|
2535
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);}
|
|
2536
2557
|
function lqAct(){LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
|
|
2537
2558
|
function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
|
|
2538
|
-
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;
|
|
2539
2560
|
// scrolled up to read back: the whole history shows, clear; back at the bottom it rolls on again
|
|
2540
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);});}}
|
|
2541
|
-
|
|
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();
|
|
2542
2569
|
var low=q.toLowerCase().replace(/^(please |can you |could you )/,''),hit='';
|
|
2543
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;});
|
|
2544
2571
|
if(hit){lqTalkMode(true);lqUse(hit,null,'talk');lqGo(hit);return;}
|
|
2545
2572
|
if(/^(go|start|go for it|start them|start it)[.! ]*$/.test(low)){lqTalkMode(true);lqGoWork();return;}
|
|
2546
2573
|
if(/^(home|back)[.! ]*$/.test(low)&&LQ.scene==='work'){lqHome();return;}
|
|
2547
|
-
LQ.talk.push({me:true,text:q});LQ.talk=LQ.talk.slice(-8);lqTalkShow(true);
|
|
2548
|
-
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);});}
|
|
2549
2576
|
// The physics: each droplet a critically damped spring to its place (no wobble,
|
|
2550
2577
|
// no overshoot), pushed off its neighbours and the core where they'd overlap.
|
|
2551
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;
|
|
@@ -2640,7 +2667,9 @@ if(document.addEventListener){document.addEventListener('contextmenu',function(e
|
|
|
2640
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();}});}
|
|
2641
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();});}
|
|
2642
2669
|
var f=$('lqform');if(f&&f.addEventListener)f.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();lqSay();});
|
|
2643
|
-
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();}});}
|
|
2644
2673
|
var co=$('lqcore');if(co&&co.addEventListener)co.addEventListener('click',function(){var i=$('lqask');if(i&&i.focus)i.focus();});
|
|
2645
2674
|
var sk=$('lqsink');if(sk&&sk.addEventListener)sk.addEventListener('click',lqSink);
|
|
2646
2675
|
var bk=$('lqback');if(bk&&bk.addEventListener)bk.addEventListener('click',lqHome);
|
package/work.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// time. Another agent (Codex, Aider, Gemini) writes plain text: its last lines
|
|
9
9
|
// come through as what it said. Pure: text in, data out.
|
|
10
10
|
|
|
11
|
+
import { statSync, openSync, readSync, closeSync } from "node:fs";
|
|
12
|
+
|
|
11
13
|
const MAX_STEPS = 60;
|
|
12
14
|
const short = (s, n = 80) => { s = String(s ?? "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
|
|
13
15
|
const base = (p) => String(p || "").split("/").filter(Boolean).slice(-2).join("/");
|
|
@@ -103,6 +105,29 @@ function parseRun(text) {
|
|
|
103
105
|
return out;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
// The newest run of an agent.log, from its "=== " header on, read from the end of the
|
|
109
|
+
// file. A log keeps every run and everything they read (one reached 144 MB: 96 runs and
|
|
110
|
+
// the screenshots they looked at), and every view only needs the newest, so it never
|
|
111
|
+
// reads the rest; and it's kept until the file changes, so a view that asks again
|
|
112
|
+
// costs nothing. Past RUN_CAP of one run, it's that run's last RUN_CAP.
|
|
113
|
+
const RUN_CAP = 16 * 1024 * 1024, RUN_CACHE = new Map();
|
|
114
|
+
function readRunLog(file) {
|
|
115
|
+
let st; try { st = statSync(file); } catch { return ""; }
|
|
116
|
+
const hit = RUN_CACHE.get(file); if (hit && hit.size === st.size && hit.mtime === st.mtimeMs) return hit.text;
|
|
117
|
+
let fd; try { fd = openSync(file, "r"); } catch { return ""; }
|
|
118
|
+
let buf = Buffer.alloc(0), at = -1;
|
|
119
|
+
try {
|
|
120
|
+
for (let end = st.size; end > 0 && buf.length < RUN_CAP;) {
|
|
121
|
+
const start = Math.max(0, end - (1 << 20)), b = Buffer.alloc(end - start);
|
|
122
|
+
readSync(fd, b, 0, b.length, start); buf = Buffer.concat([b, buf]); end = start;
|
|
123
|
+
at = buf.lastIndexOf("\n=== "); if (at >= 0) break;
|
|
124
|
+
}
|
|
125
|
+
} catch { buf = Buffer.alloc(0); } finally { try { closeSync(fd); } catch {} }
|
|
126
|
+
const text = (at >= 0 ? buf.subarray(at) : buf).toString("utf8");
|
|
127
|
+
RUN_CACHE.delete(file); RUN_CACHE.set(file, { size: st.size, mtime: st.mtimeMs, text });
|
|
128
|
+
if (RUN_CACHE.size > 40) RUN_CACHE.delete(RUN_CACHE.keys().next().value);
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
106
131
|
// The newest run's part of an agent.log (after its header and command line).
|
|
107
132
|
function lastRunText(log) {
|
|
108
133
|
const s = String(log || ""), at = s.lastIndexOf("\n=== ");
|
|
@@ -111,4 +136,4 @@ function lastRunText(log) {
|
|
|
111
136
|
// What the agent said last: its final answer, from either kind of log.
|
|
112
137
|
function finalOf(log) { return parseRun(lastRunText(log)).final; }
|
|
113
138
|
|
|
114
|
-
export { parseRun, lastRunText, finalOf, describe, testsIn };
|
|
139
|
+
export { parseRun, lastRunText, finalOf, describe, testsIn, readRunLog };
|