Skip to content

Fix Settings/Rules panes going stale after cross-window tab drag (APP-5311) - #14950

Draft
warp-agent-staging[bot] wants to merge 3 commits into
masterfrom
fix/app-5311-settings-reopen-after-detach
Draft

Fix Settings/Rules panes going stale after cross-window tab drag (APP-5311)#14950
warp-agent-staging[bot] wants to merge 3 commits into
masterfrom
fix/app-5311-settings-reopen-after-detach

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes APP-5311: Settings (and the AI-facts "Rules" pane) could not be reopened after their tab was dragged into a new window and that window was closed.

Root cause. A cross-window tab drag intentionally suppresses the normal PaneContent::detach/attach hooks on the source window (Workspace::prepare_for_transferred_tab_attach). This left three related bugs, all rooted in per-window singleton state (SettingsPaneManager, AIFactManager) and per-view event subscriptions not knowing the pane had moved:

  • A. SettingsPaneManager/AIFactManager kept a locator on the source window pointing at a pane group that no longer lived there. Reopening Settings/Rules from that window silently no-op'd forever, because focus_pane resolves the stale locator to nothing and returns early.
  • B. A transferred SettingsView's SettingsViewEvent subscription is registered once, in Workspace::build_settings_views, against whichever workspace created it. It does not follow the view when the view transfers to another window. So an action taken from a Settings pane now hosted in window B (e.g. clicking "Rules") executed in the stale window A instead of window B.
  • C. The AIFactManager equivalent of A, compounded by B's routing bug (clicking "Rules" from a transferred Settings pane routed to the wrong window's open_ai_fact_collection_pane, which then hit its own stale/absent locator and silently did nothing).

Fix (initial):

  • PaneGroup::on_window_transferred re-keys the SettingsPaneManager/AIFactManager locator from the source window to the destination window whenever a transferred pane group contains a SettingsPane/AIFactPane, and re-homes the SettingsView/AIFactView event subscription from the source workspace to the destination workspace.
  • Workspace::open_settings_pane and open_ai_fact_collection_pane are defensive: if the registered locator does not resolve to a live pane in the current window, they clear it and open a fresh tab/pane instead of silently doing nothing.

Fix (after adversarial review — this revision):

  • Re-entrancy panic. The subscription rehoming above ran synchronously inside on_window_transferred, but in a real drag that runs while the source Workspace is already mid-update (handle_action(DropTab)perform_handoffCrossWindowTabDrag::execute_handoff_single_tab_to_otherAppContext::transfer_view_tree_to_window). Calling ViewHandle::update on that same, currently-removed-from-its-window view panicked with "Circular view update". It's now dispatched as a self-targeted, deferred PaneGroupAction (not run synchronously, and not routed through the ancestor chain, which isn't reliably populated for a just-transferred view until the next render pass).
  • Wrong view updated. open_settings_pane/open_ai_fact_collection_pane were updating this window's own native (possibly unrendered) self.settings_pane/self.ai_fact_view instead of the concrete SettingsPane/AIFactPane the locator actually points at. Page/search navigation and OpenAIFactCollection routing now resolve and update the pane that's actually hosting the transferred view.
  • Narrower visibility. SettingsPane::settings_view is now pub(crate) instead of pub.
  • One-pane-per-window invariant. Per product decision, Warp intentionally allows at most one Settings pane and one Rules pane per window. SettingsPaneManager/AIFactManager gained register_transferred_pane, which detects a collision with an existing pane in the destination window instead of silently overwriting the locator (which used to leave the pre-existing pane live but untracked). On collision, the transferred pane is discarded — closing its whole tab if it was the tab's only content, otherwise just the pane — and the pre-existing pane is kept and focused, via a second self-targeted deferred WorkspaceAction (DiscardDuplicateTransferredPane) so the discard never touches the transferred pane group while it's still mid-update.

Fix (adversarial verification round 2 -- this revision):

  • Dangling native-view crash. Workspace.settings_pane/Workspace.ai_fact_view are not private per-workspace views: SettingsPane::new/AIFactPane::new always fetch the window's registered singleton from SettingsPaneManager/AIFactManager, so the pane's embedded view is that singleton. The low-level view-tree transfer physically relocates that same view to the destination window, so the source window's own field/registration were left holding a handle to a view that no longer lived there. ViewHandle::window_id falls back to the view's original creation window once the live window mapping is gone (e.g. after the destination window closes), so open_settings_pane/open_ai_fact_collection_pane's fallback path -- the one added earlier in this PR to stop the silent no-op -- ended up dereferencing that dangling handle back in the source window and panicking with "Circular view update" (write path) or "circular view reference" (read path via AIFactPane::from_view). Confirmed via real cross-window mouse drags in the integration-test harness for both Settings and Rules.
    • Fixed by having the source workspace build itself a fresh native Settings/Rules view the moment the old one transfers out (Workspace::replace_native_settings_view/replace_native_ai_fact_view), using the same self-targeted deferred dispatch already in place for the subscription re-homing.

