Skip to content

Add persistent Browser REPL API - #368

Open
rgarcia wants to merge 28 commits into
mainfrom
rgarcia/browser-repl
Open

Add persistent Browser REPL API#368
rgarcia wants to merge 28 commits into
mainfrom
rgarcia/browser-repl

Conversation

@rgarcia

@rgarcia rgarcia commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

A persistent JavaScript control plane for every Kernel browser

This PR adds POST /repl: a stateful Browser REPL that lives alongside Chromium inside each browser VM.

Instead of sending a complete automation program on every call—or rebuilding state from serialized results—an agent can teach the browser instance reusable JavaScript once, call it incrementally, inspect rendered state, and keep going:

const browser = await kernel.browsers.create();

await browser.repl({
  code: `
    async function search(query) {
      await fillInput('input[aria-label="Search"]', query);
      await pressKey("ENTER");
      if (!await waitForElement("main", {state: "visible", timeoutSec: 15})) {
        throw new Error("results did not render");
      }
    }
  `,
});

await browser.repl({code: `await search("Kernel browsers")`});

The result is a compact, agent-native control surface with:

  • persistent JavaScript declarations and closures across calls
  • direct browser interaction without requiring a separate automation framework
  • exact-once page-function evaluation
  • semantic waits and actionability-aware input
  • unrestricted CDP, tab, target, iframe, filesystem, process, and network access
  • deterministic lifecycle and failure semantics

The API

POST /repl
Content-Type: application/json
{
  "code": "const info = await pageInfo(); repl.write(info.title)",
  "timeout_sec": 60,
  "reset": false
}

Successful responses identify the exact state-holding process and contain ordered, typed output:

{
  "success": true,
  "repl_id": "<cuid2>",
  "duration_ms": 12,
  "content_truncated": false,
  "content": [
    {
      "type": "text",
      "channel": "write",
      "text": "Example Domain"
    }
  ]
}

The endpoint is scoped to an individual browser instance, so the intended generated SDK surface is:

await browser.repl({code});

rather than exposing details of the instance transport.

Why persistence matters

Each request is evaluated as a fresh JavaScript module cell, while top-level bindings remain live across subsequent cells:

// Cell 1
let searches = 0;
const selectors = {
  query: 'input[aria-label="Search"]',
  results: "main",
};

async function runSearch(query) {
  searches++;
  await fillInput(selectors.query, query);
  await pressKey("ENTER");
  return waitForElement(selectors.results, {
    state: "visible",
    timeoutSec: 15,
  });
}
// Cell 2
const ready = await runSearch("cloud browser");
repl.write(JSON.stringify({ready, searches}));

The runtime preserves var, let, const, function, and class bindings; mutation; closures; timers; destructuring; function hoisting; TDZ behavior; and partial initialization semantics. Top-level await and dynamic import() are supported.

This is deliberately JavaScript-only. TypeScript, static imports/exports, and top-level return are rejected.

Browser control built in

Helpers are available as bare globals and through the frozen browser namespace:

await gotoUrl("https://example.com");
await browser.gotoUrl("https://example.com");

The surface covers:

Area Helpers
Navigation and state gotoUrl, pageInfo, waitForLoad, waitForNetworkIdle
Interaction click, fillInput, typeText, pressKey, scroll, dispatchKey
Page evaluation js
Tabs and targets listTabs, currentTab, switchTab, newTab, closeTab, ensureRealTab, iframeTarget
Inspection and escape hatches cdp, drainEvents, captureScreenshot, uploadFile, httpGet

WebMCP first, direct control when needed

The same frozen browser-wide client is available as webmcp and browser.webmcp:

const tools = await webmcp.listTools();
const search = tools.find(tool => tool.name === "search");

if (search) {
  const result = await webmcp.invokeTool(
    search.tool_ref,
    {query: "CVG to SFO"},
    {timeoutSec: 30},
  );
  repl.write(JSON.stringify(result));
}

This delegates to the existing Go WebMCP manager, which discovers native tools across every tab and embedded frame, owns live tool_ref routing, and preserves awaiting_submission and outcome_unknown safety. Frame-provided tools do not require switching the REPL's attached target.

WebMCP calls are scoped to the originating cell with AsyncLocalStorage and an execution-specific abort signal. HTTP waits are clamped below the destructive cell deadline, and finishing a cell aborts unfinished requests, so an unawaited invocation cannot leak into a later execution.

The intended automation ladder is: use page-declared WebMCP tools when available, semantic selector control otherwise, opt into Playwright Core when its broader API helps, use coordinates for visual interaction, and keep unrestricted CDP as the final escape hatch.

Playwright Core is an opt-in persistent library

The REPL ships a lockfile-pinned playwright-core package without downloading another browser. Users can dynamically import it, connect to the image's existing Chromium, and retain ordinary Playwright objects across cells:

var pw = await import("playwright-core");
var pwBrowser = await pw.chromium.connectOverCDP(process.env.CDP_ENDPOINT);
var pwContext = pwBrowser.contexts()[0];
var pwPage = pwContext.pages()[0] ?? await pwContext.newPage();

await pwPage.goto("https://example.com");
repl.write(await pwPage.title());

The native frozen browser namespace remains the default, so imported connections use names such as pwBrowser. Imported Playwright connections become stale when Chromium restarts and can reconnect explicitly within the same REPL while all other JavaScript state survives. Reset, timeout, crash, or API restart clears the connection and bindings with the rest of the REPL process.

One click API: semantic or coordinate

await click('button[aria-label="Search"]');
await click({x: 420, y: 315});

Selector clicks wait for one visible, enabled, stable, unobscured match, scroll it into view, hit-test it, and dispatch physical mouse input through Chromium. Coordinate clicks remain the direct computer-use escape hatch. Options support button, clickCount, and selector-only timeoutSec.

Semantic synchronization instead of blind sleeps

const ready = await waitForElement(
  '[role="option"][aria-label*="(SFO)"]',
  {state: "visible", timeoutSec: 10},
);

if (!ready) throw new Error("SFO option did not appear");

waitForElement() supports attached, detached, visible, and hidden. It considers all matches, so hidden responsive duplicates cannot mask visible controls. Expected polling timeouts return false; protocol and high-level contract failures throw.

Fast, exact-once page JavaScript

js() supports both direct expressions and serialized page functions:

const title = await js("document.title");

const data = await js(
  async ({selector, limit}) => {
    await Promise.resolve();
    return document.querySelector(selector)?.innerText.slice(0, limit) ?? null;
  },
  {arg: {selector: "main", limit: 1000}},
);

A submitted expression or function is invoked once—there is no parse-error fallback that can repeat side effects. Function mode naturally supports statements, await, and return. Arguments and results preserve arrays, plain objects, bigint, NaN, infinities, and -0.

Page functions do not capture Browser REPL closures; data crosses explicitly through options.arg.

Cross-origin and nested iframe control

Same-origin frames are available through iframe.contentDocument. Cross-site frames commonly run as separate Chromium targets and can be inspected without relying on top-page same-origin access:

const frame = await iframeTarget("checkout.example");
if (!frame) throw new Error("checkout frame not found");

const heading = await js(
  () => document.querySelector("h1")?.textContent,
  {targetId: frame.targetId},
);

For physical selector interaction, attach to that target and restore the parent afterward:

const parent = await currentTab();
const frame = await iframeTarget("checkout.example");

try {
  await switchTab(frame.targetId);
  await fillInput('input[name="cardholder"]', "Jane Doe");
  await click('button[aria-label="Continue"]');
} finally {
  await switchTab(parent.targetId);
}

For lower-level frame cases, callers retain unrestricted access to Page.getFrameTree, Page.createIsolatedWorld, Runtime.evaluate, and the rest of CDP.

