@bivy/bivy 0.18.3 → 0.18.4-staging.10
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 +77 -25
- package/bin/codex-app-server-shim.mjs +6 -1
- package/dist/apps/gateway.js +82 -8
- package/dist/apps/inspector.js +7 -2
- package/dist/apps/preview-shell.js +81 -29
- package/dist/apps/registry.js +7 -1
- package/dist/apps/remote-preview.js +4 -2
- package/dist/apps/styles.css +40 -48
- package/dist/apps/tokens.css +58 -51
- package/dist/git-auth.js +13 -5
- package/dist/runtime/activation-readiness.js +15 -0
- package/dist/runtime/anthropic-preflight.js +1 -1
- package/dist/server.js +43 -177
- package/dist/session/prompt-attachments.js +135 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,17 +4,22 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
7
|
+
**Build, try, and review apps from your phone. Your coding agent and dev
|
|
8
|
+
environment stay on your own machines.**
|
|
9
9
|
|
|
10
|
-
Bivy is an open-source
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Bivy is an open-source remote development environment built around coding
|
|
11
|
+
agents. Ask for a change, try the running app, and point to or draw on what
|
|
12
|
+
needs fixing. Send that feedback to the agent and try again—all in the same
|
|
13
|
+
session, without going back to your computer.
|
|
14
|
+
|
|
15
|
+
Live previews bring your app to your phone or browser without deploying it to
|
|
16
|
+
Render, building a Docker image, or setting up a separate preview server.
|
|
17
|
+
Share a preview link so someone else can try it and leave notes, too.
|
|
14
18
|
|
|
15
19
|
Keep Claude Code, Codex, Pi, OpenCode, or another supported agent. Keep your
|
|
16
|
-
repos, tools, and development environment. Bivy connects them
|
|
17
|
-
|
|
20
|
+
repos, tools, databases, and development environment. Bivy connects them to
|
|
21
|
+
chat, a terminal, app previews, and code review wherever you are. Start work
|
|
22
|
+
yourself, or let an issue, CI failure, message, schedule, or webhook start it.
|
|
18
23
|
|
|
19
24
|
**[Start free on Bivy Cloud](https://app.bivy.sh)** ·
|
|
20
25
|
**[Quickstart](docs/quickstart.md)** ·
|
|
@@ -47,11 +52,41 @@ entire remote-access stack.
|
|
|
47
52
|
> sandboxing depend on the runtime. See the
|
|
48
53
|
> [runtime support matrix](docs/runtime-support-matrix.md).
|
|
49
54
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
Remote
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
## Build → preview → mark up → iterate
|
|
56
|
+
|
|
57
|
+
Remote chat lets you ask an agent for a change. Bivy also lets you **use what
|
|
58
|
+
it built and show it what to fix**, without a separate deployment step.
|
|
59
|
+
|
|
60
|
+
1. **Ask for a change.** The agent works in your repo with your existing tools
|
|
61
|
+
and local services.
|
|
62
|
+
2. **Try the app.** Open a live preview from the session. Use the running app,
|
|
63
|
+
not just a screenshot of what the agent says it finished. Bivy can discover
|
|
64
|
+
dev servers running in the session workspace; tap **Preview** to open one.
|
|
65
|
+
3. **Mark up what needs work.** Point at an element or circle and draw on the
|
|
66
|
+
preview. Add a note, then send the draft to the agent with the element and
|
|
67
|
+
page context. Marked-up screenshots and before/after comparison are available
|
|
68
|
+
when agent screenshots are enabled.
|
|
69
|
+
4. **Iterate and review.** Try the next version, inspect the diff, review checks,
|
|
70
|
+
and decide when the work is ready—not just when the agent stops.
|
|
71
|
+
5. **Bring someone else into the review.** Copy a public preview link so they
|
|
72
|
+
can use the app without a Bivy account and leave notes on specific elements.
|
|
73
|
+
Bring their notes into the session when you're ready to act on them.
|
|
74
|
+
|
|
75
|
+
On Bivy Cloud, preview delivery is built in: no per-app domains, certificates,
|
|
76
|
+
public ports, or tunnels to configure. Self-hosters configure preview delivery
|
|
77
|
+
once for their Bivy deployment, not for every app. The app still needs its normal
|
|
78
|
+
build or dev-server setup, and **the machine serving it must stay awake and
|
|
79
|
+
online**. These are development previews, not production hosting.
|
|
80
|
+
|
|
81
|
+
**Share deliberately:** anyone with a preview link can use that app, including
|
|
82
|
+
its live backend. Links expire after 24 hours and can be revoked sooner.
|
|
83
|
+
Reviewer notes aren't sent to the agent automatically; you send them or explicitly
|
|
84
|
+
allow agent access. Preview traffic uses HTTPS through the preview relay, not
|
|
85
|
+
session end-to-end encryption; the relay operator can see it.
|
|
86
|
+
|
|
87
|
+
[App previews, visual feedback, and sharing →](docs/apps.md)
|
|
88
|
+
|
|
89
|
+
## Your agents and machines, one workspace
|
|
55
90
|
|
|
56
91
|
| Capability | What it means for you |
|
|
57
92
|
|---|---|
|
|
@@ -59,8 +94,10 @@ what happened**.
|
|
|
59
94
|
| **Your machines and environment** | Work beside your existing repos, dev servers, databases, private networks, toolchains, and GPUs. |
|
|
60
95
|
| **Automations and triggers** | Let issues, failed CI, messages, schedules, and webhooks start work instead of copying requests into a chat. |
|
|
61
96
|
| **Encrypted key and OAuth sync** | Reuse Bivy-managed provider credentials across enrolled machines and compatible runtimes, with less repeated setup. |
|
|
62
|
-
| **Live sessions from anywhere** |
|
|
63
|
-
| **
|
|
97
|
+
| **Live sessions from anywhere** | Build and review from your phone, browser, or terminal; answer questions and approve supported actions in the same session. |
|
|
98
|
+
| **Live app previews** | Try the running app without deploying it to a separate hosting service. |
|
|
99
|
+
| **Visual feedback and sharing** | Point, draw, and send specific feedback to the agent; share a preview link for outside review. |
|
|
100
|
+
| **Reviewable results** | Inspect the app, changes, declared checks, artifacts, and pull requests—not just an agent's claim that it finished. |
|
|
64
101
|
| **Hosted convenience or self-hosting** | Use Bivy Cloud for managed remote access, or run the same open-source core yourself. |
|
|
65
102
|
|
|
66
103
|
## One workflow, from trigger to review
|
|
@@ -77,7 +114,13 @@ Prompt · GitHub issue · CI failure · Linear · Slack · Schedule · Webhook
|
|
|
77
114
|
Join · steer · approve · stop
|
|
78
115
|
│
|
|
79
116
|
▼
|
|
80
|
-
|
|
117
|
+
Try app · inspect changes · checks
|
|
118
|
+
│
|
|
119
|
+
▼
|
|
120
|
+
Mark up · send feedback · iterate
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
Share preview · review PR
|
|
81
124
|
```
|
|
82
125
|
|
|
83
126
|
A **Machine** is a computer or server you connect. A **Session** is live agent
|
|
@@ -206,10 +249,9 @@ answer questions, approve supported tool calls, or stop the agent.
|
|
|
206
249
|
|
|
207
250
|
- Send screenshots, images, logs, and other files from your phone.
|
|
208
251
|
- Download reports and artifacts the agent creates.
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
public ports. Terminal views use the existing connection.
|
|
252
|
+
- Open [session apps](docs/apps.md) from chat or the **Apps** menu: live web
|
|
253
|
+
previews, desktop app views, and CLI/TUI tools. Agents can publish views with
|
|
254
|
+
`bivy app publish bivy.app.json` and create share links with `bivy app share`.
|
|
213
255
|
- Use voice input and read-aloud where supported; provider-backed voice may
|
|
214
256
|
send audio or text to the selected provider.
|
|
215
257
|
- Keep a native terminal workflow or use structured chat, depending on the agent.
|
|
@@ -259,8 +301,13 @@ bivy run claude
|
|
|
259
301
|
bivy open
|
|
260
302
|
```
|
|
261
303
|
|
|
262
|
-
Open that same session on your phone while it runs.
|
|
263
|
-
|
|
304
|
+
Open that same session on your phone while it runs. For a web app, ask the agent
|
|
305
|
+
to start its dev server, then open **Apps → Running in this workspace → Preview**.
|
|
306
|
+
Try a page, point to something you'd change, and send the feedback back to the
|
|
307
|
+
agent. No separate preview deployment needed.
|
|
308
|
+
|
|
309
|
+
Once that works, share a preview for review, connect another machine, or add
|
|
310
|
+
your first automation.
|
|
264
311
|
|
|
265
312
|
**Local-only works too.** `bivy run`, `bivy resume`, and `bivy sessions` need no
|
|
266
313
|
account or server. Choose **local only for now** during setup; use `bivy login`
|
|
@@ -327,9 +374,14 @@ Your machine Hosted or self-hosted
|
|
|
327
374
|
|
|
328
375
|
- **Execution stays on your machine.** Bivy Cloud does not run your agents.
|
|
329
376
|
Your model provider still sees whatever the agent sends it.
|
|
330
|
-
- **Interactive traffic is end-to-end encrypted** between the node and
|
|
331
|
-
devices. The relay forwards opaque frames; your node dials out,
|
|
332
|
-
public port is required.
|
|
377
|
+
- **Interactive session traffic is end-to-end encrypted** between the node and
|
|
378
|
+
paired devices. The relay forwards opaque session frames; your node dials out,
|
|
379
|
+
so no inbound public port is required.
|
|
380
|
+
- **App previews have a separate security boundary.** Preview traffic uses HTTPS
|
|
381
|
+
and an outbound tunnel, not session E2E encryption. The preview ingress/relay
|
|
382
|
+
operator can see it. Public preview links grant anyone holding them access to
|
|
383
|
+
that view and its live backend until expiry or revocation. See
|
|
384
|
+
[preview security and sharing](docs/apps.md#runtime-and-security-boundaries).
|
|
333
385
|
- **Ordinary credential sync uploads ciphertext, not plaintext keys.**
|
|
334
386
|
Supported credentials and recovery limits are documented separately.
|
|
335
387
|
- **Encryption is not universal across integrations.** Slack commands and
|
|
@@ -528,7 +528,12 @@ async function onBivyCommand(msg) {
|
|
|
528
528
|
bivy({ replyTo: id, ok: true });
|
|
529
529
|
asRequest("turn/start", {
|
|
530
530
|
threadId,
|
|
531
|
-
input: [
|
|
531
|
+
input: [
|
|
532
|
+
{ type: "text", text: String(msg.text ?? "") },
|
|
533
|
+
...(msg.images ?? []).map((image) => ({
|
|
534
|
+
type: "image", url: `data:${image.mimeType || "image/png"};base64,${image.data}`,
|
|
535
|
+
})),
|
|
536
|
+
],
|
|
532
537
|
...(selectedModel ? { model: selectedModel } : {}),
|
|
533
538
|
})
|
|
534
539
|
.catch((error) => bivy({ type: "session.error", error: error instanceof Error ? error.message : String(error) }));
|
package/dist/apps/gateway.js
CHANGED
|
@@ -12,6 +12,7 @@ import { previewShell } from "./preview-shell.js";
|
|
|
12
12
|
import { displayViewer, DISPLAY_MENU_PATH, DISPLAY_SOCKET_PATH, DISPLAY_STATS_PATH, NOVNC_PATH } from "./display-viewer.js";
|
|
13
13
|
import { pressMenu, readMenuPath, readMenus } from "./menu.js";
|
|
14
14
|
import { inspectorScript } from "./inspector.js";
|
|
15
|
+
import { readStrokes } from "./annotate.js";
|
|
15
16
|
const MIME = {
|
|
16
17
|
".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8",
|
|
17
18
|
".css": "text/css; charset=utf-8", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
|
|
@@ -199,6 +200,7 @@ export class AppGateway {
|
|
|
199
200
|
registry;
|
|
200
201
|
returnOrigins;
|
|
201
202
|
signIn;
|
|
203
|
+
captureNote;
|
|
202
204
|
server;
|
|
203
205
|
template;
|
|
204
206
|
tickets = new Map();
|
|
@@ -210,13 +212,16 @@ export class AppGateway {
|
|
|
210
212
|
* `embedded`: it runs framed inside a Bivy client (a third-party frame). */
|
|
211
213
|
sessions = new Map();
|
|
212
214
|
sockets = new Map();
|
|
215
|
+
captureAfter = 0;
|
|
216
|
+
capturing = false;
|
|
213
217
|
displays = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024, perMessageDeflate: false });
|
|
214
218
|
/** `signIn` sends a signed-out visit to a view's stable address back
|
|
215
219
|
* through Bivy, which re-opens it for a signed-in device. */
|
|
216
|
-
constructor(registry, originTemplate, returnOrigins = () => [], signIn) {
|
|
220
|
+
constructor(registry, originTemplate, returnOrigins = () => [], signIn, captureNote) {
|
|
217
221
|
this.registry = registry;
|
|
218
222
|
this.returnOrigins = returnOrigins;
|
|
219
223
|
this.signIn = signIn;
|
|
224
|
+
this.captureNote = captureNote;
|
|
220
225
|
this.template = previewOriginTemplate(originTemplate);
|
|
221
226
|
this.server = http.createServer((req, res) => { void this.handle(req, res).catch(() => { if (!res.headersSent)
|
|
222
227
|
res.writeHead(502); res.end("Preview request failed."); }); });
|
|
@@ -253,12 +258,12 @@ export class AppGateway {
|
|
|
253
258
|
this.requireWeb(id);
|
|
254
259
|
return `${this.origin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: Date.now() + 60_000 })}`;
|
|
255
260
|
}
|
|
256
|
-
/**
|
|
257
|
-
*
|
|
261
|
+
/** Public reviewers use the same isolated controls as the owner, but can
|
|
262
|
+
* only submit notes. Valid until revoked or SHARE_TTL. */
|
|
258
263
|
share(id) {
|
|
259
264
|
this.requireWeb(id);
|
|
260
265
|
const expiresAt = Date.now() + SHARE_TTL;
|
|
261
|
-
return { url: `${this.
|
|
266
|
+
return { url: `${this.shellOrigin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: expiresAt, reusable: true })}`, expiresAt };
|
|
262
267
|
}
|
|
263
268
|
revoke(id) {
|
|
264
269
|
for (const map of [this.tickets, this.sessions])
|
|
@@ -366,7 +371,7 @@ export class AppGateway {
|
|
|
366
371
|
return;
|
|
367
372
|
}
|
|
368
373
|
res.setHeader("Content-Type", "application/json");
|
|
369
|
-
res.end(JSON.stringify({ name: entry.app.name, origin: this.origin(id), returnTo: grant.returnTo, ...(entry.target.kind === "display" ? { inspect: false } : {}) }));
|
|
374
|
+
res.end(JSON.stringify({ name: entry.app.name, origin: this.origin(id), returnTo: grant.returnTo, ...(grant.reusable ? { reviewer: true } : {}), ...(entry.target.kind === "display" ? { inspect: false } : {}) }));
|
|
370
375
|
return;
|
|
371
376
|
}
|
|
372
377
|
res.writeHead(404);
|
|
@@ -731,7 +736,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
731
736
|
let body = "";
|
|
732
737
|
for await (const chunk of req) {
|
|
733
738
|
body += chunk.toString();
|
|
734
|
-
if (body.length >
|
|
739
|
+
if (body.length > 256_000) {
|
|
735
740
|
res.writeHead(413);
|
|
736
741
|
res.end();
|
|
737
742
|
return;
|
|
@@ -747,6 +752,16 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
747
752
|
return;
|
|
748
753
|
}
|
|
749
754
|
const text = (value, max) => typeof value === "string" ? value.trim().slice(0, max) : "";
|
|
755
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
756
|
+
res.writeHead(400);
|
|
757
|
+
res.end();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (input.mark === undefined && input.context === undefined && body.length > 4096) {
|
|
761
|
+
res.writeHead(413);
|
|
762
|
+
res.end();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
750
765
|
const note = text(input.note, 1000);
|
|
751
766
|
if (!note) {
|
|
752
767
|
res.writeHead(400);
|
|
@@ -755,12 +770,71 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
755
770
|
}
|
|
756
771
|
const viewport = input.viewport;
|
|
757
772
|
const path = text(input.path, 2048);
|
|
773
|
+
let shot;
|
|
774
|
+
if (input.mark !== undefined) {
|
|
775
|
+
// Public input cannot choose a session, app, Compare image, or capture
|
|
776
|
+
// dimensions beyond a modest viewport. No agent commands are exposed.
|
|
777
|
+
if (!this.session(req, entry.view.id)?.reviewer) {
|
|
778
|
+
res.writeHead(403);
|
|
779
|
+
res.end();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
let mark;
|
|
783
|
+
try {
|
|
784
|
+
const raw = input.mark;
|
|
785
|
+
const strokes = readStrokes(raw?.strokes);
|
|
786
|
+
const v = raw.viewport;
|
|
787
|
+
if (!Number.isFinite(v?.width) || !Number.isFinite(v?.height) || v.width < 40 || v.width > 4000 || v.height < 40 || v.height > 4000 || v.width * v.height > 4_000_000)
|
|
788
|
+
throw Error();
|
|
789
|
+
const scroll = raw.scroll;
|
|
790
|
+
if (scroll && (![scroll.x, scroll.y].every(n => Number.isFinite(n) && n >= 0 && n <= 100_000)))
|
|
791
|
+
throw Error();
|
|
792
|
+
if (strokes.some(s => s.points.some(([x, y]) => x < 0 || y < 0 || x > 104_000 || y > 104_000)))
|
|
793
|
+
throw Error();
|
|
794
|
+
// Bound raster work as well as JSON size (long off-screen paths can
|
|
795
|
+
// otherwise consume CPU even though they paint no visible pixels).
|
|
796
|
+
const distance = strokes.reduce((total, s) => total + s.points.reduce((length, p, i) => i ? length + Math.hypot(p[0] - s.points[i - 1][0], p[1] - s.points[i - 1][1]) : length, 0), 0);
|
|
797
|
+
if (distance > 50_000)
|
|
798
|
+
throw Error();
|
|
799
|
+
mark = { appId: entry.app.id, viewId: entry.view.id, strokes, viewport: v, path: text(raw.path, 2048), scroll, dpr: 1, theme: raw.theme === "dark" ? "dark" : "light", signals: { unknown: true } };
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
res.writeHead(400);
|
|
803
|
+
res.end("Invalid or oversized annotation. Try fewer marks or a narrower preview.");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (this.capturing || Date.now() < this.captureAfter) {
|
|
807
|
+
res.writeHead(429);
|
|
808
|
+
res.end("Please wait a moment before sending another picture.");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
this.capturing = true;
|
|
812
|
+
this.captureAfter = Date.now() + 10_000;
|
|
813
|
+
try {
|
|
814
|
+
shot = await this.captureNote?.(entry, mark);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
res.writeHead(502);
|
|
818
|
+
res.end("Could not take the picture. Your note has not been sent; please retry.");
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
finally {
|
|
822
|
+
this.capturing = false;
|
|
823
|
+
}
|
|
824
|
+
if (!this.session(req, entry.view.id) || this.registry.getView(entry.view.id) !== entry) {
|
|
825
|
+
res.writeHead(401);
|
|
826
|
+
res.end();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
758
830
|
this.registry.addNote(entry.view.id, {
|
|
831
|
+
...(shot ? { shot } : {}),
|
|
832
|
+
...(input.context ? { context: text(input.context, 8000) } : {}),
|
|
759
833
|
id: randomBytes(8).toString("hex"), at: Date.now(), note, selector: text(input.selector, 300), text: text(input.text, 200),
|
|
760
834
|
path: path.startsWith("/") ? path : "/", viewport: { width: Number(viewport?.width) || 0, height: Number(viewport?.height) || 0 },
|
|
761
835
|
});
|
|
762
|
-
res.writeHead(
|
|
763
|
-
res.end();
|
|
836
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
837
|
+
res.end(JSON.stringify({ screenshot: Boolean(shot) }));
|
|
764
838
|
}
|
|
765
839
|
/** Compare shots for the shell: the list, or one PNG by index. */
|
|
766
840
|
compare(req, res, entry) {
|
package/dist/apps/inspector.js
CHANGED
|
@@ -51,7 +51,7 @@ const report=(el,hold)=>{
|
|
|
51
51
|
const r=el.getBoundingClientRect();
|
|
52
52
|
onPicked({type:'picked',selector:selector(el),tag:el.localName,text:(el.innerText||el.getAttribute('aria-label')||el.getAttribute('alt')||'').trim().replace(/\\s+/g,' ').slice(0,200),
|
|
53
53
|
rect:{x:Math.round(r.left),y:Math.round(r.top),width:Math.round(r.width),height:Math.round(r.height)},
|
|
54
|
-
viewport:{width:innerWidth,height:innerHeight},path:location.pathname+location.search+location.hash,hold});
|
|
54
|
+
viewport:{width:innerWidth,height:innerHeight},scroll:{x:Math.round(scrollX),y:Math.round(scrollY)},theme:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light',path:location.pathname+location.search+location.hash,hold});
|
|
55
55
|
};
|
|
56
56
|
// A long press picks at once and says so (the shell starts listening); the
|
|
57
57
|
// layer stays until the finger lifts, so the release never reaches the app.
|
|
@@ -123,7 +123,12 @@ const marked=r=>{
|
|
|
123
123
|
addEventListener('message',e=>{
|
|
124
124
|
if(!framed||e.origin!==SHELL||e.source!==parent)return;
|
|
125
125
|
const d=e.data||{},n=v=>Number.isFinite(+v)?+v:0;
|
|
126
|
-
if(d.type==='bivy:
|
|
126
|
+
if(d.type==='bivy:note'&&REVIEWER){
|
|
127
|
+
void fetch('/__bivy/notes',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(d.note)})
|
|
128
|
+
.then(async r=>{if(!r.ok)throw Error(r.status===429?'Please wait a moment before sending another picture.':r.status===400||r.status===502?(await r.text()).slice(0,200):'Could not send. Check that the link is still valid and try again.');return r.json();})
|
|
129
|
+
.then(result=>post({type:'note-sent',id:d.id,screenshot:result.screenshot===true}),error=>post({type:'note-sent',id:d.id,error:error.message}));
|
|
130
|
+
}
|
|
131
|
+
else if(d.type==='bivy:draw')post(drawState());
|
|
127
132
|
else if(d.type==='bivy:scroll'){scrollBy({left:n(d.dx),top:n(d.dy),behavior:'instant'});post({type:'scrolled',scroll:{x:Math.round(scrollX),y:Math.round(scrollY)}});}
|
|
128
133
|
else if(d.type==='bivy:marks'&&d.rect)post({type:'marked',id:n(d.id),elements:marked({x:n(d.rect.x),y:n(d.rect.y),width:n(d.rect.width),height:n(d.rect.height)})});
|
|
129
134
|
});
|
|
@@ -22,7 +22,7 @@ iframe { width:100%; height:100%; border:0; background:var(--bg); }
|
|
|
22
22
|
#dock > * { pointer-events:auto; }
|
|
23
23
|
/* The hint sits over the app; a tap on it is meant for the app underneath. */
|
|
24
24
|
#dock > #pointing { pointer-events:none; }
|
|
25
|
-
nav, #draw-bar { display:flex; align-items:center; gap:var(--space-1); max-width:100%; padding:var(--space-1); background:var(--surface); border:thin solid var(--line); border-radius:var(--radius-
|
|
25
|
+
nav, #draw-bar { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:var(--space-1); max-width:100%; padding:var(--space-1); background:var(--surface); border:thin solid var(--line); border-radius:var(--radius-xl); box-shadow:var(--shadow-lg); }
|
|
26
26
|
nav .btn, #draw-bar .btn { flex-shrink:0; border-radius:var(--radius-full); }
|
|
27
27
|
nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accent); }
|
|
28
28
|
#name { display:flex; flex-direction:column; min-width:0; padding:0 var(--space-2); }
|
|
@@ -46,26 +46,34 @@ nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accen
|
|
|
46
46
|
#draft-row { display:flex; align-items:flex-start; gap:var(--space-2); }
|
|
47
47
|
#draft-row #draft-text { flex:1; min-width:0; }
|
|
48
48
|
/* Hold to talk: a phone-sized target that never selects text or opens a callout. */
|
|
49
|
-
#mic { flex:none; inline-size:
|
|
49
|
+
#mic { flex:none; inline-size:var(--space-7); block-size:var(--space-7); padding:0; border-radius:var(--radius-full); touch-action:none; user-select:none; -webkit-user-select:none; -webkit-touch-callout:none; }
|
|
50
50
|
#mic[aria-pressed="true"] { background:var(--accent); border-color:var(--accent); color:var(--accent-contrast); }
|
|
51
|
-
#voice-status { font-size:var(--text-xs); margin:var(--space-2) 0 0; }
|
|
52
|
-
#voice-status:empty { display:none; }
|
|
53
|
-
/* The
|
|
54
|
-
|
|
51
|
+
#voice-status, #note-status { font-size:var(--text-xs); margin:var(--space-2) 0 0; }
|
|
52
|
+
#voice-status:empty, #note-status:empty { display:none; }
|
|
53
|
+
/* The note owns the dock while editing. Only its body scrolls; actions never
|
|
54
|
+
cover the text field, even when the keyboard leaves little vertical room. */
|
|
55
|
+
#dock { max-height:calc(100% - var(--space-4)); }
|
|
56
|
+
#dock:has(#draft:not([hidden])) nav, #dock:has(#draft:not([hidden])) #pointing { display:none; }
|
|
57
|
+
#draft { display:flex; flex-direction:column; max-height:70dvh; overflow:hidden; padding:0; }
|
|
58
|
+
#draft-body { min-height:0; overflow:auto; padding:var(--space-3); }
|
|
59
|
+
#draft .panel-actions { flex-shrink:0; margin:0; padding:var(--space-2) var(--space-3); border-top:thin solid var(--line); }
|
|
60
|
+
#draft details { margin-top:var(--space-2); }
|
|
61
|
+
#draft summary { color:var(--muted); font-size:var(--text-sm); cursor:pointer; }
|
|
55
62
|
#draft-hint { font-size:var(--text-xs); margin:var(--space-2) 0 var(--space-1); }
|
|
56
63
|
#draft-context { margin:0; padding:var(--space-2); background:var(--surface-2); border-radius:var(--radius-md); font-family:var(--font-mono); font-size:var(--text-xs); white-space:pre-wrap; overflow-wrap:anywhere; color:var(--muted); }
|
|
57
64
|
#show { position:fixed; right:var(--space-3); bottom:calc(var(--space-3) + env(safe-area-inset-bottom)); z-index:var(--z-sticky); border-radius:var(--radius-full); box-shadow:var(--shadow-lg); }
|
|
58
65
|
/* Draw: marks over the frozen app (or Compare's "after" shot). The layer takes
|
|
59
66
|
every touch, so nothing reaches the app; two fingers scroll the page. */
|
|
60
67
|
#ink { position:fixed; z-index:var(--z-sticky); touch-action:none; cursor:crosshair; user-select:none; -webkit-user-select:none; -webkit-touch-callout:none; }
|
|
68
|
+
/* An HTML hit surface owns gestures even where the SVG has no painted marks. */
|
|
69
|
+
#ink svg { display:block; width:100%; height:100%; pointer-events:none; }
|
|
61
70
|
#ink.frozen { pointer-events:none; cursor:default; }
|
|
62
71
|
#ink .halo { fill:none; stroke:var(--annotate-halo); stroke-width:7; stroke-linecap:round; stroke-linejoin:round; }
|
|
63
72
|
#ink .mark { fill:none; stroke:var(--annotate); stroke-width:4; stroke-linecap:round; stroke-linejoin:round; }
|
|
64
73
|
#dock > #drawing { pointer-events:none; }
|
|
65
74
|
/* Hints float over the app, whose page may be any colour: back them solidly. */
|
|
66
75
|
#dock > .banner.inline[data-tone="accent"] { background:color-mix(in srgb, var(--accent) 14%, var(--surface)); box-shadow:var(--shadow-sm); }
|
|
67
|
-
#
|
|
68
|
-
#draw-bar .seg-btn { min-block-size:36px; min-inline-size:48px; }
|
|
76
|
+
#dock .btn, #dock .seg-btn, #show, #down .btn { min-block-size:var(--space-7); min-inline-size:var(--space-7); font-size:var(--text-sm); }
|
|
69
77
|
#draw-bar .segmented { flex-shrink:0; }
|
|
70
78
|
#compare-draw { margin-top:var(--space-2); }
|
|
71
79
|
/* Marking a Compare shot: make it big enough to draw on with a thumb. */
|
|
@@ -78,7 +86,7 @@ nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accen
|
|
|
78
86
|
<div class="banner" data-tone="warn" id="down" hidden><span class="banner-text" id="down-text" role="status"></span><span class="banner-actions"><button class="btn sm" id="ask" hidden>Ask agent to fix</button></span></div>
|
|
79
87
|
<p id="status" role="status">Opening app…</p>
|
|
80
88
|
<div id="stage" data-lens="full"><iframe id="app" title="App preview" hidden sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-popups" referrerpolicy="no-referrer"></iframe></div>
|
|
81
|
-
<
|
|
89
|
+
<div id="ink" hidden aria-hidden="true"><svg id="ink-marks"></svg></div>
|
|
82
90
|
<div id="dock">
|
|
83
91
|
<section class="panel" id="console" hidden aria-labelledby="console-title">
|
|
84
92
|
<h2><span id="console-title">Console</span><button class="btn sm ghost" id="clear">Clear</button></h2>
|
|
@@ -93,12 +101,15 @@ nav .btn[aria-pressed="true"] { background:var(--accent-soft); color:var(--accen
|
|
|
93
101
|
<button class="btn sm" id="compare-draw" type="button" hidden>Draw on “now”</button>
|
|
94
102
|
</section>
|
|
95
103
|
<section class="panel" id="draft" hidden aria-labelledby="draft-title">
|
|
96
|
-
<
|
|
104
|
+
<div id="draft-body">
|
|
105
|
+
<h2 id="draft-title">Note for the agent</h2>
|
|
97
106
|
<div id="draft-row"><textarea class="field" id="draft-text" aria-label="What should change?" placeholder="What should change?"></textarea>
|
|
98
107
|
<button class="btn" id="mic" type="button" hidden aria-pressed="false" aria-label="Speak" title="Hold to speak, or tap to start and stop"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5a3 3 0 003-3V6a3 3 0 10-6 0v6.5a3 3 0 003 3z"/><path d="M6 11.5v1a6 6 0 0012 0v-1M12 18.5V21M9 21h6"/></svg></button></div>
|
|
99
108
|
<p class="muted" id="voice-status" role="status"></p>
|
|
100
|
-
<p class="muted" id="draft-hint">
|
|
101
|
-
<
|
|
109
|
+
<p class="muted" id="draft-hint">Review in chat before sending.</p>
|
|
110
|
+
<p class="muted" id="note-status" role="status"></p>
|
|
111
|
+
<details id="draft-details"><summary>Preview context</summary><pre id="draft-context"></pre></details>
|
|
112
|
+
</div>
|
|
102
113
|
<div class="panel-actions"><button class="btn sm ghost" id="draft-cancel">Cancel</button><button class="btn sm primary" id="draft-add">Add to chat</button></div>
|
|
103
114
|
</section>
|
|
104
115
|
<p class="banner inline" data-tone="accent" id="pointing" role="status" hidden>Tap anything in the app to point at it. <span id="pointing-voice" hidden>Hold to point and speak. </span>Press Escape or Point again to stop.</p>
|
|
@@ -136,9 +147,16 @@ const embedded=parent!==window;
|
|
|
136
147
|
const toBivy=m=>parent.postMessage(Object.assign({source:'bivy-preview'},m),location.ancestorOrigins?.[0]||new URL(metadata.returnTo).origin);
|
|
137
148
|
const storageKey='bivy-preview';
|
|
138
149
|
const safePath=p=>typeof p==='string'&&p.startsWith('/')&&!p.startsWith('//')&&p.length<=2048?p:'/';
|
|
139
|
-
const
|
|
150
|
+
const reviewerTools=ready=>{if(metadata?.reviewer)for(const id of ['point','draw','errors'])$(id).disabled=!ready;};
|
|
151
|
+
const open=(path,message)=>{status.hidden=false;status.textContent=message;resetConsole();reviewerTools(false);frame.src=metadata.origin+safePath(path);};
|
|
140
152
|
function show(data,launch){
|
|
141
153
|
metadata=data;
|
|
154
|
+
if(data.reviewer){
|
|
155
|
+
drawOk=data.inspect!==false;$('draw').hidden=!drawOk;back.hidden=true;
|
|
156
|
+
$('draft-title').textContent='Note for the app’s owner';
|
|
157
|
+
$('draft-text').maxLength=1000;
|
|
158
|
+
$('send-errors').textContent='Leave a note…';
|
|
159
|
+
}
|
|
142
160
|
$('title').textContent=data.name;
|
|
143
161
|
$('title').title=data.name;
|
|
144
162
|
document.title='Bivy · '+data.name;
|
|
@@ -151,8 +169,9 @@ function show(data,launch){
|
|
|
151
169
|
for(const b of [$('point'),$('errors')])b.hidden=data.inspect===false;
|
|
152
170
|
if(data.inspect===false)frame.allow='clipboard-read; clipboard-write';
|
|
153
171
|
frame.src=data.origin+(launch?'/__bivy/open#'+(embedded?'e:':'')+launch+(startPage?'~'+startPage:''):'/');
|
|
154
|
-
back.hidden=embedded&&Boolean(data.returnTo);
|
|
172
|
+
back.hidden=Boolean(data.reviewer)||(embedded&&Boolean(data.returnTo));
|
|
155
173
|
for(const b of [reload,$('point'),$('errors')])b.disabled=false;
|
|
174
|
+
reviewerTools(false);
|
|
156
175
|
status.textContent='Loading app…';
|
|
157
176
|
frame.onload=()=>{status.hidden=true;void loadCompare();if(revision===null){revision=-1;watch();}else if(wake)wake();};
|
|
158
177
|
// A Bivy client framing the shell says whether it can take dictation.
|
|
@@ -179,17 +198,23 @@ async function watch(){
|
|
|
179
198
|
revision=d.revision;retry=0;setTimeout(watch,0);
|
|
180
199
|
}catch{retry=Math.min(30000,(retry||1000)*2);const t=setTimeout(watch,retry);wake=()=>{clearTimeout(t);watch();};}
|
|
181
200
|
}
|
|
182
|
-
//
|
|
201
|
+
// Owner drafts go to the session's composer. Reviewer drafts use only the
|
|
202
|
+
// bounded notes endpoint, never the owner's command channel.
|
|
183
203
|
function toChat(text){
|
|
184
204
|
if(embedded)return toBivy({type:'draft',text});
|
|
185
205
|
const to=new URL(metadata.returnTo),session=to.pathname.split('/').pop();
|
|
186
206
|
location.assign(to.origin+'/share?session='+encodeURIComponent(session)+'&text='+encodeURIComponent(text));
|
|
187
207
|
}
|
|
208
|
+
let noteMark=null,pendingNote=null,noteSequence=0;
|
|
188
209
|
function draft(context,listen){
|
|
189
|
-
panels(null);
|
|
210
|
+
panels(null);noteMark=null;
|
|
211
|
+
$('draft-add').disabled=false;
|
|
212
|
+
$('note-status').textContent='';
|
|
213
|
+
$('draft-hint').textContent=metadata.reviewer?'Feedback only — no agent runs. Pictures are approximate; if screenshots are off, we save text and mark details.':'Review in chat before sending.';
|
|
190
214
|
$('draft').hidden=false;
|
|
191
|
-
$('draft-add').textContent=metadata.returnTo?'Add to chat':'Copy';
|
|
215
|
+
$('draft-add').textContent=metadata.reviewer?'Send note':metadata.returnTo?'Add to chat':'Copy';
|
|
192
216
|
$('draft-context').textContent=context;
|
|
217
|
+
$('draft-details').open=false;
|
|
193
218
|
const box=$('draft-text');box.value='';
|
|
194
219
|
// Speaking right away: don't raise the keyboard over the draft.
|
|
195
220
|
if(listen)startListening();else box.focus();
|
|
@@ -226,15 +251,37 @@ function fromBivy(d){
|
|
|
226
251
|
else if(d.type==='transcript'){setListening(false);if(typeof d.error==='string')sayVoice(d.error.slice(0,200));else addSpoken(d.text);}
|
|
227
252
|
else if(d.type==='listening'&&d.on===false&&listening){setListening(false);if(voiceStatus.textContent.startsWith('Listening'))sayVoice('');}
|
|
228
253
|
}
|
|
229
|
-
$('draft-cancel').onclick=()=>{cancelListening();$('draft').hidden=true;endDraw();};
|
|
254
|
+
$('draft-cancel').onclick=()=>{const target=marks?$('draw'):point;cancelListening();$('draft').hidden=true;endDraw();target.focus();};
|
|
230
255
|
$('draft-add').onclick=async()=>{
|
|
231
256
|
cancelListening();
|
|
232
|
-
const note=$('draft-text').value.trim()
|
|
257
|
+
const note=$('draft-text').value.trim();
|
|
258
|
+
if(metadata.reviewer){
|
|
259
|
+
if(!note){$('note-status').textContent='Write a note before sending.';$('draft-text').focus();return;}
|
|
260
|
+
const mark=marks||noteMark,id=++noteSequence;
|
|
261
|
+
$('draft-add').disabled=true;$('draft-cancel').disabled=true;$('draft-text').disabled=true;
|
|
262
|
+
$('note-status').textContent='Sending note…';$('draft-add').textContent='Sending…';
|
|
263
|
+
const timer=setTimeout(()=>finishNote({id,error:'No reply yet. Your note may have arrived; check the connection before retrying.'}),60000);
|
|
264
|
+
pendingNote={id,timer};
|
|
265
|
+
frame.contentWindow?.postMessage({type:'bivy:note',id,note:{note,context:$('draft-context').textContent,selector:mark?.selector||'',text:mark?.text||'',path:mark?.path||currentPath,viewport:mark?.viewport||{width:frame.clientWidth,height:frame.clientHeight},...(mark?{mark}:{})}},metadata.origin);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const text=(note?note+'\\n\\n':'')+$('draft-context').textContent;
|
|
233
269
|
// Marks go to the Bivy client, which adds a picture made on the machine.
|
|
234
|
-
if(marks){const mark=marks;endDraw();return toBivy({type:'annotation',text,mark});}
|
|
235
|
-
if(metadata.returnTo)return toChat(text);
|
|
270
|
+
if(marks){const mark=marks;$('draft').hidden=true;endDraw();return toBivy({type:'annotation',text,mark});}
|
|
271
|
+
if(metadata.returnTo){$('draft').hidden=true;return toChat(text);}
|
|
236
272
|
try{await navigator.clipboard.writeText(text);$('draft-add').textContent='Copied';}catch{$('draft-text').select();}
|
|
237
273
|
};
|
|
274
|
+
$('draft-text').addEventListener('input',()=>{if(!pendingNote)$('note-status').textContent='';});
|
|
275
|
+
function finishNote(d){
|
|
276
|
+
if(!pendingNote||d.id!==pendingNote.id)return;
|
|
277
|
+
clearTimeout(pendingNote.timer);pendingNote=null;
|
|
278
|
+
$('draft-cancel').disabled=false;$('draft-add').disabled=false;$('draft-text').disabled=false;$('draft-add').textContent='Send note';
|
|
279
|
+
if(d.error){$('note-status').textContent=String(d.error).slice(0,200);return;}
|
|
280
|
+
$('draft').hidden=true;endDraw();noteMark=null;
|
|
281
|
+
$('stamp').textContent=d.screenshot?'Note and approximate picture sent':'Note sent without a picture';
|
|
282
|
+
status.hidden=false;status.textContent=$('stamp').textContent+'. The app’s owner will see it.';
|
|
283
|
+
point.focus();
|
|
284
|
+
}
|
|
238
285
|
// Console
|
|
239
286
|
let entries=[];
|
|
240
287
|
function resetConsole(){entries=[];renderConsole();}
|
|
@@ -263,14 +310,16 @@ function picked(d){
|
|
|
263
310
|
draft('In the app preview "'+metadata.name+'" (page '+safePath(d.path)+', viewport '+v.width+'×'+v.height+'):\\n'
|
|
264
311
|
+'Element: '+String(d.selector).slice(0,300)+(d.text?' ("'+String(d.text).slice(0,200)+'")':'')+', '+r.width+'×'+r.height+' at '+r.x+','+r.y
|
|
265
312
|
+(errors.length?'\\nRecent errors:\\n'+errors.map(e=>'- '+e.text).join('\\n'):''),d.hold===true&&voice);
|
|
313
|
+
if(metadata.reviewer){const s=pos(d.scroll);noteMark={path:safePath(d.path),viewport:v,scroll:s,theme:d.theme,selector:String(d.selector).slice(0,300),text:String(d.text||'').slice(0,200),strokes:[{tool:'box',points:[[r.x+s.x,r.y+s.y],[r.x+r.width+s.x,r.y+r.height+s.y]]}]};}
|
|
266
314
|
}
|
|
267
315
|
// Draw: freeze the app and mark what's wrong. Marks are kept in page
|
|
268
316
|
// coordinates (so they stay on the content while two fingers scroll), and
|
|
269
317
|
// the app reports what's under each one. On Done they go into the draft box
|
|
270
318
|
// with that context; Add to chat hands them to the Bivy client, which gets a
|
|
271
|
-
// picture from the machine.
|
|
272
|
-
//
|
|
273
|
-
|
|
319
|
+
// picture from the machine. Reviewers instead submit marks with their note;
|
|
320
|
+
// pictures stay on the machine for the owner. Compare's "after" shot can be
|
|
321
|
+
// marked the same way by the owner.
|
|
322
|
+
const ink=$('ink'),inkMarks=$('ink-marks'),SVG='http://www.w3.org/2000/svg';
|
|
274
323
|
let draw=null,marks=null,drawOk=false,nextStroke=0;
|
|
275
324
|
const pos=v=>({x:Number(v?.x)||0,y:Number(v?.y)||0});
|
|
276
325
|
function startDraw(target){
|
|
@@ -292,6 +341,7 @@ function startDraw(target){
|
|
|
292
341
|
current.waiting=d=>{current.waiting=null;current.scroll=pos(d.scroll);current.state={viewport:{width:Math.round(Number(d.viewport?.width))||current.state.viewport.width,height:Math.round(Number(d.viewport?.height))||current.state.viewport.height},dpr:Number(d.dpr)||devicePixelRatio,theme:d.theme==='dark'?'dark':'light',path:safePath(d.path),signals:Object.fromEntries(Object.entries(d.signals||{}).map(([k,v])=>[k,v===true]))};renderInk();};
|
|
293
342
|
frame.contentWindow?.postMessage({type:'bivy:draw'},metadata.origin);
|
|
294
343
|
}
|
|
344
|
+
for(const b of $('draw-bar').querySelectorAll('[data-tool]'))b.setAttribute('aria-checked',String(b.dataset.tool===draw.tool));
|
|
295
345
|
$('draw-bar').querySelector('[aria-checked="true"]').focus();
|
|
296
346
|
updateDrawBar();renderInk();
|
|
297
347
|
}
|
|
@@ -299,7 +349,7 @@ function startDraw(target){
|
|
|
299
349
|
function placeInk(target){
|
|
300
350
|
const r=(target==='compare'?$('compare-after'):frame).getBoundingClientRect();
|
|
301
351
|
Object.assign(ink.style,{left:r.left+'px',top:r.top+'px',width:r.width+'px',height:r.height+'px'});
|
|
302
|
-
|
|
352
|
+
inkMarks.setAttribute('viewBox','0 0 '+r.width+' '+r.height);
|
|
303
353
|
return {left:r.left,top:r.top,width:r.width,height:r.height};
|
|
304
354
|
}
|
|
305
355
|
// A rotation or a keyboard moves things: keep the layer on them. (Marks on
|
|
@@ -308,7 +358,7 @@ addEventListener('resize',()=>{if(draw&&!draw.done)draw.rect=Object.assign(place
|
|
|
308
358
|
function endDraw(){
|
|
309
359
|
if(!draw)return;
|
|
310
360
|
delete $('compare').dataset.drawing;
|
|
311
|
-
draw=null;marks=null;ink.toggleAttribute('hidden',true);
|
|
361
|
+
draw=null;marks=null;ink.toggleAttribute('hidden',true);inkMarks.replaceChildren();
|
|
312
362
|
$('drawing').hidden=true;$('draw-bar').hidden=true;document.querySelector('nav').hidden=false;
|
|
313
363
|
}
|
|
314
364
|
function updateDrawBar(){const has=Boolean(draw?.strokes.length);for(const id of ['draw-undo','draw-clear','draw-done'])$(id).disabled=!has;}
|
|
@@ -321,7 +371,7 @@ function shape(stroke){
|
|
|
321
371
|
function renderInk(){
|
|
322
372
|
if(!draw)return;
|
|
323
373
|
const all=[...draw.strokes,...(draw.current?[draw.current]:[])];
|
|
324
|
-
|
|
374
|
+
inkMarks.replaceChildren(...all.flatMap(s=>['halo','mark'].map(c=>{const el=shape(s);el.setAttribute('class',c);return el;})));
|
|
325
375
|
}
|
|
326
376
|
function local(e){const x=e.clientX-draw.rect.left,y=e.clientY-draw.rect.top;return draw.target==='frame'?[Math.round(x+draw.scroll.x),Math.round(y+draw.scroll.y)]:[Math.round(x),Math.round(y)];}
|
|
327
377
|
let scrollAsk=null;
|
|
@@ -389,6 +439,7 @@ $('draw-done').onclick=()=>{
|
|
|
389
439
|
// Compare: screenshots around the agent's last change (agent screenshots on).
|
|
390
440
|
const compareBtn=$('compare-btn');let shots=[],urls=[],compareAfter=-1;
|
|
391
441
|
async function loadCompare(){
|
|
442
|
+
if(metadata.reviewer)return;
|
|
392
443
|
try{const r=await fetch(metadata.origin+'/__bivy/compare',{credentials:'include',cache:'no-store'});if(!r.ok)return;shots=(await r.json()).shots||[];compareBtn.hidden=shots.length<2;}catch{}
|
|
393
444
|
}
|
|
394
445
|
function panels(open){if(listening)cancelListening();if(draw&&!(open==='compare'&&draw.target==='compare'))endDraw();for(const [id,btn] of [['console','errors'],['compare','compare-btn']]){$(id).hidden=id!==open;$(btn).setAttribute('aria-pressed',String(id===open));}$('draft').hidden=true;}
|
|
@@ -431,7 +482,8 @@ addEventListener('message',e=>{
|
|
|
431
482
|
downText.textContent='Nothing is answering on port '+downPort+'.';
|
|
432
483
|
ask.hidden=!metadata.returnTo;
|
|
433
484
|
}else if(d.source==='bivy-inspector'){
|
|
434
|
-
if(d.type==='
|
|
485
|
+
if(d.type==='note-sent'&&metadata.reviewer)finishNote(d);
|
|
486
|
+
else if(d.type==='route'){currentPath=safePath(d.path);reviewerTools(true);}
|
|
435
487
|
else if(d.type==='console'&&(d.level==='error'||d.level==='warn')){entries.push({level:d.level,text:String(d.text).slice(0,500)});if(entries.length>50)entries.shift();renderConsole();}
|
|
436
488
|
else if(d.type==='picked')picked(d);
|
|
437
489
|
else if(d.type==='release'&&heldAt){heldAt=0;stopListening();}
|
|
@@ -441,7 +493,7 @@ addEventListener('message',e=>{
|
|
|
441
493
|
}
|
|
442
494
|
});
|
|
443
495
|
ask.onclick=()=>toChat('The app preview "'+metadata.name+'" isn’t loading: nothing is answering on port '+downPort+'. Please find out why the server stopped, restart it, and tell me when it’s back.');
|
|
444
|
-
addEventListener('keydown',e=>{if(e.key==='Escape'){if(listening)cancelListening();else if(draw&&!draw.done)endDraw();else if(point.getAttribute('aria-pressed')==='true')pointing(false);else panels(null);}});
|
|
496
|
+
addEventListener('keydown',e=>{if(e.key==='Escape'){if(listening)cancelListening();else if(!$('draft').hidden)$('draft-cancel').click();else if(draw&&!draw.done)endDraw();else if(point.getAttribute('aria-pressed')==='true')pointing(false);else panels(null);}});
|
|
445
497
|
back.onclick=()=>{if(metadata?.returnTo){window.close();setTimeout(()=>location.replace(metadata.returnTo),100);}else{window.close();status.hidden=false;status.textContent='You can close this tab to return to Bivy.';}};
|
|
446
498
|
reload.onclick=()=>{if(metadata)open(currentPath,'Reloading app…');};
|
|
447
499
|
(async()=>{
|