Linked Issue

Testing

Regression tests in app/src/workspace/view_tests.rs, each exercising the production cross-window-transfer primitives (AppContext::transfer_view_tree_to_window / Workspace::perform_handoff + insert_transferred_tab_at_index + remove_tab_without_undo/remove_tab) rather than mocking them away:

  • test_settings_pane_reopens_after_cross_window_transfer_and_close (symptom A)
  • test_settings_pane_actions_execute_in_hosting_window_after_cross_window_transfer (symptom B)
  • test_ai_fact_pane_reopens_after_cross_window_transfer_and_close (symptom C)
  • test_settings_pane_transfer_via_real_handoff_path_does_not_panic — drives the real single-tab Workspace::perform_handoff entry point (not a bare top-level transfer call) so the transfer runs nested inside the source workspace's own update, reproducing the call stack that used to panic.
  • test_settings_pane_page_navigation_after_transfer_updates_transferred_view — proves page navigation updates the transferred pane's own view, not this window's unused native one.
  • test_settings_pane_transfer_into_window_with_existing_pane_discards_duplicate / test_ai_fact_pane_transfer_into_window_with_existing_pane_discards_duplicate — collision reconciliation for both pane kinds: no duplicate survives, the pre-existing pane stays reachable, and closing it still lets a fresh one open.
  • test_settings_pane_native_view_is_replaced_after_transferring_out / test_ai_fact_native_view_is_replaced_after_transferring_out — after a transfer, the source window's native settings_pane/ai_fact_view field and manager registration are swapped for a freshly created view rather than left pointing at the relocated one, and a subsequently reopened tab/pane uses the replacement.

I verified each new/changed test fails against the pre-fix code and passes with the fix restored (temporarily reverting just the relevant change and re-running). The unit test harness's platform delegate no-ops window close (close_window_async), so it cannot reproduce the exact "destination window fully closes" trigger for the dangling-view panic; that path is covered by the reviewer's real-mouse-drag integration test in crates/integration, which this fix was written against and which will be re-run against this branch.

Ran:

  • cargo nextest run -p warp for the touched modules (workspace::, pane_group::, plus settings/ai_fact/cross_window filters): 519 tests, all passing.

  • ./script/format — clean.

  • cargo clippy -p warp --all-targets -- -D warnings — clean.

  • I have manually tested my changes locally with ./script/run (see note below)

Manual/GUI verification note: I attempted to verify the fix end-to-end with a live build (cargo build --bin warp succeeded, then drove the running app via computer-use) but the sandbox's WARP_API_KEY resolves to an agent identity rather than a real user account, so the app never leaves the logged-out onboarding screen (backend rejects it with "Expected a user account") and I could not reach the terminal UI to exercise the Settings tab. I'm reporting this limitation rather than claiming GUI verification that didn't happen — the recording/screenshot below document the auth blocker, not the fix in action. The fix is otherwise fully covered by the fail-before/pass-after regression tests above, including a dedicated test for the exact re-entrancy call stack that previously panicked.

Screenshots / Videos

Computer-use video recordings

Launching Warp, opening Settings, detaching the Settings tab into a new window, closing that window, and attempting to reopen Settings in the original window.
Warp Settings detach and reopen flow: Attempted run, blocked at onboarding due to the sandbox API key not being a user-account credential (see note above).

Computer-use screenshots

Warp stuck on the logged-out "Welcome to Warp" onboarding screen; the terminal UI is never reached because the backend rejects the API-key identity as "Expected a user account".
Warp stuck on the logged-out "Welcome to Warp" onboarding screen — the auth blocker described above, not the fixed flow.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Conversation: https://staging.warp.dev/conversation/69399d8d-001f-4932-a315-3ccf9430e4b9
Run: https://oz.staging.warp.dev/runs/019ff1d9-0c2c-719e-92b5-86326ccefe3a

This PR was generated with Oz.

…-5311)

When a Settings (or AI-facts "Rules") tab is dragged into a new window,
the transfer skips the normal PaneContent::detach/attach hooks
(Workspace::prepare_for_transferred_tab_attach suppresses detach on the
source window). This left three bugs:

A. SettingsPaneManager/AIFactManager kept a stale locator on the source
   window pointing at a pane group that no longer lived there, so
   reopening Settings/Rules from that window silently no-op'd forever
   after the transferred tab was closed.
B. A transferred SettingsView's SettingsViewEvent subscription remained
   bound to the workspace that originally created it, so actions taken
   from a Settings pane now hosted in window B (e.g. clicking "Rules")
   executed in the stale window A instead.
C. The AIFactManager equivalent of A, compounded by B's routing bug.

