Skip to content

Add opt in orange indicator when Stagehand controls the browser - #2457

Open
antonvishal wants to merge 9 commits into
browserbase:v4-spikefrom
antonvishal:indicator
Open

Add opt in orange indicator when Stagehand controls the browser#2457
antonvishal wants to merge 9 commits into
browserbase:v4-spikefrom
antonvishal:indicator

Conversation

@antonvishal

@antonvishal antonvishal commented Jul 27, 2026

Copy link
Copy Markdown

Why

Users need a simple visual cue showing when Stagehand is controlling the browser. This makes browser activity easier to understand without adding controls or changing page layout.

Screenshot 2026-07-28 at 12 10 45 AM

What changed

  • Added the opt-in agentIndicator: true initialization option, disabled by default for backward compatibility.
  • Added a subtle orange viewport glow with animated highlights moving around the page edges.
  • Starts the indicator after stagehand.init() and removes it during stagehand.close().
  • Applies the indicator to existing tabs, newly opened tabs, and pages after navigation.
  • Injects one CSS-only :root::after style into the top frame, with no DOM nodes or iframe rendering.
  • Keeps the page interactive with pointer-events: none and leaves locator and XPath behavior unchanged.
  • Handles restricted pages and individual tab failures as best-effort behavior without interrupting the session.
  • Respects prefers-reduced-motion by disabling animation.
  • Appears in normal viewport screenshots because it is rendered as page CSS.
  • Added shared protocol support plus TypeScript agentIndicator and Python agent_indicator SDK options.

Test plan

  • pnpm check
  • Full server test suite
  • Protocol test suite
  • TypeScript SDK test suite
  • Python SDK tests, lint, and type checks
  • Extension production build and bundle assertions
  • Controller tests for activation, deactivation, rapid state changes, navigation, restricted tabs, and retries
  • Runtime and service-worker wiring tests
  • Local Chrome integration smoke test for the computed indicator style and unchanged CSS/XPath locator counts
  • Browserbase integration smoke suite
  • Manual visual verification of the animated viewport indicator

Summary by cubic

Adds an opt-in orange viewport halo that shows when Stagehand controls the browser. It runs from init to close, works across tabs and navigations, and doesn’t block interaction. Lifecycle is hardened against rapid state changes and navigation races.

  • New Features

    • Added agentIndicator (TS) / agent_indicator (Python) init option to stagehand.init; default is false.
    • Injects a CSS-only :root::after halo with pointer-events: none; respects prefers-reduced-motion; visible in screenshots.
    • Activates after stagehand.init() and deactivates on stagehand.close(); applies to existing/new tabs and re-applies after navigation and in-flight navigations.
    • Service worker controller uses chrome.scripting.insertCSS/removeCSS (USER origin), serializes rapid state transitions, retries on tab query failures, best-effort on restricted pages; never blocks the session.
    • Updated protocol, server wiring, and TS/Python SDKs with tests and build checks.
  • Migration

    • Enable by passing agentIndicator: true in TS or agent_indicator=True in Python when constructing Stagehand.

Written for commit 15b53f4. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 15b53f4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Jul 27, 2026
@antonvishal
antonvishal marked this pull request as ready for review July 27, 2026 18:43

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 20 files

