@bivy/bivy 0.20.7-staging.2 → 0.20.7-staging.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/bin/bivy.mjs +100 -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/backend/compare.js +62 -0
  7. package/dist/apps/backend/http-file.js +66 -0
  8. package/dist/apps/backend/index.js +431 -0
  9. package/dist/apps/backend/logs.js +60 -0
  10. package/dist/apps/backend/rows.js +79 -0
  11. package/dist/apps/gateway.js +121 -3
  12. package/dist/apps/inspector.js +56 -1
  13. package/dist/apps/preview-shell.js +221 -9
  14. package/dist/apps/registry.js +60 -2
  15. package/dist/apps/scenario-proxy.js +65 -0
  16. package/dist/apps/scenarios.js +307 -0
  17. package/dist/apps/service.js +147 -8
  18. package/dist/apps/styles.css +70 -0
  19. package/dist/apps/types.js +9 -1
  20. package/dist/controllers/app-commands.js +13 -2
  21. package/dist/exec.js +4 -1
  22. package/dist/node-env.js +6 -0
  23. package/dist/protocol/client-command-routes.js +1 -1
  24. package/dist/protocol/client-command-schemas.js +10 -2
  25. package/dist/runtime/codex-sessions.js +158 -6
  26. package/dist/runtime/credential-provisioning.js +5 -0
  27. package/dist/runtime/grok-sessions.js +4 -2
  28. package/dist/runtime/index.js +26 -19
  29. package/dist/runtime/inherited-session-env.js +54 -0
  30. package/dist/runtime/model-ref.js +14 -1
  31. package/dist/runtime/opencode-auth.js +67 -0
  32. package/dist/runtime/opencode-preflight.js +10 -1
  33. package/dist/runtime/protocol.js +35 -1
  34. package/dist/runtime/tool-call-map.js +67 -3
  35. package/dist/server.js +51 -16
  36. package/dist/session/event-log.js +76 -5
  37. package/dist/session/fork-standup.js +1 -1
  38. package/dist/session/fork.js +6 -4
  39. package/dist/session/transcript-normal.js +52 -0
  40. package/dist/session/transcript-persistence.js +20 -3
  41. package/dist/session-ref.js +5 -2
  42. package/dist/terminal.js +17 -0
  43. package/dist/worktree.js +25 -3
  44. package/package.json +1 -1
  45. package/web/assets/{AutomationsView-CJSYk0ja.js → AutomationsView-DJibSXQw.js} +1 -1
  46. package/web/assets/{ChangesCard-Bp88fY6W.js → ChangesCard-s8Qtm7eu.js} +1 -1
  47. package/web/assets/{LibraryView-of9wmuzN.js → LibraryView-Bz8x-H8N.js} +2 -2
  48. package/web/assets/{ProviderConnect-DG7SP4l2.js → ProviderConnect-DZ-Rv8Aj.js} +1 -1
  49. package/web/assets/{Settings-DGH1U7wj.js → Settings-DMUdD3O5.js} +3 -3
  50. package/web/assets/{Sheet-D-EHCMlj.js → Sheet-2wuqvpxm.js} +1 -1
  51. package/web/assets/{Terminal-qqysrwl_.js → Terminal-DQeYK2KK.js} +1 -1
  52. package/web/assets/{VoiceSettings-aeqQkwcZ.js → VoiceSettings-BbqFD1Ee.js} +1 -1
  53. package/web/assets/{abnfDiagram-VCTEODGH-Ct-lmBn5.js → abnfDiagram-VCTEODGH-DLSpnM9y.js} +1 -1
  54. package/web/assets/architecture-7GRP2DOG-CefLtGFG.js +1 -0
  55. package/web/assets/{architectureDiagram-5GKGNRK7-CCq716Da.js → architectureDiagram-5GKGNRK7-DOQxmFrp.js} +1 -1
  56. package/web/assets/{blockDiagram-I7D4REHJ-Lka9F6ZB.js → blockDiagram-I7D4REHJ-BiCBLtEP.js} +1 -1
  57. package/web/assets/{c4Diagram-7LVT6UL2-nvhhyzri.js → c4Diagram-7LVT6UL2-yzmqXo9k.js} +1 -1
  58. package/web/assets/channel-kKuds40w.js +1 -0
  59. package/web/assets/{chunk-4HAMMTFA-TZ5hhHBz.js → chunk-4HAMMTFA-i-QksGpD.js} +1 -1
  60. package/web/assets/{chunk-75Z2AOVW-IahKw8kM.js → chunk-75Z2AOVW-DWjggoX1.js} +1 -1
  61. package/web/assets/{chunk-DU6HZSFF-BlcZUVOy.js → chunk-DU6HZSFF-jFwsC9ox.js} +1 -1
  62. package/web/assets/{chunk-F27PBJKO-3MGfEgKu.js → chunk-F27PBJKO-3ej2B9gg.js} +1 -1
  63. package/web/assets/{chunk-GMAD6QVW-Be72n678.js → chunk-GMAD6QVW-CzfhoJ04.js} +1 -1
  64. package/web/assets/{chunk-GVQU2GXP-GOVLT6xm.js → chunk-GVQU2GXP-DLhbmKuJ.js} +1 -1
  65. package/web/assets/{chunk-IMKFNOWR-ByOzewrT.js → chunk-IMKFNOWR-dR96ErOJ.js} +1 -1
  66. package/web/assets/{chunk-L3NEJ4N5-BCGhHmnW.js → chunk-L3NEJ4N5-1YX2kWHP.js} +1 -1
  67. package/web/assets/{chunk-OSK3NFVY-CAdliJE_.js → chunk-OSK3NFVY-kDveSID9.js} +1 -1
  68. package/web/assets/{chunk-P2QGCYS3-CM8cpgHa.js → chunk-P2QGCYS3-Cc3Uo4HQ.js} +1 -1
  69. package/web/assets/{chunk-POPQ4Y6H-CXLPhaI1.js → chunk-POPQ4Y6H-CxDXuH1x.js} +1 -1
  70. package/web/assets/{chunk-PWAF6VOD-BegFrwDe.js → chunk-PWAF6VOD-DZBN6HSl.js} +1 -1
  71. package/web/assets/{chunk-SHT3W25Y-iXj7Y5RI.js → chunk-SHT3W25Y-0W01Q-w7.js} +1 -1
  72. package/web/assets/{chunk-SVP7TREG-73pyTBZg.js → chunk-SVP7TREG-gqFFZT4I.js} +1 -1
  73. package/web/assets/{chunk-TICWLB2K-D5zwuuGI.js → chunk-TICWLB2K-BGdM7dkO.js} +1 -1
  74. package/web/assets/classDiagram-ZZMXUADV-Dg-2WnlQ.js +1 -0
  75. package/web/assets/classDiagram-v2-VYDZK3BY-Dg-2WnlQ.js +1 -0
  76. package/web/assets/{cynefin-OW5HDTMX-D3nRr9S_.js → cynefin-OW5HDTMX-D3KvEku4.js} +1 -1
  77. package/web/assets/{cynefinDiagram-5FMLGOSQ-C4CKMYMZ.js → cynefinDiagram-5FMLGOSQ-CLbDBHX9.js} +1 -1
  78. package/web/assets/{dagre-GXQ25YYZ-D-grkZ8X.js → dagre-GXQ25YYZ-D2kdt2lo.js} +1 -1
  79. package/web/assets/{diagram-S7CK7UJ4-BNPS3YQX.js → diagram-S7CK7UJ4-a-ev_USx.js} +1 -1
  80. package/web/assets/{diagram-UQ7AKVKN-Bkp_TyRg.js → diagram-UQ7AKVKN-BwYRbF-y.js} +1 -1
  81. package/web/assets/{diagram-VSXAHHWV-Dpxe_6MM.js → diagram-VSXAHHWV-Da7O55Aq.js} +1 -1
  82. package/web/assets/{diagram-VX7I27RA-DwGBvGn-.js → diagram-VX7I27RA-Bcq-P_8M.js} +1 -1
  83. package/web/assets/{diagram-Z3DM3KII-D-KvONMO.js → diagram-Z3DM3KII-DSXXMWvA.js} +1 -1
  84. package/web/assets/{ebnfDiagram-PWID7BFC-Di_D-a-0.js → ebnfDiagram-PWID7BFC-DxFerrqi.js} +1 -1
  85. package/web/assets/{erDiagram-RLTQ6QDP-2pnBfTU9.js → erDiagram-RLTQ6QDP-DfyXAe2v.js} +1 -1
  86. package/web/assets/eventmodeling-NTZA5JFV-tdyiDQoh.js +1 -0
  87. package/web/assets/flowDiagram-HODETNUW-BO23Cpex.js +1 -0
  88. package/web/assets/{ganttDiagram-EL5Y4UJY-DAkhMTlk.js → ganttDiagram-EL5Y4UJY-QkgrgMh9.js} +1 -1
  89. package/web/assets/{gitGraph-4MIJSDKK-BQvemX2W.js → gitGraph-4MIJSDKK-M1n5msce.js} +1 -1
  90. package/web/assets/{gitGraphDiagram-WWUBYQGX-Dx7ya_2q.js → gitGraphDiagram-WWUBYQGX-DC_8Cxet.js} +1 -1
  91. package/web/assets/{index-CFX8FhqJ.js → index-B0Bto4kI.js} +2 -2
  92. package/web/assets/{index-BXPRG7Pj.css → index-Da2-4YHh.css} +1 -1
  93. package/web/assets/{info-A6RAGUB7-DaDps2sq.js → info-A6RAGUB7-DmyYG6Ip.js} +1 -1
  94. package/web/assets/{infoDiagram-27XIBGKW-DBqMvPr2.js → infoDiagram-27XIBGKW-CmrMjsqT.js} +1 -1
  95. package/web/assets/{ishikawaDiagram-5VMMS53U-DzciF3O_.js → ishikawaDiagram-5VMMS53U-x_6jWkNq.js} +1 -1
  96. package/web/assets/{journeyDiagram-3NMN7TZE-D9WHgGe3.js → journeyDiagram-3NMN7TZE-COVPGUIl.js} +1 -1
  97. package/web/assets/{kanban-definition-UXKFOSKX-Diokj8By.js → kanban-definition-UXKFOSKX-BDZHzeOz.js} +1 -1
  98. package/web/assets/{line-DmBeTXFn.js → line-KdqoiWjk.js} +1 -1
  99. package/web/assets/{mermaid-parser.core-H_O1CweI.js → mermaid-parser.core-C5RHAle0.js} +3 -3
  100. package/web/assets/{mermaid.core-BAni15Ed.js → mermaid.core-2ujtWQt3.js} +4 -4
  101. package/web/assets/{mindmap-definition-YA3MSWOX-B_tXdzEa.js → mindmap-definition-YA3MSWOX-CCPoAcT0.js} +1 -1
  102. package/web/assets/mount-CaFlQTsV.js +30 -0
  103. package/web/assets/{packet-AYTQ26CC-Dt4bPH7I.js → packet-AYTQ26CC-BjPan2xl.js} +1 -1
  104. package/web/assets/{pegDiagram-XKGWAZYB-loAbaKUZ.js → pegDiagram-XKGWAZYB-CrARxusS.js} +1 -1
  105. package/web/assets/{pie-WAS4IAKB-DEmhJN7M.js → pie-WAS4IAKB-BdVo8hLO.js} +1 -1
  106. package/web/assets/{pieDiagram-E7YTZNPT-DfQvD2QY.js → pieDiagram-E7YTZNPT-DwdhmBCX.js} +1 -1
  107. package/web/assets/{quadrantDiagram-AXDQQJYC-Bhqo2rT-.js → quadrantDiagram-AXDQQJYC-DbG3R6xw.js} +1 -1
  108. package/web/assets/{radar-RG4KPBEZ-BmV5tXie.js → radar-RG4KPBEZ-pp-coSgM.js} +1 -1
  109. package/web/assets/{railroad-74A4TZTK-DoebyExe.js → railroad-74A4TZTK-CxNBhSbO.js} +1 -1
  110. package/web/assets/railroad-abnf-HS5TGJTU-g5eNqQ81.js +1 -0
  111. package/web/assets/railroad-ebnf-LZEXJU2U-BI5_cD5r.js +1 -0
  112. package/web/assets/railroad-peg-WCYAUIDC-CLl7RlXh.js +1 -0
  113. package/web/assets/{railroadDiagram-O6MQD6OU-BhlfIpCW.js → railroadDiagram-O6MQD6OU-CKbKBwCj.js} +1 -1
  114. package/web/assets/{requirementDiagram-BXWQKSXE-vkxH8U-2.js → requirementDiagram-BXWQKSXE-DRc316xB.js} +1 -1
  115. package/web/assets/{sankeyDiagram-P5KCCOFB-D2iFl0bc.js → sankeyDiagram-P5KCCOFB-C6O9zcIS.js} +1 -1
  116. package/web/assets/{sequenceDiagram-WJ2MYXX4-Ll_m9w0-.js → sequenceDiagram-WJ2MYXX4-D3Vz1DcK.js} +1 -1
  117. package/web/assets/{settingsRoute-C8p10bxH.js → settingsRoute-DxPtfqmn.js} +1 -1
  118. package/web/assets/{stateDiagram-D77RDMKH-B4KWuRz2.js → stateDiagram-D77RDMKH-DXcUXP-l.js} +1 -1
  119. package/web/assets/stateDiagram-v2-MP3YSRHH-CQER-whG.js +1 -0
  120. package/web/assets/{swimlanes-42K2YHIH-CVXwQL4b.js → swimlanes-42K2YHIH-tfw_N04-.js} +1 -1
  121. package/web/assets/swimlanesDiagram-VR7AAH4N-Cf__qhQx.js +8 -0
  122. package/web/assets/{timeline-definition-24CTP7MA-C0XuGVkJ.js → timeline-definition-24CTP7MA-BcAeG9F_.js} +1 -1
  123. package/web/assets/{treeView-Q6P3EWNA-BS4edB5m.js → treeView-Q6P3EWNA-DaZCFJDf.js} +1 -1
  124. package/web/assets/{treemap-WGGIJYW6-BZ54DPA4.js → treemap-WGGIJYW6-C3PybXBI.js} +1 -1
  125. package/web/assets/{useStore-DEfLrrMh.js → useStore-Ce5mzYro.js} +9 -7
  126. package/web/assets/{vennDiagram-4TSXK5OY-CAKXCk3I.js → vennDiagram-4TSXK5OY-BT7w_l5a.js} +1 -1
  127. package/web/assets/{wardley-WFR3VGLG-DM5IZXxI.js → wardley-WFR3VGLG-CXZlxUiB.js} +1 -1
  128. package/web/assets/{wardleyDiagram-VM6X3IG4--WWmycX2.js → wardleyDiagram-VM6X3IG4-B5muPnst.js} +1 -1
  129. package/web/assets/{xychartDiagram-S5SC5T6Z-D-_SgAQy.js → xychartDiagram-S5SC5T6Z-DSaMhJlg.js} +1 -1
  130. package/web/index.html +2 -2
  131. package/web/sw.js +1 -1
  132. package/web/assets/architecture-7GRP2DOG-CD4t1j2T.js +0 -1
  133. package/web/assets/channel-Dl0Aa963.js +0 -1
  134. package/web/assets/classDiagram-ZZMXUADV-BvMwIvP0.js +0 -1
  135. package/web/assets/classDiagram-v2-VYDZK3BY-BvMwIvP0.js +0 -1
  136. package/web/assets/eventmodeling-NTZA5JFV-CF677pnK.js +0 -1
  137. package/web/assets/flowDiagram-HODETNUW-8SiMcM5n.js +0 -1
  138. package/web/assets/mount-Fdy9Xoxs.js +0 -27
  139. package/web/assets/railroad-abnf-HS5TGJTU-C_F3QWV5.js +0 -1
  140. package/web/assets/railroad-ebnf-LZEXJU2U-BaNfyzXp.js +0 -1
  141. package/web/assets/railroad-peg-WCYAUIDC-BWyMWwxG.js +0 -1
  142. package/web/assets/stateDiagram-v2-MP3YSRHH-B_Xx-QG6.js +0 -1
  143. package/web/assets/swimlanesDiagram-VR7AAH4N-XYt-UA-z.js +0 -8
