Skip to content

[Claimed #2240] feat(cli): add browse clipboard commands#2241

Open
github-actions[bot] wants to merge 3 commits into
mainfrom
external-contributor-pr-2240
Open

[Claimed #2240] feat(cli): add browse clipboard commands#2241
github-actions[bot] wants to merge 3 commits into
mainfrom
external-contributor-pr-2240

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Mirrored from external contributor PR #2240 after approval by @shrey150.

Original author: @optimusbuilder
Original PR: #2240
Approved source head SHA: c372551cfa7f177ce7c6a391f458aad094ecc06a

@optimusbuilder, please continue any follow-up discussion on this mirrored PR. When the external PR gets new commits, this same internal PR will be marked stale until the latest external commit is approved and refreshed here.

Original description

##why
The SDK exposes context.clipboard, but the browse CLI had no way to use it — users had to fall back to raw CDP.

##what changed
-Six driver commands: clipboard.read, write, paste, copy, cut, clear
-Matching oclif commands: browse clipboard read|write|paste|copy|cut|clear
-Wired through DriverSessionManager.browserContext() → context.clipboard (no core changes)
paste supports --shortcut Control+V|Meta+V|ControlOrMeta+V

  • Unit tests for handler wiring + command registration

E2E Test Matrix

All rows ran against the local build of this branch (packages/cli, commit 2acce35c). Clipboard scoping derisk detailed in this thread.

Command / flow Observed output Confidence / sufficiency
pnpm run build && pnpm exec vitest run 16 files / 218 tests passed, incl. 8 clipboard handler tests (4 new validation-path tests) Proves wiring + zod param validation; no real browser, so supporting evidence only
browse open <url> --local --headlessclipboard writeclipboard read (session A) {"ok": true}{"text": "<written value>"} Real write/read roundtrip on managed headless local
clipboard read in second headless session B {"text": ""}; macOS pbpaste untouched Headless local sessions have isolated in-memory clipboards
clipboard write in --headed session → pbpaste; read in second headed session OS clipboard contained the written value; second session read it back Documents that headed local / --cdp targets are machine-global (now stated in help text); not a regression, inherent to navigator.clipboard
browse open <url> --remoteclipboard writeclipboard read (Browserbase session 1) {"ok": true}{"text": "<written value>"}; local clipboard untouched Real Browserbase roundtrip — the primary production target
clipboard read in second --remote session {"text": ""} Browserbase sessions are clipboard-isolated (one VM per session)
eval focus input → clipboard writeclipboard pasteeval input value (headless local AND remote w/ --shortcut Control+V) Input value equaled the written text in both targets Proves synthesized-keystroke paste path incl. the --shortcut flag
select text via evalclipboard copyread; select → cutread + input value; clearread copy: clipboard = selection; cut: clipboard = text, input ""; clear: {"text": ""} Proves copy/cut/clear end-to-end in a real browser

Original test plan

pnpm exec vitest run tests/clipboard.test.ts tests/driver-commands.test.ts

Manual: write → read → paste on a focused input


Summary by cubic

Add clipboard support to the browse CLI with commands to read, write, paste, copy, cut, and clear. Lets terminal and agent workflows use the SDK clipboard API without raw CDP.

  • New Features
    • Added driver handlers for clipboard.read|write|paste|copy|cut|clear, routed via DriverSessionManager.browserContext() to context.clipboard.
    • Exposed as oclif commands: browse clipboard read|write|paste|copy|cut|clear; paste accepts --shortcut (Control+V, Meta+V, ControlOrMeta+V).
    • Tests cover handler behavior and command registration (vitest).

Written for commit ebaaf0d. Summary will update on new commits.

Review in cubic

Expose the SDK clipboard API through the browse driver and oclif so
terminal and agent workflows can read, write, paste, copy, and clear
the browser clipboard without raw CDP calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. labels Jun 11, 2026
@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2acce35

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

Copy link
Copy Markdown
Contributor Author

This mirrored PR tracks external contributor PR #2240 at source SHA c372551cfa7f177ce7c6a391f458aad094ecc06a, approved by @shrey150.
Original PR: #2240

When the external PR gets new commits, this same internal PR will be refreshed in place after the latest external commit is approved.


static override examples = [
"browse clipboard clear",
"browse clipboard clear --session research",

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.

Do we know whether or not the clipboard API is global to the machine, or if it's truly scoped by session (aka by tab/browser)? If it's global, then I don't think session flags work right? We need to derisk

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.

Derisked empirically with the local build (packages/cli, this branch). Answer: it depends on the target, and the session flags are correct for the targets that matter — but headed/--cdp targets are machine-global.

Implementation: context.clipboard runs navigator.clipboard.readText/writeText via Runtime.evaluate inside the target browser (plus Browser.grantPermissions); paste/copy/cut are synthesized keystrokes. So scope = whatever clipboard backend that Chromium instance has.

Measured (each row actually run):

Target Result
Browserbase remote, session 1 write → read roundtrip ✓; local Mac clipboard untouched
Browserbase remote, session 2 reads ""isolated (separate VM per session)
Managed local headless, session A write → read ✓; macOS pbpaste untouched
Managed local headless, session B reads ""isolated (headless Chromium uses an in-memory clipboard per instance)
Managed local headed, session 1 write clobbered the real macOS clipboard (pbpaste showed it)
Managed local headed, session 2 read the same value → machine-global, cross-session leak

--cdp attach to a real browser behaves like headed by construction. Note clear is implemented as writeText(""), so on a headed/--cdp target it wipes the user's actual OS clipboard.

So: for the primary agent targets (--remote, managed headless local) the clipboard is effectively session-scoped and the flags work as expected. For headed local and --cdp it is global to the machine. Addressed in 2acce35: the help text claimed page-level scoping ("for the active page") — now describes session scoping accurately with an explicit per-target scope note on all six commands. Making headed local truly session-scoped would require core-level clipboard virtualization (intercepting navigator.clipboard per context) — happy to file a follow-up ticket if we want that, but it's a core change, not CLI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yeah I validated this explicitly: clipboard is shared across tabs in the same browse session, but isolated across different sessions (separate managed-local browser instances).
so for example
clip-a2: if you write A2, read => A2
clip-b2: read => empty
clip-b2: write B2, read => B2
clip-a2: read still => A2
So --session is the right scoping boundary here. If useful, I can add a brief note in docs/help text and clarify that “shared within session, isolated across sessions.”

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.

Matches my results — thanks for double-checking. The one exception to keep in mind is headed local / --cdp targets, where the browser uses the real OS clipboard (machine-global, leaks across sessions and apps). That caveat is now documented in the help text on all six commands as of 2acce35.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

thank you!

Derisked per review: context.clipboard drives navigator.clipboard in the
target browser, so scope depends on the target — Browserbase remote and
managed headless local sessions get an isolated per-browser clipboard,
while headed local sessions and --cdp attachments share the machine's OS
clipboard. Command descriptions previously claimed page-level scoping.

Also adds unit tests for the zod validation paths (write without text,
non-string text, unsupported paste shortcut, paste with no options) and
registers a clipboard oclif topic description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shrey150 added a commit that referenced this pull request Jun 12, 2026
## Summary

Fork-triggered `pull_request` workflow runs don't receive repo
configuration variables or secrets, so
`STAINLESS_ORG`/`STAINLESS_PROJECT` arrive empty and the Stainless
action dies immediately with `Input required and not supplied: project`
on every external-contributor PR. Seen on #2241 (claimed copy of #2240):
the fork PR's failing `preview` check attached to the shared head SHA,
leaving the claimed PR red through no fault of its own.

This guards both jobs on `github.event.pull_request.head.repo.full_name
== github.repository`:

- `preview` — skips on fork PRs instead of failing. Claimed copies of
external PRs run in-repo, so SDK preview coverage is preserved.
- `merge` — same defect: a directly-merged fork PR would trigger a
guaranteed-failing merge build (still no vars/secrets on the `closed`
event). Skipping is strictly better than failing; the claim flow means
merged external work is always in-repo anyway.

The `full_name` comparison is used instead of `!head.repo.fork` so a
deleted fork repo (`head.repo == null`) skips rather than
runs-and-fails.

Linear:
[STG-2257](https://linear.app/browserbase/issue/STG-2257/fixci-skip-stainless-sdk-jobs-for-fork-prs)

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| Failing fork run:
[actions/runs/27375420406](https://github.com/browserbase/stagehand/actions/runs/27375420406/job/80903332001)
(triggered by fork PR #2240) | Env dump shows `STAINLESS_ORG:` and
`STAINLESS_PROJECT:` empty; action exits with `Error: Input required and
not supplied: project` | Proves the root cause is withheld vars on fork
runs, not the spec or config — exactly the case the new guard skips |
| `actionlint .github/workflows/stainless.yml` on this branch | Exit 0,
no findings | Validates workflow schema and the new `if:` expression
syntax |
| `preview` check on **this PR** (in-repo branch) | See checks below —
job runs (guard evaluates true for same-repo PRs) and builds SDK
previews | Live proof the guard doesn't skip in-repo PRs; the fork-skip
path can only be exercised by the next real fork PR |

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Skip Stainless CI jobs for fork-origin PRs to avoid failures from
missing repo vars/secrets. Keeps external PRs green while coverage runs
on in-repo “claimed” copies. Addresses Linear STG-2257.

- **Bug Fixes**
- Skip `preview` and `merge` unless
`github.event.pull_request.head.repo.full_name == github.repository`;
`merge` still requires closed+merged into `main`.
- Use the `full_name` check to safely skip deleted forks (`head.repo`
can be null).

<sup>Written for commit 58c5891.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2243?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants