From e677d0c65a53b14ba2becc0dc1590ad13e21ecf9 Mon Sep 17 00:00:00 2001 From: danieltsam Date: Wed, 24 Jun 2026 17:51:06 +1000 Subject: [PATCH 1/6] specs(GH12306): tab transfer between standard and dedicated hotkey windows --- specs/GH12306/product.md | 94 ++++++++++++++++++++++++++++ specs/GH12306/tech.md | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 specs/GH12306/product.md create mode 100644 specs/GH12306/tech.md diff --git a/specs/GH12306/product.md b/specs/GH12306/product.md new file mode 100644 index 00000000000..e99b1079054 --- /dev/null +++ b/specs/GH12306/product.md @@ -0,0 +1,94 @@ +# Product Spec: Tab transfer between standard and dedicated hotkey windows + +**Issue:** [warpdotdev/warp#12306](https://github.com/warpdotdev/warp/issues/12306) + +## Summary + +Three command-palette actions that let users move active tabs (or multi-selections) between standard terminal windows and the dedicated hotkey (quake) window. This covers the keyboard/context-menu half of the issue's first proposal; the drag-and-drop half is being shipped concurrently by the Warp team. + +## Problem + +The dedicated hotkey window (quake overlay) is an isolated sandbox. Users who start a task there that scales up — editing a file, running a long build, researching in the AI terminal — have no way to promote those tabs to a standard window. Conversely, users in a standard window who want a distraction-free view of a particular session in the quake overlay cannot move it there. + +The issue proposes two solution families: +1. **Tab transfer** — move tabs between window types via drag-and-drop or context menu / command-palette actions +2. **Non-dedicated hotkey mode** — a setting that makes the global hotkey present the current active window as a quake-style overlay rather than using a separate dedicated window + +The Warp team is shipping cross-window tab drag (hotkey→standard direction) concurrently with this spec ([maintainer comment](https://github.com/warpdotdev/warp/issues/12306#issuecomment-4749224419)). This spec covers the complementary keyboard- and command-palette-driven half of option 1, which applies in both directions (standard↔hotkey). + +## Goals + +- Users can move active tab(s) from a standard window into the dedicated hotkey window. +- Users can move active tab(s) from the dedicated hotkey window back to a standard window (existing or newly created). +- A single shortcut (toggle) lets users move active tab(s) between window types without needing to know which direction. +- Commands are only visible when the "Dedicated hotkey window" setting is enabled. + +## Non-goals + +- **Drag-and-drop between standard and hotkey windows** — The Warp team is shipping cross-window drag (hotkey→standard) concurrently. The reverse direction (standard→hotkey) is deferred to a follow-up. This spec covers the keyboard/command-palette path instead, which is complementary to drag and works in both directions. +- **Non-dedicated hotkey mode** — The second proposal from the issue is deferred per maintainer signal. +- Moving tabs between two standard windows (already exists via cross-window drag). +- Changing auto-hide behavior of the hotkey window. + +## User experience + +### Current behavior + +1. User is in the quake overlay with a long-running build and wants to continue working in a standard window. No action exists — they must open a new terminal session from scratch. +2. User is in a standard window with a relevant session and wants to move it to the quake overlay for focused monitoring. No action exists. + +### Expected behavior + +**`workspace:move_active_tab_to_dedicated_hotkey_window`** +1. User invokes from a standard window while the hotkey window setting is enabled. +2. If the hotkey window is not open, it is programmatically launched. +3. The active tab (or selection of tabs) is removed from the source window and inserted at the end of the hotkey window's tab bar. +4. Focus moves to the hotkey window. + +**`workspace:move_active_tab_to_standard_window`** +1. User invokes from the hotkey window. +2. If another standard window is open, tabs are moved there. +3. If no standard window is open, tabs are promoted into a newly created standard window. +4. Focus moves to the target standard window. + +**`workspace:toggle_active_tab_window_type`** +1. Invoked from any window, moves the active tab(s) to the opposite window type. + +### Edge cases + +- **Hotkey window disabled in settings:** All three commands are hidden (gated behind `QUAKE_MODE_ENABLED_CONTEXT_FLAG`). +- **Multi-tab hotkey window:** Moving tabs out leaves remaining tabs in place; the window does not auto-hide. +- **Last tab moved:** Source window closes automatically (standard windows). See open questions for hotkey window behavior. +- **Multi-selection active:** All selected tabs transfer together. If no multi-selection, only the active tab transfers. +- **Hotkey window closed at invocation:** Opened programmatically first. +- **No standard window available:** A new standard window is created. +- **Stale window ID:** Verified with `ctx.is_window_open(id)` at invocation time. + +## Success criteria + +1. `workspace:move_active_tab_to_dedicated_hotkey_window` moves the active tab to the hotkey window (opens it first if needed). +2. `workspace:move_active_tab_to_standard_window` moves the active tab to an existing standard window, or creates a new one if none exists. +3. `workspace:toggle_active_tab_window_type` moves the active tab(s) to the opposite window type from any window. +4. Multi-selected tabs are transferred together by all three actions. +5. When the last tab is moved out of a standard window, the window closes. (Hotkey window behavior — see open questions.) +6. All three commands are hidden in the command palette and keybindings UI when the dedicated hotkey window setting is disabled. +7. Moving tabs out of a multi-tab hotkey window does not force the window to hide. +8. The hotkey window ID is verified dynamically (not cached) to prevent targeting a closed window. + +## Validation + +- **Unit test:** `test_move_active_tab_to_window` verifies core tab transfer between two mock workspaces. +- **Manual:** Enable hotkey window, invoke each command, verify transfer in both directions. +- **Manual:** Multi-select 2+ tabs (requires `FeatureFlag::GroupedTabs`), invoke each action, verify all selected tabs move. +- **Manual:** Disable hotkey window setting, confirm commands disappear from command palette. + +## Open questions + +- **Hotkey window lifecycle on last-tab-moved-out:** When the last tab is moved out of the hotkey window to a standard window, should the hotkey window close automatically (same as standard window behavior), or should it remain open as an empty quake overlay? An empty overlay is unusual for a quake-style window but keeps the hotkey available for immediate reuse. +- **Empty hotkey window from the start:** If the user opens the hotkey window and immediately moves its single tab to a standard window, should the hotkey close? (Same lifecycle question as above.) + +## Follow-ups + +- **Drag-and-drop standard→hotkey** — Extend the cross-window drag system to accept the hotkey overlay as a drop target (the reverse of what the team is shipping). The transfer primitives built here are the same ones a drop handler would call. +- **Non-dedicated hotkey mode** — Deferred per maintainer signal. Would require a new `GlobalHotkeyMode` variant, a different window creation path, and changes to the quake state lifecycle. +- Consider a setting to automatically close the hotkey window when its last tab is moved out. diff --git a/specs/GH12306/tech.md b/specs/GH12306/tech.md new file mode 100644 index 00000000000..0cfcf5ac948 --- /dev/null +++ b/specs/GH12306/tech.md @@ -0,0 +1,128 @@ +# Tech Spec: Tab transfer between standard and dedicated hotkey windows + +**Issue:** [warpdotdev/warp#12306](https://github.com/warpdotdev/warp/issues/12306) +**Product spec:** `specs/GH12306/product.md` + +## Context + +The dedicated hotkey window (quake mode) is created as a `WindowStyle::Pin` window tracked through a global `QUAKE_STATE` singleton in `root_view.rs`. It has its own `Workspace` instance with its own tab bar. Cross-window tab transfers already exist for standard windows via: +- `Workspace::get_tab_transfer_info_for_attach` (line 26703) — snapshots a tab for transfer +- `Workspace::prepare_for_transferred_tab_attach` (line 26716) — detaches the pane group +- `Workspace::insert_transferred_tab_at_index` (line 26734) — inserts into target workspace +- `root_view::create_transferred_window` (line 581) — creates a new window for tab(s) +- `ctx.transfer_view_tree_to_window` — moves the view tree across windows + +The binding system supports context-predicate gating via `EditableBinding::with_context_predicate`, where context flags are set per-view in `View::context()`. + +The Warp team is shipping cross-window tab drag (hotkey→standard direction) in the same release cycle ([maintainer comment](https://github.com/warpdotdev/warp/issues/12306#issuecomment-4749224419)). The command-palette actions in this spec are complementary: they provide a keyboard-driven alternative that works in both directions, and they share the same transfer primitives the drag path uses. + +Gaps in the current system: +- No `WorkspaceAction` variants for moving tabs to/from the hotkey window specifically. +- No `Workspace_InQuakeWindow` context flag — views cannot self-identify as belonging to the hotkey window. +- `toggle_quake_mode_window` is private to `root_view` (line 1331) — workspace actions cannot programmatically open the hotkey window. +- `selected_tab_indices` on `Workspace` is private to `tab_grouping.rs` (line 108) — multi-selection state is inaccessible outside that module. + +## Proposed changes + +### 1. `app/src/workspace/action.rs` (lines 136, 867) + +Add three variants to `WorkspaceAction`: + +``` +MoveActiveTabToDedicatedHotkeyWindow, +MoveActiveTabToStandardWindow, +ToggleActiveTabWindowType, +``` + +Add corresponding match arms in the `WorkspaceAction` impl block (around line 867). + +### 2. `app/src/workspace/mod.rs` (line 482) + +Register three `EditableBinding`s with context predicates: + +- `workspace:move_active_tab_to_dedicated_hotkey_window` — visible when `Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow` +- `workspace:move_active_tab_to_standard_window` — visible when `Workspace & Quake_Mode_Editor & Workspace_InQuakeWindow` +- `workspace:toggle_active_tab_window_type` — visible when `Workspace & Quake_Mode_Editor` + +The `Quake_Mode_Editor` flag (set by `root_view.rs` when `quake_mode_enabled` is true) gates on the setting. `Workspace_InQuakeWindow` (new) differentiates standard from hotkey windows. + +### 3. `app/src/workspace/view.rs` + +**Context flag** (~line 22194): Insert `Workspace_InQuakeWindow` when `quake_mode_window_id() == Some(self.window_id)`. + +**Action dispatch** (line 22973): Route the three new variants to handler methods. + +**New handler methods** (~line 27093): +- `move_active_tab_to_dedicated_hotkey_window` — Verifies the hotkey window is open (opens it via the newly-`pub(crate)` `toggle_quake_mode_window` if needed), then calls `move_active_tab_to_window`. +- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.window_ids()` (excluding self and quake). If none exists, calls `create_transferred_window`. Calls `move_active_tab_to_window` in either case. +- `toggle_active_tab_window_type` — Dispatches to the appropriate directional method based on current window. +- `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), calls `get_tab_transfer_info_for_attach` / `prepare_for_transferred_tab_attach` / `transfer_view_tree_to_window` for each, removes from source (descending index to avoid shift issues), inserts at end of target, focuses target window. + +### 4. `app/src/workspace/view/tab_grouping.rs:108` + +Change `selected_tab_indices` from `fn` to `pub(crate) fn`. + +### 5. `app/src/root_view.rs` + +Change `toggle_quake_mode_window` from `fn` to `pub(crate) fn` (line 1331). + +## End-to-end flow + +### Move to dedicated hotkey window + +1. User invokes the command-palette action (or keybinding) from a standard window. +2. The `EditableBinding` matches via context predicate (`Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow`). +3. `WorkspaceAction::MoveActiveTabToDedicatedHotkeyWindow` is dispatched to `TypedActionView`. +4. `move_active_tab_to_dedicated_hotkey_window` is called. +5. `quake_mode_window_id()` retrieves the hotkey window ID, verified with `ctx.is_window_open(id)`. +6. If the window is not open, `toggle_quake_mode_window` opens it programmatically; the window ID is re-checked. +7. `move_active_tab_to_window(target_window_id, ctx)` is called. +8. `selected_tab_indices()` gathers multi-selected indices, or falls back to `active_tab_index`. +9. For each selected tab: `get_tab_transfer_info_for_attach` → `prepare_for_transferred_tab_attach` → `ctx.transfer_view_tree_to_window`. +10. Tabs are removed from the source (descending index via `remove_tab_without_undo`, or `close_window_for_content_transfer` if it was the last tab). +11. Target workspace inserts each tab at the end via `insert_transferred_tab_at_index`. +12. Focus moves to the target window. + +### Move to standard window + +Same flow, but the target window is discovered via `ctx.window_ids()` (excluding the quake window and self). If no standard window exists, `create_transferred_window` promotes tabs into a newly created standard window. + +### Toggle + +`toggle_active_tab_window_type` checks whether the current window is the quake window and dispatches to the appropriate directional method. + +## Testing and validation + +### Behavior-to-verification mapping + +- **1 — Move to hotkey (opens first if needed):** Enable quake mode, close overlay, invoke command from standard window. +- **2 — Move to standard (create if none exist):** Invoke from quake overlay with and without other windows open. +- **3 — Toggle moves to opposite type:** Invoke toggle from both window types. +- **4 — Multi-selection:** Enable `FeatureFlag::GroupedTabs`, select 2+ tabs, invoke each action. +- **5 — Last tab closes window:** Move the only tab out of a window. +- **6 — Hidden when setting disabled:** Disable quake mode, search command palette. +- **7 — Multi-tab quake stays open:** Move one tab out of a multi-tab quake overlay. +- **8 — Dynamic window ID:** Close quake window between action invocation and execution. + +### Unit tests + +- `test_move_active_tab_to_window` — Creates two mock workspaces, adds a tab to the source, calls `move_active_tab_to_window` targeting the second workspace, asserts tab counts shifted correctly. + +### Presubmit + +- `./script/presubmit` must pass. +- `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings` must pass. +- `cargo nextest run` must pass. + +## Risks and mitigations + +- **`toggle_quake_mode_window` misidentifies state** — Window ID verified via `ctx.is_window_open(id)` before and after the toggle call. +- **Stale multi-selection indices** — Removal iterates descending to avoid index shift. +- **Race from parallel actions** — Single-threaded event loop serializes all mutations. +- **Exposing `toggle_quake_mode_window`** — Only `pub(crate)`, visible within the crate only. + +## Follow-ups + +- **Drag-and-drop standard→hotkey** — The team is shipping hotkey→standard drag this cycle. The reverse direction would extend the cross-window drag system (`Workspace::drag_tab_over`, `tab_insertion_index_for_cursor`) to accept `WindowStyle::Pin` windows as valid drop targets. +- **Non-dedicated hotkey mode** — Deferred per maintainer signal. Would require a new `GlobalHotkeyMode` variant, different window creation path, changes to quake state lifecycle. +- Consider auto-hide on last-tab-moved-out from hotkey window. From a2a319ec908fc94889656c93b1875859d79a45af Mon Sep 17 00:00:00 2001 From: danieltsam Date: Wed, 24 Jun 2026 20:27:44 +1000 Subject: [PATCH 2/6] =?UTF-8?q?specs(GH12306):=20address=20Oz=20review=20?= =?UTF-8?q?=E2=80=94=20target=20window=20selection,=20last-tab=20lifecycle?= =?UTF-8?q?,=20tab=20order,=20keybindings=20UI=20gating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/GH12306/product.md | 13 +++---------- specs/GH12306/tech.md | 16 +++++++++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/specs/GH12306/product.md b/specs/GH12306/product.md index e99b1079054..14e2a44b522 100644 --- a/specs/GH12306/product.md +++ b/specs/GH12306/product.md @@ -47,7 +47,7 @@ The Warp team is shipping cross-window tab drag (hotkey→standard direction) co **`workspace:move_active_tab_to_standard_window`** 1. User invokes from the hotkey window. -2. If another standard window is open, tabs are moved there. +2. If another standard window is open, tabs are moved there — the most recently focused one. 3. If no standard window is open, tabs are promoted into a newly created standard window. 4. Focus moves to the target standard window. @@ -58,7 +58,7 @@ The Warp team is shipping cross-window tab drag (hotkey→standard direction) co - **Hotkey window disabled in settings:** All three commands are hidden (gated behind `QUAKE_MODE_ENABLED_CONTEXT_FLAG`). - **Multi-tab hotkey window:** Moving tabs out leaves remaining tabs in place; the window does not auto-hide. -- **Last tab moved:** Source window closes automatically (standard windows). See open questions for hotkey window behavior. +- **Last tab moved:** Source window closes automatically. - **Multi-selection active:** All selected tabs transfer together. If no multi-selection, only the active tab transfers. - **Hotkey window closed at invocation:** Opened programmatically first. - **No standard window available:** A new standard window is created. @@ -70,7 +70,7 @@ The Warp team is shipping cross-window tab drag (hotkey→standard direction) co 2. `workspace:move_active_tab_to_standard_window` moves the active tab to an existing standard window, or creates a new one if none exists. 3. `workspace:toggle_active_tab_window_type` moves the active tab(s) to the opposite window type from any window. 4. Multi-selected tabs are transferred together by all three actions. -5. When the last tab is moved out of a standard window, the window closes. (Hotkey window behavior — see open questions.) +5. When the last tab is moved out of any window (standard or hotkey), the source window closes. 6. All three commands are hidden in the command palette and keybindings UI when the dedicated hotkey window setting is disabled. 7. Moving tabs out of a multi-tab hotkey window does not force the window to hide. 8. The hotkey window ID is verified dynamically (not cached) to prevent targeting a closed window. @@ -81,14 +81,7 @@ The Warp team is shipping cross-window tab drag (hotkey→standard direction) co - **Manual:** Enable hotkey window, invoke each command, verify transfer in both directions. - **Manual:** Multi-select 2+ tabs (requires `FeatureFlag::GroupedTabs`), invoke each action, verify all selected tabs move. - **Manual:** Disable hotkey window setting, confirm commands disappear from command palette. - -## Open questions - -- **Hotkey window lifecycle on last-tab-moved-out:** When the last tab is moved out of the hotkey window to a standard window, should the hotkey window close automatically (same as standard window behavior), or should it remain open as an empty quake overlay? An empty overlay is unusual for a quake-style window but keeps the hotkey available for immediate reuse. -- **Empty hotkey window from the start:** If the user opens the hotkey window and immediately moves its single tab to a standard window, should the hotkey close? (Same lifecycle question as above.) - ## Follow-ups - **Drag-and-drop standard→hotkey** — Extend the cross-window drag system to accept the hotkey overlay as a drop target (the reverse of what the team is shipping). The transfer primitives built here are the same ones a drop handler would call. - **Non-dedicated hotkey mode** — Deferred per maintainer signal. Would require a new `GlobalHotkeyMode` variant, a different window creation path, and changes to the quake state lifecycle. -- Consider a setting to automatically close the hotkey window when its last tab is moved out. diff --git a/specs/GH12306/tech.md b/specs/GH12306/tech.md index 0cfcf5ac948..efb5a761476 100644 --- a/specs/GH12306/tech.md +++ b/specs/GH12306/tech.md @@ -38,13 +38,15 @@ Add corresponding match arms in the `WorkspaceAction` impl block (around line 86 ### 2. `app/src/workspace/mod.rs` (line 482) -Register three `EditableBinding`s with context predicates: +Register three `EditableBinding`s with both a context predicate and an enabled predicate: -- `workspace:move_active_tab_to_dedicated_hotkey_window` — visible when `Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow` -- `workspace:move_active_tab_to_standard_window` — visible when `Workspace & Quake_Mode_Editor & Workspace_InQuakeWindow` -- `workspace:toggle_active_tab_window_type` — visible when `Workspace & Quake_Mode_Editor` +| Command | Context predicate | Enabled predicate | +|---|---|---| +| `workspace:move_active_tab_to_dedicated_hotkey_window` | `Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow` | `quake_mode_enabled` | +| `workspace:move_active_tab_to_standard_window` | `Workspace & Quake_Mode_Editor & Workspace_InQuakeWindow` | `quake_mode_enabled` | +| `workspace:toggle_active_tab_window_type` | `Workspace & Quake_Mode_Editor` | `quake_mode_enabled` | -The `Quake_Mode_Editor` flag (set by `root_view.rs` when `quake_mode_enabled` is true) gates on the setting. `Workspace_InQuakeWindow` (new) differentiates standard from hotkey windows. +The `Quake_Mode_Editor` flag (set by `root_view.rs` when `quake_mode_enabled` is true) and the enabled predicate both gate on the same setting. The context predicate hides from the command palette and dispatch; the enabled predicate hides from the keybindings settings UI (following the `TOGGLE_VERTICAL_TABS_PANEL` pattern in the codebase). `Workspace_InQuakeWindow` (new) differentiates standard from hotkey windows. ### 3. `app/src/workspace/view.rs` @@ -54,9 +56,9 @@ The `Quake_Mode_Editor` flag (set by `root_view.rs` when `quake_mode_enabled` is **New handler methods** (~line 27093): - `move_active_tab_to_dedicated_hotkey_window` — Verifies the hotkey window is open (opens it via the newly-`pub(crate)` `toggle_quake_mode_window` if needed), then calls `move_active_tab_to_window`. -- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.window_ids()` (excluding self and quake). If none exists, calls `create_transferred_window`. Calls `move_active_tab_to_window` in either case. +- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.window_ids()` (excluding self and quake) — the first non-self, non-quake ID corresponds to the most recently focused standard window. If none exists, calls `create_transferred_window`. Calls `move_active_tab_to_window` in either case. - `toggle_active_tab_window_type` — Dispatches to the appropriate directional method based on current window. -- `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), calls `get_tab_transfer_info_for_attach` / `prepare_for_transferred_tab_attach` / `transfer_view_tree_to_window` for each, removes from source (descending index to avoid shift issues), inserts at end of target, focuses target window. +- `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), collects transfer infos in ascending order, removes tabs from source in descending order (to avoid index shift), then inserts collected tabs at end of target in original ascending order, focuses target window. ### 4. `app/src/workspace/view/tab_grouping.rs:108` From ad6a7bcc1a69ff766e8120afd85750061a90760d Mon Sep 17 00:00:00 2001 From: danieltsam Date: Wed, 24 Jun 2026 20:37:36 +1000 Subject: [PATCH 3/6] =?UTF-8?q?specs(GH12306):=20address=20second=20Oz=20r?= =?UTF-8?q?ound=20=E2=80=94=20ordered=5Fwindow=5Fids,=20multi-tab=20new-wi?= =?UTF-8?q?ndow=20flow,=20keybindings=20UI=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/GH12306/tech.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/GH12306/tech.md b/specs/GH12306/tech.md index efb5a761476..bf8d4d55ec2 100644 --- a/specs/GH12306/tech.md +++ b/specs/GH12306/tech.md @@ -56,7 +56,7 @@ The `Quake_Mode_Editor` flag (set by `root_view.rs` when `quake_mode_enabled` is **New handler methods** (~line 27093): - `move_active_tab_to_dedicated_hotkey_window` — Verifies the hotkey window is open (opens it via the newly-`pub(crate)` `toggle_quake_mode_window` if needed), then calls `move_active_tab_to_window`. -- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.window_ids()` (excluding self and quake) — the first non-self, non-quake ID corresponds to the most recently focused standard window. If none exists, calls `create_transferred_window`. Calls `move_active_tab_to_window` in either case. +- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.ordered_window_ids()` (z-order front-to-back, skipping self and quake). If none exists, calls `create_transferred_window` for the first tab, then transfers remaining tabs to that new window. If one exists, calls `move_active_tab_to_window` with that window as target. - `toggle_active_tab_window_type` — Dispatches to the appropriate directional method based on current window. - `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), collects transfer infos in ascending order, removes tabs from source in descending order (to avoid index shift), then inserts collected tabs at end of target in original ascending order, focuses target window. @@ -87,7 +87,7 @@ Change `toggle_quake_mode_window` from `fn` to `pub(crate) fn` (line 1331). ### Move to standard window -Same flow, but the target window is discovered via `ctx.window_ids()` (excluding the quake window and self). If no standard window exists, `create_transferred_window` promotes tabs into a newly created standard window. +Same flow, but the target window is discovered via `ctx.ordered_window_ids()` (z-order front-to-back, excluding the quake window and self). If no standard window exists, `create_transferred_window` creates a new window for the first tab, then remaining tabs are transferred to that new window via `move_active_tab_to_window`. ### Toggle @@ -102,7 +102,8 @@ Same flow, but the target window is discovered via `ctx.window_ids()` (excluding - **3 — Toggle moves to opposite type:** Invoke toggle from both window types. - **4 — Multi-selection:** Enable `FeatureFlag::GroupedTabs`, select 2+ tabs, invoke each action. - **5 — Last tab closes window:** Move the only tab out of a window. -- **6 — Hidden when setting disabled:** Disable quake mode, search command palette. +- **6 — Hidden from command palette when setting disabled:** Disable quake mode, search command palette. +- **6b — Hidden from keybindings settings UI when setting disabled:** Open Settings > Keybindings, verify the three commands are absent from the list. - **7 — Multi-tab quake stays open:** Move one tab out of a multi-tab quake overlay. - **8 — Dynamic window ID:** Close quake window between action invocation and execution. From cf4e8464918d59fc93802472998d6a45041e37a5 Mon Sep 17 00:00:00 2001 From: danieltsam Date: Wed, 24 Jun 2026 20:57:29 +1000 Subject: [PATCH 4/6] =?UTF-8?q?specs(GH12306):=20address=20third=20Oz=20ro?= =?UTF-8?q?und=20=E2=80=94=20AtomicBool=20enabled=20predicate,=20quake=20v?= =?UTF-8?q?isibility=20check,=20suppress=5Fdetach=20cleanup,=20const=20fla?= =?UTF-8?q?g,=20handler=20line=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/GH12306/product.md | 2 +- specs/GH12306/tech.md | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/specs/GH12306/product.md b/specs/GH12306/product.md index 14e2a44b522..cca460c85d5 100644 --- a/specs/GH12306/product.md +++ b/specs/GH12306/product.md @@ -62,7 +62,7 @@ The Warp team is shipping cross-window tab drag (hotkey→standard direction) co - **Multi-selection active:** All selected tabs transfer together. If no multi-selection, only the active tab transfers. - **Hotkey window closed at invocation:** Opened programmatically first. - **No standard window available:** A new standard window is created. -- **Stale window ID:** Verified with `ctx.is_window_open(id)` at invocation time. +- **Stale window ID:** Quake window existence and visibility verified via `quake_mode_window_is_open()` at invocation time. ## Success criteria diff --git a/specs/GH12306/tech.md b/specs/GH12306/tech.md index bf8d4d55ec2..9d080662c17 100644 --- a/specs/GH12306/tech.md +++ b/specs/GH12306/tech.md @@ -36,29 +36,35 @@ ToggleActiveTabWindowType, Add corresponding match arms in the `WorkspaceAction` impl block (around line 867). -### 2. `app/src/workspace/mod.rs` (line 482) +### 2. `app/src/workspace/mod.rs` (line 482) and `app/src/root_view.rs` + +Add a global `static QUAKE_MODE_SETTING_ENABLED: AtomicBool` in `root_view.rs`, updated whenever the dedicated hotkey window setting changes (in the settings update handler). Register three `EditableBinding`s with both a context predicate and an enabled predicate: | Command | Context predicate | Enabled predicate | |---|---|---| -| `workspace:move_active_tab_to_dedicated_hotkey_window` | `Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow` | `quake_mode_enabled` | -| `workspace:move_active_tab_to_standard_window` | `Workspace & Quake_Mode_Editor & Workspace_InQuakeWindow` | `quake_mode_enabled` | -| `workspace:toggle_active_tab_window_type` | `Workspace & Quake_Mode_Editor` | `quake_mode_enabled` | +| `workspace:move_active_tab_to_dedicated_hotkey_window` | `Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow` | `QUAKE_MODE_SETTING_ENABLED.load(Ordering::Relaxed)` | +| `workspace:move_active_tab_to_standard_window` | `Workspace & Quake_Mode_Editor & Workspace_InQuakeWindow` | `QUAKE_MODE_SETTING_ENABLED.load(Ordering::Relaxed)` | +| `workspace:toggle_active_tab_window_type` | `Workspace & Quake_Mode_Editor` | `QUAKE_MODE_SETTING_ENABLED.load(Ordering::Relaxed)` | + +(Implemented as closures `|| QUAKE_MODE_SETTING_ENABLED.load(Ordering::Relaxed)` coercing to `fn() -> bool`.) + +The context predicate hides from the command palette and dispatch; the enabled predicate (a `fn() -> bool` from a global atomic) hides from the keybindings settings UI — matching the established pattern of `FeatureFlag::is_enabled()` and test-statics in the codebase. `Workspace_InQuakeWindow` (new) differentiates standard from hotkey windows. -The `Quake_Mode_Editor` flag (set by `root_view.rs` when `quake_mode_enabled` is true) and the enabled predicate both gate on the same setting. The context predicate hides from the command palette and dispatch; the enabled predicate hides from the keybindings settings UI (following the `TOGGLE_VERTICAL_TABS_PANEL` pattern in the codebase). `Workspace_InQuakeWindow` (new) differentiates standard from hotkey windows. +### 3. `app/src/workspace/view.rs` and `app/src/settings_view/mod.rs` -### 3. `app/src/workspace/view.rs` +Add `pub const WORKSPACE_IN_QUAKE_WINDOW_FLAG: &str = "Workspace_InQuakeWindow";` to the settings flags module (alongside `QUAKE_MODE_ENABLED_CONTEXT_FLAG`). **Context flag** (~line 22194): Insert `Workspace_InQuakeWindow` when `quake_mode_window_id() == Some(self.window_id)`. **Action dispatch** (line 22973): Route the three new variants to handler methods. -**New handler methods** (~line 27093): -- `move_active_tab_to_dedicated_hotkey_window` — Verifies the hotkey window is open (opens it via the newly-`pub(crate)` `toggle_quake_mode_window` if needed), then calls `move_active_tab_to_window`. +**New handler methods** (~line 26703, alongside existing transfer functions): +- `move_active_tab_to_dedicated_hotkey_window` — Verifies the hotkey window is open and visible via `quake_mode_window_is_open()` (opens it via the newly-`pub(crate)` `toggle_quake_mode_window` if closed or hidden), then calls `move_active_tab_to_window`. - `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.ordered_window_ids()` (z-order front-to-back, skipping self and quake). If none exists, calls `create_transferred_window` for the first tab, then transfers remaining tabs to that new window. If one exists, calls `move_active_tab_to_window` with that window as target. - `toggle_active_tab_window_type` — Dispatches to the appropriate directional method based on current window. -- `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), collects transfer infos in ascending order, removes tabs from source in descending order (to avoid index shift), then inserts collected tabs at end of target in original ascending order, focuses target window. +- `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), collects transfer infos in ascending order. For each tab: calls `prepare_for_transferred_tab_attach` on the source workspace, then `ctx.transfer_view_tree_to_window`. After all transfers, removes tabs from source in descending order (to avoid index shift). If the source still has remaining tabs, clears `suppress_detach_panes_on_window_close` via `set_suppress_detach_panes_on_window_close(false)`. Inserts collected tabs at end of target in original ascending order. Focuses target window. ### 4. `app/src/workspace/view/tab_grouping.rs:108` @@ -76,12 +82,12 @@ Change `toggle_quake_mode_window` from `fn` to `pub(crate) fn` (line 1331). 2. The `EditableBinding` matches via context predicate (`Workspace & Quake_Mode_Editor & !Workspace_InQuakeWindow`). 3. `WorkspaceAction::MoveActiveTabToDedicatedHotkeyWindow` is dispatched to `TypedActionView`. 4. `move_active_tab_to_dedicated_hotkey_window` is called. -5. `quake_mode_window_id()` retrieves the hotkey window ID, verified with `ctx.is_window_open(id)`. -6. If the window is not open, `toggle_quake_mode_window` opens it programmatically; the window ID is re-checked. +5. `quake_mode_window_id()` retrieves the hotkey window ID, verified with `quake_mode_window_is_open()`. +6. If the window is not open or is hidden, `toggle_quake_mode_window` opens or reveals it; the window ID is re-checked. 7. `move_active_tab_to_window(target_window_id, ctx)` is called. 8. `selected_tab_indices()` gathers multi-selected indices, or falls back to `active_tab_index`. 9. For each selected tab: `get_tab_transfer_info_for_attach` → `prepare_for_transferred_tab_attach` → `ctx.transfer_view_tree_to_window`. -10. Tabs are removed from the source (descending index via `remove_tab_without_undo`, or `close_window_for_content_transfer` if it was the last tab). +10. Tabs are removed from the source (descending index via `remove_tab_without_undo`, or `close_window_for_content_transfer` if it was the last tab). If the source still has remaining tabs after removal, `suppress_detach_panes_on_window_close` is cleared via `set_suppress_detach_panes_on_window_close(false)`. 11. Target workspace inserts each tab at the end via `insert_transferred_tab_at_index`. 12. Focus moves to the target window. @@ -119,8 +125,9 @@ Same flow, but the target window is discovered via `ctx.ordered_window_ids()` (z ## Risks and mitigations -- **`toggle_quake_mode_window` misidentifies state** — Window ID verified via `ctx.is_window_open(id)` before and after the toggle call. +- **`toggle_quake_mode_window` misidentifies state** — Window state verified via `quake_mode_window_is_open()` before and after the toggle call, which distinguishes hidden from truly closed. - **Stale multi-selection indices** — Removal iterates descending to avoid index shift. +- **Leaked `suppress_detach_panes_on_window_close`** — Source window always clears the flag (via `close_window_for_content_transfer` on last tab, or `set_suppress_detach_panes_on_window_close(false)` if tabs remain). - **Race from parallel actions** — Single-threaded event loop serializes all mutations. - **Exposing `toggle_quake_mode_window`** — Only `pub(crate)`, visible within the crate only. From 76933800bfb04831914514555de16f26f9f3850a Mon Sep 17 00:00:00 2001 From: danieltsam Date: Wed, 24 Jun 2026 23:47:55 +1000 Subject: [PATCH 5/6] =?UTF-8?q?specs(GH12306):=20address=20fourth=20Oz=20r?= =?UTF-8?q?ound=20=E2=80=94=20remove=20context=20menu=20claim,=20detail=20?= =?UTF-8?q?multi-tab=20no-window=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/GH12306/product.md | 4 ++-- specs/GH12306/tech.md | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/specs/GH12306/product.md b/specs/GH12306/product.md index cca460c85d5..6f2d832f1c1 100644 --- a/specs/GH12306/product.md +++ b/specs/GH12306/product.md @@ -4,14 +4,14 @@ ## Summary -Three command-palette actions that let users move active tabs (or multi-selections) between standard terminal windows and the dedicated hotkey (quake) window. This covers the keyboard/context-menu half of the issue's first proposal; the drag-and-drop half is being shipped concurrently by the Warp team. +Three command-palette actions that let users move active tabs (or multi-selections) between standard terminal windows and the dedicated hotkey (quake) window. This covers the command-palette half of the issue's first proposal; the drag-and-drop half is being shipped concurrently by the Warp team. ## Problem The dedicated hotkey window (quake overlay) is an isolated sandbox. Users who start a task there that scales up — editing a file, running a long build, researching in the AI terminal — have no way to promote those tabs to a standard window. Conversely, users in a standard window who want a distraction-free view of a particular session in the quake overlay cannot move it there. The issue proposes two solution families: -1. **Tab transfer** — move tabs between window types via drag-and-drop or context menu / command-palette actions +1. **Tab transfer** — move tabs between window types via drag-and-drop or command-palette actions 2. **Non-dedicated hotkey mode** — a setting that makes the global hotkey present the current active window as a quake-style overlay rather than using a separate dedicated window The Warp team is shipping cross-window tab drag (hotkey→standard direction) concurrently with this spec ([maintainer comment](https://github.com/warpdotdev/warp/issues/12306#issuecomment-4749224419)). This spec covers the complementary keyboard- and command-palette-driven half of option 1, which applies in both directions (standard↔hotkey). diff --git a/specs/GH12306/tech.md b/specs/GH12306/tech.md index 9d080662c17..0f187bea730 100644 --- a/specs/GH12306/tech.md +++ b/specs/GH12306/tech.md @@ -62,7 +62,7 @@ Add `pub const WORKSPACE_IN_QUAKE_WINDOW_FLAG: &str = "Workspace_InQuakeWindow"; **New handler methods** (~line 26703, alongside existing transfer functions): - `move_active_tab_to_dedicated_hotkey_window` — Verifies the hotkey window is open and visible via `quake_mode_window_is_open()` (opens it via the newly-`pub(crate)` `toggle_quake_mode_window` if closed or hidden), then calls `move_active_tab_to_window`. -- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.ordered_window_ids()` (z-order front-to-back, skipping self and quake). If none exists, calls `create_transferred_window` for the first tab, then transfers remaining tabs to that new window. If one exists, calls `move_active_tab_to_window` with that window as target. +- `move_active_tab_to_standard_window` — Finds an existing standard window via `ctx.ordered_window_ids()` (z-order front-to-back, skipping self and quake). If one exists, calls `move_active_tab_to_window` with that window as target. If none exists, collects all transfer infos in ascending order, calls `prepare_for_transferred_tab_attach` on the source for each, creates a new window for the first tab via `create_transferred_window`, then transfers remaining tabs to that new window via `transfer_view_tree_to_window` + `insert_transferred_tab_at_index` for each. Removes all source tabs in descending order, clears `suppress_detach_panes_on_window_close` if source stays alive. - `toggle_active_tab_window_type` — Dispatches to the appropriate directional method based on current window. - `move_active_tab_to_window(target_window_id, ctx)` — Core logic: gathers selected indices (multi-selection from `selected_tab_indices()` or fallback to `active_tab_index`), collects transfer infos in ascending order. For each tab: calls `prepare_for_transferred_tab_attach` on the source workspace, then `ctx.transfer_view_tree_to_window`. After all transfers, removes tabs from source in descending order (to avoid index shift). If the source still has remaining tabs, clears `suppress_detach_panes_on_window_close` via `set_suppress_detach_panes_on_window_close(false)`. Inserts collected tabs at end of target in original ascending order. Focuses target window. @@ -93,7 +93,9 @@ Change `toggle_quake_mode_window` from `fn` to `pub(crate) fn` (line 1331). ### Move to standard window -Same flow, but the target window is discovered via `ctx.ordered_window_ids()` (z-order front-to-back, excluding the quake window and self). If no standard window exists, `create_transferred_window` creates a new window for the first tab, then remaining tabs are transferred to that new window via `move_active_tab_to_window`. +If an existing standard window is found: same flow as above (collect, prep, transfer, insert, remove, cleanup), targeting that window via `move_active_tab_to_window`. + +If no standard window exists: collect all transfer infos in ascending order, call `prepare_for_transferred_tab_attach` on the source for each, create a new window via `create_transferred_window` with the first tab's info, then transfer remaining tabs to the new window via `transfer_view_tree_to_window` + `insert_transferred_tab_at_index` for each. Remove all source tabs in descending order. Clear `suppress_detach_panes_on_window_close` if the source window remains alive. Focus moves to the new standard window. ### Toggle From 880961d8cb83628d0fbc82bc439bb5f5f8ff4cfc Mon Sep 17 00:00:00 2001 From: danieltsam Date: Thu, 25 Jun 2026 00:03:54 +1000 Subject: [PATCH 6/6] specs(GH12306): remove conflicting auto-hide follow-up per Oz nit --- specs/GH12306/tech.md | 1 - 1 file changed, 1 deletion(-) diff --git a/specs/GH12306/tech.md b/specs/GH12306/tech.md index 0f187bea730..bfac758faaa 100644 --- a/specs/GH12306/tech.md +++ b/specs/GH12306/tech.md @@ -137,4 +137,3 @@ If no standard window exists: collect all transfer infos in ascending order, cal - **Drag-and-drop standard→hotkey** — The team is shipping hotkey→standard drag this cycle. The reverse direction would extend the cross-window drag system (`Workspace::drag_tab_over`, `tab_insertion_index_for_cursor`) to accept `WindowStyle::Pin` windows as valid drop targets. - **Non-dedicated hotkey mode** — Deferred per maintainer signal. Would require a new `GlobalHotkeyMode` variant, different window creation path, changes to quake state lifecycle. -- Consider auto-hide on last-tab-moved-out from hotkey window.