Skip to content

fix(desktop): new tabs the agent opens from links no longer steal composer focus - #29023

Open
juliusmarminge wants to merge 8 commits into
mainfrom
fix/issue-29009
Open

juliusmarminge wants to merge 8 commits into
mainfrom
fix/issue-29009

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Problem

When the agent clicks a target="_blank" link in a desktop-hosted Browser tab, the new tab opens in the foreground and keyboard focus leaves the composer. The desktop sent such links with background only for background-tab dispositions, so a plain left click from the agent looked like a user action to the web client, which then moved focus into the right panel. It also happens when the agent works in another thread: the right panel of the thread on screen takes focus.

Reproduced on main (05c0ed7) with the Linux desktop dev app: a Codex agent opened a local page and clicked its _blank link with preview_act click while the composer had focus.

  • Same thread: focus moved to the Browser panel's "Search or enter URL" field.
  • Agent in thread A, user typing in thread B with B's right panel open on Files: B's composer lost focus to "Search proj files" 140 ms after the agent's click.

Change

The guest's window-open handler now opens a new-tab link in the background when the agent sent input to that guest in the last 5 seconds and no frame in the guest has focus. This reuses the agent-input timestamp the desktop already keeps for downloads, renamed from humanStartedDownload to agentActedRecently. The download decision is unchanged.

The focused-frame check keeps a person's click in the foreground, even right after agent input. A person's click focuses a frame in the guest. The agent's click does not: the host gets focus back after the agent's press, and guests of other threads are hidden. The web client already handles background: true without a user action, so nothing changes there.

Scope and approval

Fixes #29009

Verification

  • vp test run apps/desktop/src/preview/Manager.test.ts apps/desktop/src/preview/DesktopBrowserHost.test.ts: 93 passed.
  • New test PreviewManager > opens a new-tab link the agent clicked in the background covers three cases: a person's click opens in the foreground, an agent click opens in the background, and a person's click right after agent input opens in the foreground. On main, the agent-click case fails (background: false, expected true).
  • vp run check:changed: format, lint, knip, typecheck and tests pass.

Desktop app run (Linux, vp run dev:desktop under Xvfb, fresh database, Codex agent driving preview_open and preview_act click on a local page with a _blank link). A CDP monitor logged the app's focused element. Before is main 05c0ed7; after is ae2972e.

Case Before After
Agent clicks a _blank link, same thread focus → Browser "Search or enter URL" composer keeps focus; the new tab exists in the background
Agent clicks in thread A, user in thread B with Files open focus → B's "Search proj files" B's composer keeps focus; the new tab exists in A
Agent clicks in thread A, user in thread B with the panel closed composer keeps focus not run
Click sent to the guest page itself 17 ms after the agent's tab appeared (stand-in for a person taking over) not run the agent's tab stays in the background; the person's tab opens in front with focus → "Search or enter URL"

Same-thread agent click, before:

02:19:39.580Z DIV[contenteditable] area=composer
02:19:55.758Z WEBVIEW
02:19:55.871Z DIV[contenteditable] area=composer
02:19:56.801Z INPUT area=panel label="Search or enter URL"

After: the press briefly focuses the webview, focus comes back to the composer, and nothing moves when the tab opens.

03:06:35.519Z DIV[contenteditable] area=composer
03:06:55.377Z WEBVIEW
03:06:55.649Z DIV[contenteditable] area=composer

Cross-thread click with B's Files panel open, before. The agent clicks in A at 02:21:54.548:

02:21:49.758Z DIV[contenteditable] area=composer label="Message New thread"
02:21:54.688Z INPUT area=panel label="Search proj files"

After. The agent clicks in A at 03:07:36.129, and B's focus does not change:

03:07:27.245Z DIV[contenteditable] area=composer label="Message New thread"

Not checked:

  • No before/after recordings are attached.
  • macOS and Windows window activation. Under Xvfb, webContents.isFocused() is false for every guest, which is why the check uses focusedFrame.
  • A person's click from real OS input. The person case above is a CDP click sent to the guest itself.