Fix:
- PaneGroup::on_window_transferred now re-keys the SettingsPaneManager/
  AIFactManager locator from the source window to the destination
  window, and re-homes the SettingsView/AIFactView event subscription
  from the source workspace to the destination workspace.
- Workspace::open_settings_pane and open_ai_fact_collection_pane are
  now defensive: if the registered locator does not resolve to a live
  pane in the current window, they clear it and open a fresh tab/pane
  instead of silently doing nothing.

Added regression tests in app/src/workspace/view_tests.rs covering all
three symptoms via the same cross-window transfer primitives production
code uses (transfer_view_tree_to_window + insert_transferred_tab_at_index
+ remove_tab_without_undo).

Co-Authored-By: Warp Agent <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 11, 2026

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Overview

Fixes Settings/Rules panes going stale after a cross-window tab drag, covering all three reported symptoms. Two findings below need a human decision rather than a mechanical fix; the other review findings are already being addressed on this branch.

Concerns

  • Singleton collision on transfer is unspecified behavior (app/src/pane_group/mod.rs:8246, :8285). Dragging Settings or Rules into a destination window that already has that pane silently overwrites the destination manager locator while both panes stay live; when the transferred pane later closes, its detach clears the map and the original destination pane becomes untracked, so the next open creates a duplicate. The code's invariant is one Settings/Rules pane per window, but the intended collision behavior is a product decision: reject the transfer, focus the destination singleton and discard the source pane, or replace the existing destination pane. Please pick one and it will be enforced and tested for both pane types.
  • Visual proof is missing for a user-facing change. This changes desktop window behavior, but the only capture attached shows the login/onboarding blocker rather than the three flows working end to end. The agent environment's API key resolves to an agent identity that the backend rejects, so the app never got past sign-in; a user-account key is being arranged, after which a recording of all three flows will be attached here.

Verdict

Checks: build pass, tests pass, CI no failing checks (heavy jobs skipped while draft), visual proof missing

Found: 0 critical, 2 important, 0 suggestions, 0 nits (findings needing your judgment only)

Comment thread app/src/pane_group/mod.rs Outdated
…sion handling

- PaneGroup::on_window_transferred no longer runs the Settings/AI-fact
  subscription rehoming synchronously; it's dispatched as a self-targeted
  deferred PaneGroupAction so a real drag (which runs while the source
  Workspace is already mid-update) doesn't panic with "Circular view
  update".
- open_settings_pane/open_ai_fact_collection_pane now resolve the concrete
  SettingsPane/AIFactPane's own view via the locator instead of updating
  this window's native (possibly different) settings_pane/ai_fact_view,
  so page/search navigation and OpenSettings/OpenAIFactCollection routing
  target the pane that's actually hosting the request.
- Narrowed SettingsPane::settings_view to pub(crate).
- Enforced the one-Settings-pane/one-Rules-pane-per-window invariant on
  transfer: SettingsPaneManager/AIFactManager gained
  register_transferred_pane, which detects a collision with an existing
  pane instead of silently overwriting the destination locator. On
  collision, the transferred pane is discarded (its whole tab if it was
  the tab's only content, otherwise just the pane) and the pre-existing
  pane is kept and focused, via a second self-targeted deferred
  WorkspaceAction so the pane group being discarded is never touched
  while still mid-update.
- Added regression tests: real single-tab-drag handoff path (proves no
  panic), page-navigation targeting the transferred pane, and collision
  reconciliation for both Settings and Rules panes.

Co-Authored-By: Warp Agent <agent@warp.dev>
Workspace.settings_pane / Workspace.ai_fact_view are not private
per-workspace views: SettingsPane::new / AIFactPane::new always fetch
the window's registered singleton from SettingsPaneManager/
AIFactManager, so the pane's embedded view *is* that singleton.
AppContext::transfer_view_tree_to_window physically relocates the
entire transferred pane's view subtree -- including that view -- to
the destination window, so the source window's own field and manager
registration were left holding a handle to a view that no longer lived
there. ViewHandle::window_id falls back to the view's *original*
creation window once the current window/view_to_window mapping is
gone (e.g. after the destination window closes), so open_settings_pane
and open_ai_fact_collection_pane's fallback path -- which the previous
commit added to stop the silent no-op -- ended up dereferencing that
dangling handle back in the source window and panicking with "Circular
view update" (write path) or "circular view reference" (read path via
AIFactPane::from_view).

Fixed by having the source workspace build itself a fresh native
Settings/Rules view the moment the old one transfers out, in
PaneGroup::rehome_pane_event_subscription (same self-targeted deferred
dispatch already used for the subscription re-homing, so this also
never touches a pane group that's still mid-update). Added
Workspace::replace_native_settings_view / replace_native_ai_fact_view,
factored out of the existing view-construction helpers, and two
regression tests proving the native view is swapped for a fresh one
and that reopening Settings/Rules afterward uses the replacement
rather than the transferred view.

Co-Authored-By: Warp Agent <agent@warp.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant