Skip to content

agent_ui: Fix agent panel focus stealing from modals#50511

Merged
SomeoneToIgnore merged 14 commits intozed-industries:mainfrom
Dnreikronos:fix/agent-panel-focus-stealing-from-modals
Mar 6, 2026
Merged

agent_ui: Fix agent panel focus stealing from modals#50511
SomeoneToIgnore merged 14 commits intozed-industries:mainfrom
Dnreikronos:fix/agent-panel-focus-stealing-from-modals

Conversation

@Dnreikronos
Copy link
Copy Markdown
Contributor

@Dnreikronos Dnreikronos commented Mar 2, 2026

Closes #49336

Before you mark this PR as ready for review, make sure that you have:

  • Added a solid test coverage and/or screenshots from doing manual testing
  • Done a self-review taking into account security and performance aspects
  • Aligned any UI changes with the UI
    checklist

Video:

https://drive.google.com/file/d/1qAwAoDr4wr8cs1dosvLocU-a4pngJZvr/view?usp=sharing

Release Notes:

  • Fixed agent panel stealing keyboard focus from modals during workspace restoration

Dnreikronos and others added 6 commits February 24, 2026 08:59
…tore

When opening a modal (e.g. "Open Recent" via cmd-alt-o) with no active
   window, the AgentPanel async thread restoration would call
   set_active_view with focus: true, stealing keyboard focus from the
   modal. This is a regression of zed-industries#43180, where the focus guard was lost
   during subsequent refactoring.

   Thread a focus parameter through external_thread and _external_thread
   so that AgentPanel::load() passes false during startup restoration,
   while all user-initiated actions continue to pass true.

Closes zed-industries#49336
The previous fix only addressed the 'load' path (restoring a saved
thread), but missed the 'set_active' path where dock restoration
activates the panel and creates a new thread with focus: true.
   When the agent choice is already specified, call _external_thread
   synchronously instead of spawning an async task. This ensures focus
   is applied in the same frame as the panel activation, preventing
   race conditions where the async resolution would apply focus too
   late after a modal had already taken it.

   The async path is preserved only for the case where no agent is
   specified and a KVP store lookup is needed.
@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Mar 2, 2026
@Dnreikronos
Copy link
Copy Markdown
Contributor Author

Reworked fix for the agent panel stealing keyboard focus from modals during workspace restoration. This is a successor to
#49630, addressing all focus-stealing sources identified during review.

Root cause: When a user had no active window and triggered a modal (e.g., "Open Recent" via Cmd-Alt-O / Ctrl-Alt-O), multiple
async restoration paths would race against the modal and steal keyboard focus:

  1. initialize_workspace() unconditionally called workspace.focus_handle().focus() after panel initialization, overriding
    whatever UI element currently held focus.
  2. AgentPanel::load() asynchronously restored a saved thread via set_active_view(..., true, ...), grabbing focus when the async
    task completed.
  3. Panel::set_active() created a new agent thread with focus: true during dock restoration, stealing focus as the dock layout
    was rebuilt.
  4. external_thread() always spawned an async task via cx.spawn_in(), even when the agent choice was already known. This meant
    focus was applied in a later frame, racing with modal focus.

What changed

crates/zed/src/zed.rs

  • Modal guard in Uinitialize_workspace(): The workspace focus call is now guarded by !workspace.has_active_modal(window, cx). If
    a modal is already open when workspace initialization completes, focus is left on the modal instead of being ripped away.

crates/agent_ui/src/agent_panel.rs

  • focus parameter threading: A focus: bool parameter is threaded through external_thread(), _external_thread(),
    load_agent_thread_inner(), and new_agent_thread_inner() down to set_active_view(). During startup restoration (load() and
    set_active() with Uninitialized state), false is passed. All user-initiated actions (new thread, switch agent, load history,
    etc.) pass true.
  • Synchronous external_thread() when agent is known: When the agent choice is Some(agent), _external_thread() is called
    synchronously instead of spawning an async task. This ensures focus is applied in the same frame as the panel activation,
    preventing race conditions where the async resolution would apply focus too late. The async cx.spawn_in() path is preserved
    only for the None case where a KVP store lookup is needed to determine the last-used agent.
  • Removed stale AgentType::Gemini match arm: After merging main (which removed hardcoded Gemini variants), a leftover match arm
    caused compilation failures.