Output is explicit, optional, and ordered

Expression values are intentionally ignored. Successful code may produce no output or combine several channels:

repl.write("structured answer");
console.log("diagnostic output");
console.error("warning output");

const path = await captureScreenshot("/tmp/page.png");
await repl.emitImage({path});

The response preserves call order across:

  • write text
  • captured stdout
  • captured stderr
  • PNG, JPEG, or WebP image content

Screenshots remain file-oriented unless explicitly emitted. Output and protocol limits are bounded, and truncation is reported with content_truncated.

Failure and lifecycle contract

The API process directly owns one lazily started Node child and is its sole supervisor.

Event JavaScript state repl_id
Successful call Preserved Unchanged
Syntax error or ordinary exception Preserved Unchanged
Chromium restart Preserved; browser reconnects lazily Unchanged
Explicit reset Cleared Replaced
Execution timeout Child process group destroyed Replaced on next request
Crash, OOM, uncaught asynchronous exception, or protocol corruption Child process group destroyed Replaced on next request

Timeouts are destructive because abandoned JavaScript cannot safely coexist with a later cell. A destructive failure response carries the terminated repl_id; the replacement process starts only on the next request.

Calls are serialized so executions cannot interleave. Clients that require a specific sequence should await each call because strict request-arrival FIFO ordering is not part of the public contract.

Architecture

SDK / HTTP client
      │
      ▼
POST /repl
      │
      ▼
kernel-images API ── owns lifecycle, timeout, reset, serialization
      │ Unix socket / newline-delimited JSON
      ▼
persistent Node runtime
      │
      ├── vm.SourceTextModule cells + live context-global bindings
      ├── explicit text/image output collection
      └── persistent CDP client
              │
              ▼
       DevTools proxy :9222 → Chromium :9223

Meriyah analyzes declarations and binding patterns. Each cell is lowered to live context-global accessors, preserving binding identity rather than copying snapshots between modules. vm.SyntheticModule adapts dynamic imports into the cell context.

Chromium connectivity is lazy: pure Node code still works while the browser is unavailable, and a Chromium restart does not discard JavaScript state.

Security model

This endpoint is deliberately unrestricted remote code execution inside the browser VM.

vm.Context is a state container, not a sandbox. Code can access Node built-ins, installed packages, files, environment variables, processes, the network, and unrestricted CDP.

Integrity comes from process and protocol boundaries rather than pretending evaluation is isolated:

  • API-owned process groups and Linux parent-death signaling
  • destructive recovery after unsafe failures
  • strict request decoding and bounded request/response sizes
  • private serializer references resilient to prototype pollution
  • heap limits and explicit output caps
  • protocol traffic kept off process stdout

Validation

Automated coverage includes:

  • declaration persistence, mutation, redeclaration, TDZ, closures, destructuring, partial failure, top-level await, dynamic import, and reset
  • stable/new repl_id behavior across normal calls, browser restart, timeout, crash, OOM, and API shutdown
  • zero-output execution, ordered mixed content, image handling, pollution resistance, and output limits
  • exact-once js() execution and page-value serialization
  • keyboard normalization through a self-contained US layout
  • actionability-aware selector clicking and filling with hidden responsive duplicates
  • attached, detached, visible, and hidden waits
  • nested cross-origin iframe target evaluation and physical selector input
  • browser-wide WebMCP discovery/invocation delegation, shared frozen namespace identity, and per-execution cancellation
  • pinned Playwright Core import, live CDP control, cross-cell object identity, and explicit reconnection after Chromium restart
  • tabs, dialogs, browser reconnects, uploads, screenshots, network idle, raw CDP, and event draining
  • OpenAPI generation with SSE regression checks
  • both Chromium image variants

Commands run locally after synchronizing with current main:

cd server && make test-runtime
cd server && go test ./cmd/api/api ./lib/oapi -count=1
cd server && go vet ./...
cd server && go test ./e2e -run '^$'

