Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions specs/GH12306/product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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 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 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 — 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.

**`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.
- **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:** Quake window existence and visibility verified via `quake_mode_window_is_open()` 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 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.

## 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.
## 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.
139 changes: 139 additions & 0 deletions specs/GH12306/tech.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# 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) 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_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.

### 3. `app/src/workspace/view.rs` and `app/src/settings_view/mod.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 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 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.

### 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 `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). 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.

### Move to standard 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

`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 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.

### 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 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.

## 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.