Files
galaxy/specs/APP-3875/TECH.md
T

476 lines
21 KiB
Markdown

# APP-3875: Tech Spec — Summary Tab Item Mode for Vertical Tabs
## Problem
APP-3875 adds a second tab-level representation for vertical tabs when `View as = Tabs`.
Today, Tabs mode is implemented by reusing the existing pane-row UI for the tab's focused pane. That keeps the implementation simple, but it makes the tab item fundamentally pane-scoped:
- render logic is driven by a single representative `PaneId`
- search logic is driven by the same representative pane's search fragments
- diff stats and PR chips are rendered as pane-level terminal metadata
The new `Summary` mode needs a different unit of representation:
- the card must summarize the whole tab, not one pane
- the lower rows are branch lines, not pane previews
- search needs to match hidden summary content, not just visible rows
- the popup must hide the focused-session controls when Summary is selected, without losing the underlying settings
Technically, this means we need to introduce a Tabs-only item mode and a tab-level aggregation model while preserving the existing focused-session behavior and reusing as much of the current selection / hover / rename / drag infrastructure as possible.
## Relevant code
- `specs/APP-3875/PRODUCT.md` — agreed user-facing behavior for Summary mode
- `specs/APP-3828/PRODUCT.md` — current `View as = Tabs` focused-session behavior this feature extends
- `app/src/workspace/tab_settings.rs (171-279)` — current synced vertical-tabs settings (`VerticalTabsViewMode`, `VerticalTabsDisplayGranularity`, `VerticalTabsPrimaryInfo`, `VerticalTabsCompactSubtitle`)
- `app/src/workspace/action.rs (234-241)` — existing vertical-tabs setting actions
- `app/src/workspace/action.rs (624-848)``should_save_app_state_on_action` coverage for vertical-tabs popup actions
- `app/src/workspace/action_tests.rs (1-50)` — current tests for vertical-tabs action persistence behavior
- `app/src/workspace/view.rs (18224-18337)` — workspace-side action handling for the vertical-tabs popup settings
- `app/src/workspace/view/vertical_tabs.rs (231-327)` — shared row wrapper behavior (`render_pane_row_element`) for click, hover, selection styling, and inline rename
- `app/src/workspace/view/vertical_tabs.rs (428-669)``VerticalTabsPanelState`, popup-local mouse state, and `matching_tab_indices`
- `app/src/workspace/view/vertical_tabs.rs (1048-1314)` — main vertical-tabs render path (`render_groups`, `render_tab_group`), including the current flat Tabs-mode list
- `app/src/workspace/view/vertical_tabs.rs (1982-2180)``PaneProps`, search-fragment generation, and terminal primary-line helpers
- `app/src/workspace/view/vertical_tabs.rs (2155-2781)` — terminal metadata helpers and current clickable diff/PR badge rendering
- `app/src/workspace/view/vertical_tabs.rs (2398-2492)``render_title_override` and inline rename editor behavior
- `app/src/workspace/view/vertical_tabs.rs (2890-3319)``render_settings_popup`
- `app/src/workspace/view/vertical_tabs.rs (4087-4369)` — compact row rendering and the current vertical-tabs unit-test module hookup
- `app/src/workspace/view/vertical_tabs_tests.rs (1-259)` — existing pure helper tests for vertical-tabs behavior
- `app/src/terminal/view/tab_metadata.rs (24-119)` — terminal metadata accessors used by the current focused-session rows (`display_working_directory`, `current_git_branch`, `current_pull_request_url`, `current_diff_line_changes`)
- `app/src/terminal/view.rs:2753``TerminalView::current_repo_path`, needed to distinguish same-named branches across repositories
## Current state
### Settings and popup model
The vertical-tabs popup currently has two orthogonal axes:
- `View as``Panes` vs `Tabs`
- `Density``Compact` vs `Expanded`
It also exposes focused-pane controls:
- `Pane title as`
- `Additional metadata` (compact only)
- `Show` (expanded only)
There is no Tabs-only concept of "how should a tab item be represented?" The popup assumes that every rendered item is still a pane row.
### Tabs mode is already a flat list
Current `View as = Tabs` behavior is implemented as a flat list, not a grouped header + body UI:
- `uses_outer_group_container` returns `false` for `VerticalTabsDisplayGranularity::Tabs`
- `render_groups` adds spacing between flat tab items in Tabs mode
- `render_tab_group` skips the outer group header/container path when `uses_outer_group_container` is false
This matches the mock and is the right foundation for Summary mode. We do not need to invent a new container hierarchy for this feature.
### Rendering and search are pane-derived
The current Tabs-mode implementation still relies on a representative pane:
- `pane_ids_for_display_granularity(...)` returns one representative `PaneId` in Tabs mode
- `matching_tab_indices(...)` searches by building `PaneProps` for that representative pane
- the search branch in `render_groups(...)` also matches representative-pane `PaneProps`
- `render_tab_group(...)` iterates the selected pane IDs and renders either `render_compact_pane_row(...)` or `render_pane_row(...)`
This means render and search both inherit the limitations of pane-scoped data.
### Pane metadata is available in the right places, but not yet aggregated
The required summary inputs already exist in the codebase, mostly on `TerminalView`:
- work-label inputs:
- conversation display title
- CLI agent display title
- terminal title
- last completed command
- directory input:
- `display_working_directory(...)`
- branch metadata:
- `current_git_branch(...)`
- `current_diff_line_changes(...)`
- `current_pull_request_url(...)`
- `current_repo_path(...)`
However, these values are only consumed today for a single pane at a time. There is no tab-level aggregation model, no stable dedupe / coalescing logic, and no summary-specific search fragments.
### Diff and PR badges are interactive today
The existing helpers:
- `render_terminal_diff_stats_badge(...)`
- `render_terminal_pull_request_badge(...)`
wrap the visual chip content in `Hoverable` and dispatch actions on click. That matches focused-session rows, but it does not match Summary mode, where branch lines and chips are informational only.
## Proposed changes
### 1. Add a synced Tabs-only item-mode setting
Add a new enum in `app/src/workspace/tab_settings.rs`:
```rust
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, PartialEq, Copy, Clone)]
pub enum VerticalTabsTabItemMode {
#[default]
FocusedSession,
Summary,
}
```
Register it in `TabSettings` with the same sync behavior as the existing vertical-tabs popup settings:
- `SupportedPlatforms::ALL`
- `SyncToCloud::Globally(RespectUserSyncSetting::Yes)`
- `hierarchy: "appearance.tabs"`
This setting is only used when `vertical_tabs_display_granularity == Tabs`, but it should still be persisted independently so Warp can restore the user's last chosen Tabs-mode representation.
### 2. Add a workspace action for the new setting
Add `WorkspaceAction::SetVerticalTabsTabItemMode(VerticalTabsTabItemMode)` beside the existing vertical-tabs popup actions.
Handle it in `Workspace::handle_action` exactly like the other vertical-tabs setting writes:
- write to `settings.vertical_tabs_tab_item_mode`
- call `ctx.notify()`
Update `should_save_app_state_on_action` and `action_tests.rs` so the new action is explicitly marked as not requiring workspace-state persistence.
### 3. Extend popup-local state and restructure `render_settings_popup`
Add two mouse states to `VerticalTabsPanelState`:
- `focused_session_option_mouse_state`
- `summary_option_mouse_state`
Keep the existing density and focused-session option mouse states unchanged so Summary mode can temporarily hide, rather than replace, those controls.
Update `render_settings_popup(...)` so the popup hierarchy becomes:
1. `View as`
2. `Tab item` (only when `View as = Tabs`)
3. `Density` and the focused-session controls (only when `View as = Tabs && Tab item = FocusedSession`, or when `View as = Panes`)
Concretely:
- if `View as = Panes`, do not render `Tab item`
- if `View as = Tabs`, render a `Tab item` section directly under `View as`
- if `Tab item = Summary`, do not render:
- `Density`
- `Pane title as`
- `Additional metadata`
- `Show`
This preserves the stored values of those controls while making the Summary-mode popup match the product behavior.
### 4. Introduce a tab-level Summary aggregation model
Add a new pure aggregation layer in `vertical_tabs.rs` for Summary mode rather than pushing summary derivation directly into the renderer.
Suggested data shape:
```rust
struct VerticalTabsSummaryData {
primary_labels: Vec<String>,
working_directories: Vec<String>,
branch_entries: Vec<VerticalTabsSummaryBranchEntry>,
}
struct VerticalTabsSummaryBranchEntry {
repo_path: PathBuf,
branch_name: String,
diff_stats: Option<GitLineChanges>,
pull_request_label: Option<String>,
}
```
The exact field names can vary, but the design should keep two properties:
1. the full aggregated data is available for search, even when some items are visually hidden behind overflow
2. ordering is explicit and stable; do not rely on `HashMap` iteration order for rendering
Implementation guidance:
- preserve first-seen order with `Vec` plus a set / index map keyed by normalized value
- use one aggregation pass over the tab's `visible_pane_ids()`
- build Summary data from the full tab, not just the representative pane
Summary mode also needs a small tab-level icon selection helper:
- map each visible pane to a stable pane-kind enum used only for Summary icon rendering
- distinguish terminal-backed agent sessions from plain terminals so Oz/ambient-agent and CLI-agent terminal panes render their semantic agent icons rather than the generic terminal icon
- sort candidates by pane creation order, using the pane view `EntityId` as the current stable creation-order key
- render `Single(kind)` if all visible panes share the same kind
- render `Pair { primary, secondary }` for heterogeneous tabs, where `primary` is the oldest pane kind and `secondary` is the second-oldest distinct pane kind
- position the secondary icon using the same bottom-right anchor, offset, and cutout-ring sizing as the existing agent/status composite icon
- recompute from the current visible panes each render so closing the oldest pane naturally changes the selected icons
### 5. Reuse existing pane-derived helpers instead of re-implementing label logic
Summary mode should reuse the same data precedence rules the focused-session code already uses wherever possible.
For work labels:
- reuse `terminal_primary_line_data(...)` for terminal panes
- reuse `PaneProps::new(...)` / `displayed_title()` for non-terminal panes where practical
For working-directory contributions:
- terminal panes use `TerminalView::display_working_directory(...)`
- code panes can contribute the already-derived parent-directory subtitle that `PaneProps::new(...)` computes
- other panes should only contribute if they already expose a meaningful directory-like value; otherwise they contribute nothing
This avoids creating a second set of heuristics that drift from the current row behavior.
### 6. Coalesce branch lines by repository + branch
Summary branch lines must be keyed by repository + branch, not branch name alone.
Implementation plan:
- only panes that can expose both `current_repo_path()` and `current_git_branch()` contribute a branch line
- use `(repo_path, branch_name)` as the grouping key
- preserve first-seen order by recording the first time each key appears while scanning visible panes
- store diff stats and PR label once per coalesced entry
This keeps same-named branches in different repositories distinct and matches the product spec's branch-line behavior.
For v1, if multiple panes in the same coalesced group expose the same logical metadata, simply keep the first observed non-empty values for:
- diff stats
- PR label
That is sufficient because the product semantics treat these as logically branch-scoped. We do not need a more complex merge strategy.
### 7. Add summary-specific normalization and search helpers
Add pure helpers for:
- work-label normalization
- work-label deduplication with first-seen display preservation
- summary search-fragment generation
Search fragments for Summary mode should include the full, untruncated data set:
- all work labels
- all working directories
- all branch names
- all PR labels
- diff-stat text for all coalesced branch entries
This is intentionally different from the render path, which only shows a prefix and then `+ N more`.
### 8. Keep row-level interaction, selection, hover, and rename by reusing the existing wrapper
Do not build Summary mode as an entirely separate interaction surface.
Instead, keep a representative `PaneProps` for the tab's focused pane and use it to drive the existing shared row wrapper:
- `render_pane_row_element(...)` for click, hover, selection background, and detail-sidecar targeting
- `render_title_override(...)` for custom tab title and inline rename editor behavior
The Summary renderer should therefore have the shape:
- build `SummaryData` from the full tab
- build representative `PaneProps` from the focused pane
- render Summary content inside the existing wrapper using the representative `PaneProps`
This gives Summary mode the current Tabs-mode behavior "for free":
- click focuses the tab's active pane
- hover uses the existing tab-level styling
- double-click rename continues to work in the same place
- detail-sidecar hover targeting remains tab-scoped via `VerticalTabsDetailTarget::Tab`
### 9. Add a dedicated Summary renderer
Add a new renderer in `vertical_tabs.rs`, for example:
```rust
fn render_summary_tab_item(
representative_props: PaneProps<'_>,
summary: &VerticalTabsSummaryData,
app: &AppContext,
) -> Box<dyn Element>
```
This renderer should:
- render a fixed expanded-style card
- render a Summary-specific pane-kind icon rather than the focused pane's icon
- render the primary line, working-directory line, visible branch lines, and optional overflow line
- cap visible work labels and visible branch entries independently
- omit empty sections instead of rendering placeholders
Because Summary mode is informational-only at the branch level, the branch lines should not introduce nested click handlers or badge hover handlers.
### 10. Split badge visuals from badge interactivity
Refactor the current terminal diff / PR badge helpers so the visual badge content can be reused without the focused-session click behavior.
Suggested direction:
- keep existing interactive helpers for focused-session rows
- extract passive visual helpers for:
- diff-stats badge content
- PR badge content
- Summary mode uses the passive versions in branch lines
This avoids duplicating the chip visuals while keeping Summary mode intentionally non-interactive.
### 11. Branch render and search logic based on mode, not only granularity
Current render and search logic branch only on `VerticalTabsDisplayGranularity`.
Update the Tabs-mode path so it branches on both:
- display granularity
- tab item mode
Suggested structure:
- `Panes` → existing pane-based render and search
- `Tabs + FocusedSession` → existing representative-pane render and search
- `Tabs + Summary` → summary render and summary search
This can be implemented with a small resolved-mode helper rather than scattering nested conditionals throughout the file.
### 12. Add pure tests for the new aggregation behavior
Extend `vertical_tabs_tests.rs` with pure helper tests covering:
- work-label normalization
- exact-equivalent dedupe while preserving first-seen display text
- working-directory dedupe and stable ordering
- branch coalescing by repository + branch
- same branch name in different repos stays distinct
- overflow-count behavior for primary labels and branch lines
- summary search fragments include hidden-overflow values
Keep these helpers pure so they do not require a full `AppContext` or UI harness.
### 13. Keep focused-session behavior untouched
Do not refactor the existing focused-session row renderers beyond what is needed to:
- add the new setting/action
- hide irrelevant popup sections in Summary mode
- share badge visuals where necessary
Focused-session Tabs mode is already shipped behavior from APP-3828. The safest implementation is to keep that path intact and add Summary as a parallel path.
## End-to-end flow
```mermaid
flowchart TD
A[User opens vertical-tabs popup] --> B[Selects View as = Tabs]
B --> C[Selects Tab item = Summary]
C --> D[WorkspaceAction::SetVerticalTabsTabItemMode]
D --> E[TabSettings.vertical_tabs_tab_item_mode updated]
E --> F[render_settings_popup hides Density and pane-row controls]
E --> G[render_groups resolves Tabs + Summary mode]
G --> H[Aggregate full-tab SummaryData from visible panes]
H --> I[Build representative PaneProps from focused pane]
I --> J[render_summary_tab_item inside existing row wrapper]
H --> K[summary_search_fragments]
K --> L[matching_tab_indices and render_groups search path]
```
At runtime, Summary mode therefore follows the same top-level control flow as the current popup settings, but diverges at render/search time into a tab-level aggregation path.
## Risks and mitigations
### Risk: render and search drift apart
If Summary render data and Summary search fragments are built in separate ad hoc code paths, the panel will render one thing and search another.
Mitigation:
- use one `VerticalTabsSummaryData` aggregation path
- derive both render output and search fragments from that shared data
### Risk: same-named branches collapse incorrectly
If branch grouping keys only use the branch name, two different repos on `main` will merge incorrectly.
Mitigation:
- group by `(repo_path, branch_name)`
- add a unit test for two repos on `main`
### Risk: custom tab rename behavior regresses in Summary mode
Current Tabs-mode rows support inline rename because the row wrapper and `render_title_override(...)` are wired to tab-level rename state.
Mitigation:
- keep representative `PaneProps`
- route Summary primary-line rendering through the same title-override / rename-editor path
### Risk: Summary data is unavailable for some non-terminal panes
Branch metadata is readily available on `TerminalView`, but not every pane type exposes repo / branch context today.
Mitigation:
- let non-terminal panes contribute work labels and directories where available
- omit branch contributions from panes that do not expose branch context
- render sparse summary cards without placeholder text
### Risk: passive Summary chips drift visually from focused-session chips
If Summary mode re-implements diff/PR chips from scratch, the appearance can drift from the current vertical-tabs styling.
Mitigation:
- extract shared visual badge helpers
- keep only the event-handling wrapper different between focused-session and Summary mode
## Testing and validation
### Unit tests
- `app/src/workspace/action_tests.rs`
- add coverage for `SetVerticalTabsTabItemMode(...)`
- `app/src/workspace/view/vertical_tabs_tests.rs`
- label normalization and dedupe
- directory dedupe and stable ordering
- branch coalescing by repo + branch
- same branch name in different repos
- overflow counting
- summary search fragments include hidden entries
- Summary pane-kind icon selection for homogeneous tabs, heterogeneous tabs, and oldest-pane removal
### Manual / UI validation
- verify `Tab item` appears only when `View as = Tabs`
- verify `Summary` hides `Density`, `Pane title as`, `Additional metadata`, and `Show`
- verify switching back to `Focused session` restores the prior focused-session settings
- verify a multi-pane tab renders a summary card instead of a focused-pane row
- verify homogeneous Summary tabs render one pane-kind icon
- verify heterogeneous Summary tabs render the two oldest distinct pane-kind icons with the secondary icon in the bottom-right
- verify branch lines are grouped by repo + branch and carry diff stats / PR chips
- verify branch lines and chips are not independently clickable
- verify clicking the card still focuses the active pane
- verify search matches hidden overflow data
- verify double-click rename still works in Summary mode
### Broader validation
When implementation lands, run the same validation expected for other vertical-tabs work:
- targeted unit tests
- relevant workspace / vertical-tabs test suites
- visual verification of the new Summary mode in a real app session
## Follow-ups
- Add interactive branch-line affordances (for example, opening code review from a branch line) if product later wants child-level actions
- Consider a compact Summary representation if product later decides Summary should coexist with `Density`
- Expand branch-context contribution beyond terminal panes if more pane types gain stable repo / branch metadata
- Consolidate focused-session and Summary badge rendering around a single shared visual badge API once both paths exist