|
11 | 11 | // `{log, error, warn, info}` API as the real console. |
12 | 12 | // standard JS globals. |
13 | 13 | // |
14 | | -// Nothing is auto-loaded. To reuse code from a previous snippet the agent |
15 | | -// writes plain `await import("/abs/path/foo.ts?t=" + Date.now())` against a |
16 | | -// `.ts` file it owns under `<projectDir>/.bcode/agent-workspace/`. Same |
17 | | -// mechanism for a 5-line wrapper and a 500-line scrape script. The Level-2 |
18 | | -// wrapper supplies `ctx.workspaceDir` so `.ts` files written under it can be |
19 | | -// addressed by absolute path; this resolver creates the dir on first use. |
| 14 | +// When BU_CDP_WS or BU_CDP_URL binds the process to a provisioned browser, |
| 15 | +// the tool connects and attaches its existing page before running a snippet. |
| 16 | +// Local sessions keep explicit connection behavior. To reuse code from a |
| 17 | +// previous snippet the agent writes plain |
| 18 | +// `await import("/abs/path/foo.ts?t=" + Date.now())` against a `.ts` file it |
| 19 | +// owns under `<projectDir>/.bcode/agent-workspace/`. Same mechanism for a |
| 20 | +// 5-line wrapper and a 500-line scrape script. The Level-2 wrapper supplies |
| 21 | +// `ctx.workspaceDir` so `.ts` files written under it can be addressed by |
| 22 | +// absolute path; this resolver creates the dir on first use. |
20 | 23 | // |
21 | 24 | // Output capture: a per-call `console` object (`{log, error, warn, info}`) |
22 | 25 | // is bound into the snippet's lexical scope as the second AsyncFunction |
@@ -53,6 +56,8 @@ const DEFAULT_TIMEOUT_MS = 60 * 1000 |
53 | 56 | const MAX_TIMEOUT_MS = 10 * 60 * 1000 |
54 | 57 | const MAX_TIMEOUT_OUTPUT_BYTES = 8 * 1024 |
55 | 58 | const TIMEOUT_OUTPUT_TRUNCATED = "[partial console output truncated; showing final bytes]\n" |
| 59 | +const v4Connections = new Map<string, Promise<void>>() |
| 60 | +const v4Bootstrapped = new Set<string>() |
56 | 61 |
|
57 | 62 | // Tail-cap the captured output for the timeout error: last 8 KiB, snapped |
58 | 63 | // forward to a UTF-8 sequence start so multibyte characters survive the cut. |
@@ -86,9 +91,7 @@ export type Parameters = Schema.Schema.Type<typeof parameters> |
86 | 91 |
|
87 | 92 | export interface ExecuteContext { |
88 | 93 | // Identifies the per-opencode-session CDP Session to bind into the snippet. |
89 | | - // The same Session is reused across calls — the agent calls |
90 | | - // `session.connect(...)` in one snippet and subsequent snippets find the |
91 | | - // already-connected Session. |
| 94 | + // Provisioned endpoints auto-connect and attach; local sessions connect explicitly. |
92 | 95 | readonly sessionID: string |
93 | 96 | // Per-project workspace dir: <projectDir>/.bcode/agent-workspace/. Created |
94 | 97 | // on first call. The agent reads/writes/edits .ts files here via the |
@@ -159,9 +162,8 @@ const serialize = (v: unknown): string => { |
159 | 162 | } |
160 | 163 |
|
161 | 164 | // Snippet executor. The CDP Session is resolved per-call from `SessionStore` |
162 | | -// keyed on `ctx.sessionID`. The agent connects with `await session.connect(...)` |
163 | | -// in one snippet (Way 1 / Way 2 / Way 3 in skills/browser-execute/SKILL.md); the Session persists |
164 | | -// for follow-up snippets in the same opencode session. |
| 165 | +// keyed on `ctx.sessionID`. Provisioned endpoints auto-connect and attach |
| 166 | +// before the snippet; local sessions connect explicitly. |
165 | 167 | // |
166 | 168 | // `dataDir` is opencode's XDG_DATA_HOME for bcode (~/.local/share/bcode/ on |
167 | 169 | // Linux/Mac). Compiled-mode skills are extracted to `<dataDir>/skills/` once |
@@ -189,6 +191,11 @@ export const make = Effect.fn("BrowserExecute.make")(function* (dataDir: string) |
189 | 191 | catch: (err) => new Error(`syntax error in browser_execute snippet: ${err}`), |
190 | 192 | }) |
191 | 193 |
|
| 194 | + yield* Effect.tryPromise({ |
| 195 | + try: () => ensureCloudConnected(ctx.sessionID, session), |
| 196 | + catch: (err) => (err instanceof Error ? err : new Error(String(err))), |
| 197 | + }) |
| 198 | + |
192 | 199 | const tee = (...a: unknown[]) => { |
193 | 200 | if (!captured.active) return |
194 | 201 | captured.output += a.map((x) => (typeof x === "string" ? x : serialize(x))).join(" ") + "\n" |
@@ -277,4 +284,32 @@ export const make = Effect.fn("BrowserExecute.make")(function* (dataDir: string) |
277 | 284 | return { parameters, execute, skillsDir } |
278 | 285 | }) |
279 | 286 |
|
| 287 | +async function ensureCloudConnected(sessionID: string, session: ReturnType<typeof SessionStore.get>) { |
| 288 | + if (!process.env.V4_RUN_ID || (!process.env.BU_CDP_WS && !process.env.BU_CDP_URL)) return |
| 289 | + if (v4Bootstrapped.has(sessionID)) return |
| 290 | + |
| 291 | + const existing = v4Connections.get(sessionID) |
| 292 | + if (existing) return existing |
| 293 | + |
| 294 | + const connecting = (async () => { |
| 295 | + if (!session.isConnected()) await session.connect() |
| 296 | + if (session.getActiveSession()) return |
| 297 | + const page = (await session.domains.Target.getTargets({})).targetInfos.find( |
| 298 | + (target) => target.type === "page" && !target.url.startsWith("chrome://"), |
| 299 | + ) |
| 300 | + if (page) await session.use(page.targetId) |
| 301 | + })() |
| 302 | + v4Connections.set(sessionID, connecting) |
| 303 | + try { |
| 304 | + await connecting |
| 305 | + } finally { |
| 306 | + // One automatic attempt per logical BrowserCode session. A later disconnect |
| 307 | + // (including after timeout replacement) must be surfaced: |
| 308 | + // BU_CDP_WS is the browser selected at run start, not necessarily a newer |
| 309 | + // browser the agent explicitly switched to during this run. |
| 310 | + v4Bootstrapped.add(sessionID) |
| 311 | + if (v4Connections.get(sessionID) === connecting) v4Connections.delete(sessionID) |
| 312 | + } |
| 313 | +} |
| 314 | + |
280 | 315 | export * as BrowserExecute from "./browser-execute" |
0 commit comments