@bivy/bivy 0.20.7-staging.2 → 0.20.7-staging.3
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 +39 -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/gateway.js +91 -3
- package/dist/apps/inspector.js +56 -1
- package/dist/apps/preview-shell.js +197 -5
- package/dist/apps/registry.js +4 -1
- package/dist/apps/scenario-proxy.js +65 -0
- package/dist/apps/scenarios.js +307 -0
- package/dist/apps/service.js +111 -7
- package/dist/apps/styles.css +4 -0
- package/dist/apps/types.js +2 -1
- package/dist/controllers/app-commands.js +3 -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 +3 -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 +49 -15
- 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/worktree.js +25 -3
- package/package.json +1 -1
- package/web/assets/{AutomationsView-CJSYk0ja.js → AutomationsView-DuWxbo5B.js} +1 -1
- package/web/assets/{ChangesCard-Bp88fY6W.js → ChangesCard-BnHllUb0.js} +1 -1
- package/web/assets/{LibraryView-of9wmuzN.js → LibraryView-DpNuD-T7.js} +1 -1
- package/web/assets/{ProviderConnect-DG7SP4l2.js → ProviderConnect-4vjf567A.js} +1 -1
- package/web/assets/{Settings-DGH1U7wj.js → Settings-CHofnxyn.js} +3 -3
- package/web/assets/{Sheet-D-EHCMlj.js → Sheet-DwouI-Tt.js} +1 -1
- package/web/assets/{Terminal-qqysrwl_.js → Terminal-3mNEW7UT.js} +1 -1
- package/web/assets/{VoiceSettings-aeqQkwcZ.js → VoiceSettings-BI4B-zS1.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-Ct-lmBn5.js → abnfDiagram-VCTEODGH-DPP1TRyh.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-DUHnGffN.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-CCq716Da.js → architectureDiagram-5GKGNRK7-CvgInp30.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-Lka9F6ZB.js → blockDiagram-I7D4REHJ-CZAAYK-I.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-nvhhyzri.js → c4Diagram-7LVT6UL2-BYJlvZiZ.js} +1 -1
- package/web/assets/channel-vvvNMjoB.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-TZ5hhHBz.js → chunk-4HAMMTFA-Cj4-BAMD.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-IahKw8kM.js → chunk-75Z2AOVW-3eT-aS0t.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-BlcZUVOy.js → chunk-DU6HZSFF-D_4JI7OY.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-3MGfEgKu.js → chunk-F27PBJKO-5wTG02yk.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-Be72n678.js → chunk-GMAD6QVW-IliCLU5B.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-GOVLT6xm.js → chunk-GVQU2GXP-CB3DCzUu.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-ByOzewrT.js → chunk-IMKFNOWR-Bg3eY-xc.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-BCGhHmnW.js → chunk-L3NEJ4N5-BEMaPYT9.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-CAdliJE_.js → chunk-OSK3NFVY-DMc-FLYk.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-CM8cpgHa.js → chunk-P2QGCYS3-CaqPruPs.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CXLPhaI1.js → chunk-POPQ4Y6H-3h-I9lV1.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BegFrwDe.js → chunk-PWAF6VOD-CPLNMq3b.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-iXj7Y5RI.js → chunk-SHT3W25Y-ErtwLBXR.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-73pyTBZg.js → chunk-SVP7TREG-GNUjH0zF.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-D5zwuuGI.js → chunk-TICWLB2K-DtbyVpEE.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-C3eKmWom.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-C3eKmWom.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-D3nRr9S_.js → cynefin-OW5HDTMX-CQt68k-b.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-C4CKMYMZ.js → cynefinDiagram-5FMLGOSQ-BeEQcUmX.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-D-grkZ8X.js → dagre-GXQ25YYZ-CoekQT_h.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-BNPS3YQX.js → diagram-S7CK7UJ4-DQBF8H5Z.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-Bkp_TyRg.js → diagram-UQ7AKVKN-2Tw1WGQF.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-Dpxe_6MM.js → diagram-VSXAHHWV-C4Ns08EL.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DwGBvGn-.js → diagram-VX7I27RA-CMUQSfon.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-D-KvONMO.js → diagram-Z3DM3KII-Lqvr8Yf6.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-Di_D-a-0.js → ebnfDiagram-PWID7BFC-BAYJ-dgh.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-2pnBfTU9.js → erDiagram-RLTQ6QDP-Dg8VYnq4.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-ChDRNWM1.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-PojVb4l3.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-DAkhMTlk.js → ganttDiagram-EL5Y4UJY-ywnauJ7D.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-BQvemX2W.js → gitGraph-4MIJSDKK-bYI8lc-g.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-Dx7ya_2q.js → gitGraphDiagram-WWUBYQGX-erBDwxX8.js} +1 -1
- package/web/assets/{index-BXPRG7Pj.css → index-Ch1zstZB.css} +1 -1
- package/web/assets/{index-CFX8FhqJ.js → index-DzT1Tnf2.js} +2 -2
- package/web/assets/{info-A6RAGUB7-DaDps2sq.js → info-A6RAGUB7-BZbsPdZJ.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-DBqMvPr2.js → infoDiagram-27XIBGKW-qqTv-8cL.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-DzciF3O_.js → ishikawaDiagram-5VMMS53U-v-e3YR9X.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-D9WHgGe3.js → journeyDiagram-3NMN7TZE-D61ZGndw.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-Diokj8By.js → kanban-definition-UXKFOSKX-NeYWFrdx.js} +1 -1
- package/web/assets/{line-DmBeTXFn.js → line-BNxdvAy9.js} +1 -1
- package/web/assets/{mermaid-parser.core-H_O1CweI.js → mermaid-parser.core-BjJ-qoss.js} +3 -3
- package/web/assets/{mermaid.core-BAni15Ed.js → mermaid.core-DT4QwB_n.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-B_tXdzEa.js → mindmap-definition-YA3MSWOX-BxuVgGhr.js} +1 -1
- package/web/assets/{mount-Fdy9Xoxs.js → mount-CK5h6xJ4.js} +7 -7
- package/web/assets/{packet-AYTQ26CC-Dt4bPH7I.js → packet-AYTQ26CC-DIff_vXS.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-loAbaKUZ.js → pegDiagram-XKGWAZYB-oDNo32c7.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-DEmhJN7M.js → pie-WAS4IAKB-C0cdo_NU.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-DfQvD2QY.js → pieDiagram-E7YTZNPT-BbThivUY.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-Bhqo2rT-.js → quadrantDiagram-AXDQQJYC-CONfSoMt.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BmV5tXie.js → radar-RG4KPBEZ-teYstrbs.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-DoebyExe.js → railroad-74A4TZTK-B4VusJHl.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-DIF_YU01.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-BoHyJK5s.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-uG1Bcrgs.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-BhlfIpCW.js → railroadDiagram-O6MQD6OU-CKrYutQ_.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-vkxH8U-2.js → requirementDiagram-BXWQKSXE-COt2S2Y4.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-D2iFl0bc.js → sankeyDiagram-P5KCCOFB-D72oeNbf.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-Ll_m9w0-.js → sequenceDiagram-WJ2MYXX4-DFpVcNjb.js} +1 -1
- package/web/assets/{settingsRoute-C8p10bxH.js → settingsRoute-Clztmzl_.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-B4KWuRz2.js → stateDiagram-D77RDMKH-BKW8l6Ru.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-C1t9bUgR.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-CVXwQL4b.js → swimlanes-42K2YHIH-GRXgsapd.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-Dp35pflJ.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-C0XuGVkJ.js → timeline-definition-24CTP7MA-C8DELzYs.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-BS4edB5m.js → treeView-Q6P3EWNA-C1MDeOPi.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-BZ54DPA4.js → treemap-WGGIJYW6-CcwgMCe9.js} +1 -1
- package/web/assets/{useStore-DEfLrrMh.js → useStore-CaQamKgO.js} +7 -5
- package/web/assets/{vennDiagram-4TSXK5OY-CAKXCk3I.js → vennDiagram-4TSXK5OY-Co-Nw6M-.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-DM5IZXxI.js → wardley-WFR3VGLG-FbKVF2Pu.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4--WWmycX2.js → wardleyDiagram-VM6X3IG4-Bt7NGXJX.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-D-_SgAQy.js → xychartDiagram-S5SC5T6Z-BsgyT9WB.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/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/bin/bivy.mjs
CHANGED
|
@@ -1221,6 +1221,18 @@ const AGENT_SESSION_ID_FLAG = {
|
|
|
1221
1221
|
gemini: "--session-id",
|
|
1222
1222
|
};
|
|
1223
1223
|
|
|
1224
|
+
// Args a native `bivy run <agent>` adds so the session stays one Bivy can take
|
|
1225
|
+
// over as a chat. Codex 0.160's TUI otherwise runs its threads in a shared
|
|
1226
|
+
// background app-server that outlives the terminal and keeps the thread's
|
|
1227
|
+
// writer lock, so "Use chat" failed with "thread … already has an active writer".
|
|
1228
|
+
const AGENT_RUN_ARGS = {
|
|
1229
|
+
codex: ["--no-daemon"],
|
|
1230
|
+
};
|
|
1231
|
+
function applyAgentRunArgs(agentId, spec) {
|
|
1232
|
+
const add = (AGENT_RUN_ARGS[agentId] ?? []).filter((arg) => !(spec.args ?? []).includes(arg));
|
|
1233
|
+
if (add.length) spec.args = [...add, ...(spec.args ?? [])];
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1224
1236
|
// Args that mean the caller already chose a session (pin or resume), so we must
|
|
1225
1237
|
// not inject our own --session-id over the top.
|
|
1226
1238
|
const SESSION_ID_CONFLICTS = ["--session-id", "--resume", "-r", "-c", "--continue"];
|
|
@@ -2057,6 +2069,7 @@ async function cmdRun(args = []) {
|
|
|
2057
2069
|
// didn't already choose one), so the on-disk session is a known, deterministic
|
|
2058
2070
|
// resume target — the anchor for later "continue as chat" adoption.
|
|
2059
2071
|
const pinnedSessionId = bivySession ? undefined : pinRunSessionId(agentId, resolved.spec);
|
|
2072
|
+
applyAgentRunArgs(agentId, resolved.spec);
|
|
2060
2073
|
if (bivySession) resolved.spec.sessionId = bivySession;
|
|
2061
2074
|
if (pinnedSessionId) {
|
|
2062
2075
|
console.log(c.dim(`session id ${pinnedSessionId} — resume in a terminal with '${agentId} --resume ${pinnedSessionId}'`));
|
|
@@ -3111,7 +3124,8 @@ async function cmdApp(args = []) {
|
|
|
3111
3124
|
bivy app list [--session <id>]
|
|
3112
3125
|
bivy app remove <app-id> [--session <id>]
|
|
3113
3126
|
bivy app shot [app-id] [--widths 390,1280] [--themes light,dark] [--path /page] [--session <id>]
|
|
3114
|
-
bivy app present [view] [--path /page] [--note "…"] [--session <id>]
|
|
3127
|
+
bivy app present [view] [--path /page] [--note "…"] [--try a,b] [--session <id>]
|
|
3128
|
+
bivy app scenarios [view] [--session <id>]
|
|
3115
3129
|
bivy app share [app-id] [--view <view>] [--for 1h|1d|7d] [--view-only] [--session <id>]
|
|
3116
3130
|
bivy app notes [app-id] [--view <view>] [--since <time>] [--session <id>]
|
|
3117
3131
|
bivy app run [--name <name>] [--restart-on-change] [--session <id>] -- <command> [args…]
|
|
@@ -3157,6 +3171,25 @@ chooses an item — more reliable than clicking, and prints a screenshot after.
|
|
|
3157
3171
|
the chat with the app at phone width (and before/after), and it opens the live
|
|
3158
3172
|
preview. Use it when you finish a visible change, or when the user asks to see
|
|
3159
3173
|
it. [view] is an app or view name or ID (default: the one opened last).
|
|
3174
|
+
--try lists scenarios (by file name) the card offers: each opens the live
|
|
3175
|
+
preview in that state.
|
|
3176
|
+
"scenarios" lists a view's scenarios and any file that can't be opened, with
|
|
3177
|
+
what's wrong. A scenario is a JSON file in .bivy/scenarios/<id>.json: a named
|
|
3178
|
+
starting point the user can open, use, and reset from the preview:
|
|
3179
|
+
{"name":"Payment API down","open":"/checkout",
|
|
3180
|
+
"steps":[{"click":"text=Pay"}],
|
|
3181
|
+
"network":[{"match":"POST /api/payments*","status":503,"json":{"error":"down"}}]}
|
|
3182
|
+
Optional: "description", "view" (app or view name), "from" (another scenario
|
|
3183
|
+
to start from), "fresh": true (start as a new visitor). Steps: {"click":sel},
|
|
3184
|
+
{"fill":sel,"with":"text"}, {"press":"Enter"}, {"wait":sel or ms}; sel is a CSS
|
|
3185
|
+
selector or text=Visible text. Network rules answer matching requests for that
|
|
3186
|
+
viewer only: "status", "json" or "body", "delayMs", or "offline": true.
|
|
3187
|
+
Desktop apps restart in a scenario: "args" and "env" are added to the app's
|
|
3188
|
+
command, "api": {"env":"API_URL","target":"http://127.0.0.1:4000"} points the
|
|
3189
|
+
app at a proxy that applies "network", and steps are {"click":[x,y]},
|
|
3190
|
+
{"type":"text"}, {"press":"cmd+s"}, {"menu":"File > Open"}, {"wait":ms}. When
|
|
3191
|
+
you finish a visible change, add scenarios for the states it affects (errors,
|
|
3192
|
+
empty, slow) and pass them to present --try.
|
|
3160
3193
|
"share" mints a reusable link to one web view and prints its URL and expiry
|
|
3161
3194
|
(--for: 1h, 1d or 7d, default 1d; sooner if the user stops sharing in Apps, the
|
|
3162
3195
|
app is removed or the machine restarts). [app-id] is an app ID or name, --view
|
|
@@ -3174,7 +3207,7 @@ Web previews require operator setup; see docs/apps.md.`);
|
|
|
3174
3207
|
}
|
|
3175
3208
|
if (INPUT_ACTIONS[action]) return appInput(action, rest);
|
|
3176
3209
|
if (action === "menu") return appMenu(rest, { json });
|
|
3177
|
-
if (!["publish", "list", "remove", "shot", "run", "present", "share", "notes"].includes(action)) throw new Error("Unknown app command. Run bivy app --help.");
|
|
3210
|
+
if (!["publish", "list", "remove", "shot", "run", "present", "scenarios", "share", "notes"].includes(action)) throw new Error("Unknown app command. Run bivy app --help.");
|
|
3178
3211
|
// `run -- <command> [args…]`: publish a one-window desktop app without a manifest file.
|
|
3179
3212
|
let runManifest;
|
|
3180
3213
|
if (action === "run") {
|
|
@@ -3220,7 +3253,7 @@ Web previews require operator setup; see docs/apps.md.`);
|
|
|
3220
3253
|
}
|
|
3221
3254
|
}
|
|
3222
3255
|
if (action === "shot" || action === "present") {
|
|
3223
|
-
for (const flag of action === "shot" ? ["--widths", "--themes", "--path"] : ["--path", "--note"]) {
|
|
3256
|
+
for (const flag of action === "shot" ? ["--widths", "--themes", "--path"] : ["--path", "--note", "--try"]) {
|
|
3224
3257
|
const i = rest.indexOf(flag);
|
|
3225
3258
|
if (i < 0) continue;
|
|
3226
3259
|
const value = rest[i + 1];
|
|
@@ -3228,6 +3261,7 @@ Web previews require operator setup; see docs/apps.md.`);
|
|
|
3228
3261
|
if (flag === "--widths") shot.widths = value.split(",").map((w) => Number(w.trim()));
|
|
3229
3262
|
else if (flag === "--themes") shot.themes = value.split(",").map((t) => t.trim());
|
|
3230
3263
|
else if (flag === "--note") shot.note = value;
|
|
3264
|
+
else if (flag === "--try") shot.try = value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
3231
3265
|
else shot.path = value;
|
|
3232
3266
|
rest.splice(i, 2);
|
|
3233
3267
|
}
|
|
@@ -3237,10 +3271,10 @@ Web previews require operator setup; see docs/apps.md.`);
|
|
|
3237
3271
|
const sessionId = resolveAttachSessionId({ sessionFlag: sessionIndex >= 0 ? rest[sessionIndex + 1] : undefined, env: process.env });
|
|
3238
3272
|
if (!sessionId) throw new Error("Set --session <id> or run inside a Bivy agent session.");
|
|
3239
3273
|
const positional = rest.filter((_, i) => i !== sessionIndex && (sessionIndex < 0 || i !== sessionIndex + 1));
|
|
3240
|
-
if (positional.some((a) => a.startsWith("-")) || (action === "shot" || action === "present" || action === "share" || action === "notes" ? positional.length > 1 : positional.length !== (action === "list" || action === "run" ? 0 : 1))) throw new Error("Invalid arguments. Run bivy app --help.");
|
|
3274
|
+
if (positional.some((a) => a.startsWith("-")) || (action === "shot" || action === "present" || action === "scenarios" || action === "share" || action === "notes" ? positional.length > 1 : positional.length !== (action === "list" || action === "run" ? 0 : 1))) throw new Error("Invalid arguments. Run bivy app --help.");
|
|
3241
3275
|
const body = { sessionId, ...shot };
|
|
3242
3276
|
if (action === "shot" && positional[0]) body.appId = positional[0];
|
|
3243
|
-
if (action === "present" && positional[0]) body.target = positional[0];
|
|
3277
|
+
if ((action === "present" || action === "scenarios") && positional[0]) body.target = positional[0];
|
|
3244
3278
|
if ((action === "share" || action === "notes") && positional[0]) body.appId = positional[0];
|
|
3245
3279
|
if (action === "publish") {
|
|
3246
3280
|
const file = path.resolve(positional[0]);
|
|
@@ -70,6 +70,10 @@ const itemOutputs = new Map();
|
|
|
70
70
|
const agentMessageItems = new Set();
|
|
71
71
|
const reasoningItems = new Set();
|
|
72
72
|
const activeObservedItems = new Set();
|
|
73
|
+
// Child thread id -> the card that represents that sub-agent in the parent
|
|
74
|
+
// transcript, so the child's own tool calls nest under it instead of reading
|
|
75
|
+
// as the parent's work.
|
|
76
|
+
const childThreadCards = new Map();
|
|
73
77
|
let pendingTurnCompletion = false;
|
|
74
78
|
let turnCompletionTimer = null;
|
|
75
79
|
|
|
@@ -95,8 +99,31 @@ function paramsItemId(params, fallback) {
|
|
|
95
99
|
return String(params?.itemId || params?.callId || item?.id || fallback || "");
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
function rememberChildThreads(item) {
|
|
103
|
+
if (item.type === "subAgentActivity" && item.agentThreadId) {
|
|
104
|
+
childThreadCards.set(item.agentThreadId, `subagent-${item.agentThreadId}`);
|
|
105
|
+
} else if (item.type === "collabAgentToolCall" && item.id) {
|
|
106
|
+
for (const child of item.receiverThreadIds ?? []) {
|
|
107
|
+
if (!childThreadCards.has(child)) childThreadCards.set(child, String(item.id));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** True when a notification belongs to a delegated child thread, not ours. */
|
|
113
|
+
function fromChildThread(params) {
|
|
114
|
+
return Boolean(params?.threadId && threadId && params.threadId !== threadId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `{ parentToolCallId }` for an item a child thread ran, else `{}`. */
|
|
118
|
+
function childParent(params) {
|
|
119
|
+
if (!fromChildThread(params)) return {};
|
|
120
|
+
const card = childThreadCards.get(params.threadId) ?? `subagent-${params.threadId}`;
|
|
121
|
+
return { parentToolCallId: card };
|
|
122
|
+
}
|
|
123
|
+
|
|
98
124
|
function rememberItem(item) {
|
|
99
125
|
if (!item || typeof item !== "object") return;
|
|
126
|
+
rememberChildThreads(item);
|
|
100
127
|
const id = item.type === "subAgentActivity" && item.agentThreadId
|
|
101
128
|
? `subagent-${item.agentThreadId}`
|
|
102
129
|
: String(item.id || "");
|
|
@@ -146,18 +173,18 @@ function appendOutput(itemId, delta) {
|
|
|
146
173
|
return output;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
|
-
function emitToolUpdate(itemId, name, input) {
|
|
176
|
+
function emitToolUpdate(itemId, name, input, parent = {}) {
|
|
150
177
|
if (!itemId) return;
|
|
151
|
-
bivy({ type: "tool.update", toolCallId: itemId, name, input });
|
|
178
|
+
bivy({ type: "tool.update", toolCallId: itemId, name, input, ...parent });
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
// App-server item/started is observational: approval-capable shell/patch calls
|
|
155
182
|
// were already surfaced by requestApproval, while MCP, dynamic, and collaboration
|
|
156
183
|
// items may already be running. `tool.observe` gives all of them a live,
|
|
157
184
|
// persisted card without asking the user to approve work that has already begun.
|
|
158
|
-
function emitToolObserved(itemId, name, input) {
|
|
185
|
+
function emitToolObserved(itemId, name, input, parent = {}) {
|
|
159
186
|
if (!itemId) return;
|
|
160
|
-
bivy({ type: "tool.observe", toolCallId: itemId, name, input });
|
|
187
|
+
bivy({ type: "tool.observe", toolCallId: itemId, name, input, ...parent });
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
// `turn/plan/updated` (the update_plan tool) carries the whole plan with a
|
|
@@ -214,11 +241,12 @@ function handleCompletedItem(params) {
|
|
|
214
241
|
rememberItem(item);
|
|
215
242
|
const itemId = paramsItemId(params);
|
|
216
243
|
if (!item || typeof item !== "object" || !itemId) return;
|
|
244
|
+
const parent = childParent(params);
|
|
217
245
|
switch (item.type) {
|
|
218
246
|
case "agentMessage":
|
|
219
247
|
// Child-thread prose belongs to the delegated agent, not the parent's
|
|
220
248
|
// answer stream. Its lifecycle is represented by the collaboration items.
|
|
221
|
-
if (params
|
|
249
|
+
if (fromChildThread(params)) return;
|
|
222
250
|
if (!agentMessageItems.has(itemId) && item.text) {
|
|
223
251
|
agentMessageItems.add(itemId);
|
|
224
252
|
bivy({ type: "message.delta", text: String(item.text) });
|
|
@@ -240,7 +268,7 @@ function handleCompletedItem(params) {
|
|
|
240
268
|
case "commandExecution": {
|
|
241
269
|
const output = text(item.aggregatedOutput ?? itemOutputs.get(itemId));
|
|
242
270
|
if (output) itemOutputs.set(itemId, output.slice(-4000));
|
|
243
|
-
emitToolUpdate(itemId, "shell", updateInput(itemId, { output }));
|
|
271
|
+
emitToolUpdate(itemId, "shell", updateInput(itemId, { output }), parent);
|
|
244
272
|
const exitCode = typeof item.exitCode === "number" ? item.exitCode : undefined;
|
|
245
273
|
bivy({
|
|
246
274
|
type: "tool.result",
|
|
@@ -248,36 +276,37 @@ function handleCompletedItem(params) {
|
|
|
248
276
|
name: "shell",
|
|
249
277
|
result: { content: output || text(item.status || "completed"), ...(exitCode === undefined ? {} : { exitCode }) },
|
|
250
278
|
isError: (exitCode !== undefined && exitCode !== 0) || item.status === "failed",
|
|
279
|
+
...parent,
|
|
251
280
|
});
|
|
252
281
|
return;
|
|
253
282
|
}
|
|
254
283
|
case "fileChange": {
|
|
255
284
|
const status = text(item.status || "completed");
|
|
256
285
|
itemInputs.set(itemId, updateInput(itemId, { changes: item.changes ?? [], status }));
|
|
257
|
-
emitToolUpdate(itemId, "apply_patch", updateInput(itemId));
|
|
258
|
-
bivy({ type: "tool.result", toolCallId: itemId, name: "apply_patch", result: status, isError: status === "failed" });
|
|
286
|
+
emitToolUpdate(itemId, "apply_patch", updateInput(itemId), parent);
|
|
287
|
+
bivy({ type: "tool.result", toolCallId: itemId, name: "apply_patch", result: status, isError: status === "failed", ...parent });
|
|
259
288
|
return;
|
|
260
289
|
}
|
|
261
290
|
case "mcpToolCall": {
|
|
262
291
|
const name = text(item.tool || "mcp");
|
|
263
292
|
const result = text(item.result ?? item.error ?? "");
|
|
264
|
-
bivy({ type: "tool.result", toolCallId: itemId, name, result, isError: Boolean(item.error) || item.status === "failed" });
|
|
293
|
+
bivy({ type: "tool.result", toolCallId: itemId, name, result, isError: Boolean(item.error) || item.status === "failed", ...parent });
|
|
265
294
|
return;
|
|
266
295
|
}
|
|
267
296
|
case "dynamicToolCall": {
|
|
268
297
|
const name = text(item.tool || "tool");
|
|
269
298
|
const result = text(item.contentItems ?? item.result ?? item.status ?? "completed");
|
|
270
|
-
bivy({ type: "tool.result", toolCallId: itemId, name, result, isError: item.success === false || item.status === "failed" });
|
|
299
|
+
bivy({ type: "tool.result", toolCallId: itemId, name, result, isError: item.success === false || item.status === "failed", ...parent });
|
|
271
300
|
return;
|
|
272
301
|
}
|
|
273
302
|
case "collabAgentToolCall": {
|
|
274
303
|
const result = text(item.status || "completed");
|
|
275
|
-
bivy({ type: "tool.result", toolCallId: itemId, name: "spawn_agent", result, isError: item.status === "failed" });
|
|
304
|
+
bivy({ type: "tool.result", toolCallId: itemId, name: "spawn_agent", result, isError: item.status === "failed", ...parent });
|
|
276
305
|
return;
|
|
277
306
|
}
|
|
278
307
|
case "subAgentActivity": {
|
|
279
308
|
const result = text(item.kind || "completed");
|
|
280
|
-
bivy({ type: "tool.result", toolCallId: itemId, name: "subagent_activity", result, isError: item.kind === "interrupted" });
|
|
309
|
+
bivy({ type: "tool.result", toolCallId: itemId, name: "subagent_activity", result, isError: item.kind === "interrupted", ...parent });
|
|
281
310
|
return;
|
|
282
311
|
}
|
|
283
312
|
default:
|
|
@@ -356,7 +385,8 @@ function onNotification(m) {
|
|
|
356
385
|
// both token streams together produces character-level corruption whenever a
|
|
357
386
|
// delegated agent answers concurrently. Child lifecycle remains visible via
|
|
358
387
|
// collabAgentToolCall/subAgentActivity; only prose/reasoning is scoped here.
|
|
359
|
-
const fromCurrentThread = !params
|
|
388
|
+
const fromCurrentThread = !fromChildThread(params);
|
|
389
|
+
const parent = childParent(params);
|
|
360
390
|
switch (method) {
|
|
361
391
|
case "item/agentMessage/delta": {
|
|
362
392
|
const itemId = paramsItemId(params);
|
|
@@ -384,19 +414,19 @@ function onNotification(m) {
|
|
|
384
414
|
case "item/commandExecution/outputDelta": {
|
|
385
415
|
const itemId = paramsItemId(params);
|
|
386
416
|
const output = appendOutput(itemId, params.delta);
|
|
387
|
-
emitToolUpdate(itemId, "shell", updateInput(itemId, { output }));
|
|
417
|
+
emitToolUpdate(itemId, "shell", updateInput(itemId, { output }), parent);
|
|
388
418
|
return;
|
|
389
419
|
}
|
|
390
420
|
case "item/fileChange/outputDelta": {
|
|
391
421
|
const itemId = paramsItemId(params);
|
|
392
422
|
const output = appendOutput(itemId, params.delta);
|
|
393
|
-
emitToolUpdate(itemId, "apply_patch", updateInput(itemId, { output }));
|
|
423
|
+
emitToolUpdate(itemId, "apply_patch", updateInput(itemId, { output }), parent);
|
|
394
424
|
return;
|
|
395
425
|
}
|
|
396
426
|
case "item/fileChange/patchUpdated": {
|
|
397
427
|
const itemId = paramsItemId(params);
|
|
398
428
|
itemInputs.set(itemId, updateInput(itemId, { changes: params.changes ?? [] }));
|
|
399
|
-
emitToolUpdate(itemId, "apply_patch", updateInput(itemId));
|
|
429
|
+
emitToolUpdate(itemId, "apply_patch", updateInput(itemId), parent);
|
|
400
430
|
return;
|
|
401
431
|
}
|
|
402
432
|
case "item/started": {
|
|
@@ -404,12 +434,12 @@ function onNotification(m) {
|
|
|
404
434
|
rememberItem(item);
|
|
405
435
|
const itemId = paramsItemId(params);
|
|
406
436
|
if (["commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "collabAgentToolCall", "subAgentActivity"].includes(item?.type) && itemId) activeObservedItems.add(itemId);
|
|
407
|
-
if (item?.type === "commandExecution") emitToolObserved(itemId, "shell", updateInput(itemId));
|
|
408
|
-
if (item?.type === "fileChange") emitToolObserved(itemId, "apply_patch", updateInput(itemId));
|
|
409
|
-
if (item?.type === "mcpToolCall") emitToolObserved(itemId, text(item.tool || "mcp"), updateInput(itemId));
|
|
410
|
-
if (item?.type === "dynamicToolCall") emitToolObserved(itemId, text(item.tool || "tool"), updateInput(itemId));
|
|
411
|
-
if (item?.type === "collabAgentToolCall") emitToolObserved(itemId, "spawn_agent", updateInput(itemId));
|
|
412
|
-
if (item?.type === "subAgentActivity") emitToolObserved(itemId, "subagent_activity", updateInput(itemId));
|
|
437
|
+
if (item?.type === "commandExecution") emitToolObserved(itemId, "shell", updateInput(itemId), parent);
|
|
438
|
+
if (item?.type === "fileChange") emitToolObserved(itemId, "apply_patch", updateInput(itemId), parent);
|
|
439
|
+
if (item?.type === "mcpToolCall") emitToolObserved(itemId, text(item.tool || "mcp"), updateInput(itemId), parent);
|
|
440
|
+
if (item?.type === "dynamicToolCall") emitToolObserved(itemId, text(item.tool || "tool"), updateInput(itemId), parent);
|
|
441
|
+
if (item?.type === "collabAgentToolCall") emitToolObserved(itemId, "spawn_agent", updateInput(itemId), parent);
|
|
442
|
+
if (item?.type === "subAgentActivity") emitToolObserved(itemId, "subagent_activity", updateInput(itemId), parent);
|
|
413
443
|
return;
|
|
414
444
|
}
|
|
415
445
|
case "item/completed": {
|
|
@@ -419,15 +449,20 @@ function onNotification(m) {
|
|
|
419
449
|
return;
|
|
420
450
|
}
|
|
421
451
|
case "thread/tokenUsage/updated":
|
|
422
|
-
if (params?.tokenUsage) bivy({ type: "usage", usage: params.tokenUsage.last ?? params.tokenUsage.total ?? params.tokenUsage });
|
|
452
|
+
if (fromCurrentThread && params?.tokenUsage) bivy({ type: "usage", usage: params.tokenUsage.last ?? params.tokenUsage.total ?? params.tokenUsage });
|
|
423
453
|
return;
|
|
424
454
|
case "rawResponse/completed":
|
|
425
455
|
if (params?.usage) bivy({ type: "usage", usage: params.usage });
|
|
426
456
|
return;
|
|
457
|
+
// A delegated child runs its own turns on the same connection. Only the
|
|
458
|
+
// parent thread's turn lifecycle opens and seals the Bivy turn: sealing on
|
|
459
|
+
// a child's turn/completed ended the parent turn early and the parent's
|
|
460
|
+
// remaining tools and answer landed after it was closed.
|
|
427
461
|
case "turn/started":
|
|
428
|
-
bivy({ type: "session.status", status: "working" });
|
|
462
|
+
if (fromCurrentThread) bivy({ type: "session.status", status: "working" });
|
|
429
463
|
return;
|
|
430
464
|
case "turn/completed": {
|
|
465
|
+
if (!fromCurrentThread) return;
|
|
431
466
|
// The completion event is authoritative about a terminal failure: a turn
|
|
432
467
|
// that hit a usage limit (or any provider error) arrives here as
|
|
433
468
|
// `status: "failed"` carrying `turn.error`. Capture it directly instead of
|
|
@@ -448,6 +483,7 @@ function onNotification(m) {
|
|
|
448
483
|
return;
|
|
449
484
|
}
|
|
450
485
|
case "turn/failed": {
|
|
486
|
+
if (!fromCurrentThread) return;
|
|
451
487
|
// A failed turn is TERMINAL in the app-server — no `turn/completed` follows
|
|
452
488
|
// it — so end the turn here rather than merely stashing the error for a
|
|
453
489
|
// completion event that never arrives. Stashing (the old behavior) left the
|
|
@@ -470,7 +506,9 @@ function onNotification(m) {
|
|
|
470
506
|
// A non-turn-scoped error notification (e.g. transient reconnect noise, per
|
|
471
507
|
// the codex exec parser's read of this event). Stash it so a following
|
|
472
508
|
// turn/completed surfaces it; a genuinely turn-ending failure arrives as
|
|
473
|
-
// turn/failed above, which never leaves the turn open.
|
|
509
|
+
// turn/failed above, which never leaves the turn open. A child thread's
|
|
510
|
+
// error is reported on its sub-agent card, not as the parent's failure.
|
|
511
|
+
if (!fromCurrentThread) return;
|
|
474
512
|
sawError = params?.error?.message || params?.message || "Codex turn failed";
|
|
475
513
|
return;
|
|
476
514
|
default:
|
|
@@ -485,6 +523,19 @@ async function ensureInitialized() {
|
|
|
485
523
|
initialized = true;
|
|
486
524
|
}
|
|
487
525
|
|
|
526
|
+
// Codex keeps one writer per thread. A thread still open in Codex elsewhere
|
|
527
|
+
// (its TUI, or the shared background app-server Codex 0.160 runs for it) can't
|
|
528
|
+
// be resumed here; say what to do instead of passing the bare error through.
|
|
529
|
+
async function resumeThread(params) {
|
|
530
|
+
try {
|
|
531
|
+
return await asRequest("thread/resume", params);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
534
|
+
if (!/active writer/i.test(message)) throw error;
|
|
535
|
+
throw new Error(`${message}. The thread is still open in Codex outside Bivy: close it there, or stop Codex's background server with \`codex app-server daemon stop\`, then try again.`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
488
539
|
function threadParams(msg, extra = {}) {
|
|
489
540
|
return {
|
|
490
541
|
cwd: msg.workspace || process.cwd(),
|
|
@@ -511,7 +562,7 @@ async function onBivyCommand(msg) {
|
|
|
511
562
|
// field on session.create. New hosts use the explicit session.resume
|
|
512
563
|
// primitive below.
|
|
513
564
|
if (typeof msg.resume === "string" && msg.resume) {
|
|
514
|
-
const resumed = await
|
|
565
|
+
const resumed = await resumeThread(threadParams(msg, { threadId: msg.resume }));
|
|
515
566
|
threadId = resumed?.thread?.id ?? resumed?.threadId ?? msg.resume;
|
|
516
567
|
bivy({ replyTo: id, ok: true, runtimeSessionRef: threadId });
|
|
517
568
|
return;
|
|
@@ -525,7 +576,7 @@ async function onBivyCommand(msg) {
|
|
|
525
576
|
await ensureInitialized();
|
|
526
577
|
const resumeRef = String(msg.runtimeSessionRef || msg.resumeRef || msg.sessionId || "");
|
|
527
578
|
if (!resumeRef) { bivy({ replyTo: id, ok: false, error: "missing resume ref" }); return; }
|
|
528
|
-
const resumed = await
|
|
579
|
+
const resumed = await resumeThread(threadParams(msg, { threadId: resumeRef }));
|
|
529
580
|
threadId = resumed?.thread?.id ?? resumed?.threadId ?? resumeRef;
|
|
530
581
|
bivy({ replyTo: id, ok: true, runtimeSessionRef: threadId });
|
|
531
582
|
return;
|
|
@@ -36,7 +36,8 @@ export const BIVY_AGENT_NOTE = [
|
|
|
36
36
|
" does not render a local path. A REMOTE `https://` image in markdown does render.",
|
|
37
37
|
"- Something with a UI: `bivy app publish <manifest.json>` (a web server's port, a static build, a terminal) or " +
|
|
38
38
|
"`bivy app run -- <command>` (a desktop app) gives the user a live preview; `bivy app shot` screenshots it so you " +
|
|
39
|
-
"can check your work; `bivy app present` tells the user a visible change is ready to look at
|
|
39
|
+
"can check your work; `bivy app present` tells the user a visible change is ready to look at, and " +
|
|
40
|
+
"`.bivy/scenarios/*.json` files let them open it in the states that matter (errors, empty, slow) with `--try`. " +
|
|
40
41
|
"`bivy app --help` has the details.",
|
|
41
42
|
"- Proposing tasks the user could hand you (next steps, ideas, options): post each with " +
|
|
42
43
|
'`bivy suggest "<complete instruction>" [--title "short label"] [--run here|subagents|new]` instead of only ' +
|
|
@@ -548,7 +548,8 @@ function loadClaudeTranscript(sessionId) {
|
|
|
548
548
|
}
|
|
549
549
|
if (role === "user" && !(Array.isArray(content) && content.some((block) => block?.type === "tool_result")))
|
|
550
550
|
lastPrompt = content;
|
|
551
|
-
|
|
551
|
+
const model = role === "assistant" && typeof entry?.message?.model === "string" && entry.message.model !== "<synthetic>" ? entry.message.model : undefined;
|
|
552
|
+
messages.push({ role, content, timestamp: new Date(entry?.timestamp ?? entry?.createdAt ?? Date.now()).getTime(), ...(model ? { model } : {}) });
|
|
552
553
|
lastErrorIdx = entry?.isApiErrorMessage === true ? messages.length - 1 : -1;
|
|
553
554
|
}
|
|
554
555
|
}
|
package/dist/agents/profiles.js
CHANGED
|
@@ -32,7 +32,7 @@ export const AGENT_PROFILES = {
|
|
|
32
32
|
},
|
|
33
33
|
opencode: {
|
|
34
34
|
displayName: "OpenCode",
|
|
35
|
-
behaviors: { preflight: "opencode", slashCommands: "opencode", sessionStore: "opencode" },
|
|
35
|
+
behaviors: { preflight: "opencode", prepare: "opencode-auth", slashCommands: "opencode", sessionStore: "opencode" },
|
|
36
36
|
command: "opencode",
|
|
37
37
|
packageName: "opencode-ai",
|
|
38
38
|
// `opencode run "<prompt>"` runs one non-interactive turn and streams the
|
|
@@ -44,6 +44,9 @@ export const AGENT_PROFILES = {
|
|
|
44
44
|
// Claude Code, and Codex clear. See `acp` below for the version fallback.
|
|
45
45
|
supportTier: "supported",
|
|
46
46
|
testedVersion: "1.18.33",
|
|
47
|
+
// Mixed: OpenCode keeps its own logins, and Bivy adds what it holds — API
|
|
48
|
+
// keys as env vars, a connected ChatGPT plan via the opencode-auth prepare.
|
|
49
|
+
authOwner: "mixed",
|
|
47
50
|
blurb: "The most widely used open-source coding harness (OpenCode CLI).",
|
|
48
51
|
// `opencode run -s <id> "<prompt>"` continues a prior session by its own id
|
|
49
52
|
// (`-s, --session session id to continue`, per `opencode run --help`).
|
|
@@ -403,12 +406,12 @@ export const AGENT_PROFILES = {
|
|
|
403
406
|
model: {
|
|
404
407
|
flag: "-m",
|
|
405
408
|
models: [
|
|
406
|
-
// Official Grok CLI (1.
|
|
407
|
-
// model (
|
|
408
|
-
// origin cli-chat-proxy.grok.com/v1/models); the older grok-4.5 /
|
|
409
|
-
// grok-4-latest / grok-code-fast-1 ids now return "unknown model id".
|
|
409
|
+
// Official Grok CLI (1.0.46) lists grok-4.7 as the default subscription
|
|
410
|
+
// model (`grok models`), with its faster build variant and grok-4.6.
|
|
410
411
|
// Keep the list honest; operators can override with BIVY_GROK_MODELS if
|
|
411
|
-
// their install exposes more.
|
|
412
|
+
// their install exposes more. The governed ACP path reads the live list.
|
|
413
|
+
{ id: "grok-4.7", name: "Grok 4.7", provider: "xai" },
|
|
414
|
+
{ id: "grok-4.7-build-fast", name: "Grok 4.7 Build Fast", provider: "xai" },
|
|
412
415
|
{ id: "grok-4.6", name: "Grok 4.6", provider: "xai" },
|
|
413
416
|
],
|
|
414
417
|
},
|
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,9 @@ 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 SCENARIOS_PATH = "/__bivy/scenarios";
|
|
27
|
+
const SCENARIO_PATH = "/__bivy/scenario";
|
|
28
|
+
const scenarioKind = (entry) => entry.target.kind === "display" ? "desktop" : "web";
|
|
24
29
|
/** Larger HTML documents pass through without the inspector. */
|
|
25
30
|
const MAX_INJECT_BYTES = 5 * 1024 * 1024;
|
|
26
31
|
/** Put the inspector first in <head>, so it sees errors from the app's own scripts. */
|
|
@@ -205,9 +210,6 @@ export class AppGateway {
|
|
|
205
210
|
["/__bivy/tokens.css", readFileSync(new URL("./tokens.css", import.meta.url))],
|
|
206
211
|
["/__bivy/styles.css", readFileSync(new URL("./styles.css", import.meta.url))],
|
|
207
212
|
]);
|
|
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
213
|
sessions = new Map();
|
|
212
214
|
sockets = new Map();
|
|
213
215
|
/** Connections opened by people with a copied link, so Stop sharing can end
|
|
@@ -486,6 +488,11 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
486
488
|
res.end("Preview access expired. Open a new preview link from Bivy.");
|
|
487
489
|
return;
|
|
488
490
|
}
|
|
491
|
+
// The shell (its own origin) chooses this browser's scenario; nothing else may.
|
|
492
|
+
if (req.url === SCENARIO_PATH && req.method === "POST") {
|
|
493
|
+
await this.enterScenario(req, res, entry, session);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
489
496
|
// Block cross-site mutations even if a client sends the preview cookie.
|
|
490
497
|
if (!["GET", "HEAD"].includes(req.method ?? "") && req.headers.origin !== this.origin(id)) {
|
|
491
498
|
res.writeHead(403);
|
|
@@ -511,6 +518,10 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
511
518
|
this.compare(req, res, entry);
|
|
512
519
|
return;
|
|
513
520
|
}
|
|
521
|
+
if (req.url === SCENARIOS_PATH && req.method === "GET") {
|
|
522
|
+
this.scenarios(res, entry, session);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
514
525
|
if (req.url === INSPECTOR_PATH && req.method === "GET") {
|
|
515
526
|
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" });
|
|
516
527
|
res.end(inspectorScript(this.shellOrigin(id), session.reviewer === true && !session.viewOnly, session.embedded === true));
|
|
@@ -530,6 +541,9 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
530
541
|
await this.display(req, res, entry);
|
|
531
542
|
return;
|
|
532
543
|
}
|
|
544
|
+
const rule = session.scenario && matchRule(session.scenario.network, req.method, req.url);
|
|
545
|
+
if (rule && await simulate(req, res, rule, responseHeaders({}, this.ancestors(id))))
|
|
546
|
+
return;
|
|
533
547
|
const inspect = isPageLoad(req);
|
|
534
548
|
// Remember the framed page so a turn reload lands where the user was.
|
|
535
549
|
if (req.method === "GET" && req.headers["sec-fetch-dest"] === "iframe" && req.url.length <= 2048)
|
|
@@ -871,6 +885,80 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
|
|
|
871
885
|
res.writeHead(200, { "content-type": "application/json" });
|
|
872
886
|
res.end(JSON.stringify({ screenshot: Boolean(shot) }));
|
|
873
887
|
}
|
|
888
|
+
/** The view's scenarios for the shell's switcher, and the one it's in: this
|
|
889
|
+
* browser's for a web page, the app's for a desktop app. */
|
|
890
|
+
scenarios(res, entry, session) {
|
|
891
|
+
const { scenarios, problems } = loadScenarios(entry.workspace);
|
|
892
|
+
const kind = scenarioKind(entry);
|
|
893
|
+
const own = scenariosFor(scenarios, entry.app.name, entry.view.name, kind);
|
|
894
|
+
const active = kind === "desktop" ? entry.scenario : session.scenario;
|
|
895
|
+
res.writeHead(200, { "access-control-allow-origin": this.shellOrigin(entry.view.id), "access-control-allow-credentials": "true", vary: "Origin", "content-type": "application/json" });
|
|
896
|
+
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 }));
|
|
897
|
+
}
|
|
898
|
+
/** Puts this browser in a scenario (or back in none, for an empty body) and
|
|
899
|
+
* returns the plan the shell walks through: pages to open and steps to take.
|
|
900
|
+
* A "fresh" scenario also expires the app's cookies this browser sent, so it
|
|
901
|
+
* starts as a new visitor. Only the shell's origin may ask. */
|
|
902
|
+
async enterScenario(req, res, entry, session) {
|
|
903
|
+
const shell = this.shellOrigin(entry.view.id);
|
|
904
|
+
const cors = { "access-control-allow-origin": shell, "access-control-allow-credentials": "true", vary: "Origin" };
|
|
905
|
+
if (req.headers.origin !== shell) {
|
|
906
|
+
res.writeHead(403);
|
|
907
|
+
res.end();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
let body = "";
|
|
911
|
+
for await (const chunk of req) {
|
|
912
|
+
body += chunk.toString();
|
|
913
|
+
if (body.length > 64) {
|
|
914
|
+
res.writeHead(413, cors);
|
|
915
|
+
res.end();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const id = body.trim();
|
|
920
|
+
const json = (status, value) => { res.writeHead(status, { ...cors, "content-type": "application/json" }); res.end(JSON.stringify(value)); };
|
|
921
|
+
// A desktop app is one program for everyone watching it: its scenario
|
|
922
|
+
// restarts it, and the steps run in its window before this answers.
|
|
923
|
+
if (scenarioKind(entry) === "desktop") {
|
|
924
|
+
if (!this.registry.desktopScenario) {
|
|
925
|
+
json(400, { error: "Desktop scenarios aren't available on this machine." });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
json(200, await this.registry.desktopScenario(entry.view.id, id));
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
json(400, { error: error.message });
|
|
933
|
+
}
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (!id) {
|
|
937
|
+
session.scenario = undefined;
|
|
938
|
+
res.writeHead(200, { ...cors, "content-type": "application/json" });
|
|
939
|
+
res.end(JSON.stringify({ plan: null }));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
let plan;
|
|
943
|
+
try {
|
|
944
|
+
const own = scenariosFor(loadScenarios(entry.workspace).scenarios, entry.app.name, entry.view.name, "web");
|
|
945
|
+
plan = planScenario(own, id, "web");
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
res.writeHead(400, { ...cors, "content-type": "application/json" });
|
|
949
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
session.scenario = plan;
|
|
953
|
+
const headers = { ...cors, "content-type": "application/json" };
|
|
954
|
+
if (plan.fresh) {
|
|
955
|
+
const names = (req.headers.cookie ?? "").split(";").map((part) => part.split("=", 1)[0].trim()).filter((name) => name && name !== COOKIE && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name));
|
|
956
|
+
const expire = (name) => `${name}=; Path=/; Max-Age=0`;
|
|
957
|
+
headers["set-cookie"] = names.map((name) => session.embedded ? partitionCookie(expire(name)) : expire(name));
|
|
958
|
+
}
|
|
959
|
+
res.writeHead(200, headers);
|
|
960
|
+
res.end(JSON.stringify({ plan }));
|
|
961
|
+
}
|
|
874
962
|
/** Compare shots for the shell: the list, or one PNG by index. */
|
|
875
963
|
compare(req, res, entry) {
|
|
876
964
|
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}));
|