Live Docker E2E also passed for the Browser REPL suite, including selector actions, wait states, Chromium restart recovery, and cross-origin iframe control.

Three independent agents additionally completed rendered Google Flights searches through /repl, extracting and validating visible flight results without entering a booking flow.

Review map

  • Public contract: server/openapi.yaml
  • API ownership and lifecycle: server/cmd/api/api/browser_repl.go
  • Persistent evaluator: server/runtime/browser-repl.ts, cell-runtime.ts, cell-analysis.ts
  • Browser/CDP surface: server/runtime/browser-cdp-client.ts, browser-helpers.ts
  • Page-function serialization: server/runtime/page-evaluation.ts
  • Keyboard fidelity: server/runtime/us-keyboard-layout.ts
  • Complete behavior reference: server/docs/repl.md
  • Agent operational guidance: server/docs/repl-agent-guidance.md
  • Architecture and invariants: plans/persistent-browser-repl.md

rgarcia added 25 commits August 2, 2026 13:57
Design for POST /browser/execute: a persistent Node.js execution runtime
preloaded with browser-control helpers, owned directly by the API process
with lazy startup, CUID2 repl_id values, destructive timeouts, and typed
ordered text/image content.
Three bundled TypeScript modules backing /browser/execute:

- browser-repl.ts: Unix-socket server speaking newline-delimited JSON, a
  persistent vm context (state container, not a sandbox), top-level await
  via an acorn-based declaration rewrite (Node REPL style), dynamic
  import() through the main context loader, TypeScript via esbuild, and
  ordered text/image output capture with response limits (8 MiB per image,
  16 MiB aggregate images, 256 KiB text, 256 KiB serialized result).
- browser-cdp-client.ts: raw CDP client over Node's built-in WebSocket
  with command correlation, per-command timeouts, a bounded event ring,
  dialog/network tracking, lazy connection, and automatic reattachment
  after Chromium restarts (JS bindings and repl_id survive).
- browser-helpers.ts: the complete v1 helper surface (cdp escape hatch,
  navigation, page info, input, screenshots, tab management, waiting,
  evaluation, uploads, http_get, and recording helpers that delegate to
  the Kernel recording API), exposed as bare globals and on a frozen
  browser namespace.

acorn v8.18.0 (MIT) is vendored under server/runtime/vendor so the daemon
does not depend on image-global packages for parsing.
The API process is the sole owner and supervisor of the REPL child:

- Lazy startup on the first request with a fresh CUID2 repl_id, stale
  socket removal, and socket-poll readiness.
- Child runs in its own process group with Linux parent-death signaling
  (SIGKILL), so an API crash can never orphan a REPL; orphaned REPLs are
  never adopted.
- Graceful API shutdown explicitly terminates the process group
  (SIGTERM, escalating to SIGKILL) and reaps it.
- Destructive timeouts: when the execution timeout or HTTP deadline
  fires, the API kills the process group, removes the socket, clears the
  handle, and returns success=false with the terminated repl_id and
  repl_terminated=true. The next request lazily starts a fresh REPL.
- Request ID and repl_id validation on every daemon response; mismatches
  or undecodable responses terminate the child as protocol corruption.
- reset: true terminates the current REPL and evaluates in a fresh one;
  empty code is valid only with reset.
- Executions are serialized with browserReplMu, defense in depth on top
  of the daemon's internal queue.

OpenAPI gains the endpoint, request/response schemas, and the ordered
BrowserExecutionContent discriminated union (text/write|stdout|stderr
and image items); lib/oapi is regenerated with the pinned oapi-codegen.
Unit tests cover persistence, repl_id stability, reset, timeout kill,
crash recovery, shutdown, serialization, content ordering, image
validation, and truncation, building the real daemon bundle with esbuild.
- Both headful and headless Dockerfiles bundle the runtime to
  /usr/local/lib/browser-repl.js (CJS, esbuild kept external so the
  daemon can transform user TypeScript at runtime; vendored acorn is
  inlined).
