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
+216
View File
@@ -0,0 +1,216 @@
# APP-3832: Vertical Tabs v2 — Hover Detail Sidecar
## Summary
Add a hover-activated detail sidecar to the vertical tabs panel that shows full, un-elided information for the currently hovered item without changing focus.
- When `View as = Panes`, hovering an eligible pane row shows a single pane-scoped sidecar for that pane.
- When `View as = Tabs`, hovering an eligible tab representative row shows a tab-scoped sidecar composed of one pane-scoped section per visible pane in that tab.
In this first iteration, the sidecar supports terminal / agent terminal panes, code panes, and supported Warp Drive object panes. It is hover-only; keyboard focus and selection do not open it.
## Problem
The vertical tabs panel intentionally compresses information so it stays scannable. That compression creates two gaps:
- even in `View as = Panes`, important metadata is clipped or omitted in the row itself
- in `View as = Tabs`, non-active panes disappear from the panel entirely, so the user cannot quickly inspect the rest of a split tab without focusing it
Users need a way to inspect full pane detail from the panel itself without changing focus, opening the tab, or losing the higher-level overview that `View as = Tabs` provides.
## Goals
- Show full, un-elided detail for the currently hovered vertical-tabs item.
- Preserve the user’s current focus; hover detail must not activate tabs or panes just by appearing.
- Make `View as = Tabs` inspectable by exposing all visible panes in the hovered tab.
- Reuse a single pane-scoped detail pattern in both modes so the UI feels consistent.
- Keep the vertical tabs panel layout stable; the detail view should not resize the panel or the workspace.
## Non-goals
- Opening the sidecar from keyboard focus, keyboard navigation, or selection state.
- Adding support for the remaining pane types that still do not render a sidecar in this iteration.
- Changing the existing `View as`, `Density`, `Pane title as`, `Additional metadata`, or `Show` settings.
- Adding new tab naming or tab summarization behavior.
- Turning the sidecar into a general-purpose inspector with editable controls.
## Figma / design references
- Tabs-scoped sidecar mock: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7633-130645&t=LSuxL7FNk3EXOfvJ-0
- Pane-scoped sidecar mock: https://www.figma.com/design/n5d1rK2dMGKqf97XDBN1BV/Agents-mgmt?node-id=7029-53964&m=dev
There is no separate code-pane mock. In this iteration, code panes reuse the same pane-scoped sidecar shell and follow the code-specific content rules defined below.
## User experience
### General behavior
- The detail sidecar is a floating overlay anchored to the right side of the vertical tabs panel.
- It does not resize the vertical tabs panel or the main workspace content.
- Only one detail sidecar is shown at a time.
- The sidecar opens only from pointer hover over an eligible item.
- Keyboard focus, selected state, and programmatic pane activation do not open the sidecar on their own.
- Hovering a different eligible item updates the sidecar to the newly hovered item.
- Moving the pointer from the hovered item into the sidecar keeps the sidecar open; it must not flicker closed during that cursor transition.
- The sidecar closes when the pointer is no longer over either the source item or the sidecar.
### Eligibility
In this first iteration, the sidecar is supported for:
- plain terminal panes
- Oz agent terminal panes
- CLI agent terminal panes
- code panes
- notebooks and plans
- workflows
- environment variable collections
- rules
- MCP servers
The sidecar is not shown for the remaining pane types in this iteration.
### Panes mode
When `View as = Panes`, hovering an eligible pane row opens a pane-scoped sidecar for that exact pane.
- The sidecar contains a single pane-scoped section.
- There is no extra tab-level wrapper, heading, or pane-count summary.
- Hovering an unsupported pane type shows no sidecar.
### Tabs mode
When `View as = Tabs`, hovering an eligible tab representative row opens a tab-scoped sidecar for that tab.
- The sidecar contains one pane-scoped section per visible pane in the hovered tab.
- Sections appear in the same top-to-bottom pane order the tab uses internally; the sidecar must not reorder panes just because one is active.
- Each section uses the same pane-scoped layout as the single-pane sidecar from `View as = Panes`.
- The tab-scoped sidecar is purely a stacked collection of pane sections; it does not introduce a separate tab summary header in this iteration.
Because this first iteration only supports the pane types listed above, a tab-scoped sidecar is shown only when every visible pane in the hovered tab is one of those supported types. Tabs containing any other visible pane show no sidecar in this iteration.
### Sidecar shell and sizing
- The sidecar uses a fixed-width card-like container matching the Figma mocks.
- The sidecar has a bounded maximum height based on the available window height; it must not grow off-screen just because a hovered tab contains many panes.
- When the sidecar content exceeds that bounded height, the sidecar content area scrolls vertically within the card.
- The vertical tabs panel itself does not scroll as a side effect of interacting with the sidecar; overflow is handled inside the sidecar.
- Text inside the sidecar wraps as needed; it is not ellipsized just because the corresponding row in the panel is clipped.
- In tabs mode, pane sections are separated by dividers.
- In panes mode, the single pane section uses the same shell but without internal section dividers.
### Relationship to existing display settings
The sidecar is a fixed detail view, not a larger version of the row.
- `Density` does not change the sidecar layout.
- `Pane title as` does not reorder the sidecar fields.
- `Additional metadata` does not hide or swap sidecar fields.
- `Show` does not remove sidecar badges that are part of the fixed detail layout.
The sidecar’s job is to expose the pane’s full supported detail, even when the row is configured to prioritize different information.
### Terminal and agent pane sections
Terminal and agent panes use the pane-scoped layout shown in the mocks.
#### Content order
From top to bottom, a terminal / agent pane section shows:
1. An optional agent-status pill when the pane is an agent session and a conversation status is available (for example `Working` or `Done`).
2. The full working directory, if available.
3. A git-branch row with branch icon and full branch name, if available.
4. The full command / conversation text as the primary descriptive body text.
5. A metadata row containing the pane kind badge on the left and any supported badges on the right.
#### Content rules
- Plain terminal panes omit the status pill.
- Agent panes show the status pill above the directory / branch block.
- The command / conversation text uses the same identity precedence as the row’s terminal title logic, but without clipping:
- agent conversation title when present
- CLI agent title when present
- terminal title when present
- last completed command text when needed
- final fallback text such as `New session`
- The working directory and branch are shown as separate rows when available; missing values are omitted rather than replaced with placeholder text.
- The metadata row always includes the pane kind badge (`Terminal`, `Oz`, `Claude Code`, etc.).
- If diff stats are available, show the diff-stats badge in the metadata row.
- If a pull request link is available, show the PR badge in the metadata row.
#### Interaction rules
- The diff-stats badge keeps its current behavior: clicking it opens the code review flow for that pane.
- The PR badge keeps its current behavior: clicking it opens the PR link.
- Clicking empty space inside the pane section does not focus the pane or tab.
### Code pane sections
Code panes use the same pane-scoped shell, but with code-specific content.
#### Content order
From top to bottom, a code pane section shows:
1. The full active file name as the primary line.
2. The full parent directory / path for that file.
3. An optional additional line when the pane has multiple open files, using the same underlying multi-file summary concept as the vertical-tabs row (for example `and N more` when relevant).
4. A metadata row containing the `Code` kind badge and any supported dirty-state indication.
#### Content rules
- Code pane text is never intentionally ellipsized inside the sidecar; wrap instead.
- If the pane has unsaved changes, surface that dirty state in the section using the existing unsaved indicator treatment.
- Do not show clean-state filler text such as `No unsaved changes`.
- Terminal-specific content such as status pills, git branch, diff stats, and PR badges is not shown for code panes in this iteration.
### Selection and focus behavior
- Hovering an item opens the sidecar without changing which pane or tab is focused.
- Clicking the original row keeps its existing behavior; the presence of the sidecar does not change row activation semantics.
- In tabs mode, the sidecar may show sections for non-focused panes in the hovered tab, but it does not change which pane is active.
### Empty and unsupported states
- If a supported pane is missing some metadata, omit the missing row rather than showing placeholder copy.
- If the hovered item is unsupported, show no sidecar.
- If a hovered tab in `View as = Tabs` contains any unsupported visible pane, show no sidecar for that tab in this iteration.
## Success criteria
1. Hovering an eligible item in the vertical tabs panel shows a floating detail sidecar without changing focus.
2. The sidecar is hover-only; keyboard focus or selection alone does not open it.
3. In `View as = Panes`, hovering an eligible pane row shows exactly one pane-scoped section.
4. In `View as = Tabs`, hovering an eligible representative row shows one pane-scoped section per visible pane in that tab.
5. The tab-scoped sidecar preserves the tab’s pane order rather than reordering sections around the active pane.
6. The sidecar stays open while the pointer moves from the source item into the sidecar and closes only after the pointer leaves both regions.
7. Terminal and agent pane sections show full, un-elided directory, branch, command / conversation text, and metadata badges when those fields exist.
8. Agent pane sections show a status pill when conversation status is available; plain terminal sections do not.
9. Code pane sections show full file and path information, plus dirty-state information when applicable, without terminal-specific metadata.
10. Supported Warp Drive object panes show their full title and pane-kind metadata in the sidecar.
11. `Density`, `Pane title as`, `Additional metadata`, and `Show` do not rearrange or remove the sidecar’s fixed detail layout.
12. Hovering an unsupported pane type shows no sidecar.
13. In `View as = Tabs`, a tab containing any unsupported visible pane shows no sidecar in this iteration.
14. Clicking the diff-stats or PR badge inside a terminal / agent sidecar section preserves the badge’s existing action.
15. The sidecar never resizes the vertical tabs panel or main workspace content.
16. When a tab-scoped sidecar is taller than the available window space, the sidecar remains bounded in height and becomes internally scrollable instead of extending off-screen.
## Validation
- **Panes mode / terminal**: Hover a plain terminal row and verify a single pane-scoped sidecar appears with full working directory, branch, command text, kind badge, and any available diff / PR badges.
- **Panes mode / agent**: Hover an Oz or CLI agent row and verify the sidecar shows the status pill, full conversation text, and terminal metadata without clipping.
- **Panes mode / code**: Hover a code row and verify the sidecar shows the full filename and path, plus dirty-state indication when applicable.
- **Panes mode / Warp Drive object**: Hover a supported notebook, plan, workflow, environment-variable collection, rule, or MCP server row and verify the sidecar shows the full title with the correct kind badge.
- **Tabs mode / multi-pane tab**: Hover a tab representative row for a tab with multiple supported panes and verify the sidecar shows one section per visible pane in the same pane order as the tab.
- **Large multi-pane tab**: Hover a representative row for a tab with enough supported panes to exceed the available vertical space and verify the sidecar stays bounded and scrolls internally.
- **Focus preservation**: Hover items and verify the currently focused pane does not change until the user explicitly clicks a row or an existing interactive badge.
- **Cursor transition**: Move the pointer diagonally from a row into the sidecar and verify the sidecar does not flicker closed.
- **Unsupported panes**: Hover an unsupported pane type in `View as = Panes` and verify no sidecar appears.
- **Mixed-type tab**: Hover a tab in `View as = Tabs` that contains at least one unsupported visible pane and verify no sidecar appears.
- **Settings independence**: Change `Density`, `Pane title as`, `Additional metadata`, and `Show`, then hover the same item and verify the sidecar layout and field order remain fixed.
- **Badge actions**: In a terminal / agent sidecar, click the diff-stats badge and PR badge and verify they perform the same actions they do from the row.
## Open questions
None for this iteration.
+410
View File
@@ -0,0 +1,410 @@
# APP-3832: Tech Spec — Vertical Tabs Hover Detail Sidecar
## Problem
APP-3832 adds a hover-only detail sidecar to the vertical tabs panel.
The product behavior is intentionally specific:
- in `View as = Panes`, hovering a supported pane row shows one pane-scoped detail card
- in `View as = Tabs`, hovering a supported representative row shows a tab-scoped sidecar with one pane section per visible pane in that tab
- the sidecar is floating, does not change focus, stays open while the pointer moves from the row into the sidecar, and becomes internally scrollable when tall
- v1 only supports terminal / agent terminal panes and code panes
Technically, the current vertical tabs implementation has the row hover primitives we need, but it has no concept of:
- a row-anchored overlay other than the settings popup / tab menus
- ephemeral hover-detail state that persists across the gap between a row and a sidecar
- a fixed-detail renderer that is independent of the existing row display settings
- tabs-mode eligibility rules that depend on the full set of visible panes in a tab
The implementation should add that behavior without disturbing the existing row renderers, click behavior, or synced settings flow.
## Relevant code
- `specs/APP-3832/PRODUCT.md` — agreed user-facing behavior for the hover detail sidecar
- `specs/APP-3828/PRODUCT.md` — current `View as = Panes / Tabs` behavior that determines row granularity
- `specs/APP-3828/TECH.md` — existing implementation pattern for vertical-tabs display granularity
- `app/src/workspace/view/vertical_tabs.rs (246-344)` — `VerticalTabsPanelState`, row mouse-state ownership, and other panel-local UI state
- `app/src/workspace/view/vertical_tabs.rs (458-557)` — `matching_tab_indices`; useful context for the current pane-vs-tab granularity flow
- `app/src/workspace/view/vertical_tabs.rs (630-1045)` — `render_groups` and `render_tab_group`, where row lists are built for both `Panes` and `Tabs`
- `app/src/workspace/view/vertical_tabs.rs (1020-1277)` — `render_pane_row_element`, the current row hover / click wrapper
- `app/src/workspace/view/vertical_tabs.rs (1320-1710)` — `TypedPane`, `PaneProps::new`, code-pane title/path derivation, and badge helpers
- `app/src/workspace/view/vertical_tabs.rs (1858-2340)` — terminal metadata rendering helpers and badge interactions that the sidecar should reuse semantically
- `app/src/workspace/view.rs (17784-17839)` — workspace action handling for vertical-tabs settings
- `app/src/workspace/view.rs (19201-19399)` — `Workspace::render`, which already hosts workspace-root overlays like the settings popup and tab menus
- `app/src/safe_triangle.rs` — generic safe-triangle logic for keeping hover sidecars stable during diagonal cursor movement
- `app/src/menu.rs (1852-1860, 2171-2367)` — current safe-triangle integration for menus and submenus
- `app/src/terminal/profile_model_selector.rs (1040-1078)` — concrete pattern for reading a sidecar rect from the previous frame and feeding it into safe-triangle state
- `app/src/workspace/view/vertical_tabs_tests.rs` — current home for pure vertical-tabs helper tests
## Current state
### Panel-local state
`VerticalTabsPanelState` already owns:
- the panel scroll and resize handles
- per-tab-group hover state
- per-pane-row hover state
- per-pane badge hover state
- settings-popup hover state and popup visibility
It does not currently track:
- which row is the current detail-sidecar source
- whether a sidecar itself is hovered
- a scroll state for a sidecar
- safe-triangle state for row-to-sidecar transitions
### Row rendering and granularity
`render_tab_group` already centralizes the pane ids that become visible rows:
- `Panes` mode renders all visible pane ids
- `Tabs` mode renders one representative pane id via `pane_ids_for_display_granularity(...)`
Each rendered row already has a stable `MouseStateHandle`, but rows are not wrapped in `SavePosition`, so there is no stable anchor id for a row-relative overlay.
### Overlay placement
The vertical tabs panel itself is rendered inside `Workspace::render_panels`, but floating overlays that must escape local panel bounds are rendered from the workspace root `Stack` in `Workspace::render`. This is how the vertical-tabs settings popup and tab context menus are currently placed.
That makes the workspace root the right place to render the hover detail sidecar as well.
### Existing row data extraction
The current row renderers already know how to derive most of the underlying metadata we need:
- terminal title / conversation title fallback logic
- working directory and git branch
- diff-stats and PR badge data
- code-pane filename / parent-path split
- code dirty state
- pane kind labels and icons
However, the row renderers are the wrong level of reuse for the sidecar itself because:
- they are intentionally clipped and density-dependent
- they are coupled to `Pane title as`, `Additional metadata`, and `Show`
- the sidecar layout is fixed and should not rearrange around those settings
The sidecar should therefore reuse the same data sources, not the same element tree.
### Safe-triangle precedent
The repo already has a safe-triangle implementation for hover menus that open sidecars or submenus. The existing pattern is:
- keep ephemeral hover state outside of synced settings / workspace snapshotting
- record the sidecar rect from the previous frame
- suppress intermediate hover changes while the cursor moves diagonally toward that rect
That is the right interaction primitive for this feature.
## Proposed changes
### 1. Add panel-local detail-overlay state
Extend `VerticalTabsPanelState` with state for the hover detail overlay:
- `detail_scroll_state: ClippedScrollStateHandle` — internal scrolling for tall sidecars
- `detail_sidecar_mouse_state: MouseStateHandle` — hover tracking for the sidecar itself
- `detail_overlay_state: Arc<Mutex<VerticalTabsDetailOverlayState>>` — ephemeral hover-detail state shared by row and sidecar callbacks
Introduce a small internal state type in `vertical_tabs.rs`:
```rust path=null start=null
struct VerticalTabsDetailOverlayState {
active_target: Option<VerticalTabsDetailTarget>,
safe_triangle: SafeTriangle,
}
enum VerticalTabsDetailTarget {
Pane {
pane_group_id: EntityId,
pane_id: PaneId,
},
Tab {
pane_group_id: EntityId,
source_pane_id: PaneId,
},
}
```
Use panel-local ephemeral state rather than new synced settings or persisted workspace actions. The sidecar is transient UI state, more like menu hover than workspace configuration.
### 2. Add helpers for detail-sidecar eligibility and anchoring
Add pure helpers in `vertical_tabs.rs` for:
- deciding whether a `TypedPane` is supported by the v1 sidecar
- converting a hovered rendered row into a `VerticalTabsDetailTarget`
- resolving the pane ids that a target should render as sidecar sections
- producing a stable save-position id for a rendered row
Suggested helper shape:
```rust path=null start=null
fn vtab_pane_row_position_id(pane_group_id: EntityId, pane_id: PaneId) -> String
fn supports_vertical_tabs_detail_sidecar(typed: &TypedPane<'_>) -> bool
fn detail_target_for_hovered_row(
pane_group_id: EntityId,
pane_id: PaneId,
granularity: VerticalTabsDisplayGranularity,
) -> VerticalTabsDetailTarget
fn pane_ids_for_detail_target(
pane_group: &PaneGroup,
target: &VerticalTabsDetailTarget,
app: &AppContext,
) -> Option<Vec<PaneId>>
```
Behavior:
- `Panes` mode:
- supported pane row => pane-scoped target
- unsupported pane row => no sidecar
- `Tabs` mode:
- representative row => tab-scoped target
- resolve all visible panes in the hovered tab
- if any visible pane is unsupported, return `None` so the whole tab has no sidecar in v1
This keeps the mixed-tab gating rule centralized and testable.
### 3. Save row positions and attach hover callbacks at the row wrapper
Update `render_pane_row_element` so the final row element is wrapped in `SavePosition` using the new row-position helper.
Add a hover callback there rather than in individual compact / expanded row renderers. That keeps the hover-detail behavior shared across all row variants.
The row wrapper should:
- derive the appropriate `VerticalTabsDetailTarget` for that row
- update `detail_overlay_state.active_target` on supported hover-in
- ignore unsupported rows
- update safe-triangle state with the current pointer position
- use `with_skip_synthetic_hover_out()` so overlay insertion does not immediately force a synthetic close
This approach preserves the current row click / double-click behavior and avoids duplicating hover-detail wiring in multiple render paths.
### 4. Render the sidecar from `Workspace::render`, not inside the panel surface
Add a new helper in `vertical_tabs.rs`:
```rust path=null start=null
pub(super) fn render_detail_sidecar(
state: &VerticalTabsPanelState,
workspace: &Workspace,
app: &AppContext,
) -> Option<(String, Box<dyn Element>)>
```
This helper should:
- read `detail_overlay_state.active_target`
- validate that the referenced pane group / pane(s) still exist
- resolve the pane ids that should become sections
- build the sidecar element if the target is still valid
- return the source row position id plus the sidecar element
Render the returned sidecar from the workspace-root `Stack` in `Workspace::render`, alongside the existing settings popup and tab menus. This avoids clipping by the vertical-tabs panel container and matches the existing overlay pattern in this codepath.
Position the sidecar with `OffsetPositioning::offset_from_save_position_element(...)` using the hovered row’s save-position id and `PositionedElementOffsetBounds::WindowByPosition`.
Also give the sidecar its own `SavePosition` id so its rect can be read on subsequent frames for the safe triangle.
### 5. Build a dedicated detail renderer on top of existing metadata sources
Add a dedicated sidecar renderer instead of trying to stretch the existing row renderers.
Introduce a small data model for the detail content:
```rust path=null start=null
enum VerticalTabsDetailSectionData {
Terminal(TerminalDetailSectionData),
Code(CodeDetailSectionData),
}
```
Back it with helper builders that reuse the same underlying sources as the row code:
- terminal title / conversation fallback logic from the existing terminal helpers
- `TerminalView` for working directory, branch, diff stats, PR link, and agent status
- `PaneProps::new` / `TypedPane` for pane kind labels and code-pane title/path derivation
- `TypedPane::badge(app)` / `CodePane` dirty checks for unsaved state
Do not make the sidecar renderer depend on:
- `VerticalTabsViewMode`
- `VerticalTabsPrimaryInfo`
- `VerticalTabsCompactSubtitle`
- `vertical_tabs_show_*`
Those settings shape rows; the sidecar is a fixed detail view.
### 6. Sidecar layout and scrolling
Render the sidecar as:
- fixed-width outer card
- bounded-height container
- internal scrollable content using `ClippedScrollable::vertical(...)` or `NewScrollable::vertical(...)`
- overlayed scrollbar styling consistent with existing panel/menu scrollables
In `Tabs` mode:
- render one section per resolved pane id
- insert dividers between sections
In `Panes` mode:
- render a single section with no internal divider treatment
Use the workspace window bounds to keep the sidecar on-screen. `WindowByPosition` plus a bounded max height is sufficient for v1; there is no need for a separate left/right flip behavior because the vertical tabs panel already lives on the left side of the workspace.
### 7. Keep the sidecar open across row-to-sidecar cursor movement
Reuse the existing safe-triangle pattern rather than inventing a new hover heuristic.
Implementation approach:
- the sidecar root is wrapped in `Hoverable::new(detail_sidecar_mouse_state, ...)`
- on row hover changes, update the `SafeTriangle` with the latest pointer position
- on subsequent hover events, suppress replacing or clearing the active target while the cursor is moving through the safe triangle toward the current sidecar
- like `ProfileModelSelector`, read the sidecar rect from the previous frame and feed it back into `SafeTriangle::set_target_rect(...)`
This keeps the interaction logic close to the existing menu/sidecar behavior already used elsewhere in the app.
### 8. Do not add new workspace actions unless implementation proves they are needed
The initial design should keep the hover-detail state entirely inside `VerticalTabsPanelState` via shared ephemeral state handles.
That avoids:
- new `WorkspaceAction` variants for non-persistent hover state
- extra `handle_action` branches in `Workspace::handle_action`
- confusion about whether the sidecar is part of saved workspace state
If the actual UI framework constraints force action-based updates later, that can be introduced during implementation, but it should not be the default plan.
## End-to-end flow
1. The user hovers a rendered vertical-tabs row.
2. The row wrapper in `render_pane_row_element` derives a `VerticalTabsDetailTarget` from:
- the row’s pane id
- the row’s pane group id
- the current `VerticalTabsDisplayGranularity`
3. If the hovered row is unsupported, nothing happens.
4. If the row is supported, the row hover callback stores the target in `detail_overlay_state.active_target`.
5. On the next render, `Workspace::render` asks `render_detail_sidecar(...)` for an overlay.
6. That helper:
- validates the target
- resolves the pane ids to show
- rejects mixed-support tabs in `Tabs` mode
- builds the sidecar sections from the current pane data
7. The workspace root `Stack` positions the sidecar to the right of the hovered row using the row’s save-position id.
8. The user moves the pointer from the row into the sidecar.
9. The safe triangle suppresses intermediate hover changes, so the sidecar stays open.
10. If the sidecar becomes tall, its content scrolls internally.
11. If the pointer leaves both the source row and the sidecar, the overlay state is cleared and the sidecar disappears.
## Risks and mitigations
### Overlay-induced synthetic hover churn
Risk:
- inserting a floating overlay can trigger synthetic hover-out events on the source row, causing flicker
Mitigation:
- attach hover handling at the row wrapper
- use `with_skip_synthetic_hover_out()`
- keep the last active target in ephemeral overlay state instead of deriving visibility strictly from the current row hover bit
### Mixed-support tabs in `Tabs` mode
Risk:
- the product rule for mixed tabs is easy to accidentally implement as “render only supported panes”
Mitigation:
- centralize the rule in `pane_ids_for_detail_target(...)`
- make the helper return `None` for any tab that includes unsupported visible panes
- add explicit tests for mixed tabs
### Stale hover target after close / focus / reorder changes
Risk:
- the hovered source pane or tab may disappear while the sidecar is open
Mitigation:
- validate the target on every render before building the sidecar
- clear the target if the pane group or source pane no longer exists
- resolve tab sections from live `visible_pane_ids()` each render rather than caching pane lists in the target
### Duplication of terminal / code metadata logic
Risk:
- the sidecar could fork the row metadata logic and drift over time
Mitigation:
- reuse existing helper functions and model reads wherever possible
- keep new data-extraction helpers in `vertical_tabs.rs`, close to the row rendering code they parallel
- avoid re-encoding fallback rules in multiple places
### Root-overlay vs panel-overlay confusion
Risk:
- `render_vertical_tabs_panel` already contains local overlay structure for the settings popup, while `Workspace::render` also renders the popup globally
Mitigation:
- treat `Workspace::render` as the source of truth for the new sidecar overlay
- keep the sidecar rooted at the workspace stack from the start
- do not introduce another panel-local overlay path for this feature
## Testing and validation
### Unit tests
Add pure tests in `app/src/workspace/view/vertical_tabs_tests.rs` for:
- `supports_vertical_tabs_detail_sidecar(...)`
- terminal panes supported
- code panes supported
- unsupported pane types rejected
- `pane_ids_for_detail_target(...)`
- panes mode returns just the hovered pane
- tabs mode returns all visible panes when every pane is supported
- tabs mode returns `None` when any visible pane is unsupported
- stale / missing source pane returns `None`
If needed, add small pure tests around row-position-id helpers or target resolution helpers, but prioritize the eligibility logic above.
### Manual validation
- hover a supported terminal row in `Panes` mode and verify a single pane-scoped sidecar appears
- hover a supported code row in `Panes` mode and verify a single code sidecar appears
- hover a representative row in `Tabs` mode for a tab with several supported panes and verify one section per visible pane
- hover a representative row in `Tabs` mode for a mixed-support tab and verify no sidecar appears
- move the cursor diagonally from the row into the sidecar and verify it does not flicker closed
- click diff-stats and PR badges inside the sidecar and verify the existing actions still fire
- hover a large multi-pane tab and verify the sidecar becomes internally scrollable rather than extending off-screen
- switch `Density`, `Pane title as`, `Additional metadata`, and `Show`, then verify the sidecar layout stays fixed
- close or mutate the hovered tab while the sidecar is open and verify the overlay disappears cleanly without stale content
## Follow-ups
- Support additional pane types once product behavior is defined for their detail sections.
- Add keyboard-accessible detail affordances in a follow-up ticket if the hover-only behavior proves valuable.
- If more hover sidecars are added elsewhere in Warp, consider extracting a reusable “row + sidecar + safe triangle” helper instead of keeping the logic local to vertical tabs.