# 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_badge` - `app/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` - `app/src/terminal/model/block.rs:2143` — `Block::command_to_string()`, `Block::finished()`, `BlockState` - `app/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>` - `app/src/pane_group/pane/code_pane.rs:53` — `CodePane::file_view()` returns `ViewHandle` - `app/src/workspace/action.rs:99,227-230` — `WorkspaceAction::CloseTab`, `ToggleVerticalTabsGroupCollapsed`, `ToggleAllVerticalTabsGroupsCollapsed` - `ui/src/text_layout.rs:455-460` — `ClipConfig::start()` already exists ## Current state ### Terminal pane rows `render_terminal_row_content` builds three lines: 1. **Primary** (`render_terminal_primary_line`): working directory + git branch, main text color 2. **Secondary** (`render_terminal_secondary_line`): conversation title/status, or terminal title if it differs from working directory; sub text color. Returns `None` if terminal title matches working directory. 3. **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): 1. Title row (main text, `ClipConfig::end()`) 2. Optional subtitle row (sub text) 3. Meta row: `render_kind_badge(icon, kind_label)` on left, optional `render_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`. 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`. 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: ```rust pub fn last_completed_command_text(&self) -> Option { 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`: ```rust 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: 1. **Primary line**: Determine content using the precedence rules from the product spec: - If `conversation_display_title` is `Some`: 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()` returns `Some`: render command in monospace font - Else: render "New session" in UI font All variants use main text color. 2. **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. 3. **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: 1. **Primary row**: `Flex::row` with kind icon (12px) + title text. For code panes, resolve the icon via `crate::code::icon_from_file_path` on the title string (which is the file path), falling back to `WarpIcon::Code2.to_warpui_icon(sub_text_color)`. For other types, use `TypedPane::icon().to_warpui_icon(sub_text_color)`. 2. **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: ```rust fn code_tab_count(&self, app: &AppContext) -> Option { 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 constant `WarpIcon::X` (or `UiIcon::X`). - Change the click handler from dispatching `WorkspaceAction::ToggleVerticalTabsGroupCollapsed` to `WorkspaceAction::CloseTab(tab_index)`. - Remove the `is_collapsed` parameter from `GroupHeaderProps`. ### 8. Remove collapse state `app/src/workspace/view/vertical_tabs.rs`: - Remove `collapsed_tab_groups: HashSet` from `VerticalTabsPanelState`. - Remove `toggle_group_collapsed`, `toggle_all_groups_collapsed`, `is_group_collapsed` methods. - Remove the `collapse: MouseStateHandle` from `PaneGroupStateHandles` — rename the field to `close` for clarity. - Remove the `is_collapsed` check in `render_tab_group` that conditionally skips rendering pane rows. `app/src/workspace/action.rs`: - Remove `ToggleVerticalTabsGroupCollapsed` and `ToggleAllVerticalTabsGroupsCollapsed` variants from `WorkspaceAction`. `app/src/workspace/view.rs`: - Remove the match arms for `ToggleVerticalTabsGroupCollapsed` and `ToggleAllVerticalTabsGroupsCollapsed` (lines 16759-16764). - Remove `toggle_vertical_tabs_group_collapsed` and `toggle_all_vertical_tabs_groups_collapsed` methods (lines 6125-6139). ## End-to-end flow ### Terminal pane row rendering 1. `render_pane_row` is called for each visible pane in a tab group. 2. For terminal panes, `render_terminal_row_content` is called with the `TerminalView` reference. 3. It calls `terminal_title_from_shell()`, `display_working_directory()`, `selected_conversation_display_title()`, and the new `last_completed_command_text()`. 4. The primary line function applies the precedence rules and returns the appropriate element. 5. The secondary line always renders working directory + git branch in sub text color with `ClipConfig::start()`. 6. The tertiary line renders unchanged. ### Close button 1. User clicks X on a tab group header. 2. The click handler dispatches `WorkspaceAction::CloseTab(tab_index)`. 3. 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 `.rs` file (Rust icon), single `.txt` file (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_text` if profiling shows the block iteration is a hot path (unlikely given reverse iteration with early exit). - The `render_kind_badge` and `render_row_badge` functions can be cleaned up if they become unused after future terminal row changes.