🤖 Generated with Claude Code (Claude Opus 5.5, issue lead; implementation and red test by Claude Opus 5.5 step agents; reviews by GPT-6.1 Sol via Codex and Claude Opus 5.5)

Takeover

Taken over from @juliusmarminge for evidence. No code changes; main merged in by merge commit (fdd40f40bd), evidence recaptured at that head and verification added below.

Evidence

Beyond the title: a person's click sent through a second web client's streamed Browser tab counts as agent input, so at this head the new tab opens in the background on the desktop host (row R1; on main it opens in the foreground).

Real Electron 44.10.0 desktop app under Linux Xvfb (--no-sandbox --disable-gpu), 1440×1000, DPR 1, 100% font scale, dark theme, renderer clock frozen, fresh T3CODE_DEV_SEED=0 databases with synthetic data, drafts reset per row. Agent clicks go through the thread-scoped MCP preview_act; person clicks and typing are real X11 input. R1's second client is Chromium at 1440×1000, DPR 2. 390-wide and light captures were skipped: the change renders nothing new. Each recording has a sidebar with timestamps and the focused element. Diff0: int32 |after − before| × 255. DiffT: channels differing by more than 16/255, in magenta. Both compare the settled final frames.

Row (1440×1000, dark) Before cefb544af3 After fdd40f40bd Diff0 DiffT
A1 Agent clicks a _blank link, same thread, Browser visible. Before: the new tab is selected and the typing lands in its URL field. After: the source tab stays selected and the composer gets the typing. mp4 · final mp4 · final diff0 diffT
A2 Agent in thread A, user in B with Files open. Before: B's composer loses focus and the same typing opens a Terminal in place of Files. After: B keeps Files and the composer gets the typing. mp4 · final mp4 · final diff0 diffT
S1 Same thread, Files open over the source tab. Before: the new tab replaces Files and takes the typing. After: Files stays and the composer gets the typing. mp4 · final mp4 · final diff0 diffT
S2 Same thread, panel closed. Before: the panel opens on the new tab, narrows the chat and takes the typing. After: the panel stays closed. mp4 · final mp4 · final diff0 diffT
S4 Labelled harness: open result delayed 2 s; person selects a sibling tab meanwhile. Before: the agent's tab is selected and shows its content while the URL field shows the sibling's URL. After: the sibling stays selected with its own URL and content. mp4 · final mp4 · final diff0 diffT
R1 Person in a second web client takes control and clicks the link through the stream. Before: the new tab opens in the foreground on the desktop. After: it opens in the background and the desktop composer keeps focus. The viewer shows the source tab on both. mp4 · final mp4 · final diff0 diffT
H1 Person's OS left-click, no recent agent input. Foreground on both. mp4 · final mp4 · final diff0 diffT
B1 Person's OS middle-click. Background on both. mp4 · final mp4 · final diff0 diffT
T1 Page opens a tab 6.5 s after the agent's click (window expired). Foreground on both. mp4 · final mp4 · final diff0 diffT
H2 Person's OS click 0.7 s after the agent's click. The agent's tab opens in the background, the person's in the foreground. Head only. not required mp4 · final n/a n/a

A1 final frames, before (cefb544af3) and after (fdd40f40bd):

A1 before: the agent's new tab is selected and holds the typing
A1 after: the source tab stays selected and the composer holds the typing

What else changed: A1/S1 selected tab, URL-field focus and composer text; A2 Terminal versus Files; S2 chat width and the thread-details card from the panel opening; S4 tab-strip widths and the selected tab; R1 desktop host tab selection and focus (viewer final frames identical at zero threshold). H1 and T1: nine address-bar icon pixels differ, none above 16/255. B1: identical at zero threshold.

Full per-row steps, timestamps, destination-inspection clips and sheets: Evidence.md · index · matrix. Rebuild: REPRODUCE · seed-reproduction.js · capture.mjs · all scripts.

