12 KiB
APP-3651: Tech Spec — Vertical Tabs Pane Row Layout Iteration
Problem
The vertical tabs panel pane rows need restructuring to improve information hierarchy (see specs/APP-3651/PRODUCT.md). The primary rendering functions for terminal and non-terminal pane rows, the tab group header collapse button, and text clipping direction all need to change within the same file.
Relevant code
app/src/workspace/view/vertical_tabs.rs— all pane row rendering;VerticalTabsPanelState,PaneProps,TypedPane,render_pane_row,render_terminal_row_content,render_terminal_primary_line,render_terminal_secondary_line,render_group_header,render_kind_badgeapp/src/terminal/view/tab_metadata.rs—terminal_title_from_shell(),display_working_directory(),current_git_branch()app/src/terminal/view/pane_impl.rs:962-973—selected_conversation_status(),selected_conversation_display_title(),is_ambient_agent_session()app/src/terminal/model/blocks.rs:1708—BlockList::blocks()returns&Vec<Block>app/src/terminal/model/block.rs:2143—Block::command_to_string(),Block::finished(),BlockStateapp/src/code/view.rs:222-231—CodeView::tab_group(private),CodeView::set_title(sets PaneConfiguration title/secondary)app/src/code/icon.rs:11—icon_from_file_path(path, appearance) -> Option<Box<dyn Element>>app/src/pane_group/pane/code_pane.rs:53—CodePane::file_view()returnsViewHandle<CodeView>app/src/workspace/action.rs:99,227-230—WorkspaceAction::CloseTab,ToggleVerticalTabsGroupCollapsed,ToggleAllVerticalTabsGroupsCollapsedui/src/text_layout.rs:455-460—ClipConfig::start()already exists
Current state
Terminal pane rows
render_terminal_row_content builds three lines:
- Primary (
render_terminal_primary_line): working directory + git branch, main text color - Secondary (
render_terminal_secondary_line): conversation title/status, or terminal title if it differs from working directory; sub text color. ReturnsNoneif terminal title matches working directory. - Tertiary (
render_terminal_tertiary_line): kind badge (Terminal/Oz icon + label) + right badges (diff stats, PR)
Non-terminal pane rows
render_pane_row builds the content in the else branch (line 739-774):
- Title row (main text,
ClipConfig::end()) - Optional subtitle row (sub text)
- Meta row:
render_kind_badge(icon, kind_label)on left, optionalrender_row_badge(badge)on right
Tab group headers
render_group_header renders a collapse chevron (ChevronDown/ChevronRight) that dispatches WorkspaceAction::ToggleVerticalTabsGroupCollapsed. The is_collapsed state in VerticalTabsPanelState::collapsed_tab_groups controls whether pane rows are rendered.
ClipConfig
All text currently uses ClipConfig::end(). ClipConfig::start() exists and fades from the leading edge.
Last completed command
TerminalModel → BlockList::blocks() → Vec<Block>. Each Block has command_to_string() -> String and state (public via finished()). There is no existing accessor on TerminalView for the last completed command. The model is behind self.model.lock() on TerminalView.
Code pane tab count
CodeView::tab_group is a private Vec<TabData>. No public accessor for tab_group.len() exists. CodeView::set_title already sets secondary title to (+N) format when tab_group.len() > 1.
Proposed changes
1. Add last_completed_command_text to TerminalView
app/src/terminal/view/tab_metadata.rs — add a new method:
pub fn last_completed_command_text(&self) -> Option<String> {
let model = self.model.lock();
model.block_list().blocks().iter().rev().find_map(|block| {
if block.finished()
&& !block.is_background()
&& !block.is_static()
{
let cmd = block.command_to_string();
if cmd.trim().is_empty() { None } else { Some(cmd) }
} else {
None
}
})
}
This iterates backwards through blocks, finding the last finished non-background, non-static block with a non-empty command. The lock scope is contained to this method.
2. Add tab_count to CodeView
app/src/code/view.rs — add a one-line public method on CodeView:
pub fn tab_count(&self) -> usize {
self.tab_group.len()
}
3. Restructure render_terminal_row_content
app/src/workspace/view/vertical_tabs.rs — rewrite render_terminal_row_content (line 969):
The new structure:
-
Primary line: Determine content using the precedence rules from the product spec:
- If
conversation_display_titleisSome: render conversation status + title (reuse existing status element rendering) - Else if
terminal_title.trim() != working_directory.trim(): render terminal title in monospace font - Else if
last_completed_command_text()returnsSome: render command in monospace font - Else: render "New session" in UI font
All variants use main text color.
- If
-
Secondary line: Always render the old primary line content (working directory + git branch) but in sub text color and using
ClipConfig::start()for the working directory. -
Tertiary line: Unchanged (
render_terminal_tertiary_line).
The existing render_terminal_primary_line and render_terminal_secondary_line functions are replaced/rewritten to match the new line assignments. render_terminal_tertiary_line stays as-is.
4. Restructure non-terminal pane rows
app/src/workspace/view/vertical_tabs.rs — rewrite the else branch in render_pane_row (line 739):
New structure:
-
Primary row:
Flex::rowwith kind icon (12px) + title text. For code panes, resolve the icon viacrate::code::icon_from_file_pathon the title string (which is the file path), falling back toWarpIcon::Code2.to_warpui_icon(sub_text_color). For other types, useTypedPane::icon().to_warpui_icon(sub_text_color). -
Secondary row (if non-empty subtitle): subtitle in sub text color.
Remove the meta row (Flex::row with render_kind_badge + render_row_badge). The render_kind_badge and render_row_badge functions are no longer called from non-terminal pane rows (they are still used by terminal pane tertiary lines).
5. Code pane multi-tab subtitle
app/src/workspace/view/vertical_tabs.rs — in TypedPane::Code handling within the non-terminal branch:
Add a method or inline logic on TypedPane to expose the code pane tab count:
fn code_tab_count(&self, app: &AppContext) -> Option<usize> {
match self {
TypedPane::Code(code_pane) => {
let count = code_pane.file_view(app).as_ref(app).tab_count();
(count > 1).then_some(count)
}
_ => None,
}
}
When rendering the subtitle for code panes, if code_tab_count returns Some(count), override the subtitle with format!("and {} more", count - 1) regardless of what PaneConfiguration.title_secondary() contains.
6. Path clipping changes
In the new secondary line for terminal panes (render_terminal_secondary_line), change the working directory Text from ClipConfig::end() to ClipConfig::start(). Git branch text remains ClipConfig::end().
In the non-terminal primary row, change the title Text from ClipConfig::end() to ClipConfig::start() when the pane type is Code (file paths). For non-path titles (Notebook, Settings, etc.), keep ClipConfig::end().
7. Replace collapse chevron with close button
app/src/workspace/view/vertical_tabs.rs — in render_group_header (line 564):
Replace the collapse button construction:
- Change icon from
chevron_icon(ChevronDown/ChevronRight) to a constantWarpIcon::X(orUiIcon::X). - Change the click handler from dispatching
WorkspaceAction::ToggleVerticalTabsGroupCollapsedtoWorkspaceAction::CloseTab(tab_index). - Remove the
is_collapsedparameter fromGroupHeaderProps.
8. Remove collapse state
app/src/workspace/view/vertical_tabs.rs:
- Remove
collapsed_tab_groups: HashSet<EntityId>fromVerticalTabsPanelState. - Remove
toggle_group_collapsed,toggle_all_groups_collapsed,is_group_collapsedmethods. - Remove the
collapse: MouseStateHandlefromPaneGroupStateHandles— rename the field toclosefor clarity. - Remove the
is_collapsedcheck inrender_tab_groupthat conditionally skips rendering pane rows.
app/src/workspace/action.rs:
- Remove
ToggleVerticalTabsGroupCollapsedandToggleAllVerticalTabsGroupsCollapsedvariants fromWorkspaceAction.
app/src/workspace/view.rs:
- Remove the match arms for
ToggleVerticalTabsGroupCollapsedandToggleAllVerticalTabsGroupsCollapsed(lines 16759-16764). - Remove
toggle_vertical_tabs_group_collapsedandtoggle_all_vertical_tabs_groups_collapsedmethods (lines 6125-6139).
End-to-end flow
Terminal pane row rendering
render_pane_rowis called for each visible pane in a tab group.- For terminal panes,
render_terminal_row_contentis called with theTerminalViewreference. - It calls
terminal_title_from_shell(),display_working_directory(),selected_conversation_display_title(), and the newlast_completed_command_text(). - The primary line function applies the precedence rules and returns the appropriate element.
- The secondary line always renders working directory + git branch in sub text color with
ClipConfig::start(). - The tertiary line renders unchanged.
Close button
- User clicks X on a tab group header.
- The click handler dispatches
WorkspaceAction::CloseTab(tab_index). - The existing workspace close-tab logic handles teardown, undo grace period, etc.
Risks and mitigations
Lock contention for last_completed_command_text: The method acquires self.model.lock() and iterates blocks. This runs on the render path. Mitigation: the iteration is backwards and short-circuits on the first match, so it's fast in practice. The lock is already acquired in terminal_title_from_shell() on the same render path, so this is consistent with existing patterns.
Removing collapse state: Any external callers of ToggleVerticalTabsGroupCollapsed or ToggleAllVerticalTabsGroupsCollapsed will fail to compile. Mitigation: grep confirms these are only dispatched from vertical_tabs.rs and handled in view.rs — no external callers.
Code pane tab count: CodeView::tab_count() requires reading through CodePane::file_view(app).as_ref(app), which is the same pattern already used by TypedPane::badge(). No additional risk.
Testing and validation
All changes are in rendering code with no persistence or protocol changes. Validation is primarily manual:
- Build and run with
cargo run, enable vertical tabs, and verify each success criterion from the product spec. - Verify terminal panes with: default shell (should show "New session"), after running a command (should show command text), running
vim(should show "vim"), agent conversation (should show conversation title + status). - Verify code panes with: single
.rsfile (Rust icon), single.txtfile (Code2 icon), multiple tabs (language icon + "and X more"). - Verify path clipping by narrowing the panel.
- Verify close button closes the tab.
- Verify no compilation errors or clippy warnings from removed collapse state.
Follow-ups
- The "compact" single-row mode shown in the Figma mock is deferred.
- Consider caching
last_completed_command_textif profiling shows the block iteration is a hot path (unlikely given reverse iteration with early exit). - The
render_kind_badgeandrender_row_badgefunctions can be cleaned up if they become unused after future terminal row changes.