Impact

  • User-facing: Modals (Open Recent, file picker, etc.) retain keyboard focus when opened during or after workspace restoration.
    The agent panel correctly takes focus only when the user explicitly interacts with it.
  • Scope: Changes are limited to agent_panel.rs and zed.rs. No architectural changes.

How to reproduce and test

Why timing matters

The focus-stealing is a race condition between async workspace restoration and modal focus. When Zed reopens a window, several
async tasks fire in the background: dock layout restoration calls set_active(), saved thread restoration calls load(), and
initialize_workspace() runs panel setup. These complete over multiple frames. If a modal is opened while these tasks are still
resolving, the async completions steal focus from the modal. This is why you need to wait a moment after opening Zed before
opening the agent panel — if you open it immediately, the restoration tasks haven't completed yet and you won't see the bug.
The race is most visible on macOS where Cmd-Shift-W closes the window but keeps the app running, so the next window open
triggers full workspace restoration alongside whatever action the user performed.

Test 1: Modal focus preserved during workspace restore

  1. Open Zed with a workspace (ensure the agent panel has been used before so it gets restored)
  2. Close the window with Cmd-Shift-W (macOS) — the app stays running
  3. Press Cmd-Alt-O to open the "Open Recent" modal
  4. Expected: The modal appears and the search input is focused — you can immediately type to filter
  5. Verify: Typing filters the list without needing to click the modal first

Test 2: Agent panel focuses correctly on explicit open

  1. Continue from Test 1 (modal was opened successfully)
  2. Press Escape to dismiss the modal
  3. Wait a moment for workspace restoration to complete
  4. Press Cmd-Shift-/ to open the agent panel
  5. Expected: The agent panel opens and the input field is focused — you can immediately type
  6. Verify: Subsequent Cmd-Shift-/ toggles continue to focus the input correctly

Test 3: User-initiated agent interactions still focus

  1. Open the agent panel via Cmd-Shift-/
  2. Create a new thread, switch agents, load a thread from history
  3. Expected: Each action focuses the agent panel input as before — no regressions

@Dnreikronos
Copy link
Copy Markdown
Contributor Author

Hi @SomeoneToIgnore, I submitted a new PR on the same branch addressing the issues you pointed out in the last review.
If there's anything you think still needs work, please let me know.

@SomeoneToIgnore SomeoneToIgnore self-assigned this Mar 2, 2026
@maxdeviant maxdeviant changed the title Fix/agent panel focus stealing from modals agent_ui: Fix agent panel focus stealing from modals Mar 2, 2026
@SomeoneToIgnore
Copy link
Copy Markdown
Contributor

Sorry for the delay: I was partially available last week and finally back to full capacity.

I have tried building 910d10b commit from this remote branch, but the compilation failed.
Happy to test it the moment it's rebased and fixed.

@Dnreikronos
Copy link
Copy Markdown
Contributor Author

Sorry for the delay: I was partially available last week and finally back to full capacity.

I have tried building 910d10b commit from this remote branch, but the compilation failed. Happy to test it the moment it's rebased and fixed.

All good, @SomeoneToIgnore, thanks for your time invested!
Can you rerun the CI pipeline, please? I adjusted the conflicts that was happening.

@Dnreikronos
Copy link
Copy Markdown
Contributor Author

Hmm, I'm looking to the CI errors again....

Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

Great, I have tested things and they seem to work, thank you for pushing through to this.

Before merging, let's understand why the certain changes are made and whether they are needed at all.

Copy link
Copy Markdown
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

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

Looks nice by now, thank you.

Another merge conflict resolution before the final re-test and merge is needed, though.

@Dnreikronos
Copy link
Copy Markdown
Contributor Author

@SomeoneToIgnore, adjusted the conflicts again.

@SomeoneToIgnore SomeoneToIgnore enabled auto-merge (squash) March 6, 2026 14:29
@SomeoneToIgnore SomeoneToIgnore merged commit db6b47a into zed-industries:main Mar 6, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open recent projects (cmd-alt-o) with no active window does not focus

2 participants