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
Draft
Fix Settings/Rules panes going stale after cross-window tab drag (APP-5311)#14950warp-agent-staging[bot] wants to merge 3 commits into
warp-agent-staging[bot] wants to merge 3 commits into
Conversation
…-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>
Contributor
Author
There was a problem hiding this comment.
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)
…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>
1 task
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/attachhooks 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:SettingsPaneManager/AIFactManagerkept 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, becausefocus_paneresolves the stale locator to nothing and returns early.SettingsView'sSettingsViewEventsubscription is registered once, inWorkspace::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.AIFactManagerequivalent of A, compounded by B's routing bug (clicking "Rules" from a transferred Settings pane routed to the wrong window'sopen_ai_fact_collection_pane, which then hit its own stale/absent locator and silently did nothing).Fix (initial):
PaneGroup::on_window_transferredre-keys theSettingsPaneManager/AIFactManagerlocator from the source window to the destination window whenever a transferred pane group contains aSettingsPane/AIFactPane, and re-homes theSettingsView/AIFactViewevent subscription from the source workspace to the destination workspace.Workspace::open_settings_paneandopen_ai_fact_collection_paneare 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):
on_window_transferred, but in a real drag that runs while the sourceWorkspaceis already mid-update (handle_action(DropTab)→perform_handoff→CrossWindowTabDrag::execute_handoff_single_tab_to_other→AppContext::transfer_view_tree_to_window). CallingViewHandle::updateon that same, currently-removed-from-its-window view panicked with "Circular view update". It's now dispatched as a self-targeted, deferredPaneGroupAction(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).open_settings_pane/open_ai_fact_collection_panewere updating this window's own native (possibly unrendered)self.settings_pane/self.ai_fact_viewinstead of the concreteSettingsPane/AIFactPanethe locator actually points at. Page/search navigation andOpenAIFactCollectionrouting now resolve and update the pane that's actually hosting the transferred view.SettingsPane::settings_viewis nowpub(crate)instead ofpub.SettingsPaneManager/AIFactManagergainedregister_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 deferredWorkspaceAction(DiscardDuplicateTransferredPane) so the discard never touches the transferred pane group while it's still mid-update.Fix (adversarial verification round 2 -- this revision):
Workspace.settings_pane/Workspace.ai_fact_vieware not private per-workspace views:SettingsPane::new/AIFactPane::newalways fetch the window's registered singleton fromSettingsPaneManager/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_idfalls back to the view's original creation window once the live window mapping is gone (e.g. after the destination window closes), soopen_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 viaAIFactPane::from_view). Confirmed via real cross-window mouse drags in the integration-test harness for both Settings and Rules.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-tabWorkspace::perform_handoffentry 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 nativesettings_pane/ai_fact_viewfield 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 incrates/integration, which this fix was written against and which will be re-run against this branch.Ran:
cargo nextest run -p warpfor the touched modules (workspace::,pane_group::, plussettings/ai_fact/cross_windowfilters): 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 warpsucceeded, then drove the running app via computer-use) but the sandbox'sWARP_API_KEYresolves 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
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 auth blocker described above, not the fixed flow.
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.