@@ -13,6 +13,12 @@ import { captureFrame, encodePng, sendInput } from "./rfb.js";
13
13
  import { inputEvents, readAction } from "./input.js";
14
14
  import { pressMenu, readMenuPath, readMenus } from "./menu.js";
15
15
  import { pngSize } from "./review.js";
16
+ import { Backend } from "./backend/index.js";
17
+ import { SCENARIO_DIR, loadScenarios, planScenario, scenariosFor, summarize } from "./scenarios.js";
18
+ import { startScenarioProxy } from "./scenario-proxy.js";
19
+ const scenarioKind = (entry) => entry.target.kind === "display" ? "desktop" : "web";
20
+ /** A desktop step in words, for the message when one fails. */
21
+ const stepWords = (step) => "click" in step ? `click ${Array.isArray(step.click) ? step.click.join(",") : step.click}` : "type" in step ? `type "${step.type.slice(0, 40)}"` : "press" in step ? `press ${step.press}` : "menu" in step ? `menu ${step.menu}` : "wait" in step ? `wait ${step.wait}` : "step";
16
22
  const NO_REVIEWS = { publish: (review) => review, expire: () => { } };
17
23
  const NO_PINS = { publish: (pin) => pin };
18
24
  /** A pin's region is in the page CSS pixels of the viewport it was marked in.
@@ -62,8 +68,12 @@ export class AppService {
62
68
  latest = new Map();
63
69
  /** Per view: the newest reviewer note a run-end card has already counted. */