- e2e tests cover persistence and repl_id stability, browser helpers
  against example.com, tab management with screenshot emission, reset,
  and destructive timeout with lazy recovery.
- server/README.md documents the endpoint, lifecycle, output limits,
  helper surface, and the explicit not-a-sandbox security model.
Addresses review findings against plans/persistent-browser-repl.md:

- Interruptible timeouts (e.g. an unresolved promise) were reported by
  the daemon as ordinary failures, so the child survived and the
  abandoned execution kept running, leaking its output into later
  executions' content. The daemon now marks its timeout responses with
  timed_out: true, and the API treats them exactly like transport
  failures: kill the process group, return the terminated repl_id with
  repl_terminated: true (plus the partial content produced before the
  deadline), and lazily start a fresh REPL on the next request.
- NaN/Infinity/-0 results no longer serialize to JSON null; they
  surface via result_repr.
- timeout_sec is validated server-side per the schema (1..300); values
  outside the range get a 400 instead of silently becoming 60s or
  holding the REPL mutex for arbitrary durations.

Test-plan coverage added:

- Unresolved-promise timeout: destructive termination, fresh repl_id,
  and no output leakage into the next execution (unit + e2e).
- Chromium-restart recovery: same repl_id and preserved bindings after
  reconnect, unit-level against a fake CDP endpoint and e2e via
  supervisorctl restart.
- Every seeded helper exercised at least once against a fake CDP
  WebSocket endpoint (unit).
- Per-image (8 MiB) and aggregate (16 MiB) image size limits.
- NaN/Infinity/-0 and error-as-result serialization.
- timeout_sec bounds.
- Both REPL e2e suites now run against headful and headless images.
Medium:
- page_info short-circuits while a modal JS dialog is pending, returning
  the dialog plus last-known target metadata instead of blocking on
  Runtime.evaluate behind the frozen renderer (30s CDP timeout).
- Result serialization no longer invokes user code: a hand-rolled
  serializer ignores toJSON/prototype pollution inside the context, so the
  result payload cannot be corrupted. Non-JSON values (undefined, Map,
  RegExp, class instances, circular structures) now route to the bounded
  repr instead of serializing lossily ({} / null).
- ensureAttached re-lists targets and retries once when a target is
  destroyed between listing and attaching (target-swap race) instead of
  surfacing a raw CDP error.

Low:
- POST /browser/execute rejects unknown request fields with 400, matching
  the schema's additionalProperties: false.
- The container wrapper (PID 1) now reaps orphaned grandchildren, so
  REPL children killed via pdeathsig after an abrupt API kill no longer
  linger as zombies.
- A child dying mid-execution (e.g. OOM near the heap cap) reports its
  exit reason in the error instead of a bare transport EOF.
- new_tab waits (best effort) for the initial navigation commit and
  close_tab for target destruction, so immediate follow-up calls observe
  consistent state.
- Uninterruptible timeouts now SIGKILL immediately at the read deadline
  (no hopeless SIGTERM grace) and share the daemon's timeout message;
  a 5s while(true) timeout returns in ~7s instead of ~11s.

Tests: unit coverage for pollution integrity, result fidelity, pending
dialogs, stale-target attach retry, protocol corruption (mismatched
ids/garbage/child death) via a fake daemon, and the unknown-fields
middleware; e2e coverage for alert/confirm/prompt dialogs, custom
recorder ids, unknown-field rejection, and the unified timeout message.
Plan doc updated for the new semantics.
- wait_for_element rejects a non-object opts argument with a clear error
  instead of silently ignoring it and waiting out the default timeout.
- Wait-style helpers (wait_for_load, wait_for_element,
  wait_for_network_idle) clamp their internal deadline to just below the
  executing request's deadline, so a routine wait miss surfaces the
  helper's clean error instead of tying the destructive execution timeout
  and killing the REPL.
