@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
|
@@ -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); }
|
|
@@ -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,7 +240,11 @@ 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();
|
|
@@ -241,6 +279,8 @@ async function watch(){
|
|
|
241
279
|
const r=await fetch(metadata.origin+'/__bivy/revision?after='+latest,{credentials:'include',cache:'no-store'});
|
|
242
280
|
if(!r.ok)throw Error();
|
|
243
281
|
const d=await r.json();
|
|
282
|
+
// A turn may have written or changed scenario files.
|
|
283
|
+
if(latest!==null&&latest!==-1&&d.revision!==latest)void loadScenarios();
|
|
244
284
|
latest=d.revision;newPath=safePath(d.path);
|
|
245
285
|
// The first poll after a load describes what the frame already has.
|
|
246
286
|
if(shown===null)shown=latest;
|
|
@@ -254,6 +294,7 @@ function waiting(on){
|
|
|
254
294
|
if($('update').hidden===!on)return;
|
|
255
295
|
$('update').hidden=!on;
|
|
256
296
|
$('stamp').textContent=on?'New version ready':'';
|
|
297
|
+
renderScenarioPill();
|
|
257
298
|
}
|
|
258
299
|
/** Asks the page where it is, so the update can put it back. Without an
|
|
259
300
|
* inspector (a desktop app) there is nothing to ask. */
|
|
@@ -407,6 +448,153 @@ function finishNote(d){
|
|
|
407
448
|
status.hidden=false;status.textContent=$('stamp').textContent+'. The app’s owner will see it.';
|
|
408
449
|
$('more').focus();
|
|
409
450
|
}
|
|
451
|
+
// Scenarios: named starting points for the live app, from the project's
|
|
452
|
+
// .bivy/scenarios. Opening one tells the gateway (so its simulated responses
|
|
453
|
+
// apply to this browser only), opens its page and walks its steps through the
|
|
454
|
+
// page's inspector, one at a time; a step that loads a new page is picked up
|
|
455
|
+
// after on the next one. Reset does it all again; "Your data" leaves it.
|
|
456
|
+
let scenarios=[],activeScenario=null,runner=null,runSeq=0,loads=0,scenariosLoaded=false;
|
|
457
|
+
const scnBtn=$('scn'),scnReset=$('scn-reset');
|
|
458
|
+
async function loadScenarios(){
|
|
459
|
+
if(!metadata||metadata.controls===false)return;
|
|
460
|
+
try{
|
|
461
|
+
const r=await fetch(metadata.origin+'/__bivy/scenarios',{credentials:'include',cache:'no-store'});if(!r.ok)return;
|
|
462
|
+
const d=await r.json();
|
|
463
|
+
scenarios=(Array.isArray(d.scenarios)?d.scenarios:[]).slice(0,100);
|
|
464
|
+
if(!runner)activeScenario=d.active&&typeof d.active.id==='string'?d.active:null;
|
|
465
|
+
scenariosLoaded=true;
|
|
466
|
+
renderScenarioPill();
|
|
467
|
+
if(!$('scenarios').hidden)renderScenarioList();
|
|
468
|
+
if(pendingScenario){const id=pendingScenario;pendingScenario='';void enterScenario(id);}
|
|
469
|
+
}catch{}
|
|
470
|
+
}
|
|
471
|
+
function renderScenarioPill(){
|
|
472
|
+
if(!metadata)return;
|
|
473
|
+
const on=scenarios.length>0||Boolean(activeScenario);
|
|
474
|
+
scnBtn.hidden=!on;$('name').hidden=on;scnReset.hidden=!activeScenario||Boolean(runner);
|
|
475
|
+
if(!on)return;
|
|
476
|
+
const sim=activeScenario?.simulated;
|
|
477
|
+
$('scn-title').textContent=activeScenario?activeScenario.name:metadata.name;
|
|
478
|
+
$('scn-sub').textContent=runner?'Opening…':activeScenario?(sim?'Simulated: '+sim:metadata.name):$('stamp').textContent||(scenarios.length===1?'1 scenario':scenarios.length+' scenarios');
|
|
479
|
+
scnBtn.toggleAttribute('data-active',Boolean(activeScenario));
|
|
480
|
+
scnBtn.setAttribute('aria-label',(activeScenario?'Scenario: '+activeScenario.name+(sim?', simulated':'')+'. ':'')+'Choose a scenario');
|
|
481
|
+
scnReset.setAttribute('aria-label','Reset '+(activeScenario?.name||'scenario'));
|
|
482
|
+
}
|
|
483
|
+
function scenarioRow(s){
|
|
484
|
+
const b=document.createElement('button');b.type='button';b.className='scn-row';
|
|
485
|
+
const current=(activeScenario?.id||'')===s.id;
|
|
486
|
+
if(current)b.setAttribute('aria-current','true');
|
|
487
|
+
const text=document.createElement('span');text.className='scn-row-text';
|
|
488
|
+
const name=document.createElement('span');name.className='scn-row-name';name.textContent=s.name;
|
|
489
|
+
text.append(name);
|
|
490
|
+
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(' · ')];
|
|
491
|
+
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);}
|
|
492
|
+
b.append(text);
|
|
493
|
+
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);}
|
|
494
|
+
if(s.error&&(!metadata.returnTo||metadata.reviewer))b.disabled=true;
|
|
495
|
+
b.onclick=()=>{if(s.error)return toChat(fixRequest(s.name,s.id,s.error));void enterScenario(s.id);};
|
|
496
|
+
return b;
|
|
497
|
+
}
|
|
498
|
+
function renderScenarioList(){
|
|
499
|
+
const body=$('scenarios-body'),fresh=scenarios.filter(s=>s.isNew&&!s.error);
|
|
500
|
+
const group=(label,rows)=>{
|
|
501
|
+
const h=document.createElement('h3');h.className='scn-group';h.textContent=label;
|
|
502
|
+
const ul=document.createElement('ul');ul.className='scn-list';
|
|
503
|
+
for(const row of rows){const li=document.createElement('li');li.append(scenarioRow(row));ul.append(li);}
|
|
504
|
+
return [h,ul];
|
|
505
|
+
};
|
|
506
|
+
const mine={id:'',name:'Your data',details:'The app as it is, no scenario'};
|
|
507
|
+
body.replaceChildren(...(fresh.length?group('New in this session',fresh):[]),...group(fresh.length?'All':'Start from',[mine,...scenarios.filter(s=>!fresh.includes(s))]));
|
|
508
|
+
}
|
|
509
|
+
scnBtn.onclick=()=>{
|
|
510
|
+
if(!$('scenarios').hidden)return panels(null);
|
|
511
|
+
panels('scenarios');renderScenarioList();void loadScenarios();
|
|
512
|
+
($('scenarios-body').querySelector('[aria-current="true"]')||$('scenarios-body').querySelector('.scn-row:not(:disabled)'))?.focus();
|
|
513
|
+
};
|
|
514
|
+
$('scenarios-close').onclick=()=>{panels(null);scnBtn.focus();};
|
|
515
|
+
scnReset.onclick=()=>{if(activeScenario)void enterScenario(activeScenario.id);};
|
|
516
|
+
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.';
|
|
517
|
+
async function enterScenario(id){
|
|
518
|
+
panels(null);
|
|
519
|
+
const run=++runSeq;
|
|
520
|
+
if(runner)clearTimeout(runner.timer);
|
|
521
|
+
runner=null;
|
|
522
|
+
// A desktop app: the machine restarts it in the scenario and takes the
|
|
523
|
+
// steps in its window, then answers. Nothing to walk here.
|
|
524
|
+
if(metadata.inspect===false)return enterDesktopScenario(id,run);
|
|
525
|
+
let d={};
|
|
526
|
+
try{
|
|
527
|
+
const r=await fetch(metadata.origin+'/__bivy/scenario',{method:'POST',credentials:'include',cache:'no-store',headers:{'content-type':'text/plain'},body:id});
|
|
528
|
+
d=await r.json().catch(()=>({}));
|
|
529
|
+
if(!r.ok)throw Error(typeof d.error==='string'?d.error.slice(0,300):'The preview didn’t answer.');
|
|
530
|
+
}catch(error){if(run===runSeq)failScenario({id,name:scenarios.find(s=>s.id===id)?.name||id},error.message||'The preview didn’t answer.');return;}
|
|
531
|
+
if(run!==runSeq)return;
|
|
532
|
+
const plan=d.plan;
|
|
533
|
+
if(!plan){activeScenario=null;renderScenarioPill();open(currentPath,'Reloading app…');return;}
|
|
534
|
+
activeScenario={id:plan.id,name:plan.name,...(plan.simulated?{simulated:plan.simulated}:{})};
|
|
535
|
+
runner={run,plan,stage:0,step:0,timer:0};
|
|
536
|
+
// As a new visitor: the page forgets what it kept before the first page opens.
|
|
537
|
+
if(plan.fresh)frame.contentWindow?.postMessage({type:'bivy:forget'},metadata.origin);
|
|
538
|
+
renderScenarioPill();
|
|
539
|
+
setTimeout(()=>{if(runner?.run===run)goStage();},plan.fresh?200:0);
|
|
540
|
+
}
|
|
541
|
+
async function enterDesktopScenario(id,run){
|
|
542
|
+
const name=id?scenarios.find(s=>s.id===id)?.name||id:'';
|
|
543
|
+
runner={run,desktop:true,timer:0,plan:{id,name}};renderScenarioPill();
|
|
544
|
+
let d={};
|
|
545
|
+
try{
|
|
546
|
+
const r=await fetch(metadata.origin+'/__bivy/scenario',{method:'POST',credentials:'include',cache:'no-store',headers:{'content-type':'text/plain'},body:id});
|
|
547
|
+
d=await r.json().catch(()=>({}));
|
|
548
|
+
if(!r.ok)throw Error(typeof d.error==='string'?d.error.slice(0,300):'The app didn’t answer.');
|
|
549
|
+
}catch(error){if(run===runSeq){runner=null;failScenario({id,name},error.message||'The app didn’t answer.');}return;}
|
|
550
|
+
if(run!==runSeq)return;
|
|
551
|
+
activeScenario=d.plan?{id:d.plan.id,name:d.plan.name,...(d.plan.simulated?{simulated:d.plan.simulated}:{})}:null;
|
|
552
|
+
if(typeof d.error==='string')failScenario(d.plan,d.error.slice(0,400));else finishScenario();
|
|
553
|
+
}
|
|
554
|
+
/** Opens the stage's page (its steps follow when it has loaded), or walks its steps where we are. */
|
|
555
|
+
function goStage(){
|
|
556
|
+
const st=runner.plan.stages[runner.stage];
|
|
557
|
+
if(!st)return finishScenario();
|
|
558
|
+
runner.step=0;
|
|
559
|
+
if(st.open){shown=latest;waiting(false);open(st.open,'Opening '+runner.plan.name+'…');watchdog();}
|
|
560
|
+
else sendSteps();
|
|
561
|
+
}
|
|
562
|
+
function sendSteps(){
|
|
563
|
+
const st=runner.plan.stages[runner.stage];
|
|
564
|
+
if(!st)return finishScenario();
|
|
565
|
+
if(runner.step>=st.steps.length){runner.stage++;return goStage();}
|
|
566
|
+
frame.contentWindow?.postMessage({type:'bivy:steps',run:runner.run,load:loads,steps:st.steps,from:runner.step},metadata.origin);
|
|
567
|
+
watchdog();
|
|
568
|
+
}
|
|
569
|
+
/** A page that stops answering (no inspector, a hung script) fails the scenario rather than leaving it opening forever. */
|
|
570
|
+
function watchdog(){
|
|
571
|
+
const at=runner;clearTimeout(at.timer);
|
|
572
|
+
at.timer=setTimeout(()=>{if(runner===at)failScenario(at.plan,'The page stopped answering at step '+(at.step+1)+' of “'+at.plan.name+'”.');},12000);
|
|
573
|
+
}
|
|
574
|
+
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';
|
|
575
|
+
function stepReport(d){
|
|
576
|
+
if(d.type==='steps-done'){runner.stage++;return goStage();}
|
|
577
|
+
const i=Number(d.i)||0;
|
|
578
|
+
if(d.ok){runner.step=i+1;return watchdog();}
|
|
579
|
+
const st=runner.plan.stages[runner.stage],before=runner.plan.stages.slice(0,runner.stage).reduce((n,x)=>n+x.steps.length,0);
|
|
580
|
+
failScenario(runner.plan,'Step '+(before+i+1)+' ('+stepWords(st.steps[i])+'): '+String(d.error||'it failed').slice(0,300)+'.');
|
|
581
|
+
}
|
|
582
|
+
function finishScenario(){
|
|
583
|
+
if(runner)clearTimeout(runner.timer);
|
|
584
|
+
runner=null;status.hidden=true;renderScenarioPill();
|
|
585
|
+
}
|
|
586
|
+
function failScenario(s,message){
|
|
587
|
+
if(runner)clearTimeout(runner.timer);
|
|
588
|
+
runner=null;status.hidden=true;renderScenarioPill();
|
|
589
|
+
panels(null);
|
|
590
|
+
$('scn-fail-title').textContent='Couldn’t finish “'+s.name+'”';
|
|
591
|
+
$('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.');
|
|
592
|
+
$('scn-fix').hidden=!metadata.returnTo||Boolean(metadata.reviewer);
|
|
593
|
+
$('scn-fix').onclick=()=>{$('scn-fail').hidden=true;toChat(fixRequest(s.name,s.id,message));};
|
|
594
|
+
$('scn-fail').hidden=false;
|
|
595
|
+
($('scn-fix').hidden?$('scn-stay'):$('scn-fix')).focus();
|
|
596
|
+
}
|
|
597
|
+
$('scn-stay').onclick=()=>{$('scn-fail').hidden=true;scnBtn.hidden?more.focus():scnBtn.focus();};
|
|
410
598
|
// Console
|
|
411
599
|
let entries=[];
|
|
412
600
|
function resetConsole(){entries=[];renderConsole();}
|
|
@@ -666,7 +854,7 @@ async function loadCompare(){
|
|
|
666
854
|
if(metadata.reviewer)return;
|
|
667
855
|
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
856
|
}
|
|
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;}
|
|
857
|
+
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
858
|
compareBtn.onclick=async()=>{
|
|
671
859
|
if(!$('compare').hidden)return panels(null);
|
|
672
860
|
panels('compare');
|
|
@@ -736,7 +924,11 @@ addEventListener('message',e=>{
|
|
|
736
924
|
ask.hidden=!metadata.returnTo;
|
|
737
925
|
}else if(d.source==='bivy-inspector'){
|
|
738
926
|
if(d.type==='note-sent'&&metadata.reviewer)finishNote(d);
|
|
739
|
-
else if(d.type==='route'){
|
|
927
|
+
else if(d.type==='route'){
|
|
928
|
+
currentPath=safePath(d.path);reviewerTools(true);
|
|
929
|
+
if(!scenariosLoaded)void loadScenarios();
|
|
930
|
+
}
|
|
931
|
+
else if((d.type==='step'||d.type==='steps-done')&&runner&&d.run===runner.run&&d.load===loads)stepReport(d);
|
|
740
932
|
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
933
|
else if(d.type==='release'&&heldAt){heldAt=0;stopListening();}
|
|
742
934
|
else if(d.type==='draw-state'){if(draw&&draw.waiting)draw.waiting(d);else if(stateWaiter)stateWaiter(d);}
|
package/dist/apps/registry.js
CHANGED
|
@@ -104,6 +104,9 @@ export class AppRegistry extends EventEmitter {
|
|
|
104
104
|
apps = new Map();
|
|
105
105
|
views = new Map();
|
|
106
106
|
persisted = new Map();
|
|
107
|
+
/** Puts a desktop app in a scenario (or none, for ""): set by the service,
|
|
108
|
+
* which runs programs; asked by the gateway, which hears the shell. */
|
|
109
|
+
desktopScenario;
|
|
107
110
|
constructor(reservedPorts = [], file) {
|
|
108
111
|
super();
|
|
109
112
|
this.reservedPorts = reservedPorts;
|
|
@@ -200,7 +203,7 @@ export class AppRegistry extends EventEmitter {
|
|
|
200
203
|
app.views.push(view);
|
|
201
204
|
const stored = restore?.notes?.[view.id];
|
|
202
205
|
const notes = view.kind === "web" && Array.isArray(stored) ? stored.filter(isNote).slice(-MAX_NOTES) : [];
|
|
203
|
-
entries.push({ app, view, target, revision: 0, source, ...(notes.length ? { notes } : {}) });
|
|
206
|
+
entries.push({ app, view, target, workspace, revision: 0, source, ...(notes.length ? { notes } : {}) });
|
|
204
207
|
}
|
|
205
208
|
// Commit all views together: a bad later view cannot leave a partial app.
|
|
206
209
|
this.apps.set(app.id, app);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import https from "node:https";
|
|
5
|
+
import { once } from "node:events";
|
|
6
|
+
import { matchRule } from "./scenarios.js";
|
|
7
|
+
/** Marks a response Bivy made up for a scenario, not the app's API. */
|
|
8
|
+
export const SIMULATED = "x-bivy-simulated";
|
|
9
|
+
/** A scenario's network rule met a request: wait if it says so, then answer
|
|
10
|
+
* for the API, drop the connection ("offline"), or let the request through
|
|
11
|
+
* late (a delay alone). Returns whether the request was answered here. The
|
|
12
|
+
* preview gateway (web pages) and the API proxy (desktop apps) share it. */
|
|
13
|
+
export async function simulate(req, res, rule, headers = {}) {
|
|
14
|
+
if (rule.delayMs) {
|
|
15
|
+
const gone = await new Promise((resolve) => {
|
|
16
|
+
const timer = setTimeout(() => { res.off("close", closed); resolve(false); }, rule.delayMs);
|
|
17
|
+
const closed = () => { clearTimeout(timer); resolve(true); };
|
|
18
|
+
res.once("close", closed);
|
|
19
|
+
});
|
|
20
|
+
if (gone)
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (rule.offline) {
|
|
24
|
+
req.socket.destroy();
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (rule.status === undefined && rule.json === undefined && rule.body === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
const json = rule.json !== undefined;
|
|
30
|
+
const body = Buffer.from(json ? JSON.stringify(rule.json) : rule.body ?? "");
|
|
31
|
+
res.writeHead(rule.status ?? 200, { ...headers, "content-type": json ? "application/json" : "text/plain; charset=utf-8", "content-length": body.length, [SIMULATED]: "1" });
|
|
32
|
+
res.end(req.method === "HEAD" ? undefined : body);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** A desktop app calls its API directly, so a scenario's network rules can't
|
|
36
|
+
* be applied in the preview. This proxy sits between them instead: the app is
|
|
37
|
+
* started with its API address pointed here (loopback only), matching
|
|
38
|
+
* requests are answered by the rules, and the rest go on to `target`. */
|
|
39
|
+
export async function startScenarioProxy(target, rules) {
|
|
40
|
+
const base = new URL(target);
|
|
41
|
+
const client = base.protocol === "https:" ? https : http;
|
|
42
|
+
// The app is given the target's path too, so its requests already carry it.
|
|
43
|
+
const prefix = base.pathname.replace(/\/$/, "");
|
|
44
|
+
const server = http.createServer((req, res) => {
|
|
45
|
+
void (async () => {
|
|
46
|
+
const url = req.url?.startsWith("/") ? req.url : "/";
|
|
47
|
+
const rule = matchRule(rules, req.method, url);
|
|
48
|
+
if (rule && await simulate(req, res, rule))
|
|
49
|
+
return;
|
|
50
|
+
const headers = { ...req.headers, host: base.host };
|
|
51
|
+
const upstream = client.request({ protocol: base.protocol, hostname: base.hostname, port: base.port, path: url, method: req.method, headers }, (response) => {
|
|
52
|
+
res.writeHead(response.statusCode ?? 502, response.headers);
|
|
53
|
+
response.pipe(res);
|
|
54
|
+
});
|
|
55
|
+
upstream.on("error", () => { if (!res.headersSent)
|
|
56
|
+
res.writeHead(502, { "content-type": "text/plain" }); res.end("The API isn't answering."); });
|
|
57
|
+
res.on("close", () => upstream.destroy());
|
|
58
|
+
req.pipe(upstream);
|
|
59
|
+
})().catch(() => res.destroy());
|
|
60
|
+
});
|
|
61
|
+
server.listen(0, "127.0.0.1");
|
|
62
|
+
await once(server, "listening");
|
|
63
|
+
const { port } = server.address();
|
|
64
|
+
return { url: `http://127.0.0.1:${port}${prefix}`, close: () => { server.close(); server.closeAllConnections(); } };
|
|
65
|
+
}
|