Architecture diagram
sequenceDiagram
    participant SDK as TS/Python SDK
    participant Protocol as Protocol Layer
    participant Runtime as StagehandRuntime
    participant SW as Service Worker
    participant Controller as AgentIndicatorController
    participant Chrome as chrome.tabs/scripting API
    participant Page as Browser Page

    Note over SDK,Page: NEW: Opt-in Agent Indicator Flow

    SDK->>Protocol: stagehand.init({agentIndicator: true})
    Protocol->>Runtime: initialize() with agentIndicator: true
    alt agentIndicator === true
        Runtime->>Runtime: Store init state
        Runtime->>SW: setAgentIndicatorActive(true)
        SW->>Controller: create singular controller per session
        Controller->>Chrome: tabs.query({})
        Chrome-->>Controller: [tab12, tab34]
        par For each tab
            Controller->>Chrome: scripting.insertCSS({target:{tabId}, css, origin:"USER"})
            Chrome->>Page: Inject :root::after style
            Page-->>Chrome: CSS applied (pointer-events:none)
            Chrome-->>Controller: Success
        and Restricted page (e.g., chrome:// URL)
            Chrome->>Controller: Error (tab skipped, best-effort)
        end
        Controller-->>SW: All tabs styled
        SW-->>Runtime: Indicator active
    else agentIndicator === false (default)
        Runtime->>Runtime: No indicator activation
    end
    Runtime-->>Protocol: {initialized: true}
    Protocol-->>SDK: init response

    Note over Controller,Chrome: Navigation Re-application

    alt User navigates to new page (status=loading)
        Chrome->>Controller: tabs.onUpdated(tabId, "loading")
        Controller->>Controller: Remove from styledTabs set
        alt Indicator still active (desiredActive=true)
            Controller->>Chrome: scripting.insertCSS({target:{tabId}})
            Chrome->>Page: Re-apply indicator after navigation
        end
    end

    Note over SDK,Page: Shutdown: Deactivate Indicator

    SDK->>Protocol: stagehand.close()
    Protocol->>Runtime: close()
    Runtime->>Runtime: Check state: agentIndicator was true
    Runtime->>SW: setAgentIndicatorActive(false)
    SW->>Controller: setActive(false)
    Controller->>Chrome: scripting.removeCSS() for each styled tab
    Chrome->>Page: Remove :root::after style
    Page-->>Chrome: Style removed
    Chrome-->>Controller: Success
    Controller-->>SW: All tabs cleaned
    SW-->>Runtime: Indicator deactivated
    Runtime->>Runtime: Proceed with session.close()

    Note over Controller: Rapid state change protection
    alt Controller.setActive(true) then immediately setActive(false)
        Controller->>Controller: Serialize transitions via queue
        Controller->>Controller: Skip stale activation (desiredActive already false)
        Controller->>Chrome: Only removeCSS (no insertCSS)
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/server/agentIndicatorController.ts Outdated
Comment thread packages/server/agentIndicatorController.ts Outdated
Comment thread packages/server/tests/service-worker-agent-indicator.test.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Confidence score: 4/5

  • In packages/server/agentIndicatorController.ts, navigationGenerations keeps entries for tabs after close, so long-running sessions with frequent tab churn can accumulate stale state and steadily increase memory use, eventually degrading server responsiveness—add tab-close cleanup for navigationGenerations and the related cached tab state.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/server/agentIndicatorController.ts">

<violation number="1" location="packages/server/agentIndicatorController.ts:84">
P3: Long-running sessions retain one navigation-generation entry for every closed tab that ever loads, so memory grows with tab churn. Add tab-close cleanup for `navigationGenerations` (and the corresponding cached tab state).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

let desiredActive = false;
let transition = Promise.resolve();
const styledTabs = new Set<number>();
const navigationGenerations = new Map<number, number>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Long-running sessions retain one navigation-generation entry for every closed tab that ever loads, so memory grows with tab churn. Add tab-close cleanup for navigationGenerations (and the corresponding cached tab state).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/agentIndicatorController.ts, line 84:

<comment>Long-running sessions retain one navigation-generation entry for every closed tab that ever loads, so memory grows with tab churn. Add tab-close cleanup for `navigationGenerations` (and the corresponding cached tab state).</comment>

<file context>
@@ -81,6 +81,7 @@ export function createAgentIndicatorController(
   let desiredActive = false;
   let transition = Promise.resolve();
   const styledTabs = new Set<number>();
+  const navigationGenerations = new Map<number, number>();
 
   const injectionFor = (tabId: number): AgentIndicatorCssInjection => ({
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant