@bivy/bivy 0.20.7-staging.2 → 0.20.7-staging.4
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/bin/bivy.mjs +100 -5
- package/bin/codex-app-server-shim.mjs +78 -27
- package/dist/agent-instructions.js +2 -1
- package/dist/agents/claude-code/runtime.js +2 -1
- package/dist/agents/profiles.js +9 -6
- package/dist/apps/backend/compare.js +62 -0
- package/dist/apps/backend/http-file.js +66 -0
- package/dist/apps/backend/index.js +431 -0
- package/dist/apps/backend/logs.js +60 -0
- package/dist/apps/backend/rows.js +79 -0
- package/dist/apps/gateway.js +121 -3
- package/dist/apps/inspector.js +56 -1
- package/dist/apps/preview-shell.js +221 -9
- package/dist/apps/registry.js +60 -2
- package/dist/apps/scenario-proxy.js +65 -0
- package/dist/apps/scenarios.js +307 -0
- package/dist/apps/service.js +147 -8
- package/dist/apps/styles.css +70 -0
- package/dist/apps/types.js +9 -1
- package/dist/controllers/app-commands.js +13 -2
- package/dist/exec.js +4 -1
- package/dist/node-env.js +6 -0
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +10 -2
- package/dist/runtime/codex-sessions.js +158 -6
- package/dist/runtime/credential-provisioning.js +5 -0
- package/dist/runtime/grok-sessions.js +4 -2
- package/dist/runtime/index.js +26 -19
- package/dist/runtime/inherited-session-env.js +54 -0
- package/dist/runtime/model-ref.js +14 -1
- package/dist/runtime/opencode-auth.js +67 -0
- package/dist/runtime/opencode-preflight.js +10 -1
- package/dist/runtime/protocol.js +35 -1
- package/dist/runtime/tool-call-map.js +67 -3
- package/dist/server.js +51 -16
- package/dist/session/event-log.js +76 -5
- package/dist/session/fork-standup.js +1 -1
- package/dist/session/fork.js +6 -4
- package/dist/session/transcript-normal.js +52 -0
- package/dist/session/transcript-persistence.js +20 -3
- package/dist/session-ref.js +5 -2
- package/dist/terminal.js +17 -0
- package/dist/worktree.js +25 -3
- package/package.json +1 -1
- package/web/assets/{AutomationsView-CJSYk0ja.js → AutomationsView-DJibSXQw.js} +1 -1
- package/web/assets/{ChangesCard-Bp88fY6W.js → ChangesCard-s8Qtm7eu.js} +1 -1
- package/web/assets/{LibraryView-of9wmuzN.js → LibraryView-Bz8x-H8N.js} +2 -2
- package/web/assets/{ProviderConnect-DG7SP4l2.js → ProviderConnect-DZ-Rv8Aj.js} +1 -1
- package/web/assets/{Settings-DGH1U7wj.js → Settings-DMUdD3O5.js} +3 -3
- package/web/assets/{Sheet-D-EHCMlj.js → Sheet-2wuqvpxm.js} +1 -1
- package/web/assets/{Terminal-qqysrwl_.js → Terminal-DQeYK2KK.js} +1 -1
- package/web/assets/{VoiceSettings-aeqQkwcZ.js → VoiceSettings-BbqFD1Ee.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-Ct-lmBn5.js → abnfDiagram-VCTEODGH-DLSpnM9y.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-CefLtGFG.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-CCq716Da.js → architectureDiagram-5GKGNRK7-DOQxmFrp.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-Lka9F6ZB.js → blockDiagram-I7D4REHJ-BiCBLtEP.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-nvhhyzri.js → c4Diagram-7LVT6UL2-yzmqXo9k.js} +1 -1
- package/web/assets/channel-kKuds40w.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-TZ5hhHBz.js → chunk-4HAMMTFA-i-QksGpD.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-IahKw8kM.js → chunk-75Z2AOVW-DWjggoX1.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-BlcZUVOy.js → chunk-DU6HZSFF-jFwsC9ox.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-3MGfEgKu.js → chunk-F27PBJKO-3ej2B9gg.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-Be72n678.js → chunk-GMAD6QVW-CzfhoJ04.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-GOVLT6xm.js → chunk-GVQU2GXP-DLhbmKuJ.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-ByOzewrT.js → chunk-IMKFNOWR-dR96ErOJ.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-BCGhHmnW.js → chunk-L3NEJ4N5-1YX2kWHP.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-CAdliJE_.js → chunk-OSK3NFVY-kDveSID9.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-CM8cpgHa.js → chunk-P2QGCYS3-Cc3Uo4HQ.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CXLPhaI1.js → chunk-POPQ4Y6H-CxDXuH1x.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BegFrwDe.js → chunk-PWAF6VOD-DZBN6HSl.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-iXj7Y5RI.js → chunk-SHT3W25Y-0W01Q-w7.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-73pyTBZg.js → chunk-SVP7TREG-gqFFZT4I.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-D5zwuuGI.js → chunk-TICWLB2K-BGdM7dkO.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-Dg-2WnlQ.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-Dg-2WnlQ.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-D3nRr9S_.js → cynefin-OW5HDTMX-D3KvEku4.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-C4CKMYMZ.js → cynefinDiagram-5FMLGOSQ-CLbDBHX9.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-D-grkZ8X.js → dagre-GXQ25YYZ-D2kdt2lo.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-BNPS3YQX.js → diagram-S7CK7UJ4-a-ev_USx.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-Bkp_TyRg.js → diagram-UQ7AKVKN-BwYRbF-y.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-Dpxe_6MM.js → diagram-VSXAHHWV-Da7O55Aq.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DwGBvGn-.js → diagram-VX7I27RA-Bcq-P_8M.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-D-KvONMO.js → diagram-Z3DM3KII-DSXXMWvA.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-Di_D-a-0.js → ebnfDiagram-PWID7BFC-DxFerrqi.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-2pnBfTU9.js → erDiagram-RLTQ6QDP-DfyXAe2v.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-tdyiDQoh.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-BO23Cpex.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-DAkhMTlk.js → ganttDiagram-EL5Y4UJY-QkgrgMh9.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-BQvemX2W.js → gitGraph-4MIJSDKK-M1n5msce.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-Dx7ya_2q.js → gitGraphDiagram-WWUBYQGX-DC_8Cxet.js} +1 -1
- package/web/assets/{index-CFX8FhqJ.js → index-B0Bto4kI.js} +2 -2
- package/web/assets/{index-BXPRG7Pj.css → index-Da2-4YHh.css} +1 -1
- package/web/assets/{info-A6RAGUB7-DaDps2sq.js → info-A6RAGUB7-DmyYG6Ip.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-DBqMvPr2.js → infoDiagram-27XIBGKW-CmrMjsqT.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-DzciF3O_.js → ishikawaDiagram-5VMMS53U-x_6jWkNq.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-D9WHgGe3.js → journeyDiagram-3NMN7TZE-COVPGUIl.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-Diokj8By.js → kanban-definition-UXKFOSKX-BDZHzeOz.js} +1 -1
- package/web/assets/{line-DmBeTXFn.js → line-KdqoiWjk.js} +1 -1
- package/web/assets/{mermaid-parser.core-H_O1CweI.js → mermaid-parser.core-C5RHAle0.js} +3 -3
- package/web/assets/{mermaid.core-BAni15Ed.js → mermaid.core-2ujtWQt3.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-B_tXdzEa.js → mindmap-definition-YA3MSWOX-CCPoAcT0.js} +1 -1
- package/web/assets/mount-CaFlQTsV.js +30 -0
- package/web/assets/{packet-AYTQ26CC-Dt4bPH7I.js → packet-AYTQ26CC-BjPan2xl.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-loAbaKUZ.js → pegDiagram-XKGWAZYB-CrARxusS.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-DEmhJN7M.js → pie-WAS4IAKB-BdVo8hLO.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-DfQvD2QY.js → pieDiagram-E7YTZNPT-DwdhmBCX.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-Bhqo2rT-.js → quadrantDiagram-AXDQQJYC-DbG3R6xw.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BmV5tXie.js → radar-RG4KPBEZ-pp-coSgM.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-DoebyExe.js → railroad-74A4TZTK-CxNBhSbO.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-g5eNqQ81.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-BI5_cD5r.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-CLl7RlXh.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-BhlfIpCW.js → railroadDiagram-O6MQD6OU-CKbKBwCj.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-vkxH8U-2.js → requirementDiagram-BXWQKSXE-DRc316xB.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-D2iFl0bc.js → sankeyDiagram-P5KCCOFB-C6O9zcIS.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-Ll_m9w0-.js → sequenceDiagram-WJ2MYXX4-D3Vz1DcK.js} +1 -1
- package/web/assets/{settingsRoute-C8p10bxH.js → settingsRoute-DxPtfqmn.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-B4KWuRz2.js → stateDiagram-D77RDMKH-DXcUXP-l.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-CQER-whG.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-CVXwQL4b.js → swimlanes-42K2YHIH-tfw_N04-.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-Cf__qhQx.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-C0XuGVkJ.js → timeline-definition-24CTP7MA-BcAeG9F_.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-BS4edB5m.js → treeView-Q6P3EWNA-DaZCFJDf.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-BZ54DPA4.js → treemap-WGGIJYW6-C3PybXBI.js} +1 -1
- package/web/assets/{useStore-DEfLrrMh.js → useStore-Ce5mzYro.js} +9 -7
- package/web/assets/{vennDiagram-4TSXK5OY-CAKXCk3I.js → vennDiagram-4TSXK5OY-BT7w_l5a.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-DM5IZXxI.js → wardley-WFR3VGLG-CXZlxUiB.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4--WWmycX2.js → wardleyDiagram-VM6X3IG4-B5muPnst.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-D-_SgAQy.js → xychartDiagram-S5SC5T6Z-DSaMhJlg.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/web/assets/architecture-7GRP2DOG-CD4t1j2T.js +0 -1
- package/web/assets/channel-Dl0Aa963.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BvMwIvP0.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BvMwIvP0.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-CF677pnK.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-8SiMcM5n.js +0 -1
- package/web/assets/mount-Fdy9Xoxs.js +0 -27
- package/web/assets/railroad-abnf-HS5TGJTU-C_F3QWV5.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-BaNfyzXp.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-BWyMWwxG.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-B_Xx-QG6.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-XYt-UA-z.js +0 -8
package/dist/apps/gateway.js
CHANGED
|
@@ -14,6 +14,8 @@ import { pressMenu, readMenuPath, readMenus } from "./menu.js";
|
|
|
14
14
|
import { inspectorScript } from "./inspector.js";
|
|
15
15
|
import { readStrokes, readElementScrolls } from "./annotate.js";
|
|
16
16
|
import { contentType } from "./mime.js";
|
|
17
|
+
import { loadScenarios, matchRule, planScenario, scenariosFor, summarize } from "./scenarios.js";
|
|
18
|
+
import { simulate } from "./scenario-proxy.js";
|
|
17
19
|
const COOKIE = "__Host-bivy-preview";
|
|
18
20
|
const OPEN_PATH = "/__bivy/open";
|
|
19
21
|
const REDEEM_PATH = "/__bivy/redeem";
|
|
@@ -21,6 +23,10 @@ const REVISION_PATH = "/__bivy/revision";
|
|
|
21
23
|
const INSPECTOR_PATH = "/__bivy/inspector.js";
|
|
22
24
|
const COMPARE_PATH = "/__bivy/compare";
|
|
23
25
|
const NOTES_PATH = "/__bivy/notes";
|
|
26
|
+
const SERVER_ERRORS_PATH = "/__bivy/server-errors";
|
|
27
|
+
const SCENARIOS_PATH = "/__bivy/scenarios";
|
|
28
|
+
const SCENARIO_PATH = "/__bivy/scenario";
|
|
29
|
+
const scenarioKind = (entry) => entry.target.kind === "display" ? "desktop" : "web";
|
|
24
30
|
/** Larger HTML documents pass through without the inspector. */
|
|
25
31
|
const MAX_INJECT_BYTES = 5 * 1024 * 1024;
|
|
26
32
|
/** Put the inspector first in <head>, so it sees errors from the app's own scripts. */
|
|
@@ -205,9 +211,6 @@ export class AppGateway {
|
|
|
205
211
|
["/__bivy/tokens.css", readFileSync(new URL("./tokens.css", import.meta.url))],
|
|
206
212
|
["/__bivy/styles.css", readFileSync(new URL("./styles.css", import.meta.url))],
|
|
207
213
|
]);
|
|
208
|
-
/** `reviewer`: the browser session came from a copied (shared) link.
|
|
209
|
-
* `viewOnly`: that link came without the reviewer tools.
|
|
210
|
-
* `embedded`: it runs framed inside a Bivy client (a third-party frame). */
|
|
211
214
|
sessions = new Map();
|
|
212
215
|
sockets = new Map();
|
|
213
216
|
/** Connections opened by people with a copied link, so Stop sharing can end
|
|
@@ -486,6 +489,11 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
486
489
|
res.end("Preview access expired. Open a new preview link from Bivy.");
|
|
487
490
|
return;
|
|
488
491
|
}
|
|
492
|
+
// The shell (its own origin) chooses this browser's scenario; nothing else may.
|
|
493
|
+
if (req.url === SCENARIO_PATH && req.method === "POST") {
|
|
494
|
+
await this.enterScenario(req, res, entry, session);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
489
497
|
// Block cross-site mutations even if a client sends the preview cookie.
|
|
490
498
|
if (!["GET", "HEAD"].includes(req.method ?? "") && req.headers.origin !== this.origin(id)) {
|
|
491
499
|
res.writeHead(403);
|
|
@@ -511,6 +519,14 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
511
519
|
this.compare(req, res, entry);
|
|
512
520
|
return;
|
|
513
521
|
}
|
|
522
|
+
if (req.url.startsWith(SERVER_ERRORS_PATH) && req.method === "GET") {
|
|
523
|
+
this.serverErrors(req, res, entry, session);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (req.url === SCENARIOS_PATH && req.method === "GET") {
|
|
527
|
+
this.scenarios(res, entry, session);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
514
530
|
if (req.url === INSPECTOR_PATH && req.method === "GET") {
|
|
515
531
|
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" });
|
|
516
532
|
res.end(inspectorScript(this.shellOrigin(id), session.reviewer === true && !session.viewOnly, session.embedded === true));
|
|
@@ -530,7 +546,19 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
530
546
|
await this.display(req, res, entry);
|
|
531
547
|
return;
|
|
532
548
|
}
|
|
549
|
+
const rule = session.scenario && matchRule(session.scenario.network, req.method, req.url);
|
|
550
|
+
if (rule && await simulate(req, res, rule, responseHeaders({}, this.ancestors(id))))
|
|
551
|
+
return;
|
|
533
552
|
const inspect = isPageLoad(req);
|
|
553
|
+
// What the person does in the preview marks the app's logs ("your last
|
|
554
|
+
// action"): opening a page, or sending something. Assets don't count.
|
|
555
|
+
const pathOnly = req.url.split("?")[0].slice(0, 200);
|
|
556
|
+
if (inspect) {
|
|
557
|
+
entry.loadedAt = Date.now();
|
|
558
|
+
this.registry.emit("action", id, `Opened ${pathOnly}`);
|
|
559
|
+
}
|
|
560
|
+
else if (!["GET", "HEAD", "OPTIONS"].includes(req.method ?? ""))
|
|
561
|
+
this.registry.emit("action", id, `${req.method} ${pathOnly} from the preview`);
|
|
534
562
|
// Remember the framed page so a turn reload lands where the user was.
|
|
535
563
|
if (req.method === "GET" && req.headers["sec-fetch-dest"] === "iframe" && req.url.length <= 2048)
|
|
536
564
|
entry.lastPath = req.url;
|
|
@@ -871,6 +899,96 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
871
899
|
res.writeHead(200, { "content-type": "application/json" });
|
|
872
900
|
res.end(JSON.stringify({ screenshot: Boolean(shot) }));
|
|
873
901
|
}
|
|
902
|
+
/** Server errors for the shell's Console: since the page loaded ("page"),
|
|
903
|
+
* or since the last answer's `now`. The owner's only: a server's log is not
|
|
904
|
+
* for people with a shared link. */
|
|
905
|
+
serverErrors(req, res, entry, session) {
|
|
906
|
+
const cors = { "access-control-allow-origin": this.shellOrigin(entry.view.id), "access-control-allow-credentials": "true", vary: "Origin" };
|
|
907
|
+
if (session.reviewer) {
|
|
908
|
+
res.writeHead(403, cors);
|
|
909
|
+
res.end();
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const raw = new URL(req.url, "http://gateway").searchParams.get("since");
|
|
913
|
+
const since = raw === "page" || raw === null ? (entry.loadedAt ?? Date.now()) - 1 : Number(raw);
|
|
914
|
+
const found = Number.isFinite(since) ? this.registry.serverErrors?.(entry.view.id, since) : undefined;
|
|
915
|
+
res.writeHead(200, { ...cors, "content-type": "application/json", "cache-control": "no-store" });
|
|
916
|
+
res.end(JSON.stringify(found ?? { now: Date.now(), lines: [] }));
|
|
917
|
+
}
|
|
918
|
+
/** The view's scenarios for the shell's switcher, and the one it's in: this
|
|
919
|
+
* browser's for a web page, the app's for a desktop app. */
|
|
920
|
+
scenarios(res, entry, session) {
|
|
921
|
+
const { scenarios, problems } = loadScenarios(entry.workspace);
|
|
922
|
+
const kind = scenarioKind(entry);
|
|
923
|
+
const own = scenariosFor(scenarios, entry.app.name, entry.view.name, kind);
|
|
924
|
+
const active = kind === "desktop" ? entry.scenario : session.scenario;
|
|
925
|
+
res.writeHead(200, { "access-control-allow-origin": this.shellOrigin(entry.view.id), "access-control-allow-credentials": "true", vary: "Origin", "content-type": "application/json" });
|
|
926
|
+
res.end(JSON.stringify({ kind, scenarios: summarize(own, problems, entry.app.createdAt, kind), active: active ? { id: active.id, name: active.name, ...(active.simulated ? { simulated: active.simulated } : {}) } : null }));
|
|
927
|
+
}
|
|
928
|
+
/** Puts this browser in a scenario (or back in none, for an empty body) and
|
|
929
|
+
* returns the plan the shell walks through: pages to open and steps to take.
|
|
930
|
+
* A "fresh" scenario also expires the app's cookies this browser sent, so it
|
|
931
|
+
* starts as a new visitor. Only the shell's origin may ask. */
|
|
932
|
+
async enterScenario(req, res, entry, session) {
|
|
933
|
+
const shell = this.shellOrigin(entry.view.id);
|
|
934
|
+
const cors = { "access-control-allow-origin": shell, "access-control-allow-credentials": "true", vary: "Origin" };
|
|
935
|
+
if (req.headers.origin !== shell) {
|
|
936
|
+
res.writeHead(403);
|
|
937
|
+
res.end();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
let body = "";
|
|
941
|
+
for await (const chunk of req) {
|
|
942
|
+
body += chunk.toString();
|
|
943
|
+
if (body.length > 64) {
|
|
944
|
+
res.writeHead(413, cors);
|
|
945
|
+
res.end();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const id = body.trim();
|
|
950
|
+
const json = (status, value) => { res.writeHead(status, { ...cors, "content-type": "application/json" }); res.end(JSON.stringify(value)); };
|
|
951
|
+
// A desktop app is one program for everyone watching it: its scenario
|
|
952
|
+
// restarts it, and the steps run in its window before this answers.
|
|
953
|
+
if (scenarioKind(entry) === "desktop") {
|
|
954
|
+
if (!this.registry.desktopScenario) {
|
|
955
|
+
json(400, { error: "Desktop scenarios aren't available on this machine." });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
json(200, await this.registry.desktopScenario(entry.view.id, id));
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
json(400, { error: error.message });
|
|
963
|
+
}
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (!id) {
|
|
967
|
+
session.scenario = undefined;
|
|
968
|
+
res.writeHead(200, { ...cors, "content-type": "application/json" });
|
|
969
|
+
res.end(JSON.stringify({ plan: null }));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
let plan;
|
|
973
|
+
try {
|
|
974
|
+
const own = scenariosFor(loadScenarios(entry.workspace).scenarios, entry.app.name, entry.view.name, "web");
|
|
975
|
+
plan = planScenario(own, id, "web");
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
res.writeHead(400, { ...cors, "content-type": "application/json" });
|
|
979
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
session.scenario = plan;
|
|
983
|
+
const headers = { ...cors, "content-type": "application/json" };
|
|
984
|
+
if (plan.fresh) {
|
|
985
|
+
const names = (req.headers.cookie ?? "").split(";").map((part) => part.split("=", 1)[0].trim()).filter((name) => name && name !== COOKIE && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name));
|
|
986
|
+
const expire = (name) => `${name}=; Path=/; Max-Age=0`;
|
|
987
|
+
headers["set-cookie"] = names.map((name) => session.embedded ? partitionCookie(expire(name)) : expire(name));
|
|
988
|
+
}
|
|
989
|
+
res.writeHead(200, headers);
|
|
990
|
+
res.end(JSON.stringify({ plan }));
|
|
991
|
+
}
|
|
874
992
|
/** Compare shots for the shell: the list, or one PNG by index. */
|
|
875
993
|
compare(req, res, entry) {
|
|
876
994
|
const shots = entry.shots ?? [];
|
package/dist/apps/inspector.js
CHANGED
|
@@ -128,10 +128,65 @@ const marked=r=>{
|
|
|
128
128
|
text:(el.innerText||el.getAttribute('aria-label')||el.getAttribute('alt')||'').trim().replace(/\\s+/g,' ').slice(0,120),
|
|
129
129
|
rect:{x:Math.round(b.left+scrollX),y:Math.round(b.top+scrollY),width:Math.round(b.width),height:Math.round(b.height)}};});
|
|
130
130
|
};
|
|
131
|
+
// Scenarios: the shell walks a scenario's steps here, one at a time, the way a
|
|
132
|
+
// person would (find it, scroll to it, use it), and hears back after each one.
|
|
133
|
+
// A step that leaves the page ends this run; the shell picks up after it on the
|
|
134
|
+
// next page. "load" ties each answer to the page load it came from.
|
|
135
|
+
let leaving=false;
|
|
136
|
+
addEventListener('pagehide',()=>{leaving=true;});addEventListener('beforeunload',()=>{leaving=true;});
|
|
137
|
+
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
|
138
|
+
const shown=el=>Boolean(el&&el.getClientRects().length);
|
|
139
|
+
const words=el=>(el.innerText||el.value||el.getAttribute('aria-label')||'').trim().replace(/\\s+/g,' ').toLowerCase();
|
|
140
|
+
const find=q=>{
|
|
141
|
+
if(typeof q!=='string')return null;
|
|
142
|
+
if(q.startsWith('text=')){
|
|
143
|
+
const want=q.slice(5).trim().toLowerCase();
|
|
144
|
+
const all=[...document.querySelectorAll('a,button,[role=button],[role=link],[role=tab],[role=menuitem],[role=option],label,summary,input[type=submit],input[type=button]')].filter(shown);
|
|
145
|
+
return all.find(el=>words(el)===want)||all.find(el=>words(el).includes(want))||null;
|
|
146
|
+
}
|
|
147
|
+
try{const el=document.querySelector(q);return shown(el)?el:null;}catch{throw Error('“'+q+'” isn’t a valid selector');}
|
|
148
|
+
};
|
|
149
|
+
const waitFor=async q=>{for(let t=0;t<50&&!leaving;t++){const el=find(q);if(el)return el;await sleep(100);}return null;};
|
|
150
|
+
const missing=q=>Error('Couldn’t find “'+q+'” within 5 seconds');
|
|
151
|
+
const STEPS={
|
|
152
|
+
click:async s=>{const el=await waitFor(s.click);if(!el)throw missing(s.click);el.scrollIntoView({block:'center'});el.click();},
|
|
153
|
+
fill:async s=>{
|
|
154
|
+
const el=await waitFor(s.fill);if(!el)throw missing(s.fill);
|
|
155
|
+
const proto=el.localName==='textarea'?HTMLTextAreaElement.prototype:el.localName==='select'?HTMLSelectElement.prototype:HTMLInputElement.prototype;
|
|
156
|
+
const set=Object.getOwnPropertyDescriptor(proto,'value')?.set;if(!set)throw Error('“'+s.fill+'” isn’t a field');
|
|
157
|
+
el.scrollIntoView({block:'center'});el.focus();set.call(el,String(s.with));
|
|
158
|
+
el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));
|
|
159
|
+
},
|
|
160
|
+
press:async s=>{
|
|
161
|
+
const el=document.activeElement||document.body,key=String(s.press),o={key,bubbles:true,cancelable:true};
|
|
162
|
+
const go=el.dispatchEvent(new KeyboardEvent('keydown',o));el.dispatchEvent(new KeyboardEvent('keyup',o));
|
|
163
|
+
if(go&&key==='Enter'&&el.form)el.form.requestSubmit();
|
|
164
|
+
},
|
|
165
|
+
wait:async s=>{if(typeof s.wait==='number')await sleep(Math.min(10000,Math.max(0,s.wait)));else if(!await waitFor(s.wait))throw missing(s.wait);},
|
|
166
|
+
};
|
|
167
|
+
async function runSteps(d){
|
|
168
|
+
const steps=Array.isArray(d.steps)?d.steps.slice(0,50):[],run=d.run,load=d.load;
|
|
169
|
+
for(let i=Math.max(0,Number(d.from)||0);i<steps.length;i++){
|
|
170
|
+
const step=steps[i]||{},kind=Object.keys(STEPS).find(k=>k in step);
|
|
171
|
+
try{if(!kind)throw Error('Unknown step');await STEPS[kind](step);}
|
|
172
|
+
catch(error){if(!leaving)post({type:'step',run,load,i,error:String(error?.message||error).slice(0,300)});return;}
|
|
173
|
+
post({type:'step',run,load,i,ok:true});
|
|
174
|
+
await sleep(250);if(leaving)return;
|
|
175
|
+
}
|
|
176
|
+
post({type:'steps-done',run,load});
|
|
177
|
+
}
|
|
178
|
+
// A fresh start: what this page keeps in the browser goes (the gateway expires
|
|
179
|
+
// the cookies the server can see).
|
|
180
|
+
const forget=()=>{
|
|
181
|
+
try{localStorage.clear();sessionStorage.clear();}catch{}
|
|
182
|
+
for(const c of document.cookie.split(';')){const name=c.split('=')[0].trim();if(name)document.cookie=name+'=; Max-Age=0; Path=/';}
|
|
183
|
+
};
|
|
131
184
|
addEventListener('message',e=>{
|
|
132
185
|
if(!framed||e.origin!==SHELL||e.source!==parent)return;
|
|
133
186
|
const d=e.data||{},n=v=>Number.isFinite(+v)?+v:0;
|
|
134
|
-
if(d.type==='bivy:
|
|
187
|
+
if(d.type==='bivy:steps')void runSteps(d);
|
|
188
|
+
else if(d.type==='bivy:forget'){forget();post({type:'forgot'});}
|
|
189
|
+
else if(d.type==='bivy:note'&&REVIEWER){
|
|
135
190
|
void fetch('/__bivy/notes',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(d.note)})
|
|
136
191
|
.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();})
|
|
137
192
|
.then(result=>post({type:'note-sent',id:d.id,screenshot:result.screenshot===true}),error=>post({type:'note-sent',id:d.id,error:error.message}));
|
|
@@ -89,6 +89,27 @@ nav::-webkit-scrollbar { display:none; }
|
|
|
89
89
|
#menu kbd { flex:none; padding:0 var(--space-1); border:thin solid var(--line); border-radius:var(--radius-sm); color:var(--muted); font-family:var(--font-mono); font-size:var(--text-xs); }
|
|
90
90
|
/* The name is a label, not a target: one pill, three buttons at most. */
|
|
91
91
|
#name { pointer-events:none; }
|
|
92
|
+
/* With scenarios, the name becomes their switcher: the title says which one
|
|
93
|
+
you're in, the line under it whether Bivy is simulating anything. */
|
|
94
|
+
#scn { display:flex; align-items:center; gap:var(--space-1); min-width:0; padding-inline:var(--space-2); text-align:start; }
|
|
95
|
+
#scn[data-active] { background:color-mix(in srgb, var(--accent) 14%, var(--surface)); }
|
|
96
|
+
.scn-text { display:flex; flex-direction:column; min-width:0; }
|
|
97
|
+
#scn-title { font-weight:var(--weight-semibold); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:14em; }
|
|
98
|
+
#scn-sub { font-size:var(--text-xs); font-weight:var(--weight-normal); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:18em; }
|
|
99
|
+
#scn svg { flex:none; }
|
|
100
|
+
#dock:not([data-edge="top"]) #scn svg { transform:rotate(180deg); }
|
|
101
|
+
.scn-group { margin:var(--space-3) 0 var(--space-1); font-size:var(--text-xs); font-weight:var(--weight-semibold); color:var(--muted); }
|
|
102
|
+
.scn-group:first-of-type { margin-top:0; }
|
|
103
|
+
.scn-list { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:var(--space-2); }
|
|
104
|
+
.scn-row { display:flex; align-items:flex-start; gap:var(--space-2); width:100%; height:100%; min-block-size:var(--space-7); box-sizing:border-box; padding:var(--space-2) var(--space-3); border:thin solid var(--line); border-radius:var(--radius-md); background:var(--surface); color:var(--ink); font:inherit; text-align:start; cursor:pointer; }
|
|
105
|
+
.scn-row:hover { background:var(--surface-2); }
|
|
106
|
+
.scn-row:focus-visible { outline:2px solid var(--focus-ring); outline-offset:1px; }
|
|
107
|
+
.scn-row[aria-current="true"] { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
|
|
108
|
+
.scn-row-text { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
|
|
109
|
+
.scn-row-name { font-size:var(--text-sm); font-weight:var(--weight-semibold); overflow-wrap:anywhere; }
|
|
110
|
+
.scn-row-sub { font-size:var(--text-xs); color:var(--muted); overflow-wrap:anywhere; }
|
|
111
|
+
.scn-row .badge { flex:none; }
|
|
112
|
+
#scn-fail-text { font-size:var(--text-sm); margin:0; overflow-wrap:anywhere; }
|
|
92
113
|
#more { position:relative; gap:var(--space-1); }
|
|
93
114
|
#dock:not([data-edge="top"]) #more svg { transform:rotate(180deg); }
|
|
94
115
|
#compare-draw { margin-top:var(--space-2); }
|
|
@@ -96,7 +117,7 @@ nav::-webkit-scrollbar { display:none; }
|
|
|
96
117
|
#compare[data-drawing] #compare-stage img { max-height:min(60vh, 560px); }
|
|
97
118
|
#compare[data-drawing] #compare-slider, #compare[data-drawing] #compare-hint, #compare[data-drawing] #compare-draw { display:none; }
|
|
98
119
|
.label-narrow { display:none; }
|
|
99
|
-
@media (max-width: 699px) { .wide-only { display:none; } .label-wide { display:none; } .label-narrow { display:inline; } #title { max-width:9em; } }
|
|
120
|
+
@media (max-width: 699px) { .wide-only { display:none; } .label-wide { display:none; } .label-narrow { display:inline; } #title, #scn-title { max-width:9em; } #scn-sub { max-width:12em; } }
|
|
100
121
|
/* "Made with Bivy": a quiet strip under a shared app, never over it. */
|
|
101
122
|
#made-with { flex-shrink:0; display:flex; align-items:center; justify-content:center; gap:var(--space-1); min-height:var(--space-6); padding:0 var(--space-3) env(safe-area-inset-bottom); box-sizing:border-box; background:var(--surface); border-top:thin solid var(--line); color:var(--muted); font-size:var(--text-xs); text-decoration:none; }
|
|
102
123
|
#made-with:hover, #made-with:focus-visible { color:var(--ink); }
|
|
@@ -111,7 +132,7 @@ body.badged #dock:not([data-edge="top"]) { bottom:calc(var(--space-2) + var(--sp
|
|
|
111
132
|
<div id="dock">
|
|
112
133
|
<section class="panel" id="console" hidden aria-labelledby="console-title">
|
|
113
134
|
<h2><span id="console-title">Console</span><button class="btn sm ghost" id="clear">Clear</button></h2>
|
|
114
|
-
<p class="muted" id="console-empty">No errors or warnings since this page loaded.</p>
|
|
135
|
+
<p class="muted" id="console-empty">No errors or warnings since this page loaded, from the page or its server.</p>
|
|
115
136
|
<ul id="entries"></ul>
|
|
116
137
|
<div class="panel-actions"><button class="btn sm" id="send-errors" disabled>Send to agent…</button></div>
|
|
117
138
|
</section>
|
|
@@ -121,6 +142,15 @@ body.badged #dock:not([data-edge="top"]) { bottom:calc(var(--space-2) + var(--sp
|
|
|
121
142
|
<p class="muted" id="compare-hint">Left of the handle: before. Right: now. Screenshots at phone width.</p>
|
|
122
143
|
<button class="btn sm" id="compare-draw" type="button" hidden>Draw on “now”</button>
|
|
123
144
|
</section>
|
|
145
|
+
<section class="panel" id="scenarios" hidden aria-labelledby="scenarios-title">
|
|
146
|
+
<h2><span id="scenarios-title">Scenarios</span><button class="btn sm ghost" id="scenarios-close" type="button" aria-label="Close scenarios">✕</button></h2>
|
|
147
|
+
<div id="scenarios-body"></div>
|
|
148
|
+
</section>
|
|
149
|
+
<section class="panel" id="scn-fail" hidden aria-labelledby="scn-fail-title" role="alertdialog">
|
|
150
|
+
<h2 id="scn-fail-title">Couldn’t finish the scenario</h2>
|
|
151
|
+
<p id="scn-fail-text"></p>
|
|
152
|
+
<div class="panel-actions"><button class="btn sm ghost" id="scn-stay" type="button">Stay here</button><button class="btn sm primary" id="scn-fix" type="button">Ask agent to fix</button></div>
|
|
153
|
+
</section>
|
|
124
154
|
<section class="panel" id="draft" hidden aria-labelledby="draft-title">
|
|
125
155
|
<div id="draft-body">
|
|
126
156
|
<h2 id="draft-title">Note for the agent</h2>
|
|
@@ -157,6 +187,8 @@ body.badged #dock:not([data-edge="top"]) { bottom:calc(var(--space-2) + var(--sp
|
|
|
157
187
|
<nav aria-label="Bivy preview controls">
|
|
158
188
|
<button class="btn sm ghost" id="back" aria-label="Back to chat">‹ <span class="label-wide">Chat</span></button>
|
|
159
189
|
<span id="name"><span id="title">App preview</span><span id="stamp" class="muted" role="status"></span></span>
|
|
190
|
+
<button class="btn sm ghost" id="scn" type="button" hidden aria-haspopup="dialog" aria-expanded="false" aria-controls="scenarios"><span class="scn-text"><span id="scn-title"></span><span id="scn-sub" class="muted" aria-live="polite"></span></span><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button>
|
|
191
|
+
<button class="btn sm ghost" id="scn-reset" type="button" hidden aria-label="Reset scenario" title="Start this scenario over"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg></button>
|
|
160
192
|
<button class="btn sm primary" id="update" type="button" hidden><span class="label-wide">Show new version</span><span class="label-narrow">New</span></button>
|
|
161
193
|
<button class="btn sm ghost" id="more" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="Preview options"><span class="badge" data-variant="solid" data-tone="danger" id="error-count" hidden></span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button>
|
|
162
194
|
</nav>
|
|
@@ -166,7 +198,9 @@ body.badged #dock:not([data-edge="top"]) { bottom:calc(var(--space-2) + var(--sp
|
|
|
166
198
|
const $=id=>document.getElementById(id);
|
|
167
199
|
const frame=$('app'),status=$('status'),back=$('back'),reload=$('reload'),stage=$('stage');
|
|
168
200
|
// "#ticket~page": a review card opens the preview on the page it shows.
|
|
169
|
-
|
|
201
|
+
// "#ticket~page~scenario": a review card's "Try it" opens the preview in a scenario.
|
|
202
|
+
const [ticket,startPage='',startAt='']=location.hash.slice(1).split('~');history.replaceState(null,'',location.pathname);
|
|
203
|
+
let pendingScenario=/^[a-z0-9][a-z0-9-]{0,63}$/.test(startAt)?startAt:'';
|
|
170
204
|
let metadata,currentPath='/';
|
|
171
205
|
// Peek: framed by a Bivy client, which owns closing and the composer.
|
|
172
206
|
const embedded=parent!==window;
|
|
@@ -206,10 +240,15 @@ function show(data,launch){
|
|
|
206
240
|
reviewerTools(false);
|
|
207
241
|
status.textContent='Loading app…';
|
|
208
242
|
frame.onload=()=>{
|
|
209
|
-
|
|
243
|
+
loads++;
|
|
244
|
+
if(runner&&!runner.desktop)sendSteps();else status.hidden=true;
|
|
245
|
+
// A desktop app's viewer has no inspector to say it has loaded.
|
|
246
|
+
if(metadata.inspect===false&&!scenariosLoaded)void loadScenarios();
|
|
247
|
+
void loadCompare();
|
|
210
248
|
// Put them back where they were before an update they asked for.
|
|
211
249
|
if(restore){const at=restore;restore=null;frame.contentWindow?.postMessage(Object.assign({type:'bivy:restore'},at),metadata.origin);}
|
|
212
250
|
if(!watching){watching=true;latest=-1;shown=null;watch();firstHint();}else if(wake)wake();
|
|
251
|
+
if(!serverPolling)void pollServer();
|
|
213
252
|
};
|
|
214
253
|
// A Bivy client framing the shell says whether it can take dictation.
|
|
215
254
|
if(embedded&&data.returnTo)toBivy({type:'hello'});
|
|
@@ -241,6 +280,8 @@ async function watch(){
|
|
|
241
280
|
const r=await fetch(metadata.origin+'/__bivy/revision?after='+latest,{credentials:'include',cache:'no-store'});
|
|
242
281
|
if(!r.ok)throw Error();
|
|
243
282
|
const d=await r.json();
|
|
283
|
+
// A turn may have written or changed scenario files.
|
|
284
|
+
if(latest!==null&&latest!==-1&&d.revision!==latest)void loadScenarios();
|
|
244
285
|
latest=d.revision;newPath=safePath(d.path);
|
|
245
286
|
// The first poll after a load describes what the frame already has.
|
|
246
287
|
if(shown===null)shown=latest;
|
|
@@ -254,6 +295,7 @@ function waiting(on){
|
|
|
254
295
|
if($('update').hidden===!on)return;
|
|
255
296
|
$('update').hidden=!on;
|
|
256
297
|
$('stamp').textContent=on?'New version ready':'';
|
|
298
|
+
renderScenarioPill();
|
|
257
299
|
}
|
|
258
300
|
/** Asks the page where it is, so the update can put it back. Without an
|
|
259
301
|
* inspector (a desktop app) there is nothing to ask. */
|
|
@@ -407,20 +449,186 @@ function finishNote(d){
|
|
|
407
449
|
status.hidden=false;status.textContent=$('stamp').textContent+'. The app’s owner will see it.';
|
|
408
450
|
$('more').focus();
|
|
409
451
|
}
|
|
452
|
+
// Scenarios: named starting points for the live app, from the project's
|
|
453
|
+
// .bivy/scenarios. Opening one tells the gateway (so its simulated responses
|
|
454
|
+
// apply to this browser only), opens its page and walks its steps through the
|
|
455
|
+
// page's inspector, one at a time; a step that loads a new page is picked up
|
|
456
|
+
// after on the next one. Reset does it all again; "Your data" leaves it.
|
|
457
|
+
let scenarios=[],activeScenario=null,runner=null,runSeq=0,loads=0,scenariosLoaded=false;
|
|
458
|
+
const scnBtn=$('scn'),scnReset=$('scn-reset');
|
|
459
|
+
async function loadScenarios(){
|
|
460
|
+
if(!metadata||metadata.controls===false)return;
|
|
461
|
+
try{
|
|
462
|
+
const r=await fetch(metadata.origin+'/__bivy/scenarios',{credentials:'include',cache:'no-store'});if(!r.ok)return;
|
|
463
|
+
const d=await r.json();
|
|
464
|
+
scenarios=(Array.isArray(d.scenarios)?d.scenarios:[]).slice(0,100);
|
|
465
|
+
if(!runner)activeScenario=d.active&&typeof d.active.id==='string'?d.active:null;
|
|
466
|
+
scenariosLoaded=true;
|
|
467
|
+
renderScenarioPill();
|
|
468
|
+
if(!$('scenarios').hidden)renderScenarioList();
|
|
469
|
+
if(pendingScenario){const id=pendingScenario;pendingScenario='';void enterScenario(id);}
|
|
470
|
+
}catch{}
|
|
471
|
+
}
|
|
472
|
+
function renderScenarioPill(){
|
|
473
|
+
if(!metadata)return;
|
|
474
|
+
const on=scenarios.length>0||Boolean(activeScenario);
|
|
475
|
+
scnBtn.hidden=!on;$('name').hidden=on;scnReset.hidden=!activeScenario||Boolean(runner);
|
|
476
|
+
if(!on)return;
|
|
477
|
+
const sim=activeScenario?.simulated;
|
|
478
|
+
$('scn-title').textContent=activeScenario?activeScenario.name:metadata.name;
|
|
479
|
+
$('scn-sub').textContent=runner?'Opening…':activeScenario?(sim?'Simulated: '+sim:metadata.name):$('stamp').textContent||(scenarios.length===1?'1 scenario':scenarios.length+' scenarios');
|
|
480
|
+
scnBtn.toggleAttribute('data-active',Boolean(activeScenario));
|
|
481
|
+
scnBtn.setAttribute('aria-label',(activeScenario?'Scenario: '+activeScenario.name+(sim?', simulated':'')+'. ':'')+'Choose a scenario');
|
|
482
|
+
scnReset.setAttribute('aria-label','Reset '+(activeScenario?.name||'scenario'));
|
|
483
|
+
}
|
|
484
|
+
function scenarioRow(s){
|
|
485
|
+
const b=document.createElement('button');b.type='button';b.className='scn-row';
|
|
486
|
+
const current=(activeScenario?.id||'')===s.id;
|
|
487
|
+
if(current)b.setAttribute('aria-current','true');
|
|
488
|
+
const text=document.createElement('span');text.className='scn-row-text';
|
|
489
|
+
const name=document.createElement('span');name.className='scn-row-name';name.textContent=s.name;
|
|
490
|
+
text.append(name);
|
|
491
|
+
const lines=s.error?[s.error]:[s.description,s.details??[s.open,s.simulated?'simulates '+s.simulated:'',s.steps?s.steps+' step'+(s.steps===1?'':'s'):'',s.fresh?'as a new visitor':''].filter(Boolean).join(' · ')];
|
|
492
|
+
for(const line of lines.filter(Boolean)){const sub=document.createElement('span');sub.className='scn-row-sub';sub.textContent=String(line).slice(0,300);text.append(sub);}
|
|
493
|
+
b.append(text);
|
|
494
|
+
if(s.error||s.isNew){const tag=document.createElement('span');tag.className='badge';tag.dataset.tone=s.error?'warn':'accent';tag.textContent=s.error?'Needs a fix':'New';b.append(tag);}
|
|
495
|
+
if(s.error&&(!metadata.returnTo||metadata.reviewer))b.disabled=true;
|
|
496
|
+
b.onclick=()=>{if(s.error)return toChat(fixRequest(s.name,s.id,s.error));void enterScenario(s.id);};
|
|
497
|
+
return b;
|
|
498
|
+
}
|
|
499
|
+
function renderScenarioList(){
|
|
500
|
+
const body=$('scenarios-body'),fresh=scenarios.filter(s=>s.isNew&&!s.error);
|
|
501
|
+
const group=(label,rows)=>{
|
|
502
|
+
const h=document.createElement('h3');h.className='scn-group';h.textContent=label;
|
|
503
|
+
const ul=document.createElement('ul');ul.className='scn-list';
|
|
504
|
+
for(const row of rows){const li=document.createElement('li');li.append(scenarioRow(row));ul.append(li);}
|
|
505
|
+
return [h,ul];
|
|
506
|
+
};
|
|
507
|
+
const mine={id:'',name:'Your data',details:'The app as it is, no scenario'};
|
|
508
|
+
body.replaceChildren(...(fresh.length?group('New in this session',fresh):[]),...group(fresh.length?'All':'Start from',[mine,...scenarios.filter(s=>!fresh.includes(s))]));
|
|
509
|
+
}
|
|
510
|
+
scnBtn.onclick=()=>{
|
|
511
|
+
if(!$('scenarios').hidden)return panels(null);
|
|
512
|
+
panels('scenarios');renderScenarioList();void loadScenarios();
|
|
513
|
+
($('scenarios-body').querySelector('[aria-current="true"]')||$('scenarios-body').querySelector('.scn-row:not(:disabled)'))?.focus();
|
|
514
|
+
};
|
|
515
|
+
$('scenarios-close').onclick=()=>{panels(null);scnBtn.focus();};
|
|
516
|
+
scnReset.onclick=()=>{if(activeScenario)void enterScenario(activeScenario.id);};
|
|
517
|
+
const fixRequest=(name,id,problem)=>'The scenario “'+name+'” (.bivy/scenarios/'+id+'.json) for the app preview "'+metadata.name+'" doesn’t work: '+problem+'\\n\\nPlease fix the scenario, or the app if that’s what broke, and tell me when it opens cleanly.';
|
|
518
|
+
async function enterScenario(id){
|
|
519
|
+
panels(null);
|
|
520
|
+
const run=++runSeq;
|
|
521
|
+
if(runner)clearTimeout(runner.timer);
|
|
522
|
+
runner=null;
|
|
523
|
+
// A desktop app: the machine restarts it in the scenario and takes the
|
|
524
|
+
// steps in its window, then answers. Nothing to walk here.
|
|
525
|
+
if(metadata.inspect===false)return enterDesktopScenario(id,run);
|
|
526
|
+
let d={};
|
|
527
|
+
try{
|
|
528
|
+
const r=await fetch(metadata.origin+'/__bivy/scenario',{method:'POST',credentials:'include',cache:'no-store',headers:{'content-type':'text/plain'},body:id});
|
|
529
|
+
d=await r.json().catch(()=>({}));
|
|
530
|
+
if(!r.ok)throw Error(typeof d.error==='string'?d.error.slice(0,300):'The preview didn’t answer.');
|
|
531
|
+
}catch(error){if(run===runSeq)failScenario({id,name:scenarios.find(s=>s.id===id)?.name||id},error.message||'The preview didn’t answer.');return;}
|
|
532
|
+
if(run!==runSeq)return;
|
|
533
|
+
const plan=d.plan;
|
|
534
|
+
if(!plan){activeScenario=null;renderScenarioPill();open(currentPath,'Reloading app…');return;}
|
|
535
|
+
activeScenario={id:plan.id,name:plan.name,...(plan.simulated?{simulated:plan.simulated}:{})};
|
|
536
|
+
runner={run,plan,stage:0,step:0,timer:0};
|
|
537
|
+
// As a new visitor: the page forgets what it kept before the first page opens.
|
|
538
|
+
if(plan.fresh)frame.contentWindow?.postMessage({type:'bivy:forget'},metadata.origin);
|
|
539
|
+
renderScenarioPill();
|
|
540
|
+
setTimeout(()=>{if(runner?.run===run)goStage();},plan.fresh?200:0);
|
|
541
|
+
}
|
|
542
|
+
async function enterDesktopScenario(id,run){
|
|
543
|
+
const name=id?scenarios.find(s=>s.id===id)?.name||id:'';
|
|
544
|
+
runner={run,desktop:true,timer:0,plan:{id,name}};renderScenarioPill();
|
|
545
|
+
let d={};
|
|
546
|
+
try{
|
|
547
|
+
const r=await fetch(metadata.origin+'/__bivy/scenario',{method:'POST',credentials:'include',cache:'no-store',headers:{'content-type':'text/plain'},body:id});
|
|
548
|
+
d=await r.json().catch(()=>({}));
|
|
549
|
+
if(!r.ok)throw Error(typeof d.error==='string'?d.error.slice(0,300):'The app didn’t answer.');
|
|
550
|
+
}catch(error){if(run===runSeq){runner=null;failScenario({id,name},error.message||'The app didn’t answer.');}return;}
|
|
551
|
+
if(run!==runSeq)return;
|
|
552
|
+
activeScenario=d.plan?{id:d.plan.id,name:d.plan.name,...(d.plan.simulated?{simulated:d.plan.simulated}:{})}:null;
|
|
553
|
+
if(typeof d.error==='string')failScenario(d.plan,d.error.slice(0,400));else finishScenario();
|
|
554
|
+
}
|
|
555
|
+
/** Opens the stage's page (its steps follow when it has loaded), or walks its steps where we are. */
|
|
556
|
+
function goStage(){
|
|
557
|
+
const st=runner.plan.stages[runner.stage];
|
|
558
|
+
if(!st)return finishScenario();
|
|
559
|
+
runner.step=0;
|
|
560
|
+
if(st.open){shown=latest;waiting(false);open(st.open,'Opening '+runner.plan.name+'…');watchdog();}
|
|
561
|
+
else sendSteps();
|
|
562
|
+
}
|
|
563
|
+
function sendSteps(){
|
|
564
|
+
const st=runner.plan.stages[runner.stage];
|
|
565
|
+
if(!st)return finishScenario();
|
|
566
|
+
if(runner.step>=st.steps.length){runner.stage++;return goStage();}
|
|
567
|
+
frame.contentWindow?.postMessage({type:'bivy:steps',run:runner.run,load:loads,steps:st.steps,from:runner.step},metadata.origin);
|
|
568
|
+
watchdog();
|
|
569
|
+
}
|
|
570
|
+
/** A page that stops answering (no inspector, a hung script) fails the scenario rather than leaving it opening forever. */
|
|
571
|
+
function watchdog(){
|
|
572
|
+
const at=runner;clearTimeout(at.timer);
|
|
573
|
+
at.timer=setTimeout(()=>{if(runner===at)failScenario(at.plan,'The page stopped answering at step '+(at.step+1)+' of “'+at.plan.name+'”.');},12000);
|
|
574
|
+
}
|
|
575
|
+
const stepWords=s=>s?('click' in s?'click '+s.click:'fill' in s?'fill '+s.fill:'press' in s?'press '+s.press:'wait' in s?'wait for '+s.wait:'step'):'step';
|
|
576
|
+
function stepReport(d){
|
|
577
|
+
if(d.type==='steps-done'){runner.stage++;return goStage();}
|
|
578
|
+
const i=Number(d.i)||0;
|
|
579
|
+
if(d.ok){runner.step=i+1;return watchdog();}
|
|
580
|
+
const st=runner.plan.stages[runner.stage],before=runner.plan.stages.slice(0,runner.stage).reduce((n,x)=>n+x.steps.length,0);
|
|
581
|
+
failScenario(runner.plan,'Step '+(before+i+1)+' ('+stepWords(st.steps[i])+'): '+String(d.error||'it failed').slice(0,300)+'.');
|
|
582
|
+
}
|
|
583
|
+
function finishScenario(){
|
|
584
|
+
if(runner)clearTimeout(runner.timer);
|
|
585
|
+
runner=null;status.hidden=true;renderScenarioPill();
|
|
586
|
+
}
|
|
587
|
+
function failScenario(s,message){
|
|
588
|
+
if(runner)clearTimeout(runner.timer);
|
|
589
|
+
runner=null;status.hidden=true;renderScenarioPill();
|
|
590
|
+
panels(null);
|
|
591
|
+
$('scn-fail-title').textContent='Couldn’t finish “'+s.name+'”';
|
|
592
|
+
$('scn-fail-text').textContent=message+(metadata.inspect===false?' The app is running with what ran so far.':' You’re on '+currentPath+' with what ran so far.');
|
|
593
|
+
$('scn-fix').hidden=!metadata.returnTo||Boolean(metadata.reviewer);
|
|
594
|
+
$('scn-fix').onclick=()=>{$('scn-fail').hidden=true;toChat(fixRequest(s.name,s.id,message));};
|
|
595
|
+
$('scn-fail').hidden=false;
|
|
596
|
+
($('scn-fix').hidden?$('scn-stay'):$('scn-fix')).focus();
|
|
597
|
+
}
|
|
598
|
+
$('scn-stay').onclick=()=>{$('scn-fail').hidden=true;scnBtn.hidden?more.focus():scnBtn.focus();};
|
|
410
599
|
// Console
|
|
411
600
|
let entries=[];
|
|
412
|
-
function resetConsole(){entries=[];renderConsole();}
|
|
601
|
+
function resetConsole(){entries=[];serverSince='page';renderConsole();}
|
|
602
|
+
// The server's errors join the page's: what the app's server logged since this
|
|
603
|
+
// page loaded (Bivy follows the output of servers it runs). The owner's only.
|
|
604
|
+
let serverSince='page',serverPolling=false;
|
|
605
|
+
async function pollServer(){
|
|
606
|
+
serverPolling=true;
|
|
607
|
+
if(metadata&&!metadata.reviewer&&metadata.inspect!==false){
|
|
608
|
+
try{
|
|
609
|
+
const r=await fetch(metadata.origin+'/__bivy/server-errors?since='+serverSince,{credentials:'include',cache:'no-store'});
|
|
610
|
+
if(r.ok){
|
|
611
|
+
const d=await r.json();
|
|
612
|
+
for(const line of (Array.isArray(d.lines)?d.lines:[]).slice(-20))entries.push({level:'error',source:'server',text:String(line.text).slice(0,500)});
|
|
613
|
+
if(entries.length>50)entries.splice(0,entries.length-50);
|
|
614
|
+
if(d.lines?.length)renderConsole();
|
|
615
|
+
if(Number.isFinite(d.now))serverSince=String(d.now);
|
|
616
|
+
}
|
|
617
|
+
}catch{}
|
|
618
|
+
}
|
|
619
|
+
setTimeout(()=>void pollServer(),document.hidden?10000:3000);
|
|
620
|
+
}
|
|
413
621
|
function renderConsole(){
|
|
414
622
|
const errors=entries.filter(e=>e.level==='error').length,count=$('error-count');
|
|
415
623
|
count.hidden=!errors;count.textContent=String(errors);
|
|
416
624
|
// The number is shown once, on the pill, so it is visible without opening it.
|
|
417
625
|
$('more').setAttribute('aria-label',errors?'Preview options, '+errors+' error'+(errors===1?'':'s'):'Preview options');
|
|
418
626
|
$('console-empty').hidden=entries.length>0;$('send-errors').disabled=!entries.length;
|
|
419
|
-
$('entries').replaceChildren(...entries.map(e=>{const li=document.createElement('li');const tag=document.createElement('span');tag.className='badge';tag.dataset.tone=e.level==='error'?'danger':'warn';tag.textContent=e.level;const text=document.createElement('span');text.textContent=e.text;li.append(tag,text);return li;}));
|
|
627
|
+
$('entries').replaceChildren(...entries.map(e=>{const li=document.createElement('li');const tag=document.createElement('span');tag.className='badge';tag.dataset.tone=e.level==='error'?'danger':'warn';tag.textContent=e.source==='server'?'server':e.level;const text=document.createElement('span');text.textContent=e.text;li.append(tag,text);return li;}));
|
|
420
628
|
}
|
|
421
629
|
$('errors').onclick=()=>panels($('console').hidden?'console':null);
|
|
422
630
|
$('clear').onclick=resetConsole;
|
|
423
|
-
$('send-errors').onclick=()=>draft('Console output in the app preview "'+metadata.name+'" (page '+currentPath+'):\\n'+entries.slice(-20).map(e=>'- '+e.level+': '+e.text).join('\\n'));
|
|
631
|
+
$('send-errors').onclick=()=>draft('Console output in the app preview "'+metadata.name+'" (page '+currentPath+'):\\n'+entries.slice(-20).map(e=>'- '+(e.source==='server'?'server error':e.level)+': '+e.text).join('\\n'));
|
|
424
632
|
// Marking: one gesture, two results. Pointing at an element and circling an
|
|
425
633
|
// area were two modes with a button each; they are one mode now, because they
|
|
426
634
|
// are one intention — "this, here".
|
|
@@ -666,7 +874,7 @@ async function loadCompare(){
|
|
|
666
874
|
if(metadata.reviewer)return;
|
|
667
875
|
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{}
|
|
668
876
|
}
|
|
669
|
-
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-checked',String(id===open));}$('draft').hidden=true;}
|
|
877
|
+
function panels(open){if(listening)cancelListening();if(draw&&!(open==='compare'&&draw.target==='compare'))endDraw();for(const [id,btn] of [['console','errors'],['compare','compare-btn'],['scenarios','scn']]){$(id).hidden=id!==open;$(btn).setAttribute(btn==='scn'?'aria-expanded':'aria-checked',String(id===open));}$('draft').hidden=true;$('scn-fail').hidden=true;}
|
|
670
878
|
compareBtn.onclick=async()=>{
|
|
671
879
|
if(!$('compare').hidden)return panels(null);
|
|
672
880
|
panels('compare');
|
|
@@ -736,7 +944,11 @@ addEventListener('message',e=>{
|
|
|
736
944
|
ask.hidden=!metadata.returnTo;
|
|
737
945
|
}else if(d.source==='bivy-inspector'){
|
|
738
946
|
if(d.type==='note-sent'&&metadata.reviewer)finishNote(d);
|
|
739
|
-
else if(d.type==='route'){
|
|
947
|
+
else if(d.type==='route'){
|
|
948
|
+
currentPath=safePath(d.path);reviewerTools(true);
|
|
949
|
+
if(!scenariosLoaded)void loadScenarios();
|
|
950
|
+
}
|
|
951
|
+
else if((d.type==='step'||d.type==='steps-done')&&runner&&d.run===runner.run&&d.load===loads)stepReport(d);
|
|
740
952
|
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();}
|
|
741
953
|
else if(d.type==='release'&&heldAt){heldAt=0;stopListening();}
|
|
742
954
|
else if(d.type==='draw-state'){if(draw&&draw.waiting)draw.waiting(d);else if(stateWaiter)stateWaiter(d);}
|