64
70
  handedOver = new Map();
71
+ /** Per desktop app in a scenario with an API: the proxy in front of it and the variable pointing there. */
72
+ proxies = new Map();
65
73
  /** Screenshots in progress, so a card, Compare and a baseline share one browser run. */
66
74
  inflight = new Map();
75
+ /** Requests, data and logs views: what a backend change did. */
76
+ backend;
67
77
  constructor(registry, gateway, terminals, options = {}) {
68
78
  this.registry = registry;
69
79
  this.gateway = gateway;
@@ -76,6 +86,8 @@ export class AppService {
76
86
  this.reviews = options.reviews ?? NO_REVIEWS;
77
87
  this.pinSink = options.pins ?? NO_PINS;
78
88
  this.settleMs = options.settleMs ?? COMPARE_SETTLE_MS;
89
+ this.backend = new Backend(registry, terminals);
90
+ registry.desktopScenario = (viewId, scenario) => this.desktopScenario(viewId, scenario);
79
91
  }
80
92
  /** A session's apps, or every app on this machine when no session is given. */
81
93
  list(sessionId) {
@@ -117,6 +129,11 @@ export class AppService {
117
129
  const server = this.servers.get(id);
118
130
  if (entry?.target.kind !== "display" || !server?.termId)
119
131
  continue;
132
+ // In a scenario, the new code starts in it again, steps and all.
133
+ if (entry.scenario) {
134
+ void this.desktopScenario(id, entry.scenario.id).catch(() => { });
135
+ continue;
136
+ }
120
137
  this.terminals.close(server.termId);
121
138
  server.termId = undefined;
122
139
  void this.ensureServer(entry).catch(() => { });
@@ -187,11 +204,16 @@ export class AppService {
187
204
  const views = this.reviewable(sessionId);
188
205
  const run = { start: new Map(views.map((entry) => [entry.view.id, entry.revision])), baseline: new Map(), reviews: new Map(), muted: false, ended: false };
189
206
  this.runs.set(sessionId, run);
207
+ if (this.backend.views(sessionId).length)
208
+ run.backend = this.backend.baseline(sessionId).catch(() => { });
209
+ // Settles when the backend's "before" is taken (tests wait on it; the server doesn't need to).
210
+ const ready = run.backend ?? Promise.resolve();
190
211
  if (!this.screenshots.enabled())
191
- return;
212
+ return ready;
192
213
  for (const entry of views)
193
214
  void this.shotOf(entry).then((png) => { if (png && entry.revision === run.start.get(entry.view.id))
194
215
  run.baseline.set(entry.view.id, png); });
216
+ return ready;
195
217
  }
196
218
  /** The run ended (done, or waiting for the user). Views whose revision
197
219
  * changed are screenshotted and compared with the page before the run; a
@@ -203,6 +225,12 @@ export class AppService {
203
225
  return undefined;
204
226
  run.ended = true;
205
227
  let latest;
228
+ // Backend views run again; what changed is evidence, on the app's card.
229
+ let evidence = new Map();
230
+ if (run.backend) {
231
+ await run.backend;
232
+ evidence = await this.backend.evidence(sessionId).catch(() => evidence);
233
+ }
206
234
  const changed = [...run.start].map(([id, revision]) => ({ entry: this.registry.getView(id), revision })).filter(({ entry, revision }) => entry && entry.revision !== revision);
207
235
  if (changed.length && this.screenshots.enabled() && !run.muted)
208
236
  await new Promise((r) => setTimeout(r, this.settleMs));
@@ -225,7 +253,7 @@ export class AppService {
225
253
  const presented = current && run.reviews.get(entry.app.id) === current.id;
226
254
  if (!shouldReview({ trigger: "run", mode, muted: run.muted, revisionChanged: true, change }) && !(presented && after))
227
255
  continue;
228
- latest = this.review(entry, run, { trigger: presented ? current.trigger : "run", note: presented ? current.note : undefined, path: entry.lastPath ?? "/", shot: after, before });
256
+ latest = this.review(entry, run, { trigger: presented ? current.trigger : "run", note: presented ? current.note : undefined, try: presented ? current.try : undefined, path: entry.lastPath ?? "/", shot: after, before, evidence: evidence.get(entry.app.id)?.changed ? evidence.get(entry.app.id).rows : undefined });
229
257
  }
230
258
  // Reviewer notes that arrived since the last hand-over ride on the view's
231
259
  // card for this run, or get a card of their own. A count only: the notes
@@ -250,6 +278,14 @@ export class AppService {
250
278
  latest ??= card;
251
279
  }
252
280
  }
281
+ // A backend-only change: the evidence is the card.
282
+ for (const [appId, found] of evidence) {
283
+ if (!found.changed || run.muted || run.reviews.has(appId) || this.registry.reviewMode(appId) === "off")
284
+ continue;
285
+ const entry = this.registry.getView(found.rows[0].viewId);
286
+ if (entry)
287
+ latest = this.review(entry, run, { trigger: "run", path: "/", evidence: found.rows });
288
+ }
253
289
  if (!latest) {
254
290
  const id = [...run.reviews.values()].at(-1);
255
291
  latest = [...this.latest.values()].find((review) => review.id === id);
@@ -264,6 +300,7 @@ export class AppService {
264
300
  throw new Error("Path must start with /.");
265
301
  const note = typeof input.note === "string" ? input.note.trim().slice(0, 500) : undefined;
266
302
  const entry = this.pickView(sessionId, input.target);
303
+ const tries = input.try?.length ? this.tryScenarios(entry, input.try) : undefined;
267
304
  const mode = this.registry.reviewMode(entry.app.id);
268
305
  const active = this.runs.get(sessionId);
269
306
  const run = active && !active.ended ? active : undefined;
@@ -279,9 +316,90 @@ export class AppService {
279
316
  const shot = enabled ? await this.shotOf(entry, path) : undefined;
280
317
  const baseline = run?.baseline.get(entry.view.id);
281
318
  const before = baseline && shot && !baseline.equals(shot) && path === (entry.lastPath ?? "/") ? baseline : undefined;
282
- const review = this.review(entry, run, { trigger, note, path, shot, before, screenshotsOff: !enabled });
319
+ const review = this.review(entry, run, { trigger, note, path, shot, before, screenshotsOff: !enabled, ...(tries ? { try: tries } : {}) });
283
320
  return { review, message: enabled ? (shot ? "The user sees it in the chat." : "The user sees a card in the chat, but the screenshot failed.") : "The user sees a card in the chat. Screenshots are off on this machine, so it has no picture." };
284
321
  }
322
+ /** `bivy app scenarios`: a view's scenarios as the preview shows them, and
323
+ * files that can't be opened, with what's wrong (picked like `present`). */
324
+ scenarios(sessionId, target) {
325
+ const entry = this.pickView(sessionId, target);
326
+ const { scenarios, problems } = loadScenarios(entry.workspace);
327
+ const kind = scenarioKind(entry);
328
+ return { app: entry.app.name, view: entry.view.name, dir: SCENARIO_DIR, scenarios: summarize(scenariosFor(scenarios, entry.app.name, entry.view.name, kind), problems, entry.app.createdAt, kind) };
329
+ }
330
+ /** Scenarios a review card offers to try, by ID, each one that can be opened. */
331
+ tryScenarios(entry, ids) {
332
+ const { scenarios, problems } = loadScenarios(entry.workspace);
333
+ const kind = scenarioKind(entry);
334
+ const known = summarize(scenariosFor(scenarios, entry.app.name, entry.view.name, kind), problems, entry.app.createdAt, kind);
335
+ const wanted = [...new Set(ids.map((id) => id.trim().toLowerCase().replace(/\.json$/, "")).filter(Boolean))].slice(0, 6);
336
+ return wanted.map((id) => {
337
+ const found = known.find((item) => item.id === id);
338
+ if (!found)
339
+ throw new Error(`No scenario "${id}" for ${entry.view.name}. ${known.length ? `Its scenarios: ${known.map((item) => item.id).join(", ")}.` : `Write one in ${SCENARIO_DIR}/${id}.json.`}`);
340
+ if (found.error)
341
+ throw new Error(`Scenario "${id}" can't be opened: ${found.error}`);
342
+ return { id: found.id, name: found.name };
343
+ });
344
+ }
345
+ /** Puts a desktop app in a scenario ("" for none). It is one program for
346
+ * everyone watching, so the scenario is the app's: it restarts with the
347
+ * scenario's arguments and environment, its API (if the scenario has one)
348
+ * behind a proxy that applies the network rules, and then the steps run in
349
+ * its window the way an agent's input does. A step that fails leaves the app
350
+ * where it got to and says which step, rather than throwing. */
351
+ async desktopScenario(viewId, id) {
352
+ const entry = this.registry.getView(viewId);
353
+ if (entry?.target.kind !== "display")
354
+ throw new Error("Desktop scenarios are for desktop apps.");
355
+ const plan = id ? planScenario(scenariosFor(loadScenarios(entry.workspace).scenarios, entry.app.name, entry.view.name, "desktop"), id, "desktop") : null;
356
+ this.proxies.get(viewId)?.close();
357
+ this.proxies.delete(viewId);
358
+ if (plan?.api) {
359
+ const proxy = await startScenarioProxy(plan.api.target, plan.network);
360
+ this.proxies.set(viewId, { close: proxy.close, env: { [plan.api.env]: proxy.url } });
361
+ }
362
+ entry.scenario = plan ?? undefined;
363
+ const server = this.servers.get(viewId);
364
+ if (server?.termId) {
365
+ this.terminals.close(server.termId);
366
+ server.termId = undefined;
367
+ }
368
+ await this.windowShown(entry);
369
+ if (!plan)
370
+ return { plan: null };
371
+ const steps = plan.stages.flatMap((stage) => stage.steps);
372
+ for (const [index, step] of steps.entries()) {
373
+ try {
374
+ await this.desktopStep(entry, step);
375
+ }
376
+ catch (error) {
377
+ return { plan, error: `Step ${index + 1} (${stepWords(step)}): ${error.message}` };
378
+ }
379
+ await new Promise((r) => setTimeout(r, AFTER_INPUT_MS));
380
+ }
381
+ return { plan };
382
+ }
383
+ async desktopStep(entry, step) {
384
+ if ("wait" in step) {
385
+ if (typeof step.wait === "number")
386
+ await new Promise((r) => setTimeout(r, step.wait));
387
+ return;
388
+ }
389
+ if ("menu" in step) {
390
+ if (!entry.displayControl)
391
+ throw new Error("This machine's desktop apps don't expose their menus (macOS only). Use a key combo instead.");
392
+ await pressMenu(entry.displayControl, readMenuPath(step.menu));
393
+ return;
394
+ }
395
+ if (!entry.display)
396
+ throw new Error("The app isn't running. Check its Logs in the Apps sheet.");
397
+ const action = "click" in step && Array.isArray(step.click) ? { kind: "click", x: step.click[0], y: step.click[1] }
398
+ : "type" in step ? { kind: "type", text: step.type } : "press" in step ? { kind: "key", keys: step.press } : undefined;
399
+ if (!action)
400
+ throw new Error("That step is for web pages.");
401
+ await sendInput(entry.display, inputEvents(readAction(action)));
402
+ }
285
403
  /** No more cards for the rest of the session's current run. */
286
404
  mute(sessionId) {
287
405
  const run = this.runs.get(sessionId);
@@ -348,7 +466,8 @@ export class AppService {
348
466
  * returning to a view's stable address. */
349
467
  /** `scale`: the opening device's pixel density, used if this starts a display. */
350
468
  /** `page`: start on this page instead of the app's root (a review card's page). */
351
- async open(sessionId, appId, viewId, returnTo, direct = false, scale, page) {
469
+ /** `scenario`: open it in that scenario (the shell enters it once the app has loaded). */
470
+ async open(sessionId, appId, viewId, returnTo, direct = false, scale, page, scenario) {
352
471
  const entry = this.registry.requireView(sessionId, appId, viewId);
353
472
  if (entry.target.kind === "display" && !entry.display)
354
473
  entry.displayScale = scale === 2 ? 2 : 1;
@@ -358,8 +477,12 @@ export class AppService {
358
477
  if (direct && !this.gateway.openDirect)
359
478
  throw new Error("This machine can't open previews directly.");
360
479
  let url = direct ? this.gateway.openDirect(viewId) : this.gateway.open(viewId, returnTo);
361
- if (page && page.startsWith("/") && !page.startsWith("//") && page.length <= 2048)
362
- url += `~${encodeURIComponent(page).replace(/~/g, "%7E")}`;
480
+ const start = page && page.startsWith("/") && !page.startsWith("//") && page.length <= 2048 ? page : undefined;
481
+ const inScenario = !direct && scenario && /^[a-z0-9][a-z0-9-]{0,63}$/.test(scenario) ? scenario : undefined;
482
+ if (start || inScenario)
483
+ url += `~${start ? encodeURIComponent(start).replace(/~/g, "%7E") : ""}`;
484
+ if (inScenario)
485
+ url += `~${inScenario}`;
363
486
  // Don't wait for a managed server to boot: the preview shows it starting
364
487
  // and reloads itself once it answers.
365
488
  if (this.servers.get(viewId))
@@ -646,8 +769,10 @@ export class AppService {
646
769
  return this.displays.ensure(entry.view.id, entry.app.name, entry.displayScale).then((display) => {
647
770
  entry.display = display.socket;
648
771
  entry.displayControl = display.control;
649
- const [command, ...args] = [...display.launch ?? [], target.command, ...target.args];
650
- return { command: command, args, workspace: target.workspace, env: display.env, name: `${entry.app.name} · ${entry.view.name}` };
772
+ // A scenario adds its arguments and environment; the display's own variables always win.
773
+ const [command, ...args] = [...display.launch ?? [], target.command, ...target.args, ...entry.scenario?.args ?? []];
774
+ const env = { ...entry.scenario?.env, ...this.proxies.get(entry.view.id)?.env, ...display.env };
775
+ return { command: command, args, workspace: target.workspace, env, name: `${entry.app.name} · ${entry.view.name}` };
651
776
  });
652
777
  }
653
778
  /** Services start synchronously; a desktop app waits for its display. */
@@ -674,6 +799,7 @@ export class AppService {
674
799
  throw new Error("App was removed while starting.");
675
800
  }
676
801
  server.termId = termId;
802
+ this.backend.attach(id, termId);
677
803
  return termId;
678
804
  })
679
805
  .finally(() => { server.pending = undefined; });
@@ -699,6 +825,8 @@ export class AppService {
699
825
  }
700
826
  stopServer(id) {
701
827
  this.displays.stop(id);
828
+ this.proxies.get(id)?.close();
829
+ this.proxies.delete(id);
702
830
  const entry = this.registry.getView(id);
703
831
  if (entry) {
704
832
  entry.display = undefined;
@@ -714,6 +842,16 @@ export class AppService {
714
842
  void server.pending?.then((termId) => this.terminals.close(termId)).catch(() => { });
715
843
  this.servers.delete(id);
716
844
  }
845
+ /** A backend view of a session, by exact IDs (the app) or by name (`bivy app requests`). */
846
+ backendView(sessionId, kind, input) {
847
+ if (input.appId && input.viewId) {
848
+ const entry = this.registry.requireView(sessionId, input.appId, input.viewId);
849
+ if (entry.target.kind !== kind)
850
+ throw new Error(`That isn't a ${kind} view.`);
851
+ return entry;
852
+ }
853
+ return this.backend.pick(sessionId, kind, input.target);
854
+ }
717
855
  share(sessionId, appId, viewId, options = {}) {
718
856
  if (this.registry.requireView(sessionId, appId, viewId).view.kind !== "web")
719
857
  throw new Error("Only web views have preview links.");
@@ -778,6 +916,7 @@ export class AppService {
778
916
  remove(sessionId, appId) {
779
917
  const app = this.registry.require(sessionId, appId);
780
918
  this.registry.remove(sessionId, appId);
919
+ this.backend.forget(app.views.map((view) => view.id), app.id);
781
920
  for (const view of app.views) {
782
921
  this.gateway?.revoke(view.id);
783
922
  this.stopServer(view.id);
@@ -3243,6 +3243,18 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3243
3243
  .review-side .seg-btn { min-block-size: 44px; min-inline-size: 72px; }
3244
3244
  .review-banner { margin: 0 var(--space-3); }
3245
3245
  .review-note { margin: var(--space-3) var(--space-3) 0; padding-inline-start: var(--space-3); border-inline-start: 2px solid var(--line-strong); color: var(--ink); font-size: var(--text-sm); white-space: pre-wrap; }
3246
+ /* Backend evidence on a review card: one line per thing a run changed. */
3247
+ .review-evidence { list-style: none; margin: 0 var(--space-3); padding: 0; border-top: 1px solid var(--line); }
3248
+ .review-evidence-row { display: grid; grid-template-columns: 4.5rem minmax(0, 1fr) auto; align-items: baseline; gap: var(--space-2); inline-size: 100%; min-block-size: var(--space-7); padding: var(--space-2) 0; border: 0; border-bottom: 1px solid var(--line); background: none; color: var(--ink); font: inherit; font-size: var(--text-sm); text-align: start; cursor: pointer; }
3249
+ .review-evidence li:last-child .review-evidence-row { border-bottom: 0; }
3250
+ .review-evidence-row:hover { background: var(--surface-2); }
3251
+ .review-evidence-kind { color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3252
+ .review-evidence-text { display: flex; flex-wrap: wrap; gap: 0 var(--space-2); min-inline-size: 0; overflow-wrap: anywhere; }
3253
+ .review-evidence-text code { color: var(--muted); font-family: var(--font-mono); font-size: var(--text-xs); }
3254
+ /* "Try it": scenarios the agent offered, each opening the live preview in that state. */
3255
+ .review-try { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); margin: var(--space-3) var(--space-3) 0; }
3256
+ .review-try-label { margin-inline-end: var(--space-1); color: var(--muted); font-size: var(--text-sm); }
3257
+ .review-try .btn { min-block-size: var(--space-7); }
3246
3258
  .review-status { margin: var(--space-2) var(--space-3) 0; color: var(--muted); font-size: var(--text-sm); }
3247
3259
  .review-actions { display: flex; flex-wrap: wrap; gap: var(--space-1); padding: var(--space-3); }
3248
3260
  .review-actions .btn { min-block-size: var(--space-7); }
@@ -3274,6 +3286,64 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3274
3286
  .apps-note { display: flex; flex-direction: column; gap: 2px; padding-inline-start: var(--space-3); border-inline-start: 2px solid var(--line-strong); }
3275
3287
  .apps-note-text { color: var(--ink); font-size: var(--text-sm); }
3276
3288
  .apps-notes-actions { display: flex; gap: var(--space-2); }
3289
+ /* Backend views (BackendView.tsx): Requests, Data and Logs. Rows reuse the
3290
+ Apps sheet's density; answers and rows are the app's own text, in mono. */
3291
+ .backend-view { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3) var(--space-4) var(--space-5); }
3292
+ .backend-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
3293
+ .backend-source, .backend-meta { color: var(--muted); font-size: var(--text-xs); overflow-wrap: anywhere; }
3294
+ .backend-meta code, .backend-foot code, .backend-row-sub { font-family: var(--font-mono); }
3295
+ .backend-back { align-self: flex-start; }
3296
+ .backend-group { display: flex; flex-direction: column; gap: var(--space-2); }
3297
+ .backend-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
3298
+ .backend-row { display: flex; align-items: center; gap: var(--space-2); inline-size: 100%; min-block-size: var(--space-7); box-sizing: border-box; padding: var(--space-2) var(--space-3); border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); color: var(--ink); font: inherit; text-align: start; cursor: pointer; }
3299
+ .backend-row:hover { background: var(--surface-2); }
3300
+ .backend-method { flex: none; min-inline-size: 3.25rem; font-family: var(--font-mono); font-size: var(--text-xs); font-weight: var(--weight-bold); color: var(--accent); }
3301
+ .backend-method[data-method="GET"], .backend-method[data-method="HEAD"] { color: var(--ok); }
3302
+ .backend-method[data-method="DELETE"] { color: var(--danger); }
3303
+ .backend-row-text { display: flex; flex-direction: column; flex: 1; min-inline-size: 0; gap: 2px; }
3304
+ .backend-row-name { font-size: var(--text-sm); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
3305
+ .backend-row-sub { color: var(--muted); font-size: var(--text-xs); overflow-wrap: anywhere; }
3306
+ .backend-foot, .backend-empty { margin: 0; color: var(--muted); font-size: var(--text-xs); }
3307
+ .backend-empty { font-size: var(--text-sm); }
3308
+ .backend-detail-head { display: flex; align-items: flex-start; gap: var(--space-2); }
3309
+ .backend-status { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
3310
+ .backend-switch { align-self: flex-start; }
3311
+ .backend-body { margin: 0; max-block-size: 50vh; overflow: auto; padding: var(--space-3); border-radius: var(--radius-md); background: var(--surface-2); color: var(--ink); font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
3312
+ .backend-request summary { color: var(--muted); font-size: var(--text-sm); cursor: pointer; margin-block-end: var(--space-2); }
3313
+ .backend-changes { list-style: none; margin: 0; padding: 0; border-radius: var(--radius-md); background: var(--surface-2); font-family: var(--font-mono); font-size: var(--text-xs); }
3314
+ .backend-changes li { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2); padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
3315
+ .backend-changes li:last-child { border-bottom: 0; }
3316
+ .backend-path { color: var(--muted); }
3317
+ .backend-was { color: var(--del); text-decoration: line-through; }
3318
+ .backend-now { color: var(--add); }
3319
+ .backend-gone { color: var(--del); }
3320
+ .backend-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
3321
+ .backend-actions .btn { min-block-size: var(--space-7); }
3322
+ .backend-query { display: flex; flex-direction: column; gap: var(--space-2); padding-block-start: var(--space-3); border-top: 1px solid var(--line); }
3323
+ .backend-query-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
3324
+ .backend-query-head h3 { margin: 0; font-size: var(--text-base); }
3325
+ .backend-counts { font-family: var(--font-mono); font-size: var(--text-xs); font-weight: var(--weight-semibold); }
3326
+ .backend-counts [data-tone="ok"] { color: var(--ok); } .backend-counts [data-tone="warn"] { color: var(--warn); } .backend-counts [data-tone="danger"] { color: var(--danger); }
3327
+ .backend-rowcard { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--line); border-inline-start: 3px solid var(--line-strong); border-radius: var(--radius-md); }
3328
+ .backend-rowcard[data-kind="added"] { border-inline-start-color: var(--ok); }
3329
+ .backend-rowcard[data-kind="changed"] { border-inline-start-color: var(--warn); }
3330
+ .backend-rowcard[data-kind="removed"] { border-inline-start-color: var(--danger); }
3331
+ .backend-fields { display: grid; grid-template-columns: minmax(5rem, max-content) minmax(0, 1fr); gap: 2px var(--space-3); margin: 0; font-size: var(--text-xs); }
3332
+ .backend-fields div { display: contents; }
3333
+ .backend-fields dt { color: var(--muted); }
3334
+ .backend-fields dd { margin: 0; font-family: var(--font-mono); overflow-wrap: anywhere; }
3335
+ .backend-table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-md); }
3336
+ .backend-table { border-collapse: collapse; inline-size: 100%; font-size: var(--text-xs); }
3337
+ .backend-table th, .backend-table td { padding: var(--space-1) var(--space-2); border-bottom: 1px solid var(--line); text-align: start; white-space: nowrap; }
3338
+ .backend-table th { position: sticky; top: 0; background: var(--surface-2); color: var(--muted); font-weight: var(--weight-semibold); }
3339
+ .backend-table td { font-family: var(--font-mono); }
3340
+ .backend-log { list-style: none; margin: 0; padding: var(--space-2) 0; max-block-size: 60vh; overflow: auto; border-radius: var(--radius-md); background: var(--surface-2); font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1.6; }
3341
+ .backend-line { padding: 1px var(--space-3); white-space: pre-wrap; overflow-wrap: anywhere; }
3342
+ .backend-line[data-level="error"] { background: var(--danger-soft); }
3343
+ .backend-line[data-level="warn"] { color: var(--warn); }
3344
+ .backend-line-time { color: var(--muted); }
3345
+ .backend-mark { display: flex; align-items: center; gap: var(--space-2); margin: var(--space-2) 0; padding: 0 var(--space-3); color: var(--accent); font-family: var(--font-sans); font-weight: var(--weight-semibold); scroll-margin-block-start: var(--space-2); }
3346
+ .backend-mark::before, .backend-mark::after { content: ""; flex: 1; block-size: 1px; background: color-mix(in srgb, var(--accent) 40%, var(--line)); }
3277
3347
  .apps-empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); padding: var(--space-6) var(--space-4); color: var(--muted); font-size: var(--text-sm); text-align: center; }
3278
3348
  .apps-empty strong { color: var(--ink); font-size: var(--text-base); }
3279
3349
  .apps-empty p { margin: 0; max-inline-size: 34ch; }
@@ -1,6 +1,12 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
3
  export const REVIEW_CARD_MODES = ["ready", "every", "off"];
4
+ const isEvidence = (row) => {
5
+ const r = row;
6
+ return !!r && typeof r.viewId === "string" && typeof r.view === "string" && ["requests", "data", "logs"].includes(r.backend)
7
+ && typeof r.summary === "string" && r.summary.length <= 300 && (r.detail === undefined || (typeof r.detail === "string" && r.detail.length <= 300))
8
+ && ["ok", "warn", "danger", "neutral"].includes(r.tone) && (r.item === undefined || typeof r.item === "string");
9
+ };
4
10
  export const APP_REVIEW_BLOCK = "bivy_app_review";
5
11
  const isShot = (value) => {
6
12
  const shot = value;
@@ -15,7 +21,9 @@ export function isAppReview(value) {
15
21
  && (review.trigger === "present" || review.trigger === "run" || review.trigger === "asked" || review.trigger === "notes") && typeof review.at === "number"
16
22
  && (review.shot === undefined || isShot(review.shot)) && (review.before === undefined || isShot(review.before))
17
23
  && (review.note === undefined || typeof review.note === "string")
18
- && (review.notes === undefined || (Number.isInteger(review.notes) && review.notes >= 0));
24
+ && (review.notes === undefined || (Number.isInteger(review.notes) && review.notes >= 0))
25
+ && (review.evidence === undefined || (Array.isArray(review.evidence) && review.evidence.length <= 12 && review.evidence.every(isEvidence)))
26
+ && (review.try === undefined || (Array.isArray(review.try) && review.try.length <= 6 && review.try.every((item) => !!item && typeof item.id === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(item.id) && typeof item.name === "string" && item.name.length <= 80)));
19
27
  }
20
28
  export const APP_PIN_STATES = ["open", "changed", "gone", "done"];
21
29
  export const APP_PIN_BLOCK = "bivy_app_pin";
@@ -9,6 +9,7 @@ export function createAppCommands(service, workspaceFor, published = () => { },
9
9
  throw new Error("Open the session on this machine before publishing an app.");
10
10
  return workspace;
11
11
  };
12
+ const pick = (msg) => ({ appId: typeof msg.appId === "string" ? msg.appId : undefined, viewId: typeof msg.viewId === "string" ? msg.viewId : undefined, target: typeof msg.target === "string" ? msg.target : undefined });
12
13
  const operations = {
13
14
  "apps.list": (msg) => service.list(typeof msg.sessionId === "string" ? msg.sessionId : undefined),
14
15
  "apps.publish": (msg) => {
@@ -25,11 +26,12 @@ export function createAppCommands(service, workspaceFor, published = () => { },
25
26
  published(app);
26
27
  return { app };
27
28
  },
28
- "apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined, msg.direct === true, typeof msg.scale === "number" ? msg.scale : undefined, typeof msg.path === "string" ? msg.path : undefined),
29
+ "apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined, msg.direct === true, typeof msg.scale === "number" ? msg.scale : undefined, typeof msg.path === "string" ? msg.path : undefined, typeof msg.scenario === "string" ? msg.scenario : undefined),
29
30
  "apps.shot": (msg) => service.shot(String(msg.sessionId), typeof msg.appId === "string" ? msg.appId : undefined, { widths: msg.widths, themes: msg.themes, path: typeof msg.path === "string" ? msg.path : undefined }),
30
31
  "apps.input": (msg) => service.act(String(msg.sessionId), typeof msg.target === "string" ? msg.target : undefined, msg.action),
31
32
  "apps.menu": (msg) => service.menu(String(msg.sessionId), typeof msg.target === "string" ? msg.target : undefined, msg.path),
32
- "apps.present": (msg) => service.present(String(msg.sessionId), { target: typeof msg.target === "string" ? msg.target : undefined, path: typeof msg.path === "string" ? msg.path : undefined, note: typeof msg.note === "string" ? msg.note : undefined }),
33
+ "apps.present": (msg) => service.present(String(msg.sessionId), { target: typeof msg.target === "string" ? msg.target : undefined, path: typeof msg.path === "string" ? msg.path : undefined, note: typeof msg.note === "string" ? msg.note : undefined, try: Array.isArray(msg.try) ? msg.try.filter((id) => typeof id === "string") : undefined }),
34
+ "apps.scenarios": (msg) => service.scenarios(String(msg.sessionId), typeof msg.target === "string" ? msg.target : undefined),
33
35
  "apps.showMe": (msg) => service.present(String(msg.sessionId), { target: typeof msg.appId === "string" ? msg.appId : undefined, trigger: "asked" }),
34
36
  "apps.mute": (msg) => service.mute(String(msg.sessionId)),
35
37
  "apps.reviewMode": (msg) => service.setReviewMode(String(msg.sessionId), String(msg.appId), msg.mode),
@@ -42,6 +44,15 @@ export function createAppCommands(service, workspaceFor, published = () => { },
42
44
  "apps.agentNotes": (msg) => service.setAgentNotes(String(msg.sessionId), String(msg.appId), msg.enabled === true),
43
45
  "apps.notes": (msg) => service.notes(String(msg.sessionId), { app: typeof msg.appId === "string" ? msg.appId : undefined, view: typeof msg.view === "string" ? msg.view : undefined, since: typeof msg.since === "number" ? msg.since : undefined }),
44
46
  "apps.logs": (msg) => service.logs(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
47
+ // Backend views. The app passes exact IDs; `bivy app requests|data|logs` a name, or nothing.
48
+ "apps.requests": (msg) => service.backend.requests(service.backendView(String(msg.sessionId), "requests", pick(msg))),
49
+ "apps.request": (msg) => service.backend.detail(service.backendView(String(msg.sessionId), "requests", pick(msg)), String(msg.id)),
50
+ "apps.runRequest": (msg) => {
51
+ const entry = service.backendView(String(msg.sessionId), "requests", pick(msg));
52
+ return typeof msg.id === "string" ? service.backend.run(entry, msg.id) : service.backend.runAuto(entry, "last", "Ran all requests");
53
+ },
54
+ "apps.data": (msg) => service.backend.data(service.backendView(String(msg.sessionId), "data", pick(msg)), msg.run === true),
55
+ "apps.serverLog": (msg) => service.backend.log(service.backendView(String(msg.sessionId), "logs", pick(msg)), typeof msg.since === "number" ? msg.since : 0),
45
56
  // The app UI passes exact IDs; `bivy app share` passes an app and/or view by ID or name.
46
57
  "apps.share": (msg) => {
47
58
  const options = { duration: msg.duration, controls: msg.controls !== false };
package/dist/exec.js CHANGED
@@ -174,7 +174,10 @@ async function main() {
174
174
  const model = created.model;
175
175
  if (args.model && model?.id) {
176
176
  const qualified = !model.provider || model.id.startsWith(`${model.provider}/`) ? model.id : `${model.provider}/${model.id}`;
177
- if (![model.id, qualified].some((id) => id === args.model || id.endsWith(`/${args.model}`))) {
177
+ // Compare model names: the same model can sit under another provider
178
+ // spelling in this agent (openai-codex/gpt-5.6-sol is OpenCode's openai/gpt-5.6-sol).
179
+ const name = (id) => id.slice(id.lastIndexOf("/") + 1);
180
+ if (![model.id, qualified].some((id) => id === args.model || name(id) === name(args.model))) {
178
181
  err(`Model "${args.model}" is not available for this agent; using ${qualified}.\n`);
179
182
  }
180
183
  }
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Imported first by server.ts, so the node's environment is clean before any
4
+ // module reads it or launches an agent (see runtime/inherited-session-env.ts).
5
+ import { scrubInheritedSessionEnv } from "./runtime/inherited-session-env.js";
6
+ scrubInheritedSessionEnv();
@@ -3,7 +3,7 @@
3
3
  /** Direct-mode adapters for operations whose canonical implementation is the
4
4
  * client command registry. Relay and HTTP now differ only in framing. */
5
5
  export const CLIENT_COMMAND_ROUTES = [
6
- ...["list", "publish", "offers", "adopt", "open", "logs", "shot", "input", "menu", "present", "showMe", "mute", "reviewMode", "annotate", "clearNotes", "agentNotes", "notes", "share", "unshare", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
6
+ ...["list", "publish", "offers", "adopt", "open", "logs", "requests", "request", "runRequest", "data", "serverLog", "shot", "input", "menu", "present", "scenarios", "showMe", "mute", "reviewMode", "annotate", "clearNotes", "agentNotes", "notes", "share", "unshare", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
7
7
  { method: "post", path: "/api/artifacts/list", kind: "artifacts.list" },
8
8
  { method: "post", path: "/api/access/get", kind: "access.get" },
9
9
  { method: "post", path: "/api/tailnet/machines", kind: "tailnet.machines" },
@@ -4,6 +4,8 @@ import { Type } from "typebox";
4
4
  import { SHARE_DURATIONS } from "../apps/types.js";
5
5
  const request = { requestId: Type.Optional(Type.String()) };
6
6
  const session = { sessionId: Type.String() };
7
+ /** A backend view by exact IDs (the app), or by app or view name (`bivy app requests`). */
8
+ const backendView = { appId: Type.Optional(Type.String({ maxLength: 200 })), viewId: Type.Optional(Type.String({ maxLength: 200 })), target: Type.Optional(Type.String({ maxLength: 200 })) };
7
9
  const optionalSession = { sessionId: Type.Optional(Type.String()) };
8
10
  /** Declarative validation at the client-command boundary. A command absent from
9
11
  * this table remains migration-compatible; adding validation is one data row. */
@@ -17,11 +19,12 @@ export const CLIENT_COMMAND_SCHEMAS = {
17
19
  "apps.publish": Type.Object({ ...request, ...session, manifest: Type.Unknown() }),
18
20
  "apps.offers": Type.Object({ ...request, ...optionalSession }),
19
21
  "apps.adopt": Type.Object({ ...request, ...session, port: Type.Integer({ minimum: 1024, maximum: 65535 }) }),
20
- "apps.open": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), returnTo: Type.Optional(Type.String({ maxLength: 2048 })), direct: Type.Optional(Type.Boolean()), scale: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })), path: Type.Optional(Type.String({ maxLength: 2048 })) }),
22
+ "apps.open": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), returnTo: Type.Optional(Type.String({ maxLength: 2048 })), direct: Type.Optional(Type.Boolean()), scale: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })), path: Type.Optional(Type.String({ maxLength: 2048 })), scenario: Type.Optional(Type.String({ maxLength: 64 })) }),
21
23
  "apps.shot": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String()), widths: Type.Optional(Type.Array(Type.Integer(), { maxItems: 4 })), themes: Type.Optional(Type.Array(Type.Union([Type.Literal("light"), Type.Literal("dark")]), { maxItems: 2 })), path: Type.Optional(Type.String({ maxLength: 2048 })) }),
22
24
  "apps.input": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), action: Type.Object({ kind: Type.String({ maxLength: 20 }) }) }),
23
25
  "apps.menu": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), path: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 200 }), { minItems: 1, maxItems: 8 })) }),
