@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.
Files changed (136) hide show
  1. package/bin/bivy.mjs +39 -5
  2. package/bin/codex-app-server-shim.mjs +78 -27
  3. package/dist/agent-instructions.js +2 -1
  4. package/dist/agents/claude-code/runtime.js +2 -1
  5. package/dist/agents/profiles.js +9 -6
  6. package/dist/apps/gateway.js +91 -3
  7. package/dist/apps/inspector.js +56 -1
  8. package/dist/apps/preview-shell.js +197 -5
  9. package/dist/apps/registry.js +4 -1
  10. package/dist/apps/scenario-proxy.js +65 -0
  11. package/dist/apps/scenarios.js +307 -0
  12. package/dist/apps/service.js +111 -7
  13. package/dist/apps/styles.css +4 -0
  14. package/dist/apps/types.js +2 -1
  15. package/dist/controllers/app-commands.js +3 -2
  16. package/dist/exec.js +4 -1
  17. package/dist/node-env.js +6 -0
  18. package/dist/protocol/client-command-routes.js +1 -1
  19. package/dist/protocol/client-command-schemas.js +3 -2
  20. package/dist/runtime/codex-sessions.js +158 -6
  21. package/dist/runtime/credential-provisioning.js +5 -0
  22. package/dist/runtime/grok-sessions.js +4 -2
  23. package/dist/runtime/index.js +26 -19
  24. package/dist/runtime/inherited-session-env.js +54 -0
  25. package/dist/runtime/model-ref.js +14 -1
  26. package/dist/runtime/opencode-auth.js +67 -0
  27. package/dist/runtime/opencode-preflight.js +10 -1
  28. package/dist/runtime/protocol.js +35 -1
  29. package/dist/runtime/tool-call-map.js +67 -3
  30. package/dist/server.js +49 -15
  31. package/dist/session/event-log.js +76 -5
  32. package/dist/session/fork-standup.js +1 -1
  33. package/dist/session/fork.js +6 -4
  34. package/dist/session/transcript-normal.js +52 -0
  35. package/dist/session/transcript-persistence.js +20 -3
  36. package/dist/session-ref.js +5 -2
  37. package/dist/worktree.js +25 -3
  38. package/package.json +1 -1
  39. package/web/assets/{AutomationsView-CJSYk0ja.js → AutomationsView-DuWxbo5B.js} +1 -1
  40. package/web/assets/{ChangesCard-Bp88fY6W.js → ChangesCard-BnHllUb0.js} +1 -1
  41. package/web/assets/{LibraryView-of9wmuzN.js → LibraryView-DpNuD-T7.js} +1 -1
  42. package/web/assets/{ProviderConnect-DG7SP4l2.js → ProviderConnect-4vjf567A.js} +1 -1
  43. package/web/assets/{Settings-DGH1U7wj.js → Settings-CHofnxyn.js} +3 -3
  44. package/web/assets/{Sheet-D-EHCMlj.js → Sheet-DwouI-Tt.js} +1 -1
  45. package/web/assets/{Terminal-qqysrwl_.js → Terminal-3mNEW7UT.js} +1 -1
  46. package/web/assets/{VoiceSettings-aeqQkwcZ.js → VoiceSettings-BI4B-zS1.js} +1 -1
  47. package/web/assets/{abnfDiagram-VCTEODGH-Ct-lmBn5.js → abnfDiagram-VCTEODGH-DPP1TRyh.js} +1 -1
  48. package/web/assets/architecture-7GRP2DOG-DUHnGffN.js +1 -0
  49. package/web/assets/{architectureDiagram-5GKGNRK7-CCq716Da.js → architectureDiagram-5GKGNRK7-CvgInp30.js} +1 -1
  50. package/web/assets/{blockDiagram-I7D4REHJ-Lka9F6ZB.js → blockDiagram-I7D4REHJ-CZAAYK-I.js} +1 -1
  51. package/web/assets/{c4Diagram-7LVT6UL2-nvhhyzri.js → c4Diagram-7LVT6UL2-BYJlvZiZ.js} +1 -1
  52. package/web/assets/channel-vvvNMjoB.js +1 -0
  53. package/web/assets/{chunk-4HAMMTFA-TZ5hhHBz.js → chunk-4HAMMTFA-Cj4-BAMD.js} +1 -1
  54. package/web/assets/{chunk-75Z2AOVW-IahKw8kM.js → chunk-75Z2AOVW-3eT-aS0t.js} +1 -1
  55. package/web/assets/{chunk-DU6HZSFF-BlcZUVOy.js → chunk-DU6HZSFF-D_4JI7OY.js} +1 -1
  56. package/web/assets/{chunk-F27PBJKO-3MGfEgKu.js → chunk-F27PBJKO-5wTG02yk.js} +1 -1
  57. package/web/assets/{chunk-GMAD6QVW-Be72n678.js → chunk-GMAD6QVW-IliCLU5B.js} +1 -1
  58. package/web/assets/{chunk-GVQU2GXP-GOVLT6xm.js → chunk-GVQU2GXP-CB3DCzUu.js} +1 -1
  59. package/web/assets/{chunk-IMKFNOWR-ByOzewrT.js → chunk-IMKFNOWR-Bg3eY-xc.js} +1 -1
  60. package/web/assets/{chunk-L3NEJ4N5-BCGhHmnW.js → chunk-L3NEJ4N5-BEMaPYT9.js} +1 -1
  61. package/web/assets/{chunk-OSK3NFVY-CAdliJE_.js → chunk-OSK3NFVY-DMc-FLYk.js} +1 -1
  62. package/web/assets/{chunk-P2QGCYS3-CM8cpgHa.js → chunk-P2QGCYS3-CaqPruPs.js} +1 -1
  63. package/web/assets/{chunk-POPQ4Y6H-CXLPhaI1.js → chunk-POPQ4Y6H-3h-I9lV1.js} +1 -1
  64. package/web/assets/{chunk-PWAF6VOD-BegFrwDe.js → chunk-PWAF6VOD-CPLNMq3b.js} +1 -1
  65. package/web/assets/{chunk-SHT3W25Y-iXj7Y5RI.js → chunk-SHT3W25Y-ErtwLBXR.js} +1 -1
  66. package/web/assets/{chunk-SVP7TREG-73pyTBZg.js → chunk-SVP7TREG-GNUjH0zF.js} +1 -1
  67. package/web/assets/{chunk-TICWLB2K-D5zwuuGI.js → chunk-TICWLB2K-DtbyVpEE.js} +1 -1
  68. package/web/assets/classDiagram-ZZMXUADV-C3eKmWom.js +1 -0
  69. package/web/assets/classDiagram-v2-VYDZK3BY-C3eKmWom.js +1 -0
  70. package/web/assets/{cynefin-OW5HDTMX-D3nRr9S_.js → cynefin-OW5HDTMX-CQt68k-b.js} +1 -1
  71. package/web/assets/{cynefinDiagram-5FMLGOSQ-C4CKMYMZ.js → cynefinDiagram-5FMLGOSQ-BeEQcUmX.js} +1 -1
  72. package/web/assets/{dagre-GXQ25YYZ-D-grkZ8X.js → dagre-GXQ25YYZ-CoekQT_h.js} +1 -1
  73. package/web/assets/{diagram-S7CK7UJ4-BNPS3YQX.js → diagram-S7CK7UJ4-DQBF8H5Z.js} +1 -1
  74. package/web/assets/{diagram-UQ7AKVKN-Bkp_TyRg.js → diagram-UQ7AKVKN-2Tw1WGQF.js} +1 -1
  75. package/web/assets/{diagram-VSXAHHWV-Dpxe_6MM.js → diagram-VSXAHHWV-C4Ns08EL.js} +1 -1
  76. package/web/assets/{diagram-VX7I27RA-DwGBvGn-.js → diagram-VX7I27RA-CMUQSfon.js} +1 -1
  77. package/web/assets/{diagram-Z3DM3KII-D-KvONMO.js → diagram-Z3DM3KII-Lqvr8Yf6.js} +1 -1
  78. package/web/assets/{ebnfDiagram-PWID7BFC-Di_D-a-0.js → ebnfDiagram-PWID7BFC-BAYJ-dgh.js} +1 -1
  79. package/web/assets/{erDiagram-RLTQ6QDP-2pnBfTU9.js → erDiagram-RLTQ6QDP-Dg8VYnq4.js} +1 -1
  80. package/web/assets/eventmodeling-NTZA5JFV-ChDRNWM1.js +1 -0
  81. package/web/assets/flowDiagram-HODETNUW-PojVb4l3.js +1 -0
  82. package/web/assets/{ganttDiagram-EL5Y4UJY-DAkhMTlk.js → ganttDiagram-EL5Y4UJY-ywnauJ7D.js} +1 -1
  83. package/web/assets/{gitGraph-4MIJSDKK-BQvemX2W.js → gitGraph-4MIJSDKK-bYI8lc-g.js} +1 -1
  84. package/web/assets/{gitGraphDiagram-WWUBYQGX-Dx7ya_2q.js → gitGraphDiagram-WWUBYQGX-erBDwxX8.js} +1 -1
  85. package/web/assets/{index-BXPRG7Pj.css → index-Ch1zstZB.css} +1 -1
  86. package/web/assets/{index-CFX8FhqJ.js → index-DzT1Tnf2.js} +2 -2
  87. package/web/assets/{info-A6RAGUB7-DaDps2sq.js → info-A6RAGUB7-BZbsPdZJ.js} +1 -1
  88. package/web/assets/{infoDiagram-27XIBGKW-DBqMvPr2.js → infoDiagram-27XIBGKW-qqTv-8cL.js} +1 -1
  89. package/web/assets/{ishikawaDiagram-5VMMS53U-DzciF3O_.js → ishikawaDiagram-5VMMS53U-v-e3YR9X.js} +1 -1
  90. package/web/assets/{journeyDiagram-3NMN7TZE-D9WHgGe3.js → journeyDiagram-3NMN7TZE-D61ZGndw.js} +1 -1
  91. package/web/assets/{kanban-definition-UXKFOSKX-Diokj8By.js → kanban-definition-UXKFOSKX-NeYWFrdx.js} +1 -1
  92. package/web/assets/{line-DmBeTXFn.js → line-BNxdvAy9.js} +1 -1
  93. package/web/assets/{mermaid-parser.core-H_O1CweI.js → mermaid-parser.core-BjJ-qoss.js} +3 -3
  94. package/web/assets/{mermaid.core-BAni15Ed.js → mermaid.core-DT4QwB_n.js} +4 -4
  95. package/web/assets/{mindmap-definition-YA3MSWOX-B_tXdzEa.js → mindmap-definition-YA3MSWOX-BxuVgGhr.js} +1 -1
  96. package/web/assets/{mount-Fdy9Xoxs.js → mount-CK5h6xJ4.js} +7 -7
  97. package/web/assets/{packet-AYTQ26CC-Dt4bPH7I.js → packet-AYTQ26CC-DIff_vXS.js} +1 -1
  98. package/web/assets/{pegDiagram-XKGWAZYB-loAbaKUZ.js → pegDiagram-XKGWAZYB-oDNo32c7.js} +1 -1
  99. package/web/assets/{pie-WAS4IAKB-DEmhJN7M.js → pie-WAS4IAKB-C0cdo_NU.js} +1 -1
  100. package/web/assets/{pieDiagram-E7YTZNPT-DfQvD2QY.js → pieDiagram-E7YTZNPT-BbThivUY.js} +1 -1
  101. package/web/assets/{quadrantDiagram-AXDQQJYC-Bhqo2rT-.js → quadrantDiagram-AXDQQJYC-CONfSoMt.js} +1 -1
  102. package/web/assets/{radar-RG4KPBEZ-BmV5tXie.js → radar-RG4KPBEZ-teYstrbs.js} +1 -1
  103. package/web/assets/{railroad-74A4TZTK-DoebyExe.js → railroad-74A4TZTK-B4VusJHl.js} +1 -1
  104. package/web/assets/railroad-abnf-HS5TGJTU-DIF_YU01.js +1 -0
  105. package/web/assets/railroad-ebnf-LZEXJU2U-BoHyJK5s.js +1 -0
  106. package/web/assets/railroad-peg-WCYAUIDC-uG1Bcrgs.js +1 -0
  107. package/web/assets/{railroadDiagram-O6MQD6OU-BhlfIpCW.js → railroadDiagram-O6MQD6OU-CKrYutQ_.js} +1 -1
  108. package/web/assets/{requirementDiagram-BXWQKSXE-vkxH8U-2.js → requirementDiagram-BXWQKSXE-COt2S2Y4.js} +1 -1
  109. package/web/assets/{sankeyDiagram-P5KCCOFB-D2iFl0bc.js → sankeyDiagram-P5KCCOFB-D72oeNbf.js} +1 -1
  110. package/web/assets/{sequenceDiagram-WJ2MYXX4-Ll_m9w0-.js → sequenceDiagram-WJ2MYXX4-DFpVcNjb.js} +1 -1
  111. package/web/assets/{settingsRoute-C8p10bxH.js → settingsRoute-Clztmzl_.js} +1 -1
  112. package/web/assets/{stateDiagram-D77RDMKH-B4KWuRz2.js → stateDiagram-D77RDMKH-BKW8l6Ru.js} +1 -1
  113. package/web/assets/stateDiagram-v2-MP3YSRHH-C1t9bUgR.js +1 -0
  114. package/web/assets/{swimlanes-42K2YHIH-CVXwQL4b.js → swimlanes-42K2YHIH-GRXgsapd.js} +1 -1
  115. package/web/assets/swimlanesDiagram-VR7AAH4N-Dp35pflJ.js +8 -0
  116. package/web/assets/{timeline-definition-24CTP7MA-C0XuGVkJ.js → timeline-definition-24CTP7MA-C8DELzYs.js} +1 -1
  117. package/web/assets/{treeView-Q6P3EWNA-BS4edB5m.js → treeView-Q6P3EWNA-C1MDeOPi.js} +1 -1
  118. package/web/assets/{treemap-WGGIJYW6-BZ54DPA4.js → treemap-WGGIJYW6-CcwgMCe9.js} +1 -1
  119. package/web/assets/{useStore-DEfLrrMh.js → useStore-CaQamKgO.js} +7 -5
  120. package/web/assets/{vennDiagram-4TSXK5OY-CAKXCk3I.js → vennDiagram-4TSXK5OY-Co-Nw6M-.js} +1 -1
  121. package/web/assets/{wardley-WFR3VGLG-DM5IZXxI.js → wardley-WFR3VGLG-FbKVF2Pu.js} +1 -1
  122. package/web/assets/{wardleyDiagram-VM6X3IG4--WWmycX2.js → wardleyDiagram-VM6X3IG4-Bt7NGXJX.js} +1 -1
  123. package/web/assets/{xychartDiagram-S5SC5T6Z-D-_SgAQy.js → xychartDiagram-S5SC5T6Z-BsgyT9WB.js} +1 -1
  124. package/web/index.html +2 -2
  125. package/web/sw.js +1 -1
  126. package/web/assets/architecture-7GRP2DOG-CD4t1j2T.js +0 -1
  127. package/web/assets/channel-Dl0Aa963.js +0 -1
  128. package/web/assets/classDiagram-ZZMXUADV-BvMwIvP0.js +0 -1
  129. package/web/assets/classDiagram-v2-VYDZK3BY-BvMwIvP0.js +0 -1
  130. package/web/assets/eventmodeling-NTZA5JFV-CF677pnK.js +0 -1
  131. package/web/assets/flowDiagram-HODETNUW-8SiMcM5n.js +0 -1
  132. package/web/assets/railroad-abnf-HS5TGJTU-C_F3QWV5.js +0 -1
  133. package/web/assets/railroad-ebnf-LZEXJU2U-BaNfyzXp.js +0 -1
  134. package/web/assets/railroad-peg-WCYAUIDC-BWyMWwxG.js +0 -1
  135. package/web/assets/stateDiagram-v2-MP3YSRHH-B_Xx-QG6.js +0 -1
  136. 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
- const [ticket,startPage='']=location.hash.slice(1).split('~');history.replaceState(null,'',location.pathname);
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
- status.hidden=true;void loadCompare();
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'){currentPath=safePath(d.path);reviewerTools(true);}
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);}
@@ -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
+ }