- press_key validates modifiers with a descriptive error (no more cryptic
  'not iterable' TypeError or per-character string iteration) and accepts
  the {ctrl/alt/shift/meta: true} object sugar.
- Add regression coverage for the two QA findings plus the remaining
  review gaps: BROWSER_REPL_HEAP_MB propagation to --max-old-space-size
  and the bounded CDP event ring dropping old events at capacity.
High: a stale modal JS dialog opened before REPL attach froze the
renderer and bricked /browser/execute — every fresh REPL burned on an
8s session-attach timeout with no in-API recovery. Session attach and
session-routed CDP calls now carry an internal deadline clamped below
the request deadline, so a frozen renderer yields a clean helper error
(naming recovery options) instead of a destructive timeout; browser-
level helpers keep working and callers recover via Page.reload,
new_tab, or close_tab without restarting Chromium.

Medium: repl.emitImage rejected a direct Uint8Array because
'instanceof Uint8Array' fails across vm-context realms. Use
ArrayBuffer.isView (realm-safe) so all documented ImageInput forms
work from user code.

Low fixes:
- Crash/OOM error responses now populate duration_ms and truncation
  flags like the timeout path.
- js(code, target) validates that target is a string and throws a
  clear error instead of surfacing raw CDP 'Invalid parameters'.
- Static import/export rejection is now unconditional: the esbuild
  transform runs with preserveValueImports and the acorn check runs on
  the transformed output, so unused imports preceded by TypeScript-only
  syntax can no longer be silently elided. Type-only imports remain
  allowed.
- Document the data:-URL mouse-wheel scroll no-op (upstream Chromium
  headless behavior).

Regression coverage: TestBrowserReplFrozenRendererRecovery,
TestBrowserReplCrashDuringExecutionResponse,
TestBrowserReplStaticImportRejected (incl. TS-preceding forms and
type-only imports), extended image-validation and helper-ergonomics
tests, and an e2e stale pre-attach dialog subtest. Verified with
go vet, full -race unit suite, make build, e2e TestBrowserRepl* on
fresh headless+headful images, and live container smoke.
…4 findings

- Surface unhandledRejection as a bounded stderr item; REPL survives
- uncaughtException answers the in-flight execution with exiting:true,
  mapped to repl_terminated:true; next request lazily starts a fresh REPL
- Bound scroll() mouseWheel dispatch to 5s with in-page scrollBy fallback
  on CDP timeout, surfaced via helpers.onLog
- waitForNavigationCommit polls renderer-level location.href (about:blank race)
- Enforce 8 MiB request-line cap per accumulated line, independent of chunking
- allowHalfOpen daemon: flush final queued response before ending our side
- Accept recorder_id as alias for the recording API's id field

Adds 7 regression tests; verified live against a fresh container.
… orphan kill

Medium — const/let in async-wrapper mode: top-level await used to degrade
const/let/class declarations to mutable, redeclarable global assignments.
The runtime now tracks every top-level binding in a registry across both
evaluation paths, raises redeclaration as an early SyntaxError before any
user code runs (no matter which path declared the name), and exports
async-mode let/const/class bindings via __replDefine__: const is backed by
an accessor whose setter throws TypeError "Assignment to constant
variable.", let/class become writable non-configurable properties. Names
stay reserved when an initializer throws, matching script instantiation.

Low — trailing-block implicit results: documented that with top-level
await/return only a trailing expression statement yields an implicit
result; a trailing try/if/for yields undefined (pinned by a test).

Low — first mouseWheel after navigation: Chromium (both variants) answers
the dispatch normally but never scrolls, so the timeout fallback could not
detect it. scroll() now probes the window scroll offset around the
dispatch and retries a no-op dispatch once when the window itself is
scrollable in a requested direction, surfacing the retry as a stderr
content item. Unscrollable/edge/inner-element cases stay unverified and
behave exactly as before. Verified live: the first scroll after
goto_url("https://example.com") now lands at 700 instead of 0.