Verification log
  • Mode: evidence-only takeover, round 2. Main (155 commits) merged by merge commit; no PR code changed. vp run check:changed passed (format, lint, knip, typecheck, 93 tests in Manager.test.ts and DesktopBrowserHost.test.ts). Earlier rounds' tests and reviews stand.
  • Proof audit pass 1 (GPT-6.1 Sol via Codex): 10-row matrix. A3 and S3 collapse into A2/S2/A1; U1 (untracked tab) and downloads are covered by the code. R1 added for the streamed-input path main changed.
  • Capture (GPT-6.1 Sol via Codex): 10/10 rows, 19 before/after recordings, 13 destination-inspection clips, 2 R1 viewer recordings.
  • Proof audit pass 2 (GPT-6.1 Sol via Codex): looked at all 85 PNGs and frames from all 34 recordings; rebuilt A1 at the head from the uploaded scripts with zero pixel delta. Nine rows sufficient; S4's caption was corrected to its capture (before, the agent's tab is the selected one, not the sibling). Confirmed R1's background open is new at this head.
  • Not covered: T1's exact 5,000 ms boundary. cannot-verify macos, cannot-verify windows: window activation and focusedFrame on native macOS/Windows.
  • CI: 32/32 checks green on fdd40f4; no conflicts with main.
  • Claude Opus 5.5 (lead, Claude Code in T3 Code); GPT-6.1 Sol via Codex (proof audit, capture).

🤖 Generated with Claude Code

juliusmarminge and others added 7 commits November 2, 2026 02:24
…reground (#29009)

These tests reproduce #29009: a target=_blank link the
agent's CDP input clicked is emitted with background: false, so the web
side opens it as a user action and steals focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A target=_blank link clicked by the agent's CDP input reported a
foreground-tab disposition, so the web opened it as a user action and
moved focus off the composer. The window-open handler now also treats a
link as background when the guest had agent input in the last 5 seconds,
reusing the signal downloads already used (renamed agentActedRecently).

Fixes #29009

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… foreground

Agent input marks a guest for 5 s, so a person who clicked into that guest
and opened a _blank link inside the window got a background tab. A person's
click focuses the guest while the agent's leaves focus with the composer, so
background the tab only when the guest does not hold keyboard focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocused frame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Nov 2, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 6c09b17

Macroscope's review found this PR approvable — Agent-opened preview tabs stay in the background when the guest lacks focus, preventing interruption of composer input. The change is confined to focus handling and a corresponding download-predicate refactor, with no obvious material risk.

No code changes detected at fdd40f4. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

[GPT-6.1 Sol][Codex] on behalf of shivam.

Handing issue #29009 to this PR; I stopped further testing and will not push the separate local patch.

Independently reproduced the original bug on Linux/corex with the desktop build of dd6c05ec313c6c870c7a7688e87143da5084031d, isolated fixture state, and a free Replay agent turn. The normal MCP preview_act click on a target="_blank" link emitted background: false; the new guest loaded /result, and focus moved from the composer to the Browser “Search or enter URL” input. Screenshot of that baseline result:

Linux baseline: new tab takes focus from the composer

The separate local patch at b74ce49e8b4b5c30dbaa36f41d90646e094936ec reused the same five-second agent-input timestamp. Its regression failed without the fix, and the two focused desktop suites passed 93/93, including an independent review run; check:changed also passed. These results cover that local patch, not this PR. Native verification of the fixed build and cross-thread reproduction were not completed before takeover. The local patch lacked your focused-frame refinement, so it could also background a person's click shortly after agent activity.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The desktop preview host now reports recent agent input. The preview manager uses that state when classifying new-tab link events and handling downloads. Tests cover input detection and link background state when a guest frame gains or loses focus.

Changes: Preview activity handling

Layer / File(s) Summary
Track recent agent input
apps/desktop/src/preview/DesktopBrowserHost.ts, apps/desktop/src/preview/DesktopBrowserHost.test.ts
The host replaces humanStartedDownload with agentActedRecently. The method returns true when a matching tab recorded agent input within 5,000 ms. The test checks that DOM.getDocument returns false and a mouse press returns true.
Classify preview links and downloads
apps/desktop/src/preview/Manager.ts, apps/desktop/src/preview/Manager.test.ts
A new-tab link is marked as background when its disposition is background-tab, or when recent agent input coincides with no focused guest frame. Download handling now checks recent agent input before continuing to the Downloads-folder copy path. The integration test checks link background state as focus and agent input change.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg, only21mil

Merge Risk: Minimal · up to 6c09b

Agent-opened links are intended to stay in the background. No actionable merge-blocking issue was established.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: preventing agent-opened tabs from stealing composer focus.
Linked Issues check Passed Issue [#29009] requires preventing composer focus loss when an agent opens a target="_blank" link, including from another thread. Manager.ts now sets background: true when `agentActedRecently(wc…
Out of Scope Changes check Passed The changes stay within issue [#29009]. The timestamp rename, focused-frame decision, download-path update, and tests support distinguishing agent input from a person's click. The diff contains no unr…
Description check Passed The description follows the repository template. It has Problem, Change, Scope and approval, and Verification sections. The Problem section explains the focus-stealing behavior, the reproduction on ma…
Full details: Linked Issues check

Explanation

Issue [#29009] requires preventing composer focus loss when an agent opens a target="_blank" link, including from another thread. Manager.ts now sets background: true when agentActedRecently(wc) is true and wc.focusedFrame === null. The existing background-tab behavior remains. The diff adds tests for a person's click, an agent click, and a person's click after agent input. The download behavior remains equivalent after the predicate rename.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue [#29009]. The timestamp rename, focused-frame decision, download-path update, and tests support distinguishing agent input from a person's click. The diff contains no unrelated product behavior.

Full details: Description check

Explanation

The description follows the repository template. It has Problem, Change, Scope and approval, and Verification sections. The Problem section explains the focus-stealing behavior, the reproduction on main, and the expected behavior. The Change section explains the background-open rule and the reuse of the agent-input timestamp. Verification gives focused test results and extensive before/after desktop evidence, with the unchecked items stated. The Scope and approval section links the issue (#29009) but does not link a maintainer approval comment or explain a scope exemption. The template requires an approval link or discussion with explicit maintainer approval for a behavior change, so that part is incomplete. The description also states the model and harness used, as the template asks. Most required content is present, and the single gap is the approval evidence.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between ddfc4a9 and 6c09b17.

Files selected for processing: 4
  • apps/desktop/src/preview/DesktopBrowserHost.test.ts
  • apps/desktop/src/preview/DesktopBrowserHost.ts
  • apps/desktop/src/preview/Manager.test.ts
  • apps/desktop/src/preview/Manager.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed labels Nov 2, 2026

Copy link
Copy Markdown
Member Author

Screenshots from the Linux desktop dev app runs in the PR body (Xvfb, Codex agent, local _blank test page), taken right after the agent's click. Before is main 05c0ed7; after is ae2972e, which differs from 6c09b17 only by a merge of main.

  1. Same thread, before: the Browser address bar has focus.
  2. Same thread, after: the composer keeps the caret, and the agent's new tab stays in the background.
  3. Agent clicks in another thread while this thread has Files open, before: "Search files" takes focus.
  4. The same case, after: the composer keeps focus and Files is not focused.

Before: same thread, address bar focused after the agent's click

After: same thread, composer keeps focus

Before: cross-thread, Files search focused

After: cross-thread, composer keeps focus

@juliusmarminge juliusmarminge added pr:in-progress A lead is working on this PR and removed pr:gate-failed A land gate failed labels Nov 3, 2026
@juliusmarminge juliusmarminge self-assigned this Nov 3, 2026

Copy link
Copy Markdown
Member Author

Taking over this one

@juliusmarminge juliusmarminge added pr:gate-failed A land gate failed pr:in-progress A lead is working on this PR and removed pr:in-progress A lead is working on this PR pr:gate-failed A land gate failed labels Nov 3, 2026

Copy link
Copy Markdown
Member Author

Taking over this one

@juliusmarminge juliusmarminge added pr:needs-julius Needs Julius: route gate, unverifiable surface, or blocked and removed pr:in-progress A lead is working on this PR labels Nov 3, 2026

This branch has not been deployed

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

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:needs-julius Needs Julius: route gate, unverifiable surface, or blocked size:S 10-29 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent clicks on _blank links in the Browser panel can steal composer focus

2 participants