symbiot 0.37.0 → 0.38.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/README.md +7 -4
- package/index.mjs +27 -4
- package/package.json +1 -1
- package/screens.mjs +47 -8
- package/ui.mjs +7 -1
package/README.md
CHANGED
|
@@ -414,15 +414,18 @@ agent or script can read it.
|
|
|
414
414
|
**Click here** on a region moves your mouse to the region's centre and clicks there,
|
|
415
415
|
on your real screen. It asks you to confirm every time, and can wait 3, 5 or 10
|
|
416
416
|
seconds first so you can bring the right window to the front. It clicks with
|
|
417
|
-
`cliclick` on macOS (`brew install cliclick`),
|
|
418
|
-
|
|
419
|
-
ydotool 1.x).
|
|
417
|
+
`cliclick` on macOS (`brew install cliclick`), with PowerShell on Windows (nothing to
|
|
418
|
+
install), and on Linux with `xdotool` on X11 or `ydotool` on Wayland (which needs
|
|
419
|
+
access to `/dev/uinput`, and `ydotoold` running for ydotool 1.x). Nothing types yet.
|
|
420
420
|
|
|
421
421
|
Capture uses the screenshot tool your system has: `screencapture` on macOS,
|
|
422
422
|
PowerShell on Windows, and on Linux `gnome-screenshot`, `spectacle`,
|
|
423
423
|
`cosmic-screenshot`, `grim` or `xfce4-screenshooter` (plus `scrot` / ImageMagick
|
|
424
424
|
`import` on X11). Why it's experimental: Wayland desktops restrict screenshots, so
|
|
425
|
-
capture may fail or ask for permission (use **Load image** then).
|
|
425
|
+
capture may fail or ask for permission (use **Load image** then). If your desktop
|
|
426
|
+
has screenshots turned off for the app Symbiot was started from (say, the COSMIC
|
|
427
|
+
dock), Symbiot says so and offers **Allow screenshots**, which turns them on for
|
|
428
|
+
apps started that way. And on a scaled
|
|
426
429
|
(HiDPI) display, screenshot pixels can differ from the coordinates a click tool
|
|
427
430
|
expects. Symbiot rescales for `xdotool` and for `cliclick` on a Retina screen, but
|
|
428
431
|
it can't read the scale on Wayland, and ydotool's moves follow your pointer
|
package/index.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { detectMailSources, mailActivity } from "./mail.mjs";
|
|
|
32
32
|
import { CONFIG_PATH, loadConfig, saveConfig, loadTasks, saveTasks, sh, hasCmd, repoState } from "./core.mjs";
|
|
33
33
|
import { HANDOFF_PROMPT, QUESTIONS_MAX, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, fillHandoff, runHandoff, runningHandoff, writeTasks, startHeldTasks, detectHandoffs, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, parseQuestions, agentQuestions, answerQuestions, agentsList } from "./agents.mjs";
|
|
34
34
|
import { gitDefaultBranch, loadDeploys, driftRepo } from "./drift.mjs";
|
|
35
|
-
import { loadScreens, screenImage, captureScreen, importScreen, setRegions, renameScreen, removeScreen, blueprint, clickRegion } from "./screens.mjs";
|
|
35
|
+
import { loadScreens, screenImage, captureScreen, allowScreenshots, importScreen, setRegions, renameScreen, removeScreen, blueprint, clickRegion } from "./screens.mjs";
|
|
36
36
|
import { weeklyState, setWeekly, runWeekly, startWeekly, autostartState, setAutostart } from "./desktop.mjs";
|
|
37
37
|
|
|
38
38
|
const HERE = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -105,6 +105,10 @@ function addTask(text, repo) {
|
|
|
105
105
|
text = String(text || "").trim().slice(0, 300);
|
|
106
106
|
if (!text) return { error: "empty" };
|
|
107
107
|
const t = loadTasks();
|
|
108
|
+
// The same text already open in the same repo (in review counts): that task, not a duplicate.
|
|
109
|
+
const same = (s) => String(s || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
110
|
+
const dup = t.find((x) => !x.done && !x.archived && (x.repo || "") === (repo || "") && same(x.text) === same(text));
|
|
111
|
+
if (dup) return { ...dup, duplicate: true };
|
|
108
112
|
const item = { id: randomBytes(6).toString("hex"), text, repo: repo || "", done: false, ts: Date.now() };
|
|
109
113
|
t.unshift(item); saveTasks(t); return item;
|
|
110
114
|
}
|
|
@@ -163,6 +167,23 @@ function workingDiff(path, cap = 400000) {
|
|
|
163
167
|
}
|
|
164
168
|
return d.length > cap ? d.slice(0, cap) + "\n… (diff truncated)" : d;
|
|
165
169
|
}
|
|
170
|
+
// Merged work that isn't released yet: how many commits the default branch (or
|
|
171
|
+
// origin's copy, where approved PRs merge) is past its last v* tag, or null when
|
|
172
|
+
// it isn't, or the repo doesn't release with v* tags. bump: the version these
|
|
173
|
+
// uncommitted changes set in package.json, if they change it.
|
|
174
|
+
function unreleased(path) {
|
|
175
|
+
const base = gitDefaultBranch(path); let best = null;
|
|
176
|
+
for (const ref of ["refs/heads/" + base, "refs/remotes/origin/" + base]) {
|
|
177
|
+
const tag = git(path, ["describe", "--tags", "--abbrev=0", "--match", "v*", ref]).out; if (!tag) continue;
|
|
178
|
+
const ahead = +git(path, ["rev-list", "--count", tag + ".." + ref]).out || 0;
|
|
179
|
+
if (!best || ahead > best.ahead) best = { base, tag, ahead };
|
|
180
|
+
}
|
|
181
|
+
if (!best || !best.ahead) return null;
|
|
182
|
+
const ver = (s) => { try { return String(JSON.parse(s).version || ""); } catch { return ""; } };
|
|
183
|
+
let now = ""; try { now = ver(readFileSync(join(path, "package.json"), "utf8")); } catch {}
|
|
184
|
+
const was = ver(git(path, ["show", "HEAD:package.json"]).out);
|
|
185
|
+
return { ...best, ...(now && was && now !== was ? { bump: now } : {}) };
|
|
186
|
+
}
|
|
166
187
|
// Repos with tasks awaiting review, plus repos Symbiot sent tasks to that have
|
|
167
188
|
// uncommitted changes no ticked task covers (untasked: approve them as-is).
|
|
168
189
|
// running: an agent is still editing there, so its changes may be half done.
|
|
@@ -171,10 +192,10 @@ function pendingReview() {
|
|
|
171
192
|
const sent = [...new Set(t.filter((x) => x.repo && !x.archived && !by[x.repo]).map((x) => x.repo))];
|
|
172
193
|
const map = Object.keys(by).length || sent.length ? repoPathMap() : {};
|
|
173
194
|
const am = autoMergeRepos();
|
|
174
|
-
const out = Object.keys(by).sort().map((repo) => { const path = map[repo] || ""; return { repo, path, tasks: by[repo], autoMerge: am.includes(repo), running: !!(path && runningHandoff(path)), ...(path ? workingChanges(path) : { branch: "", files: [], stat: "" }) }; });
|
|
195
|
+
const out = Object.keys(by).sort().map((repo) => { const path = map[repo] || ""; return { repo, path, tasks: by[repo], autoMerge: am.includes(repo), running: !!(path && runningHandoff(path)), ...(path ? { ...workingChanges(path), unreleased: unreleased(path) } : { branch: "", files: [], stat: "" }) }; });
|
|
175
196
|
for (const repo of sent.sort()) {
|
|
176
197
|
const path = map[repo]; if (!path || !existsSync(join(path, ".symbiot", "TASKS.md"))) continue;
|
|
177
|
-
const wc = workingChanges(path); if (wc.files.length) out.push({ repo, path, tasks: [], untasked: true, autoMerge: am.includes(repo), running: !!runningHandoff(path), ...wc });
|
|
198
|
+
const wc = workingChanges(path); if (wc.files.length) out.push({ repo, path, tasks: [], untasked: true, autoMerge: am.includes(repo), running: !!runningHandoff(path), ...wc, unreleased: unreleased(path) });
|
|
178
199
|
}
|
|
179
200
|
return out;
|
|
180
201
|
}
|
|
@@ -1514,6 +1535,8 @@ async function cmdApp() {
|
|
|
1514
1535
|
if (u.pathname === "/api/screens") return json(res, loadScreens().map(screenOut));
|
|
1515
1536
|
if (u.pathname === "/api/screens/image") { const f = screenImage(u.searchParams.get("id")); if (!f) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "content-type": "image/png", "cache-control": "private, max-age=86400" }); res.end(readFileSync(f)); return; } // a screen's image never changes
|
|
1516
1537
|
if (u.pathname === "/api/screens/capture" && req.method === "POST") { const b = await readBody(req); const d = Math.min(10, Math.max(0, Number(b.delay) || 0)); if (d) await new Promise((r) => setTimeout(r, d * 1000)); return json(res, screenOut(captureScreen(b.name))); }
|
|
1538
|
+
// Changes a desktop permission, so only on the user's confirmed click.
|
|
1539
|
+
if (u.pathname === "/api/screens/allow" && req.method === "POST") { const b = await readBody(req); if (b.confirmed !== true) return json(res, { error: "Allowing screenshots needs your confirmation." }); return json(res, allowScreenshots()); }
|
|
1517
1540
|
if (u.pathname === "/api/screens/import" && req.method === "POST") { const b = await readBody(req); return json(res, screenOut(importScreen(b.name, b.png))); }
|
|
1518
1541
|
if (u.pathname === "/api/screens/regions" && req.method === "POST") { const b = await readBody(req); return json(res, screenOut(setRegions(String(b.id || ""), b.regions))); }
|
|
1519
1542
|
if (u.pathname === "/api/screens/rename" && req.method === "POST") { const b = await readBody(req); return json(res, screenOut(renameScreen(String(b.id || ""), b.name))); }
|
|
@@ -1647,4 +1670,4 @@ const isMain = (() => {
|
|
|
1647
1670
|
})();
|
|
1648
1671
|
if (isMain) main();
|
|
1649
1672
|
|
|
1650
|
-
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, taskType, EMBEDDED_UI, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, fillHandoff, handoffCmd, setHandoffCmd, ORCA_CLAUDE_CMD, CLAUDE_CMD, HANDOFF_PROMPT, shipChanges, syncTasks, pendingReview, approveRepo, approveChanges, sendBack, pushTasks, semverGt, updateCmd, parseQuestions, agentQuestions };
|
|
1673
|
+
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, taskType, EMBEDDED_UI, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, fillHandoff, handoffCmd, setHandoffCmd, ORCA_CLAUDE_CMD, CLAUDE_CMD, HANDOFF_PROMPT, shipChanges, syncTasks, pendingReview, unreleased, addTask, approveRepo, approveChanges, sendBack, pushTasks, semverGt, updateCmd, parseQuestions, agentQuestions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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 — Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/screens.mjs
CHANGED
|
@@ -30,6 +30,13 @@ function pngSize(buf) {
|
|
|
30
30
|
return w && h ? { w, h } : null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Windows: user32 calls for PowerShell. DPI-aware first, so with display scaling
|
|
34
|
+
// the capture is the whole screen in real pixels and a click lands in the same ones.
|
|
35
|
+
const WIN_USER32 = "Add-Type -Namespace Symbiot -Name User32 -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int x, int y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, System.UIntPtr e);'; ";
|
|
36
|
+
const WIN_DPI = WIN_USER32 + "[Symbiot.User32]::SetProcessDPIAware() | Out-Null; ";
|
|
37
|
+
// -EncodedCommand (base64 UTF-16LE) carries the script's quotes through untouched.
|
|
38
|
+
const psEncoded = (script) => ["powershell", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")]];
|
|
39
|
+
|
|
33
40
|
// Screenshot tools to try, in order, as [cmd, args] that write a PNG to `file`,
|
|
34
41
|
// or [cmd, args, dir] for a tool that only takes a folder (it picks the name).
|
|
35
42
|
// scrot and import only see X11, which on Wayland is a blank screen, so they're
|
|
@@ -37,13 +44,43 @@ function pngSize(buf) {
|
|
|
37
44
|
function captureCmds(file, platform = process.platform, wayland = process.env.XDG_SESSION_TYPE === "wayland") {
|
|
38
45
|
if (platform === "darwin") return [["screencapture", ["-x", "-t", "png", file]]];
|
|
39
46
|
if (platform === "win32") {
|
|
40
|
-
const ps = "Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $b = [System.Windows.Forms.SystemInformation]::VirtualScreen; $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height; $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Left, $b.Top, 0, 0, $bmp.Size); $bmp.Save('" + file.replace(/'/g, "''") + "', [System.Drawing.Imaging.ImageFormat]::Png)";
|
|
41
|
-
return [
|
|
47
|
+
const ps = WIN_DPI + "Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $b = [System.Windows.Forms.SystemInformation]::VirtualScreen; $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height; $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Left, $b.Top, 0, 0, $bmp.Size); $bmp.Save('" + file.replace(/'/g, "''") + "', [System.Drawing.Imaging.ImageFormat]::Png)";
|
|
48
|
+
return [psEncoded(ps)];
|
|
42
49
|
}
|
|
43
50
|
const dir = file.replace(/\.png$/, ".d");
|
|
44
51
|
return [["gnome-screenshot", ["-f", file]], ["spectacle", ["-b", "-n", "-f", "-o", file]], ["cosmic-screenshot", ["--interactive=false", "--modal=false", "--notify=false", "--save-dir", dir], dir], ["grim", [file]],
|
|
45
52
|
["xfce4-screenshooter", ["-f", "-s", file]], ...(wayland ? [] : [["scrot", [file]], ["import", ["-window", "root", file]]])];
|
|
46
53
|
}
|
|
54
|
+
// On Wayland, cosmic-screenshot (and other portal tools) ask xdg-desktop-portal,
|
|
55
|
+
// which keys its screenshot permission on the app that started us: the systemd
|
|
56
|
+
// unit in our cgroup, app[-<launcher>]-<AppID>-<random>.scope or
|
|
57
|
+
// app[-<launcher>]-<AppID>[@<random>].service. Started from the COSMIC dock,
|
|
58
|
+
// that's com.system76.CosmicAppList; from a terminal, "" (no app id).
|
|
59
|
+
function portalAppId(cgroup) {
|
|
60
|
+
const unit = String(cgroup || "").trim().split("\n").pop().split("/").pop();
|
|
61
|
+
const m = unit.match(/^app-(?:[A-Za-z0-9]+-)?(.+?)(?:-[^-]*)?\.scope$/) || unit.match(/^app-(?:[A-Za-z0-9]+-)?(.+?)(?:@.*)?\.service$/);
|
|
62
|
+
const id = m ? m[1].replace(/\\x2d/g, "-") : "";
|
|
63
|
+
return /^[A-Za-z_][\w-]*(\.[A-Za-z_][\w-]*)+$/.test(id) ? id : "";
|
|
64
|
+
}
|
|
65
|
+
const PERMS = ["--session", "--dest", "org.freedesktop.impl.portal.PermissionStore", "--object-path", "/org/freedesktop/impl/portal/PermissionStore", "--method"];
|
|
66
|
+
// The app id the portal has screenshots turned off for (it answers "no" without
|
|
67
|
+
// asking again), or "" when they're allowed, unset or it can't be read.
|
|
68
|
+
function screenshotBlocked() {
|
|
69
|
+
if (process.platform !== "linux" || !hasCmd("gdbus")) return "";
|
|
70
|
+
let id = ""; try { id = portalAppId(readFileSync("/proc/self/cgroup", "utf8")); } catch {}
|
|
71
|
+
if (!id) return "";
|
|
72
|
+
const out = String(spawnSync("gdbus", ["call", ...PERMS, "org.freedesktop.impl.portal.PermissionStore.Lookup", "screenshot", "screenshot"], { encoding: "utf8", timeout: 5000 }).stdout || "");
|
|
73
|
+
const m = out.match(new RegExp("'" + id.replace(/[.\\-]/g, "\\$&") + "': \\['(\\w+)'"));
|
|
74
|
+
return m && m[1] === "no" ? id : "";
|
|
75
|
+
}
|
|
76
|
+
// Turn screenshots on for the app id that blocks them. Only when the user asks,
|
|
77
|
+
// in the app: it's their desktop's permission, and it covers every app started
|
|
78
|
+
// the same way (e.g. everything from the COSMIC dock).
|
|
79
|
+
function allowScreenshots() {
|
|
80
|
+
const id = screenshotBlocked(); if (!id) return { error: "Screenshots aren't turned off for Symbiot. Try Capture screen again." };
|
|
81
|
+
const p = spawnSync("gdbus", ["call", ...PERMS, "org.freedesktop.impl.portal.PermissionStore.SetPermission", "screenshot", "true", "screenshot", id, "['yes']"], { encoding: "utf8", timeout: 5000 });
|
|
82
|
+
return p.status === 0 ? { ok: true, app: id } : { error: "Couldn't change the permission: " + (String(p.stderr || "").trim() || "exit " + p.status) };
|
|
83
|
+
}
|
|
47
84
|
function addScreen(id, name, size, via) {
|
|
48
85
|
const s = { id, name: String(name || "").trim().slice(0, 80) || "Screen " + new Date().toLocaleString(), w: size.w, h: size.h, ts: Date.now(), via, regions: [] };
|
|
49
86
|
saveScreens([s, ...loadScreens()]);
|
|
@@ -64,6 +101,8 @@ function captureScreen(name) {
|
|
|
64
101
|
if (size) return addScreen(id, name, size, cmd);
|
|
65
102
|
try { unlinkSync(file); } catch {}
|
|
66
103
|
}
|
|
104
|
+
const blocked = tried.length && screenshotBlocked();
|
|
105
|
+
if (blocked) return { error: `Your desktop has screenshots turned off for apps started from ${blocked}, which is how Symbiot was started (tried ${tried.join(", ")}). Allow screenshots to turn them back on, or take one yourself and use Load image.`, blocked };
|
|
67
106
|
return { error: tried.length ? `Couldn't take a screenshot (tried ${tried.join(", ")}). Take one yourself and use Load image.` : "No screenshot tool found. Install gnome-screenshot, spectacle, grim or scrot, or take one yourself and use Load image." };
|
|
68
107
|
}
|
|
69
108
|
// A PNG you already have (base64, or a data: URL), for when capture can't run.
|
|
@@ -108,11 +147,12 @@ function blueprint(s) {
|
|
|
108
147
|
// mouse to (x, y) and left-click there. xdotool only reaches X11 windows, so on
|
|
109
148
|
// Wayland it's ydotool (it needs /dev/uinput). ydotool 1.x moves relative unless
|
|
110
149
|
// told --absolute and takes a button code; 0.1.x (Debian/Ubuntu) moves to the
|
|
111
|
-
// point and takes a button number.
|
|
150
|
+
// point and takes a button number. On Windows, PowerShell: SetCursorPos, offset
|
|
151
|
+
// by the virtual screen's corner (the capture's 0,0), then left down + up.
|
|
112
152
|
function clickCmds(x, y, platform = process.platform, wayland = process.env.XDG_SESSION_TYPE === "wayland", ydotool1 = false) {
|
|
113
|
-
x = String(x); y = String(y);
|
|
153
|
+
x = String(Math.round(Number(x)) || 0); y = String(Math.round(Number(y)) || 0);
|
|
114
154
|
if (platform === "darwin") return [[["cliclick", ["c:" + x + "," + y]]]];
|
|
115
|
-
if (platform === "win32") return [];
|
|
155
|
+
if (platform === "win32") return [[psEncoded(WIN_DPI + "Add-Type -AssemblyName System.Windows.Forms; $b = [System.Windows.Forms.SystemInformation]::VirtualScreen; if (-not [Symbiot.User32]::SetCursorPos($b.Left + " + x + ", $b.Top + " + y + ")) { [Console]::Error.WriteLine('SetCursorPos failed'); exit 1 }; Start-Sleep -Milliseconds 50; [Symbiot.User32]::mouse_event(2, 0, 0, 0, [UIntPtr]::Zero); [Symbiot.User32]::mouse_event(4, 0, 0, 0, [UIntPtr]::Zero)")]];
|
|
116
156
|
const ydo = ydotool1 ? [["ydotool", ["mousemove", "--absolute", "-x", x, "-y", y]], ["ydotool", ["click", "0xC0"]]] : [["ydotool", ["mousemove", x, y]], ["ydotool", ["click", "1"]]];
|
|
117
157
|
const xdo = [["xdotool", ["mousemove", "--sync", x, y, "click", "1"]]];
|
|
118
158
|
return wayland ? [ydo] : [xdo, ydo];
|
|
@@ -131,7 +171,6 @@ function clickSpace(tool) {
|
|
|
131
171
|
function clickRegion(id, regionId) {
|
|
132
172
|
const s = loadScreens().find((x) => x.id === id); if (!s) return { error: "not found" };
|
|
133
173
|
const r = (s.regions || []).find((x) => x.id === regionId); if (!r) return { error: "That region is gone. Reload the screen." };
|
|
134
|
-
if (process.platform === "win32") return { error: "Clicking isn't supported on Windows yet." };
|
|
135
174
|
const c = center(r), wayland = process.env.XDG_SESSION_TYPE === "wayland";
|
|
136
175
|
// The first tool that's installed does it. If that fails it says so, rather
|
|
137
176
|
// than trying the next one: a half-done move + click could otherwise click twice.
|
|
@@ -145,8 +184,8 @@ function clickRegion(id, regionId) {
|
|
|
145
184
|
}
|
|
146
185
|
return { ok: true, label: r.label, x, y, via: tool };
|
|
147
186
|
}
|
|
148
|
-
const want = process.platform === "darwin" ? "cliclick (brew install cliclick)" : wayland ? "ydotool" : "xdotool or ydotool";
|
|
187
|
+
const want = process.platform === "darwin" ? "cliclick (brew install cliclick)" : process.platform === "win32" ? "Windows PowerShell" : wayland ? "ydotool" : "xdotool or ydotool";
|
|
149
188
|
return { error: `No click tool found. Install ${want}.` };
|
|
150
189
|
}
|
|
151
190
|
|
|
152
|
-
export { loadScreens, screenImage, pngSize, captureCmds, captureScreen, importScreen, setRegions, renameScreen, removeScreen, blueprint, clickCmds, clickRegion };
|
|
191
|
+
export { loadScreens, screenImage, pngSize, captureCmds, portalAppId, allowScreenshots, captureScreen, importScreen, setRegions, renameScreen, removeScreen, blueprint, clickCmds, clickRegion };
|
package/ui.mjs
CHANGED
|
@@ -414,6 +414,7 @@ h+="<div class='rcard' data-repo='"+esc(r.repo)+"'><div class='rhead'><b>"+esc(r
|
|
|
414
414
|
r.tasks.forEach(function(t){PENDTASKS.push(t);h+="<div class='task' data-id='"+esc(t.id)+"'><span class='t'>"+esc(t.text)+"</span>"+askBtn(t)+"<button class='rm sendback' title='not right - send back to the agent (unticks it)'>↶</button></div>";});
|
|
415
415
|
if(r.untasked)h+="<div class='muted' style='margin:6px 0'>Uncommitted changes with no ticked task behind them. Check the diff before you approve.</div>";
|
|
416
416
|
if(r.running)h+="<div class='muted' style='margin:6px 0'>⏳ Agent still working: its changes may be half done. Approve unlocks when it finishes.</div>";
|
|
417
|
+
var ur=r.unreleased;if(ur)h+="<div class='muted' style='margin:6px 0'><span class='err'>Unreleased:</span> <code>"+esc(ur.base)+"</code> is "+ur.ahead+" commit"+(ur.ahead>1?"s":"")+" past <code>"+esc(ur.tag)+"</code>, so merged work isn't released yet. "+(ur.bump?"These changes bump the version to <b>"+esc(ur.bump)+"</b>: after the PR merges, tag <code>v"+esc(ur.bump)+"</code> on <code>"+esc(ur.base)+"</code> to release it.":"Bump the version (here or in a later PR) and tag it to release it.")+"</div>";
|
|
417
418
|
h+="<div class='row'><button class='act approve'"+(r.path&&!r.running?"":" disabled")+(r.running?" title='the agent is still editing this repo'>"+(r.untasked?"Approve changes without a task":"Approve")+" · agent still working":r.untasked?" data-untasked='1' title='commit on a branch, push and open a PR, without a task'>Approve changes without a task → PR":" title='commit on a branch, push, open a PR, then archive'>Approve → "+(r.files.length?"PR":"archive"))+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"<label title='Queue GitHub auto-merge so this PR lands once its CI checks pass. Needs Allow auto-merge on the repo.' style='margin-left:auto;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px'><input type='checkbox' class='amtoggle' style='width:auto'"+(r.autoMerge?" checked":"")+"> auto-merge on green CI</label></div><div class='rdiff hidden'></div></div>";});
|
|
418
419
|
el.innerHTML=h;
|
|
419
420
|
el.querySelectorAll('.rcard').forEach(function(card){var repo=card.getAttribute('data-repo');
|
|
@@ -633,10 +634,15 @@ if(typeof confirm!=='function'||!confirm(q))return;
|
|
|
633
634
|
b.disabled=true;$('screenmsg').innerHTML="<div class='note muted'>"+(d?"Clicking “"+esc(r.label)+"” in "+d+"s — bring the right window to the front…":"Clicking…")+"</div>";
|
|
634
635
|
api('/api/screens/click',{id:s.id,region:r.id,delay:d,confirmed:true}).then(function(x){b.disabled=false;if(!x||x.error){screenErr((x&&x.error)||'failed');return;}
|
|
635
636
|
$('screenmsg').innerHTML="<div class='note ok'>✓ Clicked “"+esc(x.label)+"” at ("+x.x+", "+x.y+") with "+esc(x.via)+".</div>";}).catch(function(e){b.disabled=false;screenErr(String((e&&e.message)||e));});}
|
|
636
|
-
function screenAdded(s){if(!s||s.error){screenErr((s&&s.error)||'failed');return;}$('screenmsg').innerHTML='';$('screenname').value='';SCREEN=s;SPEND=null;loadScreensUI();}
|
|
637
|
+
function screenAdded(s){if(!s||s.error){screenErr((s&&s.error)||'failed');if(s&&s.blocked)allowShotsBtn(s.blocked);return;}$('screenmsg').innerHTML='';$('screenname').value='';SCREEN=s;SPEND=null;loadScreensUI();}
|
|
637
638
|
function captureUI(){var b=$('capture'),d=+($('screendelay').value||0);b.disabled=true;
|
|
638
639
|
$('screenmsg').innerHTML="<div class='note muted'>"+(d?"Capturing in "+d+"s — bring the window you want to the front…":"Capturing…")+"</div>";
|
|
639
640
|
api('/api/screens/capture',{name:$('screenname').value||'',delay:d}).then(function(s){b.disabled=false;screenAdded(s);}).catch(function(e){b.disabled=false;screenErr(String((e&&e.message)||e));});}
|
|
641
|
+
// The desktop's screenshot permission is off for the app Symbiot was started
|
|
642
|
+
// from: turn it on (asked first, it covers every app started that way), then capture.
|
|
643
|
+
function allowShotsBtn(app){$('screenmsg').insertAdjacentHTML('beforeend',"<div class='row' style='margin-top:6px'><button class='act' id='scrallow'>Allow screenshots</button></div>");
|
|
644
|
+
$('scrallow').addEventListener('click',function(){if(typeof confirm!=='function'||!confirm("Turn screenshots on for apps started from "+app+"?\\n\\nYour desktop keeps this permission per app, and Symbiot shares it with "+app+", so it covers every app started that way, not only Symbiot."))return;
|
|
645
|
+
$('scrallow').disabled=true;api('/api/screens/allow',{confirmed:true}).then(function(x){if(!x||x.error){screenErr((x&&x.error)||'failed');return;}captureUI();}).catch(function(e){screenErr(String((e&&e.message)||e));});});}
|
|
640
646
|
function pickImageUI(){var f=$('screenfile');if(f&&f.click)f.click();}
|
|
641
647
|
function importUI(){var f=$('screenfile'),file=f&&f.files&&f.files[0];if(!file)return;$('screenmsg').innerHTML="<div class='note muted'>Loading…</div>";
|
|
642
648
|
var rd=new FileReader();rd.onload=function(){api('/api/screens/import',{name:$('screenname').value||file.name.replace(/[.]png$/i,''),png:rd.result}).then(function(s){f.value='';screenAdded(s);});};rd.readAsDataURL(file);}
|