Files

21 KiB

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:2753TerminalView::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 asPanes vs Tabs
  • DensityCompact 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:

#[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:

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:

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

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