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
+197
View File
@@ -0,0 +1,197 @@
# APP-3828: Vertical Tabs v2 — View as Panes / Tabs
## Summary
Add a new `View as` control to the vertical tabs display options popup with two modes:
- **Panes**: the current behavior, where each pane is rendered as its own item under its tab.
- **Tabs**: a new overview mode where each tab renders exactly one item, using that tab’s active pane as the representative row.
This first iteration only ships the `View as` toggle and the `Focused session` behavior implicitly. It does not yet add a separate Tabs-only naming control such as `Summary`.
## Problem
The current vertical tabs panel is pane-centric. That works well when a user wants fine-grained visibility into every split, but it becomes noisy when tabs contain multiple panes and the user is trying to scan the workspace at the tab level.
Users need a higher-level overview mode that reduces each tab to a single representative item without introducing a brand-new visual language. The new mode should preserve the current tab structure and reuse the existing row UI so the first iteration is easy to understand and low-risk to ship.
## Goals
- Add a new `View as` setting in the vertical tabs popup with `Panes` and `Tabs` options.
- Preserve the current behavior as the default via `View as = Panes`.
- Introduce `View as = Tabs`, where each tab renders one representative row derived from that tab’s active pane.
- Reuse the existing compact and expanded pane row UI for the representative row rather than inventing a new tab row design.
- Keep the existing tab group/header structure and interactions intact in Tabs mode.
- Persist the `View as` preference across sessions as a synced setting.
## Non-goals
- Shipping the future Tabs-only `Default name` section from the exploratory mock.
- Shipping a `Summary` naming mode or any other alternative tab naming strategy.
- Flattening the panel into a headerless list of tabs.
- Redesigning tab group headers, close affordances, rename behavior, or drag-and-drop behavior.
- Changing the current `Density`, `Pane title as`, `Additional metadata`, or `Show` controls beyond ensuring they continue to work with the reused active-pane row.
## Figma / design references
- Popup exploration: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7628-119835&t=LSuxL7FNk3EXOfvJ-0
- Tabs-selected popup state: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7628-123042&t=LSuxL7FNk3EXOfvJ-0
### Intentional deviation from the exploratory mock
The exploratory Tabs-selected popup includes a Tabs-only `Default name` section with `Focused session` and `Summary`.
This iteration intentionally does **not** ship that extra section. `Focused session` is the only supported Tabs behavior and is implicit when `View as = Tabs`.
## User experience
### Setting
The vertical tabs popup gains a new top-level setting:
- **View as: Panes**
- **View as: Tabs**
`Panes` is the default.
The control is rendered as a two-segment toggle at the top of the popup, above the existing display controls.
### Popup behavior
- Clicking `Panes` or `Tabs` updates the panel immediately.
- The popup remains open after switching modes so the user can see the change in context.
- The existing `Density` and other pane-row display controls remain present in this iteration.
- Those pane-row controls remain accessible in both `Panes` and `Tabs` modes, because the `Tabs` row is still a pane-style row derived from the active pane.
- Switching between `Panes` and `Tabs` does not reset the current density or any existing pane-row display preferences.
### Panes mode
When `View as = Panes`, the panel behaves exactly as it does today:
- each visible pane is rendered as its own item
- items are grouped under their tab header
- compact vs expanded density works as it does today
- existing pane-row display preferences continue to apply as they do today
No visual or behavioral change should be introduced in Panes mode beyond the existence of the new `View as` control in the popup.
### Tabs mode
When `View as = Tabs`, the panel stays tab-grouped, but each tab group renders exactly one representative row instead of one row per visible pane.
#### Representative row source
The representative row is always derived from the tab’s **active pane**.
In this iteration, `View as = Tabs` therefore means the tab item is effectively named and styled using the tab’s **focused session**.
#### Representative row appearance
The representative row reuses the existing pane-row UI for the active pane:
- in **compact density**, it uses the same compact row renderer the active pane would use in Panes mode
- in **expanded density**, it uses the same expanded row renderer the active pane would use in Panes mode
This includes the same icon rules, title rules, subtitle/metadata rules, badges, truncation behavior, and selection styling that already apply to the active pane’s row in Panes mode.
Because the representative row is still a pane-style row, the existing pane-row display controls continue to apply in `Tabs` mode as well. In this iteration that means:
- `Pane title as` still changes how a terminal representative row is labeled
- `Additional metadata` still affects compact terminal representative rows
- `Show` still affects expanded terminal representative rows
These controls are not hidden when `View as = Tabs`.
#### Representative row updates
The representative row updates immediately whenever the active pane for that tab changes. Examples:
- the user changes focus between split panes inside the tab
- the active pane is closed and a different pane becomes active
- the active pane’s displayed metadata changes (for example, terminal title, branch, badges, or unsaved state)
Tabs mode should always reflect the tab’s current active pane, not the pane that happened to be active when the user first switched into Tabs mode.
#### Relationship to tab headers
Tabs mode does **not** remove or redesign the existing tab group header.
The current tab group header behavior remains intact, including:
- tab title display
- pane count display
- rename behavior
- close behavior
- drag-and-drop behavior
- existing header context menu behavior
The change in Tabs mode is only the number of rows rendered beneath each header: one representative row per tab instead of one row per pane.
#### Single-pane tabs
For tabs that only contain one visible pane, Tabs mode and Panes mode look effectively the same below the header, because the active pane is also the only pane.
#### Multi-pane tabs
For tabs that contain multiple visible panes:
- **Panes mode** renders one item per visible pane
- **Tabs mode** renders one item total for that tab, based on the active pane only
Non-active panes in the tab do not get their own rows in Tabs mode.
### Interaction behavior in Tabs mode
The representative row remains actionable in the same spirit as the active pane row it reuses:
- clicking the row activates that tab and focuses its active pane
- selection/highlight state continues to represent the active tab / focused pane as it does today
This iteration should not introduce new row-specific interactions unique to Tabs mode.
### Search behavior
Search/filtering operates on the items currently rendered in the chosen mode.
That means:
- in **Panes mode**, matching remains pane-based as it is today
- in **Tabs mode**, matching is based on each tab’s representative row only
In this first iteration, a non-active pane that is hidden by Tabs mode does not create its own separate match result.
## Success criteria
1. The display options popup shows a new top-level `View as` segmented control with `Panes` and `Tabs`.
2. `Panes` is selected by default, so existing users see no change in the panel until they opt into `Tabs`.
3. Switching to `Tabs` updates the panel immediately without requiring the popup to close.
4. In `Tabs` mode, each tab group renders exactly one row beneath its header.
5. The row shown for a tab in `Tabs` mode is derived from that tab’s current active pane.
6. If the active pane changes within a tab, the representative row updates immediately to reflect the newly active pane.
7. A tab with only one visible pane looks the same in `Panes` and `Tabs` modes below the header.
8. A tab with multiple visible panes shows multiple rows in `Panes` mode and exactly one row in `Tabs` mode.
9. The representative row in `Tabs` mode reuses the same compact or expanded row UI, icons, metadata, badges, and truncation rules the active pane already uses in `Panes` mode.
10. Existing tab header behavior remains unchanged in `Tabs` mode, including pane count, close, rename, and drag behavior.
11. Existing `Density` and pane-row display preferences continue to apply after switching between `Panes` and `Tabs`.
12. `Pane title as`, `Additional metadata`, and `Show` remain visible and usable in `Tabs` mode, and they continue to affect the representative row.
13. The `View as` preference persists across app relaunches as a synced setting.
14. Tabs mode does not surface `Summary` or any other alternate naming mode in this iteration.
15. In search/filter mode, Tabs mode returns matches for representative rows only, not hidden non-active panes.
## Validation
- **Popup toggle**: Open the display options popup and verify `View as` appears above the existing controls. Toggle between `Panes` and `Tabs` and verify the panel updates immediately while the popup remains open.
- **Default behavior**: With the default setting, verify the panel still renders one row per visible pane exactly as before.
- **Single-pane tab**: Open a tab with one pane, switch between `Panes` and `Tabs`, and verify there is no meaningful change below the header.
- **Multi-pane tab**: Create a tab with multiple split panes. Verify `Panes` shows all pane rows and `Tabs` shows exactly one row for that tab.
- **Active pane switching**: In a multi-pane tab, switch focus between panes and verify the representative row in `Tabs` mode updates to match the newly active pane.
- **Density coverage**: Verify the representative row works in both compact and expanded density modes.
- **Row parity**: For a given active pane, compare its appearance in `Panes` mode vs `Tabs` mode and verify the row content matches.
- **Pane-row controls in Tabs mode**: With `View as = Tabs`, change `Pane title as`, `Additional metadata`, and `Show`, and verify they still affect the representative row rather than disappearing.
- **Header regression**: In `Tabs` mode, verify header rename, close, pane count, drag behavior, and context menu behavior still work.
- **Persistence**: Select `Tabs`, relaunch Warp, and verify the panel reopens in `Tabs` mode.
- **Search**: In a multi-pane tab, ensure only the active pane’s representative row is matched and rendered in `Tabs` mode.
## Open questions
None for this iteration.
+328
View File
@@ -0,0 +1,328 @@
# APP-3828: Tech Spec — Vertical Tabs `View as` Panes / Tabs
## Problem
APP-3828 adds a new `View as` control to the vertical tabs options popup. The product behavior is intentionally narrow:
- `Panes` preserves the current pane-centric rendering
- `Tabs` renders one representative row per tab
- the representative row is the existing row UI for that tab’s active pane (`Focused session`)
- the current compact / expanded density controls and pane-row display controls remain in place
Technically, the current vertical tabs implementation has no abstraction for “row granularity.” It assumes that every visible pane in a `PaneGroup` should be rendered and searched independently. The new feature therefore needs a low-risk way to:
- add a new synced setting
- wire a new popup control into existing action / settings flow
- centralize the decision of “which pane ids should this tab render/search right now?”
- keep the existing row renderers unchanged as much as possible
## Relevant code
- `specs/APP-3828/PRODUCT.md` — agreed user-facing behavior for this feature
- `app/src/workspace/tab_settings.rs (171-276)` — current synced vertical-tabs settings (`VerticalTabsViewMode`, `VerticalTabsPrimaryInfo`, `VerticalTabsCompactSubtitle`)
- `app/src/workspace/action.rs (237-241)` — existing vertical-tabs popup actions
- `app/src/workspace/action.rs (740-741)` — `should_save_app_state_on_action` coverage for the current vertical-tabs setting actions
- `app/src/workspace/view.rs (17761-17810)` — workspace-side action handlers that update `TabSettings`
- `app/src/workspace/view/vertical_tabs.rs (246-344)` — `VerticalTabsPanelState` and popup mouse-state ownership
- `app/src/workspace/view/vertical_tabs.rs (458-483)` — `matching_tab_indices`, which currently assumes every visible pane can make a tab searchable
- `app/src/workspace/view/vertical_tabs.rs (630-799)` — `render_groups`, including current search filtering behavior
- `app/src/workspace/view/vertical_tabs.rs (800-1034)` — `render_tab_group`, which currently renders one row per visible pane
- `app/src/workspace/view/vertical_tabs.rs (1569-1664)` — `PaneProps::new` and query matching helpers used by render/search
- `app/src/workspace/view/vertical_tabs.rs (2380-2664)` — `render_settings_popup`, including the current top-of-popup density segmented control and the existing secondary controls
- `app/src/workspace/view/vertical_tabs.rs (2845-3033)` — `render_compact_pane_row`; compact density reuses `PaneProps`
- `app/src/workspace/view/vertical_tabs.rs (1160-1250)` — `render_pane_row`; expanded density reuses `PaneProps`
- `app/src/pane_group/mod.rs:1981` — `PaneGroup::focused_pane_id`, the current source of truth for the pane last focused within a tab
- `app/src/pane_group/mod.rs (4566-4571)` — `PaneGroup::display_title`, which already derives tab-level display state from the focused pane
- `app/src/workspace/view/vertical_tabs_tests.rs (1-196)` — current unit-test home for vertical-tabs pure logic
- `app/src/workspace/action_tests.rs (1-36)` — current tests for vertical-tabs action persistence behavior
## Current state
### Settings and actions
Vertical-tabs display preferences already follow a consistent pattern:
- the setting enum lives in `TabSettings`
- the enum is registered with `implement_setting_for_enum!`
- the popup dispatches a `WorkspaceAction::*`
- `Workspace::handle_action` writes the new value into `TabSettings`
- `should_save_app_state_on_action` returns `false`, because persistence is handled by the settings framework rather than workspace snapshotting
This pattern currently exists for:
- `VerticalTabsViewMode` = compact vs expanded density
- `VerticalTabsPrimaryInfo`
- `VerticalTabsCompactSubtitle`
### Popup structure
`render_settings_popup` currently starts with the compact / expanded segmented control, then renders the existing pane-row controls below it. There is no concept of a top-level “what does each row represent?” setting.
### Row rendering
`render_tab_group` obtains `visible_pane_ids()` from the `PaneGroup`, builds `PaneProps` for each one, and then delegates to either:
- `render_compact_pane_row`
- `render_pane_row`
Those row renderers already contain the exact UI we want to reuse in `Tabs` mode.
### Search behavior
The current search flow is duplicated in two places:
- `matching_tab_indices` decides which tabs are included in keyboard navigation / search result bookkeeping
- `render_groups` computes `matching_ids` to decide which rows to render while searching
Both paths iterate all `visible_pane_ids()` and check each pane independently with `PaneProps::new` plus `pane_matches_query`.
### Tab-level “active pane” state
For this feature, the correct tab representative is not `active_session_id()` because tabs may be backed by non-terminal panes. The right primitive is `PaneGroup::focused_pane_id()`:
- it works for any pane type
- it already tracks the pane that would be focused when the tab becomes active again
- `PaneGroup::display_title()` already treats the focused pane as the tab-level source of truth
That makes `focused_pane_id()` the right backing state for “Focused session” in Tabs mode.
## Proposed changes
### 1. Add a new synced setting for row granularity
Add a new enum in `app/src/workspace/tab_settings.rs`:
```rust path=null start=null
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone)]
pub enum VerticalTabsDisplayGranularity {
#[default]
Panes,
Tabs,
}
```
Register it in `TabSettings` with the same sync / hierarchy behavior as the existing vertical-tabs settings:
- `SupportedPlatforms::ALL`
- `SyncToCloud::Globally(RespectUserSyncSetting::Yes)`
- `hierarchy: "appearance.tabs"`
Deliberately do not rename the existing `VerticalTabsViewMode` enum in this ticket. It already means compact vs expanded density in code, and renaming it would create avoidable churn across unrelated logic. The popup can relabel that control to `Density` without touching the backing enum name yet.
### 2. Add a workspace action for the new setting
Add `WorkspaceAction::SetVerticalTabsDisplayGranularity(VerticalTabsDisplayGranularity)` beside the existing vertical-tabs setting actions.
Handle it in `Workspace::handle_action` exactly like the other setting writes:
- read the enum payload
- call `settings.vertical_tabs_display_granularity.set_value(...)`
- `ctx.notify()`
Update `should_save_app_state_on_action` so this action returns `false`, and add a matching unit test in `action_tests.rs`.
### 3. Extend popup-local state for the new segmented control
Add two `MouseStateHandle`s to `VerticalTabsPanelState` for the new control:
- one for the `Panes` segment
- one for the `Tabs` segment
Keep the existing `compact_segment_mouse_state` / `expanded_segment_mouse_state` fields unchanged so the density control continues to work without refactoring unrelated code.
### 4. Restructure `render_settings_popup`
Update `render_settings_popup` so the popup composition becomes:
1. `View as` header
2. text segmented control for `Panes` / `Tabs`
3. divider
4. `Density` header
5. existing compact / expanded icon segmented control
6. existing pane-row display sections (`Pane title as`, `Additional metadata`, `Show`) in their current conditional behavior
Implementation details:
- add a new helper for a text-labeled segment control rather than forcing the existing icon helper to serve both roles
- keep the popup width fixed unless the new control proves to clip; the current 200px width is likely enough for `Panes` / `Tabs`
- leave the popup open after clicking `Panes` or `Tabs`, matching the existing “update in place” behavior of the other controls
- do not hide `Pane title as`, `Additional metadata`, or `Show` when `Tabs` is selected; the representative row still uses pane-row rendering, so those controls continue to apply in both granularities
- keep the existing conditional logic that is already based on density (`Additional metadata` only in compact, `Show` only in expanded)
The current compact / expanded segmented control should keep writing `VerticalTabsViewMode`; only the user-facing label changes to `Density`.
### 5. Centralize pane-id selection by granularity
Add a small helper in `vertical_tabs.rs` that decides which pane ids a tab should expose for rendering and search:
```rust path=null start=null
fn pane_ids_for_display_granularity(
visible_pane_ids: &[PaneId],
focused_pane_id: PaneId,
granularity: VerticalTabsDisplayGranularity,
) -> Vec<PaneId>
```
Behavior:
- `Panes` returns all visible pane ids in existing order
- `Tabs` returns exactly one pane id:
- `focused_pane_id` if it is present in `visible_pane_ids`
- otherwise the first visible pane as a defensive fallback
- otherwise an empty vec if the tab has no visible panes
This helper should be pure and small enough to unit test in `vertical_tabs_tests.rs`.
### 6. Use the helper in both render and search paths
Read the new setting once in each relevant call path and replace direct iteration of `visible_pane_ids()` with `pane_ids_for_display_granularity(...)`.
Affected paths:
- `matching_tab_indices`
- the search branch inside `render_groups`
- the row-building path inside `render_tab_group`
This keeps the meaning of `Tabs` mode consistent everywhere:
- a tab renders only its representative row
- search only considers that representative row
- tabs hidden by search are determined by the same representative row
This is the most important structural change in the ticket. It is also intentionally narrow: the existing row building stays pane-based, but the set of pane ids fed into it changes.
### 7. Keep `PaneProps` and row renderers unchanged
Do not introduce a new “tab row” prop type in this ticket.
Instead, continue to build a normal `PaneProps` from the chosen representative `PaneId` and reuse:
- `render_pane_row`
- `render_compact_pane_row`
- `render_pane_row_element`
This preserves:
- row click behavior (`FocusPane`)
- existing metadata and badge rules
- compact / expanded density behavior
- selection and hover styling
- future compatibility with any pane-row improvements already in flight on this branch
### 8. Use `focused_pane_id()` as the representative source of truth
In `render_tab_group` and search helpers, compute the representative pane from:
- `pane_group.visible_pane_ids()`
- `pane_group.focused_pane_id(app)`
Do not use `active_session_id()`:
- it is terminal-only
- it would fail for code / notebook / workflow tabs
Using `focused_pane_id()` also ensures the representative row updates automatically when focus changes within a split tab, because that state is already maintained by `PaneGroup::focus_pane`.
### 9. Keep header and action-belt behavior untouched
`render_tab_group` currently owns more than just the rows: it also owns the group container, optional custom-title header, hover background, and overlay action belt. This ticket should not fork that structure for Tabs mode.
Only the row list inside the body changes. Everything else in `render_tab_group` stays as-is.
That matches the product scope and lowers regression risk around rename, tab actions, and drag behavior.
## End-to-end flow
1. The user opens the vertical-tabs popup from the settings icon.
2. `render_settings_popup` reads `vertical_tabs_display_granularity` and shows `Panes` selected by default.
3. The user clicks `Tabs`.
4. `WorkspaceAction::SetVerticalTabsDisplayGranularity(Tabs)` is dispatched.
5. `Workspace::handle_action` writes the synced setting through `TabSettings` and calls `ctx.notify()`.
6. On re-render, `render_groups` and `render_tab_group` both read the new setting.
7. For each tab:
- the code gets `visible_pane_ids()`
- gets `focused_pane_id()`
- runs `pane_ids_for_display_granularity(...)`
- receives either all panes (`Panes`) or exactly one representative pane (`Tabs`)
8. That representative pane is passed through `PaneProps::new` and then through the existing compact / expanded row renderer.
9. Clicking the representative row still dispatches `FocusPane`, which activates the tab and focuses that pane.
10. If the user changes focus within a split tab, `PaneGroup::focus_pane` updates `focused_pane_id()`, and the next render shows a different representative row automatically.
## Risks and mitigations
### Ambiguity between existing `VerticalTabsViewMode` and the new product “View as”
Risk:
- the current code uses `VerticalTabsViewMode` to mean density, while the product copy now uses `View as` to mean pane-vs-tab granularity
Mitigation:
- introduce a separately named enum (`VerticalTabsDisplayGranularity`) instead of overloading `VerticalTabsViewMode`
- relabel the existing control in UI only
### Stale focused pane not present in visible panes
Risk:
- during close / restore edge cases, `focused_pane_id()` might not be present in the visible pane list momentarily
Mitigation:
- `pane_ids_for_display_granularity(...)` falls back to the first visible pane
- the helper returns an empty vec only when the tab truly has no visible panes
### Search/render drift
Risk:
- Tabs mode could render one pane but still search across all panes if the two code paths diverge
Mitigation:
- use the same granularity helper in `matching_tab_indices`, `render_groups`, and `render_tab_group`
- keep pane-id selection in one place
### Popup churn beyond scope
Risk:
- the Figma exploration also shows Tabs-only secondary controls, which could tempt additional conditional popup logic
Mitigation:
- explicitly keep this ticket to the top-level `View as` control only
- do not add a Tabs-only `Default name` or `Summary` section in this implementation
- do not hide the existing pane-row controls when `Tabs` is selected; they remain relevant because the representative row is still a pane row
## Testing and validation
### Unit tests
In `app/src/workspace/view/vertical_tabs_tests.rs`:
- add tests for `pane_ids_for_display_granularity(...)`
- `Panes` returns all visible panes in order
- `Tabs` returns the focused pane when present
- `Tabs` falls back to the first visible pane when the focused pane is absent
- empty visible list returns empty
In `app/src/workspace/action_tests.rs`:
- add a test that `SetVerticalTabsDisplayGranularity(...)` does not save workspace state
### Manual validation
- verify popup layout now shows `View as` first and `Density` above the existing compact / expanded toggle
- verify `Panes` remains default and current behavior is unchanged
- verify a multi-pane tab shows one representative row in `Tabs` mode
- verify switching focus within a split tab changes the representative row
- verify compact and expanded densities both work in `Tabs` mode
- verify existing `Pane title as`, `Additional metadata`, and `Show` preferences remain visible in the popup in `Tabs` mode and still affect the representative row as expected
- verify search in `Tabs` mode only matches the representative row, not hidden non-active panes
- verify the setting persists across relaunch
## Follow-ups
- Rename `VerticalTabsViewMode` to something density-specific in code if we want terminology to match the product UI more closely. This is not necessary for APP-3828.
- Add the future Tabs-only naming control (`Focused session` vs `Summary`) in a separate ticket once product behavior is finalized. At that point, pane-row-specific controls can become conditional on the focused-session path rather than always being shown for `Tabs`.