Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -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.
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user