add support for cross-window tab drag#9275
Conversation
Removes WorkspaceAction::HandoffPendingTransfer, ReverseHandoff, and FinalizeDropTab. The cross-window drag flow no longer routes through WorkspaceAction; it is now coordinated through the upcoming CrossWindowTabDrag singleton model. hide window when dragging to target fix unstable drop zone render exact copy of tab rendering consistency checkin ghost state checkpoint Fix crashes Update view.rs fix edge case issues around persistence and detachment fix typo integration: add tests for cross-window tab drag Adds four end-to-end integration tests behind the new drag_tabs_to_windows feature on the integration crate: - test_reorder_tabs_with_drag - test_detach_tab_to_new_window_with_drag - test_attach_tab_to_other_window_and_continue_drag - test_single_tab_handoff_continues_drag Wires them into both the manual integration runner and the nextest ui_tests! suite, and adds the matching feature passthrough in crates/integration/Cargo.toml. app_state: skip persistence during cross-window tab drag While a cross-window tab drag is active, the dragged tab's pane group is in flight between the source and preview windows. Both can briefly claim the same terminal_panes.uuid, which trips SQLite's UNIQUE constraint when persistence runs mid-drag. Skip persistence entirely while CrossWindowTabDrag is active; the next mouse-up or non-drag change will trigger a save. Also switches the existing per-window workspace lookup to WorkspaceRegistry, mirroring root_view, and uses the renamed is_tab_drag_preview() helper. root_view: simplify after cross-window tab drag refactor Removes the bespoke DetachTabImmediateArg / TabTransferInfo plumbing and the root_view:detach_tab_immediate global action, both of which existed only to support the old cross-window drag flow. Their responsibilities now live in CrossWindowTabDrag and the workspace view. Updates create_transferred_window to take the TransferredTab and window placement directly and return just the new WindowId, and switches workspace_for_window to look up workspaces through WorkspaceRegistry instead of scanning views_of_type::<Workspace>. workspace view: integrate cross-window tab drag Wires the workspace view into the new CrossWindowTabDrag singleton: - Drives the drag state machine from on_drag/on_drop on tabs and the tab bar. - Exposes helpers (tab_bar_rects_for_window, TransferredTab, TAB_BAR_POSITION_ID) that the singleton uses to coordinate hit testing and view-tree transfers between windows. - Renames is_drag_preview_workspace to is_tab_drag_preview to match the new state model. - Adjusts vertical-tab drag behavior so that when DragTabsToWindows is enabled, vertical tabs can be dragged horizontally out of the panel to detach into a new window. When the flag is off the existing vertical-only constraint is preserved. workspace: add CrossWindowTabDrag singleton model Adds a new singleton model that owns all cross-window tab drag state across the application. The model tracks the drag lifecycle through three phases — Floating, InsertedInTarget, and Transitioning — and exposes on_drag/on_drop entry points that workspace views call to drive the state machine. Two drag sources are supported: - SingleTabWindow: the source window itself acts as the floating preview. - MultiTabWindow: a dedicated preview window is created for the tab. Registers the singleton in workspace::init() so it is available app-wide. The workspace view integration that actually exercises the new APIs is added in a follow-up commit. warpui: track window front-to-back ordering and add window bounds helper Adds a WindowOrderingState to WindowManager so callers can find which window is topmost at a given screen position, which is needed when deciding where a dragged tab should land. Adds a matching ordered_window_ids() API for both the production and integration-test window managers. Also adds AppContext::set_and_cache_window_bounds for callers that need to move a window and update the cache atomically, and tweaks the macOS window close path so force-terminated windows close immediately while normal closes still go through performClose:. Add tab dragging product and tech specs Includes the original drag-tabs-to-windows PRODUCT.md and TECH.md, plus follow-up TECH specs for fix-drag-drop, fix-dragging-out, pane-uuid-collision-on-handoff, and put-back-plus-new-window-overlap. Co-Authored-By: Oz <oz-agent@warp.dev>
|
I'm starting a first review of this pull request. You can follow along in the session on Warp. I completed the review and posted feedback on this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
Adds feature-flagged cross-window tab dragging with new drag-state orchestration, live view transfer support, platform window primitives, specs, and integration coverage.
Concerns
- The source==target put-back finalize path closes the preview asynchronously without keeping the drag guard active, which can allow session persistence to snapshot both the source and preview while they still reference the same pane group.
- Transferred tabs flatten tab color state to a resolved color, losing automatic directory-color vs manually-cleared/manual-color semantics when attaching to another window.
- No security findings.
Verdict
Found: 0 critical, 2 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| "tab_drag: finalize_handoff source==target, closing preview_wid={}", | ||
| drag.preview_window_id() | ||
| ); | ||
| ctx.windows().close_window( |
There was a problem hiding this comment.
NoOp means finalize does not register a pending close; during a put-back drop the preview still has a TabData for the transferred pane group until on_window_closed, so save_app can snapshot both windows and hit the duplicate-pane race this guard is meant to prevent.
|
|
||
| let index = insertion_index.min(self.tabs.len()); | ||
| let mut tab_data = TabData::new(pane_group); | ||
| tab_data.selected_color = color.map_or(SelectedTabColor::Unset, SelectedTabColor::Color); |
There was a problem hiding this comment.
default_directory_color and selected_color separately instead of reconstructing from color.
|
Note that this needs to be tested on Windows before we can roll it out |
|
|
||
| ### Input focus after drag ends | ||
|
|
||
| - When the drag completes, the resulting active tab must have terminal input focus. |
There was a problem hiding this comment.
Should really just be whatever was already in focus within the tab, whether its a terminal input, overlay menu, or whatever (not necessarily terminal input)
There was a problem hiding this comment.
would like both specs to be in dir with linear ticket id (we should stop nesting them under username as well)
zachbai
left a comment
There was a problem hiding this comment.
the spec at a high level is reasonable and makes sense but as its probably obvious there are lots of little edge cases that appear to be addressed but hint at maybe the existence of more yet to be found.
Can you also verify that everything looks good when the vertical tabs panel is moved to the right via Re-arrange toolbar items
## Description Adds Chrome-style cross-window tab dragging behind the `DragTabsToWindows` feature flag. A user can drag a tab out of a window to create a new one, drag it into another window's tab bar to attach it, and keep dragging through multiple attach/detach cycles without releasing the mouse. Product behavior is fully described in `specs/pei/cross-window-tab-drag/PRODUCT.md`; the architectural rationale is in `specs/pei/cross-window-tab-drag/TECH.md`. https://www.loom.com/share/94d21b4b573c4f6893684142d66b844a ## How it works A cross-window drag has two shapes depending on where it starts: - **Single-tab window** — the source window itself follows the cursor and acts as the drag preview. No second window is created. - **Multi-tab window** — when the user drags a tab out of the tab bar, a dedicated preview window is spun up to hold the dragged tab, and the source window stays in place showing its remaining tabs. As the user drags, we continuously hit-test the cursor against the tab bars of other windows in z-order. When the cursor enters an eligible tab bar we show a lightweight ghost (insertion slot + floating chip) in the target; the live view tree only moves at drop time, except in the back-to-caller case where the tab is transferred so the source window can host real reordering. If the cursor leaves a target tab bar, any handoff is reversed and the drag continues. On drop, the preview is either promoted to a permanent window (no target), folded into the target (handoff committed), or cleaned up (no-op). ## State machines Full ASCII versions live in the module doc at `app/src/workspace/cross_window_tab_drag.rs`. Mermaid renderings: ### Single-tab source window ```mermaid stateDiagram-v2 [*] --> Floating: begin_single_tab_drag Floating --> Transitioning: cursor enters target tab bar Transitioning --> InsertedInTarget InsertedInTarget --> Floating: cursor leaves target tab bar (reverse_handoff) InsertedInTarget --> FinalizeHandoff: on_drop while inserted Floating --> FinalizeFloatingWindow: on_drop while floating FinalizeHandoff --> [*] FinalizeFloatingWindow --> [*] ``` The source window itself is the preview, so no extra window is created. `FinalizeFloatingWindow` just leaves the source window where the user dropped it. ### Multi-tab source window ```mermaid stateDiagram-v2 [*] --> Floating: begin_multi_tab_drag (creates preview window) Floating --> GhostInTarget: cursor enters target tab bar (deferred) GhostInTarget --> Floating: cursor leaves target tab bar Floating --> Transitioning: cursor re-enters source (back-to-caller) Transitioning --> InsertedInTarget InsertedInTarget --> Floating: cursor leaves source tab bar (reverse_handoff) InsertedInTarget --> FinalizeHandoff: on_drop while inserted GhostInTarget --> FinalizeHandoff: on_drop over target Floating --> FinalizePreviewAsNewWindow: on_drop while floating FinalizeHandoff --> [*] FinalizePreviewAsNewWindow --> [*] ``` `GhostInTarget` is a hover-only state — no view-tree transfer happens until drop. The `InsertedInTarget` branch is reserved for the back-to-caller path, where the preview must be kept alive so the user can drag the tab back out again. ## Infrastructure changes Most of the diff lands in shared infrastructure rather than in feature-specific code. Each piece exists to satisfy a specific product invariant: - **Move a live tab between windows without restarting it.** A user dragging a tab between windows expects their terminal, scrollback, agent state, and animations to be preserved — kill-and-respawn would break the illusion of one continuous gesture. WarpUI gains the ability to relocate a live view tree (and its non-rendered structural children) into a different window, which is what makes the dragged tab feel like the same tab no matter which window is hosting it. - **Make z-order observable so drop targeting matches what the user sees.** When windows overlap, dropping a tab "into" an occluded window through the window in front of it would feel buggy. The window manager now exposes front-to-back ordering so attach targeting only considers windows that are actually reachable from the current cursor position. - **Show preview windows without disrupting the user's typing context.** A preview window appearing under the cursor would steal focus from whatever the user was typing into and would briefly flash blank before its content is ready. A new windowing primitive lets us materialize a window at exact bounds without taking focus, paired with a focus-suppression hook that covers the gap before the preview's content paints for the first time. - **Closing a window because its tab moved should be silent.** Today, closing a window with running processes prompts "Close window?" and tears down panes — both correct for normal closes, both wrong when the window is closing only because its content moved elsewhere. New workspace flags and a dedicated termination mode let transfer-driven closes skip the prompt and the teardown, so the user never sees a dialog that suggests data loss during a harmless transfer. Snapshots also skip the temporary preview workspaces so they don't leak into persistence. - **One owner of cross-window drag state.** Multiple windows mutating each other in response to the same drag event is exactly the shape of bug that produces duplicated tabs and stale subscriptions. Concentrating the drag state machine in a singleton, with workspaces only reacting to its returned decisions, removes the re-entrancy entirely and makes "what state is the drag in?" a single question with a single answer. - **One drag implementation across tab presentations.** Horizontal tabs and the vertical tabs panel are different visual surfaces, but the user expects identical drag behavior from both. Both UIs now emit the same drag actions and feed the same orchestration code, which keeps them from drifting apart and prevents accidental "works for horizontal, broken for vertical" regressions. - **Specs and integration tests.** Product behavior and architectural decisions are checked into `specs/pei/cross-window-tab-drag/` so future changes have something to preserve. Integration coverage exercises detach, attach, reattach, reverse-handoff, target-side reorder, and drop-outside flows behind the feature flag rather than gating on a specific OS. Behind a feature flag, so no change for users until it's rolled out. ## Testing - New integration tests cover detach, attach, reattach, reverse-handoff, reorder-in-target, and drop-outside scenarios. - Manually verified single-tab and multi-tab drags against all scenarios in `specs/pei/cross-window-tab-drag/PRODUCT.md` § Success Criteria on macOS. ## Server API dependencies No server dependencies. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-NEW-FEATURE: You can now drag tabs out of a window into their own window, or between windows, similar to Chrome. --------- Co-authored-by: Oz <oz-agent@warp.dev>
|
This seems to be merged, but its not visible in 2026.05.09.21.stable_03 for me. I can only drag the tabs to "reorder" them. I read about a |
|
+1 to @baschny - latest stable relase does not appear to have this capability. |
|
Those might be compile time flags, so you'll need to download the source at a specific tag/release and compile it yourself with the flag on. |
## Description Adds Chrome-style cross-window tab dragging behind the `DragTabsToWindows` feature flag. A user can drag a tab out of a window to create a new one, drag it into another window's tab bar to attach it, and keep dragging through multiple attach/detach cycles without releasing the mouse. Product behavior is fully described in `specs/pei/cross-window-tab-drag/PRODUCT.md`; the architectural rationale is in `specs/pei/cross-window-tab-drag/TECH.md`. https://www.loom.com/share/94d21b4b573c4f6893684142d66b844a ## How it works A cross-window drag has two shapes depending on where it starts: - **Single-tab window** — the source window itself follows the cursor and acts as the drag preview. No second window is created. - **Multi-tab window** — when the user drags a tab out of the tab bar, a dedicated preview window is spun up to hold the dragged tab, and the source window stays in place showing its remaining tabs. As the user drags, we continuously hit-test the cursor against the tab bars of other windows in z-order. When the cursor enters an eligible tab bar we show a lightweight ghost (insertion slot + floating chip) in the target; the live view tree only moves at drop time, except in the back-to-caller case where the tab is transferred so the source window can host real reordering. If the cursor leaves a target tab bar, any handoff is reversed and the drag continues. On drop, the preview is either promoted to a permanent window (no target), folded into the target (handoff committed), or cleaned up (no-op). ## State machines Full ASCII versions live in the module doc at `app/src/workspace/cross_window_tab_drag.rs`. Mermaid renderings: ### Single-tab source window ```mermaid stateDiagram-v2 [*] --> Floating: begin_single_tab_drag Floating --> Transitioning: cursor enters target tab bar Transitioning --> InsertedInTarget InsertedInTarget --> Floating: cursor leaves target tab bar (reverse_handoff) InsertedInTarget --> FinalizeHandoff: on_drop while inserted Floating --> FinalizeFloatingWindow: on_drop while floating FinalizeHandoff --> [*] FinalizeFloatingWindow --> [*] ``` The source window itself is the preview, so no extra window is created. `FinalizeFloatingWindow` just leaves the source window where the user dropped it. ### Multi-tab source window ```mermaid stateDiagram-v2 [*] --> Floating: begin_multi_tab_drag (creates preview window) Floating --> GhostInTarget: cursor enters target tab bar (deferred) GhostInTarget --> Floating: cursor leaves target tab bar Floating --> Transitioning: cursor re-enters source (back-to-caller) Transitioning --> InsertedInTarget InsertedInTarget --> Floating: cursor leaves source tab bar (reverse_handoff) InsertedInTarget --> FinalizeHandoff: on_drop while inserted GhostInTarget --> FinalizeHandoff: on_drop over target Floating --> FinalizePreviewAsNewWindow: on_drop while floating FinalizeHandoff --> [*] FinalizePreviewAsNewWindow --> [*] ``` `GhostInTarget` is a hover-only state — no view-tree transfer happens until drop. The `InsertedInTarget` branch is reserved for the back-to-caller path, where the preview must be kept alive so the user can drag the tab back out again. ## Infrastructure changes Most of the diff lands in shared infrastructure rather than in feature-specific code. Each piece exists to satisfy a specific product invariant: - **Move a live tab between windows without restarting it.** A user dragging a tab between windows expects their terminal, scrollback, agent state, and animations to be preserved — kill-and-respawn would break the illusion of one continuous gesture. WarpUI gains the ability to relocate a live view tree (and its non-rendered structural children) into a different window, which is what makes the dragged tab feel like the same tab no matter which window is hosting it. - **Make z-order observable so drop targeting matches what the user sees.** When windows overlap, dropping a tab "into" an occluded window through the window in front of it would feel buggy. The window manager now exposes front-to-back ordering so attach targeting only considers windows that are actually reachable from the current cursor position. - **Show preview windows without disrupting the user's typing context.** A preview window appearing under the cursor would steal focus from whatever the user was typing into and would briefly flash blank before its content is ready. A new windowing primitive lets us materialize a window at exact bounds without taking focus, paired with a focus-suppression hook that covers the gap before the preview's content paints for the first time. - **Closing a window because its tab moved should be silent.** Today, closing a window with running processes prompts "Close window?" and tears down panes — both correct for normal closes, both wrong when the window is closing only because its content moved elsewhere. New workspace flags and a dedicated termination mode let transfer-driven closes skip the prompt and the teardown, so the user never sees a dialog that suggests data loss during a harmless transfer. Snapshots also skip the temporary preview workspaces so they don't leak into persistence. - **One owner of cross-window drag state.** Multiple windows mutating each other in response to the same drag event is exactly the shape of bug that produces duplicated tabs and stale subscriptions. Concentrating the drag state machine in a singleton, with workspaces only reacting to its returned decisions, removes the re-entrancy entirely and makes "what state is the drag in?" a single question with a single answer. - **One drag implementation across tab presentations.** Horizontal tabs and the vertical tabs panel are different visual surfaces, but the user expects identical drag behavior from both. Both UIs now emit the same drag actions and feed the same orchestration code, which keeps them from drifting apart and prevents accidental "works for horizontal, broken for vertical" regressions. - **Specs and integration tests.** Product behavior and architectural decisions are checked into `specs/pei/cross-window-tab-drag/` so future changes have something to preserve. Integration coverage exercises detach, attach, reattach, reverse-handoff, target-side reorder, and drop-outside flows behind the feature flag rather than gating on a specific OS. Behind a feature flag, so no change for users until it's rolled out. ## Testing - New integration tests cover detach, attach, reattach, reverse-handoff, reorder-in-target, and drop-outside scenarios. - Manually verified single-tab and multi-tab drags against all scenarios in `specs/pei/cross-window-tab-drag/PRODUCT.md` § Success Criteria on macOS. ## Server API dependencies No server dependencies. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-NEW-FEATURE: You can now drag tabs out of a window into their own window, or between windows, similar to Chrome. --------- Co-authored-by: Oz <oz-agent@warp.dev>
…4e67, d7c45ca) Ports the Chrome-style cross-window tab drag from upstream PR warpdotdev#9275 adapted to twarp's diverged tree. Dragging a tab from one window's strip into another window's strip moves the live pane tree (processes intact) to the drop position, with an insertion ghost (slot + floating chip) tracking the cursor in the target strip; dropping outside any strip detaches into a new window or cancels. Adds the `CrossWindowTabDrag` singleton state machine (app/src/workspace/cross_window_tab_drag.rs, ~1849 lines: Floating / GhostInTarget / InsertedInTarget / Transitioning phases, single- and multi-tab sources, deferred ghost transfer, reverse-handoff, drop-time re-resolution, pending-close persistence guard). Replaces the simpler 8b detach-only path (try_detach_tab_on_drag / detach_tab_immediate global action) with the singleton-driven flow. Rewires Workspace::on_tab_drag and adds perform_handoff / handle_drop_result / tab_insertion_index_for_cursor / insert_transferred_tab_at_index / prepare_for_transferred_tab_attach / get_tab_transfer_info_for_attach / close_window_for_content_transfer plus tab-bar ghost + floating-chip rendering in both the horizontal tab bar and vertical tabs panel. Adapted to twarp: TransferredTab gains a draggable_state field; the placeholder+adopt transfer model is bridged to upstream's insert_transferred_tab_at_index / get_tab_transfer_info_for_attach; is_drag_preview_workspace renamed to is_tab_drag_preview; adds suppress_detach_panes_on_window_close wired into on_window_closed. Platform/core: set_window_alpha across the WindowManager trait (default no-op), windowing state, mac backend (Rust + objc setAlphaValue:); and AppContext::set_and_cache_window_bounds. The winit real-manager ordered_window_ids / WindowOrderingState and the integration-test ordering changes were intentionally not ported (Linux-only; the trait default vec![] degrades to the distance-scan fallback). macOS, the target platform, has full ordered_window_ids. Behind the DragTabsToWindows dogfood flag. cargo check -p warp clean (0 errors, 0 warnings). The drag gesture and insertion ghost require visual verification in a launched build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL
* [twarp 08] specs: macOS-style UI overhaul Fill PRODUCT.md and TECH.md for feature 08 (macOS UI overhaul), covering sub-phases 8a–8f: - 8a Chrome-style top-rounded tabs (tab.rs render_tab_container_internal), preserving feature 01 colors + feature 06 rename. - 8b detach tab → new window: enable dormant DragTabsToWindows flag + create_transferred_window/transfer_view_tree_to_window (live processes carried across). - 8c drag tab between windows: port upstream (3984e67, d7c45ca) drop hit-testing + insertion ghost; may bundle with 8b. - 8d Claude chat bottom gradient fade-out via warpui foreground overlay + Fill::Gradient (theme-tracking). - 8e sessions search: single-line EditorView substring filter on StoredSession.title in left_panel.rs. - 8f macOS sidebar restyle: flat pinned-light background, pill segmented switcher, muted headers; warpui emulation, no AppKit embedding. PRODUCT.md carries owner-confirmed decisions and a per-sub-phase smoke-test checklist; TECH.md grounds each anchor in verified file:line references and maps sub-phases → invariants → smoke steps. Phase → spec-in-review. Also fixes two stale 08-rebrand → 09-rebrand references in feature 07 specs (rebrand moved to 09 when this feature was inserted as 08). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08] specs: link spec PR #81 in STATUS Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08] roadmap: owner-directed bundle — all of 8a–8f impl folded into spec PR #81 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08a-8c] tabs + cross-window drag 8a — Chrome-style tab shape (PRODUCT §1–§5): - render_tab_container_internal: top-rounded only via CornerRadius::with_top(NEW_TAB_CORNER_RADIUS); bottom corners square so the active tab seats flush onto the pane below. - Active tab drops its bottom border (merges with content); inactive tabs keep it (recessed). First-tab left-border logic preserved. - Feature 01 per-tab colors (styles.background) and feature 06 inline rename editor untouched — color/fill opacity path and rename TextInput unchanged. 8b — Drag tab → new window (PRODUCT §6–§9): - Enabled FeatureFlag::DragTabsToWindows in DOGFOOD_FLAGS. - on_tab_drag: new try_detach_tab_on_drag branch detaches a multi-tab when the drag center clears the tab bar past DETACH_SENSITIVITY (threshold ported from upstream 3984e67); below threshold falls through to the existing reorder. Dispatches the deferred root_view:detach_tab_immediate global action, reusing the existing detach_tab_with_transfer machinery (live view-tree transfer + origin reflow + last-tab window close, processes intact). Sets TabData.detached and clears stray marks on DropTab (snap-back). 8c — Drag tab between windows (PRODUCT §10–§12) — partial port: - Added the in-scope transfer primitives: Workspace::prepare_for_transferred_tab + reorder_last_tab_to (target-side placeholder/adopt/reorder) and root_view::transfer_tab_to_window, which moves a tab into an EXISTING window via the same transfer_view_tree_to_window primitive, preserving color/name/ live processes. - NOT ported: the cross-window drag-state machine, screen-space cursor hit- testing, and insertion-ghost drop feedback (PRODUCT §11). These live in upstream's workspace::cross_window_tab_drag module (+ app_state.rs, vertical_tabs.rs, global_actions.rs, mod.rs and warpui platform changes for screen-space drag tracking / window-follow), all outside this sub-phase's four-file scope. transfer_tab_to_window is the primitive a future scope- expanded phase would drive from that gesture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08d] claude chat bottom gradient fade-out Add a bottom gradient fade to the Claude Code transcript so messages slide under the floating composer instead of ending at a hard cut (PRODUCT §13–§16). A full-width band (`COMPOSER_CLEARANCE` tall) is pinned to the pane bottom as a positioned Stack child inserted between the scrolled transcript body and the floating composer: it paints above scrolled content but below the opaque composer (§14). Its background is a vertical `Fill::Gradient` running from transparent (top) to the live theme pane background (bottom); the transparent endpoint reuses the background RGB with zero alpha, so the fade never tints toward a hard-coded colour and is invisible-by-design in light and dark (§16). The band carries no event handlers, so it does not consume clicks or alter scroll extent / hit-testing (§15). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08e-8f] sessions search + macOS sidebar restyle 8e — Sessions search (PRODUCT §17–§20): - Add a single-line `EditorView` search field to `LeftPanelView`, built like keybindings_page.rs and re-rendering live on `Edited`. - `render_claude_sessions_panel` renders the field above the heading/rows and case-insensitive substring-filters by `session.title` via a new `filter_session_indices` free fn (preserves original indices so resume targets the right session — PRODUCT §20). - Distinct "No matching sessions" no-match branch separate from the zero-stored empty state; empty query is not a filter (§18). - Unit tests for the filter (empty query, case-insensitive substring, no-match + order preservation). 8f — macOS sidebar restyle (PRODUCT §21–§25): - Pin the panel root `Container` to a flat macOS-light fill (`MACOS_SIDEBAR_BG`) that does not read the active theme, so a dark terminal theme leaves the sidebar light (intended two-tone). - Replace the icon-row tool switcher with a macOS pill segmented control (rounded track Container + per-segment `render_pill_segment`); routing is unchanged — each segment dispatches the same `LeftPanelAction` as before, active segment = filled white pill, all colors pinned light. - Muted section header, restyled sessions rows (pinned primary/secondary text, soft light hover highlight), pinned the close-panel icon so it stays legible on light under any theme. Theme-leakage note: the inherited Project Explorer / Shortcuts / Timeline content still reads theme `sub_text_color`/hover fills (§25 forbids bespoke re-layout); flagged for human visual check against the light background. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08c] full interactive cross-window tab drag (port upstream 3984e67, d7c45ca) Ports the Chrome-style cross-window tab drag from upstream PR warpdotdev#9275 adapted to twarp's diverged tree. Dragging a tab from one window's strip into another window's strip moves the live pane tree (processes intact) to the drop position, with an insertion ghost (slot + floating chip) tracking the cursor in the target strip; dropping outside any strip detaches into a new window or cancels. Adds the `CrossWindowTabDrag` singleton state machine (app/src/workspace/cross_window_tab_drag.rs, ~1849 lines: Floating / GhostInTarget / InsertedInTarget / Transitioning phases, single- and multi-tab sources, deferred ghost transfer, reverse-handoff, drop-time re-resolution, pending-close persistence guard). Replaces the simpler 8b detach-only path (try_detach_tab_on_drag / detach_tab_immediate global action) with the singleton-driven flow. Rewires Workspace::on_tab_drag and adds perform_handoff / handle_drop_result / tab_insertion_index_for_cursor / insert_transferred_tab_at_index / prepare_for_transferred_tab_attach / get_tab_transfer_info_for_attach / close_window_for_content_transfer plus tab-bar ghost + floating-chip rendering in both the horizontal tab bar and vertical tabs panel. Adapted to twarp: TransferredTab gains a draggable_state field; the placeholder+adopt transfer model is bridged to upstream's insert_transferred_tab_at_index / get_tab_transfer_info_for_attach; is_drag_preview_workspace renamed to is_tab_drag_preview; adds suppress_detach_panes_on_window_close wired into on_window_closed. Platform/core: set_window_alpha across the WindowManager trait (default no-op), windowing state, mac backend (Rust + objc setAlphaValue:); and AppContext::set_and_cache_window_bounds. The winit real-manager ordered_window_ids / WindowOrderingState and the integration-test ordering changes were intentionally not ported (Linux-only; the trait default vec![] degrades to the distance-scan fallback). macOS, the target platform, has full ordered_window_ids. Behind the DragTabsToWindows dogfood flag. cargo check -p warp clean (0 errors, 0 warnings). The drag gesture and insertion ghost require visual verification in a launched build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08] roadmap: tick 8a–8f; record build status + smoke caveats Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08b] fix: enable DragTabsToWindows in the warp-oss binary Drag-a-tab-out-to-a-new-window did nothing in the binary the user runs. Root cause: DragTabsToWindows ships only in DOGFOOD_FLAGS, but warp-oss (the default ./script/run target, including --release) never enables the dogfood set. With the flag off, the tab Draggable stays locked to DragAxis::HorizontalOnly and the detach branch in workspace/view.rs is gated out — so the tab can never leave the strip and only reorders. The detach/transfer machinery itself (8b/8c) is complete and correct; it was simply dark. Force-enable the flag in TWARP_OSS_FLAGS, the same mechanism already used for GitOperationsInCodeReview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08f] polish: remove sidebar close-X; tune paddings + Timeline User-requested sidebar polish: - Remove the in-header close "X" (and its mouse-state field + close_button method). The panel still toggles via workspace:toggle_left_panel; the macOS-app sidebar has no close glyph in-header. - Give the file-tree and Warp Drive panels a comfortable shared horizontal inset (SIDEBAR_CONTENT_INSET = 8px) instead of the old flush 2px. - Add a bottom inset (SIDEBAR_BOTTOM_INSET = 8px) so the last row / Timeline section doesn't sit flush against the window edge. - Timeline header: pin its text + hover colors to the macOS sidebar consts (the old theme-derived sub_text_color/neutral_3 inverted to near-white on a dark theme and vanished on the pinned-light sidebar — the §25 theme-leakage trap), and bump its vertical padding 3px -> 5px so it reads as a real section header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08a] chrome-style tab bottom flares Add Chrome/Safari-style bottom "flares" so the active tab seats into the strip + content area below it rather than reading as a floating rounded rectangle. Two decorative quarter-disc "ears" hug the active tab's lower-left and lower-right corners. Each ear is an r x r box (r = NEW_TAB_CORNER_RADIUS, 7px) filled with the active tab's own background fill, with its inner-facing top corner rounded by r. The rounded corner cuts a transparent quarter-disc revealing the workspace terminal/content background painted behind it (the whole tab-bar + content column sits on get_terminal_background_fill), leaving a convex quarter-disc of tab-color that flares outward into the baseline. The ears are added via add_positioned_overlay_child (unclipped overlay) so they render into the strip beside the tab instead of being clipped to the tab box, and carry no event handlers so clicks/drag pass through to the tab. Active-tab-only and behind the existing NewTabStyling gate; feature 01 colors, feature 06 rename, and drag/hit-target behavior are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08] roadmap: record review-feedback refinements (flares, drag-out flag, sidebar polish) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * Revert "[twarp 08a] chrome-style tab bottom flares" This reverts commit b9e91f5. * [twarp 08a] chrome tab shape via Metal SDF flare Replace the reverted "ear overlay" hack with a real Chrome-style flared tab shape computed directly in warpui's Metal rect fragment shader. A new `tab_flare_radius` field is threaded end to end: - shader_types.h: appended to PerRectUniforms (preserves existing offsets; bindgen regenerates the Rust struct). - scene::Rect gains `tab_flare` + `with_tab_flare`; Container gains a `tab_flare` field, `with_tab_flare`, and paints it into the rect. - renderer.rs PerRectUniforms::new takes the new param; the solid-rect push scales rect.tab_flare by scale_factor, all other push sites pass 0. The shader factors the IQ rounded-box SDF into `sdf_round_box` and adds `distance_from_tab_flare`: a rounded-top body inset by the flare, unioned with a full-width foot strip, with two circles subtracted to carve the concave valleys at the body/foot junctions. When tab_flare_radius > 0.5 this replaces the per-quadrant convex distance for both the outer shape and the border-tracking inner shape; the AA alpha cut is gated on the flare flag too (a tab's outer_corner_radius may be 0). Tabs (app/src/tab.rs, NewTabStyling path) now set TAB_TOP_RADIUS (8px) + TAB_FLARE_RADIUS (8px) on all tabs and pad content horizontally by the flare, replacing the old top-radius + bottom-border seating logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08] roadmap: record 8a tab-shape rework via Metal SDF (ear hack reverted) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Eqk7zLg17E5EbuBVbTGiL * [twarp 08] sidebar: center toggles, taller pills, flush file-tree scrollbar Review feedback on the sidebar: - Center the tool-switcher pills in the header (was left-aligned). - Taller toggle pills (24 -> 30) for more vertical padding. - Drop the right inset on the Project Explorer file tree so it fills the panel width and its overlayed scrollbar sits flush against the panel's right edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uRGepJ4fJonPsJkHc2e23 * [twarp 08] sidebar: real vertical padding on toggles, strip TIMELINE padding Further review feedback: - Toggle pills now have explicit top/bottom padding (height 36, 10px top/bottom) and the sidebar header is grown to 44 so the taller pills aren't clipped — the previous 30px height was capped by the 34px pane header and showed no visible change. - Remove all padding around the TIMELINE section header so the row hugs its text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uRGepJ4fJonPsJkHc2e23 * [twarp 08a] tab flare: ogee neck via smooth-min, transparent strip, pill while dragging - Rework the Metal tab SDF to smooth-union an inset rounded-top body with a low full-width foot bar, so the concave neck is a single ogee valley rather than carved circles that read as bright keyholes. - Keep the tab-bar strip transparent so the valleys sit on the window bg. - Render dragged tabs as a self-contained pill (all corners rounded, no flare, no opaque backing slab). - Sidebar: real top/bottom padding around the tool switcher row. * [twarp 08] tab colors: drop black/white, boost vibrancy of the rest - Remove Black and White from TAB_COLOR_OPTIONS — as small chips they read as muted grays and don't work as distinguishing tab accents. - Add AnsiColor::vibrant() (HSL saturation boost + lightness compression) and AnsiColorIdentifier::to_tab_color(), and route all tab-color rendering (picker dots, tab body, directory picker, vertical tab groups) through it so the chips and tab bodies stay in sync and read as saturated accents. Terminal text keeps the raw pastel theme palette. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Description
Adds Chrome-style cross-window tab dragging behind the
DragTabsToWindowsfeature flag. A user can drag a tab out of a window to create a new one, drag it into another window's tab bar to attach it, and keep dragging through multiple attach/detach cycles without releasing the mouse.Product behavior is fully described in
specs/pei/cross-window-tab-drag/PRODUCT.md; the architectural rationale is inspecs/pei/cross-window-tab-drag/TECH.md.https://www.loom.com/share/94d21b4b573c4f6893684142d66b844a
How it works
A cross-window drag has two shapes depending on where it starts:
As the user drags, we continuously hit-test the cursor against the tab bars of other windows in z-order. When the cursor enters an eligible tab bar we show a lightweight ghost (insertion slot + floating chip) in the target; the live view tree only moves at drop time, except in the back-to-caller case where the tab is transferred so the source window can host real reordering. If the cursor leaves a target tab bar, any handoff is reversed and the drag continues. On drop, the preview is either promoted to a permanent window (no target), folded into the target (handoff committed), or cleaned up (no-op).
State machines
Full ASCII versions live in the module doc at
app/src/workspace/cross_window_tab_drag.rs. Mermaid renderings:Single-tab source window
stateDiagram-v2 [*] --> Floating: begin_single_tab_drag Floating --> Transitioning: cursor enters target tab bar Transitioning --> InsertedInTarget InsertedInTarget --> Floating: cursor leaves target tab bar (reverse_handoff) InsertedInTarget --> FinalizeHandoff: on_drop while inserted Floating --> FinalizeFloatingWindow: on_drop while floating FinalizeHandoff --> [*] FinalizeFloatingWindow --> [*]The source window itself is the preview, so no extra window is created.
FinalizeFloatingWindowjust leaves the source window where the user dropped it.Multi-tab source window
stateDiagram-v2 [*] --> Floating: begin_multi_tab_drag (creates preview window) Floating --> GhostInTarget: cursor enters target tab bar (deferred) GhostInTarget --> Floating: cursor leaves target tab bar Floating --> Transitioning: cursor re-enters source (back-to-caller) Transitioning --> InsertedInTarget InsertedInTarget --> Floating: cursor leaves source tab bar (reverse_handoff) InsertedInTarget --> FinalizeHandoff: on_drop while inserted GhostInTarget --> FinalizeHandoff: on_drop over target Floating --> FinalizePreviewAsNewWindow: on_drop while floating FinalizeHandoff --> [*] FinalizePreviewAsNewWindow --> [*]GhostInTargetis a hover-only state — no view-tree transfer happens until drop. TheInsertedInTargetbranch is reserved for the back-to-caller path, where the preview must be kept alive so the user can drag the tab back out again.Infrastructure changes
Most of the diff lands in shared infrastructure rather than in feature-specific code. Each piece exists to satisfy a specific product invariant:
specs/pei/cross-window-tab-drag/so future changes have something to preserve. Integration coverage exercises detach, attach, reattach, reverse-handoff, target-side reorder, and drop-outside flows behind the feature flag rather than gating on a specific OS.Behind a feature flag, so no change for users until it's rolled out.
Testing
specs/pei/cross-window-tab-drag/PRODUCT.md§ Success Criteria on macOS.Server API dependencies
No server dependencies.
Agent Mode
Changelog Entries for Stable
CHANGELOG-NEW-FEATURE: You can now drag tabs out of a window into their own window, or between windows, similar to Chrome.