|
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,7 @@ 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 cloudConnections = new WeakMap<ReturnType<typeof SessionStore.get>, Promise<void>>() |
56 | 60 |
|
57 | 61 | // Tail-cap the captured output for the timeout error: last 8 KiB, snapped |
58 | 62 | // forward to a UTF-8 sequence start so multibyte characters survive the cut. |
@@ -86,9 +90,7 @@ export type Parameters = Schema.Schema.Type<typeof parameters> |
86 | 90 |
|
87 | 91 | export interface ExecuteContext { |
88 | 92 | // 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. |
| 93 | + // Provisioned endpoints auto-connect and attach; local sessions connect explicitly. |
92 | 94 | readonly sessionID: string |
93 | 95 | // Per-project workspace dir: <projectDir>/.bcode/agent-workspace/. Created |
94 | 96 | // on first call. The agent reads/writes/edits .ts files here via the |
@@ -159,9 +161,8 @@ const serialize = (v: unknown): string => { |
159 | 161 | } |
160 | 162 |
|
161 | 163 | // 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. |
| 164 | +// keyed on `ctx.sessionID`. Provisioned endpoints auto-connect and attach |
| 165 | +// before the snippet; local sessions connect explicitly. |
165 | 166 | // |
166 | 167 | // `dataDir` is opencode's XDG_DATA_HOME for bcode (~/.local/share/bcode/ on |
167 | 168 | // Linux/Mac). Compiled-mode skills are extracted to `<dataDir>/skills/` once |
@@ -189,6 +190,11 @@ export const make = Effect.fn("BrowserExecute.make")(function* (dataDir: string) |
189 | 190 | catch: (err) => new Error(`syntax error in browser_execute snippet: ${err}`), |
190 | 191 | }) |
191 | 192 |
|
| 193 | + yield* Effect.tryPromise({ |
| 194 | + try: () => ensureCloudConnected(session), |
| 195 | + catch: (err) => (err instanceof Error ? err : new Error(String(err))), |
| 196 | + }) |
| 197 | + |
192 | 198 | const tee = (...a: unknown[]) => { |
193 | 199 | if (!captured.active) return |
194 | 200 | captured.output += a.map((x) => (typeof x === "string" ? x : serialize(x))).join(" ") + "\n" |
@@ -277,4 +283,28 @@ export const make = Effect.fn("BrowserExecute.make")(function* (dataDir: string) |
277 | 283 | return { parameters, execute, skillsDir } |
278 | 284 | }) |
279 | 285 |
|
| 286 | +async function ensureCloudConnected(session: ReturnType<typeof SessionStore.get>) { |
| 287 | + if (!process.env.BU_CDP_WS && !process.env.BU_CDP_URL) return |
| 288 | + if (session.isConnected() && session.getActiveSession()) return |
| 289 | + |
| 290 | + const existing = cloudConnections.get(session) |
| 291 | + if (existing) return existing |
| 292 | + |
| 293 | + const connecting = (async () => { |
| 294 | + const connected = session.isConnected() |
| 295 | + if (!connected) await session.connect() |
| 296 | + if (connected && 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 | + cloudConnections.set(session, connecting) |
| 303 | + try { |
| 304 | + await connecting |
| 305 | + } finally { |
| 306 | + if (cloudConnections.get(session) === connecting) cloudConnections.delete(session) |
| 307 | + } |
| 308 | +} |
| 309 | + |
280 | 310 | export * as BrowserExecute from "./browser-execute" |
0 commit comments