Low — orphaned REPL leak: lazy startup unlinked a stale socket but never
killed the orphaned daemon holding it. When the API finds a live listener
it does not own, it locates the owning process via /proc/net/unix and
/proc/<pid>/fd and SIGKILLs it before removing the socket file.

Also fix an e2e subtest that relied on the old redeclaration bug (a second
top-level `const info` in a shared REPL).

Regression tests: async const/let/class/var/function semantics across
both paths, cross-path conflicts, failed-initializer reservation,
trailing-block pinning, swallowed-wheel retry (fake CDP), and
orphaned-daemon kill. Full non-e2e suite with -race passes; focused
TestBrowserRepl e2e passes on freshly built headless and headful images.
…eath path, activate target on attach

- scroll(): poll the scroll offset through a 250ms settle window before
  declaring a mouseWheel dispatch had no effect. Chromium applies the
  wheel asynchronously after the CDP command answers, so the immediate
  probe false-positived and the swallowed-wheel retry double-scrolled
  every scroll on both variants. Verified live: cumulative offsets are
  now exactly 700/1400 with no retry.
- browser_repl: kill orphaned REPL daemons before unlinking the socket
  on every lazy-start path (reapBrowserReplSocket shared by
  clearBrowserReplLocked and startBrowserReplLocked). Previously the
  child-death replacement path unlinked first, making the orphan
  undetectable via /proc/net/unix and leaking it for the container
  lifetime. Verified live: orphan SIGKILLed on the child-death path.
- attach: best-effort Target.activateTarget so the attached tab is the
  foreground tab; headless Chromium auto-cancels hidden tabs' JS
  dialogs and the foreground tab after a Chromium restart is not
  deterministic, which made page_info dialog reporting flaky.

Regression tests: async-wheel scroll (fake CDP delayed application),
attach activates target, orphan killed on the child-death path, and an
e2e dialog-after-chromium-restart probe on both image variants.
…l limitations docs

Fresh QA cycle-6 findings:

1. Flaky 'CDP connection closed' on the first browser-helper call after a
   Chromium restart: the DevTools proxy can accept the WebSocket and then
   close it while the browser behind it is still coming up, rejecting the
   in-flight command. Tag connection-closed failures with whether the
   connection ever answered a command; when it never did, the command
   almost certainly never reached the browser, so reconnect and retry
   exactly once (browser-level commands in send(), session commands via
   re-attach in sessionCommand()). Regression test simulates a proxy that
   accepts then drops the first post-restart connection mid-command.

2. Plan docs: the data:-URL mouse-wheel no-op and the
   first-wheel-after-navigation swallow were observed only on
   non-activated (hidden) tabs and no longer reproduce now that attach
   foregrounds the tab via Target.activateTarget. Update Known
   Limitations and the scroll helper table entry accordingly; the
   settle/retry logic stays as defense in depth.
Comment thread server/e2e/e2e_browser_repl_test.go
Comment thread server/e2e/e2e_browser_repl_test.go
# Conflicts:
#	server/Makefile
#	server/cmd/api/api/api.go
#	server/cmd/api/main.go
#	server/lib/oapi/oapi.go
@socket-security

socket-security Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​playwright-core@​1.62.1100100799980
Addednpm/​@​types/​node@​22.15.211001008196100
Addednpm/​sharp@​0.34.5928510093100
Addednpm/​typescript@​5.6.31001009010090
Addednpm/​meriyah@​7.3.110010010095100

View full report

Comment thread server/runtime/browser-helpers.ts
Comment thread server/runtime/browser-helpers.ts
Comment thread server/runtime/browser-helpers.ts
Comment thread server/e2e/e2e_browser_repl_test.go Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9249def. Configure here.

Comment thread server/runtime/browser-repl.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant