Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
# APP-3825: Vertical Tabs — Drag Panes Between Tabs
## Summary
Add the vertical-tabs equivalent of Warp's existing horizontal-tab pane-drag flow. When vertical tabs are enabled, a user should be able to drag a pane header over a different tab in the vertical tabs panel, have that tab become the active drag destination, and then drop the pane into the target tab using the same in-tab drop-target and relayout UX that already exists for horizontal tabs.
The feature should also preserve the existing "promote to a new tab" behavior: dragging between tabs should create a new tab at that position instead of targeting an existing tab.
## Problem
In horizontal tabs mode, users can move a pane from one tab to another by dragging the pane header to the top tab strip, hovering the destination tab so it becomes active, and then dropping the pane into a new location inside that tab. That workflow is currently missing in vertical tabs mode, even though the same pane-drag and in-tab relayout concepts already exist elsewhere in the product.
This creates an inconsistent drag-and-drop model between horizontal and vertical tabs. Users who opt into vertical tabs lose an existing pane-management workflow and must fall back to indirect alternatives like creating a new tab first or rearranging panes after the fact.
## Goals
- Restore parity with the existing horizontal-tab pane-drag workflow when vertical tabs are enabled.
- Let users target an existing tab from the vertical tabs panel and then place the pane within that tab using the existing pane drop overlays and relayout rules.
- Let users create a new tab by dropping between vertical tab groups, matching the current horizontal-tab "drop between tabs" behavior.
- Make the targeting behavior work regardless of whether the vertical tabs panel is in compact or expanded mode.
## Non-goals
- Auto-opening the vertical tabs panel if it is currently closed.
- Redesigning the in-tab pane drop overlays or changing how pane placement inside a tab works.
- Changing any of the existing special-case pane-drop rules inside the target tab (for example, code-pane merge behavior when tabbed editor view is preferred).
- Adding new keyboard interactions for this drag flow.
- Extending this ticket to editor file-tab dragging; this spec is only for dragging pane headers.
## Figma / design references
Figma: none provided
Design intent should match the current horizontal-tab pane-drag behavior as closely as possible.
## User experience
### Availability
This behavior applies when:
- vertical tabs are enabled, and
- the vertical tabs panel is visible, and
- the user is dragging a pane by its pane header.
If the vertical tabs panel is closed, this ticket does not introduce a new auto-open or alternate targeting path.
### Valid tab targets in the vertical tabs panel
Each visible vertical tab group represents one workspace tab and must be targetable during a pane drag.
In practice:
- In **expanded** mode, hovering any visible part of a tab group counts as hovering that tab, including its pane rows and its optional custom-title header.
- In **compact** mode, the same rule applies to the compact rendering of that tab group.
- The user does not need a custom tab title/header in order to target a tab. Tabs that render only pane rows must still be targetable.
Hover-only panel chrome must not interfere with drag targeting:
- Drag-target feedback takes precedence over hover-only action affordances like the kebab/close button belt.
- Those controls must not prevent the user from targeting the underlying tab group while a pane drag is in progress.
### Hovering an existing tab
When the user drags a pane header over a different tab group in the vertical tabs panel, Warp should treat that tab group the same way the horizontal tab strip treats a hovered destination tab today.
That means:
- The hovered tab group is shown as the active drag target.
- The workspace switches to that tab as the drag destination.
- Once that tab is active, the user can continue moving the cursor into the workspace content area and see the existing pane relayout drop targets for that tab.
This should feel like a direct vertical-tabs analogue of the current horizontal behavior, not like a separate drag mode.
### Dropping into the target tab
After the destination tab becomes active, dropping the pane inside the workspace should reuse the existing in-tab pane drop behavior unchanged.
Specifically:
- The same drop-target affordances that already appear when rearranging panes within a tab should appear for the dragged pane in the destination tab.
- The same placement outcomes should apply as they do today in horizontal tabs mode.
- Any existing special-case behavior in the destination tab remains unchanged. For example, if a destination tab's current rules would merge a dragged code pane into an existing code-pane/editor setup instead of allowing arbitrary free placement, vertical tabs should preserve that same result rather than inventing a new one.
### Dragging between tabs to create a new tab
The vertical tabs panel must also support the vertical analogue of "drop between tabs."
When the dragged pane is positioned between two visible tab groups, Warp should show an insertion indicator between those groups. Dropping there creates a new workspace tab containing the dragged pane at that position.
The same applies after the last visible tab group:
- Hovering below the final tab group shows an insertion indicator at the end of the list.
- Dropping there creates a new tab at the end.
This should match the semantics of the current horizontal-tab strip:
- **Over a tab** targets that existing tab.
- **Between tabs** creates a new tab at that position.
### Visual feedback
During a pane drag in vertical tabs mode, the panel should provide clear, mutually exclusive feedback:
- **Over an existing tab**: that tab group is highlighted as the current destination tab.
- **Between tab groups**: show an insertion indicator between groups.
- **No valid tab target**: clear any tab-target highlight or insertion indicator.
At no point should both an existing-tab highlight and a between-tabs insertion indicator be shown at the same time.
### Cancellation and reversibility
The cross-tab drag flow must remain reversible until the drop is committed.
- Moving off a target tab group removes that target state.
- Moving from one tab group to another updates the target accordingly.
- Aborting the drag or dropping outside any valid destination leaves the tab/pane layout unchanged.
### No new behavior outside the intended scope
This ticket does not change how users reorder vertical tabs by dragging the tab groups themselves. It only adds parity for dragging a pane header from one tab into another tab or into a new tab position.
## Success criteria
1. In vertical tabs mode with the panel open, dragging a pane header over a different visible tab group makes that tab group the active drag destination.
2. When a non-active tab group becomes the drag destination, Warp switches the workspace to that tab so the user can place the pane inside it.
3. After switching to the destination tab, the existing pane relayout/drop-target UX appears in the workspace and can be used to place the pane.
4. The placement outcomes inside the destination tab match the existing horizontal-tabs flow; no new placement rules are introduced.
5. Dragging between two visible tab groups shows an insertion indicator and dropping there creates a new tab containing the pane at that position.
6. Dragging below the last visible tab group shows an end-of-list insertion indicator and dropping there creates a new tab at the end.
7. The behavior works in both compact and expanded vertical tabs panel modes.
8. Tabs without a custom title/header are still targetable via their rendered tab-group body.
9. Hover-only controls in the vertical tabs panel do not block or replace drag-target feedback.
10. Cancelling the drag, or dropping outside a valid destination, leaves the tab/pane layout unchanged and clears any temporary targeting UI.
11. Existing vertical-tab reordering behavior is unchanged.
12. Existing within-tab special cases, including code-pane merge behavior where applicable, remain unchanged.
## Validation
- **Existing-tab transfer**: In vertical tabs mode, create two tabs with multiple panes. Drag a pane header from tab A over tab B in the vertical tabs panel. Verify tab B becomes active and the pane can be dropped into a new split location using the normal in-tab drop overlays.
- **Compact mode**: Repeat the same flow with the vertical tabs panel in compact mode.
- **Expanded mode**: Repeat the same flow with the panel in expanded mode.
- **No custom header**: Verify the drag works for a destination tab that has no custom tab title and therefore renders without a separate custom header row.
- **New-tab insertion**: Drag a pane between two tab groups and verify an insertion indicator appears. Drop and confirm a new tab is created at that exact position containing the dragged pane.
- **End insertion**: Drag below the last tab group and verify dropping creates a new final tab.
- **Cancel path**: Start a cross-tab pane drag, hover a target so it highlights, then cancel or drop outside any valid target. Verify no pane move is committed and temporary highlighting clears.
- **Special-case regression check**: Use a scenario where the existing horizontal-tabs flow has a special outcome inside the target tab (for example, a code-pane/editor merge case). Verify vertical tabs preserves the same behavior.
- **Tab reordering regression**: Verify that dragging vertical tab groups themselves still reorders tabs exactly as before.
## Open questions
None.
+276
View File
@@ -0,0 +1,276 @@
# APP-3825: Tech Spec — Vertical Tabs Pane Drag Parity
## Problem
The pane-drag pipeline already supports moving a pane onto the horizontal tab strip and then reusing the existing in-tab relayout flow inside the destination tab. Vertical tabs do not participate in that pipeline today, even though the downstream workspace and pane-group logic is already generic once it receives a `TabBarHoverIndex`.
The missing piece is the vertical tabs panel itself:
- it renders tab groups as draggable items for tab reordering,
- but it does not expose any drop targets for pane-header drags,
- it does not render insertion indicators from `Workspace.hovered_tab_index`,
- and its hover-only action button belt can overlap the tab-group surface during a drag.
As a result, pane-header drags over vertical tabs fall through to `PaneDragDropLocation::Other` instead of producing the existing `OverTab` / `BeforeTab` flow.
## Relevant code
- `app/src/workspace/view/vertical_tabs.rs (710-929)` — vertical tabs panel rendering; `render_vertical_tabs_panel`, `render_groups`, `render_tab_group`
- `app/src/workspace/view/vertical_tabs.rs (968-1083)` — each vertical tab group is currently only a `Draggable` + `SavePosition` for reordering
- `app/src/workspace/view.rs (15291-15328)` — horizontal tab bar wrapper adds a workspace-level `DropTarget` with `TabBarDropTargetData { AfterTabIndex(..) }`
- `app/src/tab.rs (1366-1382)` — each horizontal tab is wrapped in `DropTarget::new(..., TabBarDropTargetData { TabIndex(..) })`
- `app/src/pane_group/pane/view/header/mod.rs (321-352)` — pane-header drag hover classification currently derives `TabBarHoverIndex` from `TabBarLocation` + geometry
- `app/src/pane_group/pane/view/header/mod.rs (939-1088)``render_pane_header_draggable`; pane drags only accept `PaneDropTargetData` and `TabBarDropTargetData`
- `app/src/pane_group/mod.rs (544-561)` — workspace-facing events: `DroppedOnTabBar`, `SwitchTabFocusAndMovePane`, `UpdateHoveredTabIndex`, `ClearHoveredTabIndex`
- `app/src/pane_group/mod.rs (1098-1153)``handle_pane_view_event`; `OverTab` hides/moves panes into the target tab, `BeforeTab` preserves “new tab” behavior
- `app/src/workspace/view.rs (11630-11889)` — workspace handling for cross-tab drag/drop and existing special cases like code-pane merge behavior
- `app/src/workspace/mod.rs (1447-1461)``TabBarDropTargetData` and `TabBarLocation`
- `app/src/workspace/view.rs (14883-14891)` — existing horizontal insertion indicator renderer
## Current state
### Horizontal tabs already provide the full drag channel
The horizontal tab strip exposes `TabBarDropTargetData` in two places:
- each concrete tab via `TabBarLocation::TabIndex(i)`, and
- the strip container via `TabBarLocation::AfterTabIndex(tab_count)`.
Pane-header drags and editor-tab drags both recognize that data. In the pane-header path, `PaneHeader::calculate_tab_focus_hover_index` interprets the dragged rect relative to the target tabs bounds and converts it into:
- `TabBarHoverIndex::OverTab(i)` when the user is targeting an existing tab
- `TabBarHoverIndex::BeforeTab(i)` when the user is inserting between tabs
From there, the rest of the flow is already shared:
- `PaneGroup::handle_pane_view_event` emits `SwitchTabFocusAndMovePane` for `OverTab` and hides the dragged pane for `BeforeTab`
- `Workspace` stores `hovered_tab_index`, switches active tabs when needed, and on drop either
- adds a new tab from the moved pane for `BeforeTab`, or
- reuses the target tabs existing placement rules for `OverTab`
### Vertical tabs only support tab reordering today
`render_tab_group` in `vertical_tabs.rs` currently builds each tab group as:
- a hoverable group surface,
- an optional overlay belt with kebab/close actions,
- a `Draggable` used for vertical tab reordering,
- and a `SavePosition(tab_position_id(tab_index))` used by the reorder math in `Workspace::calculate_updated_tab_index_vertical`.
There is no `DropTarget` around the group and no dedicated insertion targets between groups or after the final group.
Because of that, dragging a pane header over the vertical tabs panel never produces a workspace-tab target. The drag is not accepted by the panel, so the pane-header code falls back to `PaneDragDropLocation::Other`.
### `hovered_tab_index` is only rendered in the horizontal tab strip
`Workspace.hovered_tab_index` already drives two pieces of feedback in the horizontal strip:
- highlighted tab styling for `OverTab`
- insertion bars for `BeforeTab`
The vertical tabs panel does not currently read or render that state at all.
### Overlay controls can interfere with tab-group hit-testing
The vertical tabs group action belt is shown from hover state and rendered as a positioned overlay on top of the group. During a pane drag, that overlay can visually and spatially compete with the tab-group surface unless we explicitly suppress it or ensure the drop target sits above it in hit-testing.
## Proposed changes
### 1. Add explicit vertical-tabs pane drop targets
Introduce a new workspace-level drop target data type for pane-header drags in vertical tabs. Keep it separate from `TabBarDropTargetData` so this ticket stays scoped to pane headers and does not implicitly expand to editor file-tab dragging.
Proposed shape in `app/src/workspace/mod.rs`:
```rust
#[derive(PartialEq, Copy, Clone, Debug)]
pub struct VerticalTabsPaneDropTargetData {
pub tab_bar_location: TabBarLocation,
pub tab_hover_index: TabBarHoverIndex,
}
```
This reuses the existing `TabBarLocation` / `TabBarHoverIndex` semantics instead of inventing a parallel enum.
### 2. Teach pane-header draggables to accept the new vertical target data
Update `render_pane_header_draggable` in `app/src/pane_group/pane/view/header/mod.rs` to accept three drop-target kinds:
- `PaneDropTargetData`
- `TabBarDropTargetData` (existing horizontal strip path)
- `VerticalTabsPaneDropTargetData` (new vertical tabs path)
To keep the existing horizontal logic intact, extend the pane-header drag action so it can optionally carry a precomputed `TabBarHoverIndex`:
```rust
PaneHeaderDragged {
origin: ActionOrigin,
drag_location: PaneDragDropLocation,
drag_position: RectF,
explicit_tab_hover_index: Option<TabBarHoverIndex>,
}
```
Behavior:
- horizontal tab-strip drags keep sending `None` and continue to use `calculate_tab_focus_hover_index`
- vertical tabs send `Some(...)` and bypass geometry inference
That lets vertical tabs use explicit “over tab” vs “between tabs” zones instead of trying to reinterpret a large tab-group card with the horizontal strips x-axis heuristics.
### 3. Wrap vertical tab groups in `OverTab` drop targets
In `render_groups` / `render_tab_group`:
- keep the current `Draggable + SavePosition` structure for tab reordering
- wrap each rendered tab-group element in `DropTarget::new(..., VerticalTabsPaneDropTargetData { ... OverTab(tab_index) ... })`
The `SavePosition(tab_position_id(tab_index))` should remain attached to the group element used for tab reordering, not to any insertion spacer. That preserves the current reorder math in `calculate_updated_tab_index_vertical`.
This gives the entire visible tab-group surface an explicit “existing tab” meaning, which matches the product spec for both compact and expanded modes.
### 4. Add explicit insertion targets between groups and after the final group
Render small, dedicated insertion targets in the vertical list rather than deriving `BeforeTab` from pointer position inside a tab-group card.
Concretely, `render_groups` should render:
- a leading insertion target for `BeforeTab(0)` for full parity with the horizontal strip
- one insertion target before each subsequent tab group
- one trailing insertion target after the final group for `BeforeTab(tab_count)`
Each insertion target is a thin block wrapped in `DropTarget::new(..., VerticalTabsPaneDropTargetData { tab_hover_index: BeforeTab(i), ... })`.
This has two advantages:
- it matches the product behavior more closely than splitting a tab-group card into vertical thirds
- it avoids rewriting the shared pane-group/workspace drag logic, since that logic already understands `BeforeTab`
The trailing target should have enough height to remain practically hittable even when the list fills the panel.
### 5. Render vertical drag feedback from `Workspace.hovered_tab_index`
Add vertical-tabs equivalents of the horizontal tab-strip feedback in `vertical_tabs.rs`:
- when `hovered_tab_index == OverTab(i)`, the corresponding tab group renders as the active drag target
- when `hovered_tab_index == BeforeTab(i)`, render an insertion indicator before that group
- when `hovered_tab_index == BeforeTab(tab_count)`, render the insertion indicator at the end of the list
Implementation-wise:
- thread `workspace.hovered_tab_index` into `render_groups` / `render_tab_group`
- add a helper like `render_vertical_tab_hover_indicator`
- include drag-target highlighting in the group background logic, separate from normal hover and active-tab styling
The current horizontal `render_tab_hover_indicator` is a narrow vertical bar. Vertical tabs should use a horizontal divider-style accent that visually reads as “insert here” in the list layout.
### 6. Suppress the vertical tab-group action belt during pane drags
While any pane is being dragged, do not render the floating kebab/close action belt in `render_tab_group`.
This is the simplest way to avoid overlay interference with drag targeting and keeps the panel visually focused on drop feedback instead of hover controls.
Use the existing `PaneGroup::any_pane_being_dragged(app)` signal from the active pane group, similar to how the workspace already uses it for tab-bar visibility and keymap context.
### 7. Leave pane-group and workspace drop behavior unchanged
Do not redesign the downstream move/drop handlers. The current event chain is already the right abstraction boundary:
- `PaneGroup::handle_pane_view_event`
- `Workspace`s `SwitchTabFocusAndMovePane`
- `Workspace`s `DroppedOnTabBar`
That code already preserves:
- hidden-pane staging while hovering `OverTab`
- “promote to new tab” behavior for `BeforeTab`
- existing target-tab placement rules
- special cases like code-pane merge behavior under tabbed editor view
This ticket should only change how vertical tabs produce `TabBarHoverIndex`, not what happens after that.
## End-to-end flow
1. The user starts dragging a pane header.
2. `render_pane_header_draggable` enters drag mode and accepts either pane drop targets, horizontal tab-strip targets, or the new vertical-tabs pane targets.
3. The user moves over the vertical tabs panel:
- over a tab group → the drop target provides `OverTab(i)`
- over an insertion zone → the drop target provides `BeforeTab(i)`
4. The pane-header view emits `DraggedOverTabBar` with that `TabBarHoverIndex`.
5. `PaneGroup::handle_pane_view_event` reuses the existing behavior:
- `OverTab(i)` → stage the pane in the destination tab via `SwitchTabFocusAndMovePane`
- `BeforeTab(i)` → hide the pane for move and preserve “create a new tab on drop”
6. `Workspace` updates `hovered_tab_index`, which now drives vertical-panel drag feedback.
7. If the target is `OverTab(i)` and a tab switch is needed, `Workspace` activates the target tab and adds the pane as hidden in that tabs pane group.
8. The user moves into the workspace content area; the existing pane relayout drop targets appear and the pane can be placed using the current within-tab logic.
9. On drop:
- `BeforeTab(i)` → existing `DroppedOnTabBar` logic creates a new tab at `i`
- `OverTab(i)` → existing target-tab placement logic runs unchanged
10. Cancelling or leaving valid targets clears `hovered_tab_index` and reverts hidden-pane staging, as it does today.
## Risks and mitigations
### Risk: accidental scope expansion to editor file-tab dragging
If we reused `TabBarDropTargetData` directly in vertical tabs, editor file tabs would likely start targeting vertical tabs too, because `code/view.rs` already recognizes that type.
Mitigation:
- use a separate `VerticalTabsPaneDropTargetData`
- only add it to the pane-header draggable acceptance path
### Risk: overlay hit-testing blocks the drop target
The action button belt is rendered as an overlay on the group and can interfere with drag targeting.
Mitigation:
- suppress the overlay while any pane drag is active
### Risk: breaking vertical tab reordering
Adding wrappers around the group could change the bounds used by `calculate_updated_tab_index_vertical`.
Mitigation:
- keep `SavePosition(tab_position_id(..))` attached to the group element itself
- do not include insertion-target spacers in that saved position
### Risk: feedback mismatch between target state and actual drop behavior
If the vertical panel renders `OverTab` / `BeforeTab` differently from what the workspace eventually does, the interaction will feel inconsistent.
Mitigation:
- reuse `TabBarHoverIndex` end to end
- keep workspace and pane-group drop handlers unchanged
## Testing and validation
### Manual validation
Use the scenarios in `specs/APP-3825/PRODUCT.md`:
- drag a pane from one tab to another in expanded mode
- repeat in compact mode
- verify tabs without custom headers are targetable
- verify insertion between groups and after the last group creates a new tab
- verify cancel / drop-outside clears temporary state
- verify code-pane merge behavior still matches the horizontal flow
- verify vertical tab reordering still works
### Automated validation
This feature is mostly WarpUI drag/drop hit-testing, so manual validation is the primary check. Add lightweight automated coverage where it is cheap and reliable:
- if a pure helper is introduced for vertical drag-target rendering decisions, cover `OverTab` / `BeforeTab` cases in `app/src/workspace/view/vertical_tabs_tests.rs`
- add or update workspace tests only if the new helper can be exercised without full UI drag simulation
No new persistence, networking, or model migration coverage is needed.
## Follow-ups
- If we later want vertical tabs to support editor file-tab dragging as well, we can either:
- teach `code/view.rs` to accept `VerticalTabsPaneDropTargetData`, or
- unify the horizontal and vertical tab-target metadata behind a shared explicit-hover-index type
- If the vertical insertion indicator and the horizontal tab-strip indicator should share styling, we can extract a small shared helper after this behavior is stable