Files

200 lines
12 KiB
Markdown

# 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<Block>`
- `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<Box<dyn Element>>`
- `app/src/pane_group/pane/code_pane.rs:53``CodePane::file_view()` returns `ViewHandle<CodeView>`
- `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<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:
```rust
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`:
```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<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 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<EntityId>` 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.