@bivy/bivy 0.20.7-staging.2 → 0.20.7-staging.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bivy.mjs +100 -5
- package/bin/codex-app-server-shim.mjs +78 -27
- package/dist/agent-instructions.js +2 -1
- package/dist/agents/claude-code/runtime.js +2 -1
- package/dist/agents/profiles.js +9 -6
- package/dist/apps/backend/compare.js +62 -0
- package/dist/apps/backend/http-file.js +66 -0
- package/dist/apps/backend/index.js +431 -0
- package/dist/apps/backend/logs.js +60 -0
- package/dist/apps/backend/rows.js +79 -0
- package/dist/apps/gateway.js +121 -3
- package/dist/apps/inspector.js +56 -1
- package/dist/apps/preview-shell.js +221 -9
- package/dist/apps/registry.js +60 -2
- package/dist/apps/scenario-proxy.js +65 -0
- package/dist/apps/scenarios.js +307 -0
- package/dist/apps/service.js +147 -8
- package/dist/apps/styles.css +70 -0
- package/dist/apps/types.js +9 -1
- package/dist/controllers/app-commands.js +13 -2
- package/dist/exec.js +4 -1
- package/dist/node-env.js +6 -0
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +10 -2
- package/dist/runtime/codex-sessions.js +158 -6
- package/dist/runtime/credential-provisioning.js +5 -0
- package/dist/runtime/grok-sessions.js +4 -2
- package/dist/runtime/index.js +26 -19
- package/dist/runtime/inherited-session-env.js +54 -0
- package/dist/runtime/model-ref.js +14 -1
- package/dist/runtime/opencode-auth.js +67 -0
- package/dist/runtime/opencode-preflight.js +10 -1
- package/dist/runtime/protocol.js +35 -1
- package/dist/runtime/tool-call-map.js +67 -3
- package/dist/server.js +51 -16
- package/dist/session/event-log.js +76 -5
- package/dist/session/fork-standup.js +1 -1
- package/dist/session/fork.js +6 -4
- package/dist/session/transcript-normal.js +52 -0
- package/dist/session/transcript-persistence.js +20 -3
- package/dist/session-ref.js +5 -2
- package/dist/terminal.js +17 -0
- package/dist/worktree.js +25 -3
- package/package.json +1 -1
- package/web/assets/{AutomationsView-CJSYk0ja.js → AutomationsView-DJibSXQw.js} +1 -1
- package/web/assets/{ChangesCard-Bp88fY6W.js → ChangesCard-s8Qtm7eu.js} +1 -1
- package/web/assets/{LibraryView-of9wmuzN.js → LibraryView-Bz8x-H8N.js} +2 -2
- package/web/assets/{ProviderConnect-DG7SP4l2.js → ProviderConnect-DZ-Rv8Aj.js} +1 -1
- package/web/assets/{Settings-DGH1U7wj.js → Settings-DMUdD3O5.js} +3 -3
- package/web/assets/{Sheet-D-EHCMlj.js → Sheet-2wuqvpxm.js} +1 -1
- package/web/assets/{Terminal-qqysrwl_.js → Terminal-DQeYK2KK.js} +1 -1
- package/web/assets/{VoiceSettings-aeqQkwcZ.js → VoiceSettings-BbqFD1Ee.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-Ct-lmBn5.js → abnfDiagram-VCTEODGH-DLSpnM9y.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-CefLtGFG.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-CCq716Da.js → architectureDiagram-5GKGNRK7-DOQxmFrp.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-Lka9F6ZB.js → blockDiagram-I7D4REHJ-BiCBLtEP.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-nvhhyzri.js → c4Diagram-7LVT6UL2-yzmqXo9k.js} +1 -1
- package/web/assets/channel-kKuds40w.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-TZ5hhHBz.js → chunk-4HAMMTFA-i-QksGpD.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-IahKw8kM.js → chunk-75Z2AOVW-DWjggoX1.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-BlcZUVOy.js → chunk-DU6HZSFF-jFwsC9ox.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-3MGfEgKu.js → chunk-F27PBJKO-3ej2B9gg.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-Be72n678.js → chunk-GMAD6QVW-CzfhoJ04.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-GOVLT6xm.js → chunk-GVQU2GXP-DLhbmKuJ.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-ByOzewrT.js → chunk-IMKFNOWR-dR96ErOJ.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-BCGhHmnW.js → chunk-L3NEJ4N5-1YX2kWHP.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-CAdliJE_.js → chunk-OSK3NFVY-kDveSID9.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-CM8cpgHa.js → chunk-P2QGCYS3-Cc3Uo4HQ.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CXLPhaI1.js → chunk-POPQ4Y6H-CxDXuH1x.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BegFrwDe.js → chunk-PWAF6VOD-DZBN6HSl.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-iXj7Y5RI.js → chunk-SHT3W25Y-0W01Q-w7.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-73pyTBZg.js → chunk-SVP7TREG-gqFFZT4I.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-D5zwuuGI.js → chunk-TICWLB2K-BGdM7dkO.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-Dg-2WnlQ.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-Dg-2WnlQ.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-D3nRr9S_.js → cynefin-OW5HDTMX-D3KvEku4.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-C4CKMYMZ.js → cynefinDiagram-5FMLGOSQ-CLbDBHX9.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-D-grkZ8X.js → dagre-GXQ25YYZ-D2kdt2lo.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-BNPS3YQX.js → diagram-S7CK7UJ4-a-ev_USx.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-Bkp_TyRg.js → diagram-UQ7AKVKN-BwYRbF-y.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-Dpxe_6MM.js → diagram-VSXAHHWV-Da7O55Aq.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DwGBvGn-.js → diagram-VX7I27RA-Bcq-P_8M.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-D-KvONMO.js → diagram-Z3DM3KII-DSXXMWvA.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-Di_D-a-0.js → ebnfDiagram-PWID7BFC-DxFerrqi.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-2pnBfTU9.js → erDiagram-RLTQ6QDP-DfyXAe2v.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-tdyiDQoh.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-BO23Cpex.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-DAkhMTlk.js → ganttDiagram-EL5Y4UJY-QkgrgMh9.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-BQvemX2W.js → gitGraph-4MIJSDKK-M1n5msce.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-Dx7ya_2q.js → gitGraphDiagram-WWUBYQGX-DC_8Cxet.js} +1 -1
- package/web/assets/{index-CFX8FhqJ.js → index-B0Bto4kI.js} +2 -2
- package/web/assets/{index-BXPRG7Pj.css → index-Da2-4YHh.css} +1 -1
- package/web/assets/{info-A6RAGUB7-DaDps2sq.js → info-A6RAGUB7-DmyYG6Ip.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-DBqMvPr2.js → infoDiagram-27XIBGKW-CmrMjsqT.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-DzciF3O_.js → ishikawaDiagram-5VMMS53U-x_6jWkNq.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-D9WHgGe3.js → journeyDiagram-3NMN7TZE-COVPGUIl.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-Diokj8By.js → kanban-definition-UXKFOSKX-BDZHzeOz.js} +1 -1
- package/web/assets/{line-DmBeTXFn.js → line-KdqoiWjk.js} +1 -1
- package/web/assets/{mermaid-parser.core-H_O1CweI.js → mermaid-parser.core-C5RHAle0.js} +3 -3
- package/web/assets/{mermaid.core-BAni15Ed.js → mermaid.core-2ujtWQt3.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-B_tXdzEa.js → mindmap-definition-YA3MSWOX-CCPoAcT0.js} +1 -1
- package/web/assets/mount-CaFlQTsV.js +30 -0
- package/web/assets/{packet-AYTQ26CC-Dt4bPH7I.js → packet-AYTQ26CC-BjPan2xl.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-loAbaKUZ.js → pegDiagram-XKGWAZYB-CrARxusS.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-DEmhJN7M.js → pie-WAS4IAKB-BdVo8hLO.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-DfQvD2QY.js → pieDiagram-E7YTZNPT-DwdhmBCX.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-Bhqo2rT-.js → quadrantDiagram-AXDQQJYC-DbG3R6xw.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BmV5tXie.js → radar-RG4KPBEZ-pp-coSgM.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-DoebyExe.js → railroad-74A4TZTK-CxNBhSbO.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-g5eNqQ81.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-BI5_cD5r.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-CLl7RlXh.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-BhlfIpCW.js → railroadDiagram-O6MQD6OU-CKbKBwCj.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-vkxH8U-2.js → requirementDiagram-BXWQKSXE-DRc316xB.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-D2iFl0bc.js → sankeyDiagram-P5KCCOFB-C6O9zcIS.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-Ll_m9w0-.js → sequenceDiagram-WJ2MYXX4-D3Vz1DcK.js} +1 -1
- package/web/assets/{settingsRoute-C8p10bxH.js → settingsRoute-DxPtfqmn.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-B4KWuRz2.js → stateDiagram-D77RDMKH-DXcUXP-l.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-CQER-whG.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-CVXwQL4b.js → swimlanes-42K2YHIH-tfw_N04-.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-Cf__qhQx.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-C0XuGVkJ.js → timeline-definition-24CTP7MA-BcAeG9F_.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-BS4edB5m.js → treeView-Q6P3EWNA-DaZCFJDf.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-BZ54DPA4.js → treemap-WGGIJYW6-C3PybXBI.js} +1 -1
- package/web/assets/{useStore-DEfLrrMh.js → useStore-Ce5mzYro.js} +9 -7
- package/web/assets/{vennDiagram-4TSXK5OY-CAKXCk3I.js → vennDiagram-4TSXK5OY-BT7w_l5a.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-DM5IZXxI.js → wardley-WFR3VGLG-CXZlxUiB.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4--WWmycX2.js → wardleyDiagram-VM6X3IG4-B5muPnst.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-D-_SgAQy.js → xychartDiagram-S5SC5T6Z-DSaMhJlg.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/web/assets/architecture-7GRP2DOG-CD4t1j2T.js +0 -1
- package/web/assets/channel-Dl0Aa963.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BvMwIvP0.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BvMwIvP0.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-CF677pnK.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-8SiMcM5n.js +0 -1
- package/web/assets/mount-Fdy9Xoxs.js +0 -27
- package/web/assets/railroad-abnf-HS5TGJTU-C_F3QWV5.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-BaNfyzXp.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-BWyMWwxG.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-B_Xx-QG6.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-XYt-UA-z.js +0 -8
package/dist/apps/service.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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);
|
package/dist/apps/styles.css
CHANGED
|
@@ -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; }
|
package/dist/apps/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/node-env.js
ADDED
|
@@ -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() }),
|