24
- "apps.present": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), path: Type.Optional(Type.String({ maxLength: 2048 })), note: Type.Optional(Type.String({ maxLength: 2000 })) }),
26
+ "apps.present": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), path: Type.Optional(Type.String({ maxLength: 2048 })), note: Type.Optional(Type.String({ maxLength: 2000 })), try: Type.Optional(Type.Array(Type.String({ maxLength: 64 }), { maxItems: 6 })) }),
27
+ "apps.scenarios": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })) }),
25
28
  "apps.showMe": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String()) }),
26
29
  "apps.mute": Type.Object({ ...request, ...session }),
27
30
  "apps.reviewMode": Type.Object({ ...request, ...session, appId: Type.String(), mode: Type.Union([Type.Literal("ready"), Type.Literal("every"), Type.Literal("off")]) }),
@@ -35,6 +38,11 @@ export const CLIENT_COMMAND_SCHEMAS = {
35
38
  "apps.agentNotes": Type.Object({ ...request, ...session, appId: Type.String(), enabled: Type.Boolean() }),
36
39
  "apps.notes": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), view: Type.Optional(Type.String({ maxLength: 200 })), since: Type.Optional(Type.Number()) }),
37
40
  "apps.logs": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
41
+ "apps.requests": Type.Object({ ...request, ...session, ...backendView }),
42
+ "apps.request": Type.Object({ ...request, ...session, ...backendView, id: Type.String({ maxLength: 200 }) }),
43
+ "apps.runRequest": Type.Object({ ...request, ...session, ...backendView, id: Type.Optional(Type.String({ maxLength: 200 })) }),
44
+ "apps.data": Type.Object({ ...request, ...session, ...backendView, run: Type.Optional(Type.Boolean()) }),
45
+ "apps.serverLog": Type.Object({ ...request, ...session, ...backendView, since: Type.Optional(Type.Number()) }),
38
46
  "apps.share": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), viewId: Type.Optional(Type.String()), view: Type.Optional(Type.String({ maxLength: 200 })),
39
47
  duration: Type.Optional(Type.Union(SHARE_DURATIONS.map((row) => Type.Literal(row.id)))), controls: Type.Optional(Type.Boolean()) }),
40
48
  "apps.unshare": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),