Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
# APP-3651: Vertical Tabs Panel — Pane Row Layout Iteration
## Summary
Iterate on the vertical tabs panel pane row rendering to improve information density, relevance, and interaction. The changes restructure how terminal and non-terminal pane rows display their content, introduce start-clipping for path text, replace the group collapse chevron with a close button, and deduplicate redundant terminal titles.
## Problem
The current vertical tabs layout has several information hierarchy issues:
- Terminal panes show the working directory as the primary line, but users more often care about *what they're doing* (the terminal title, conversation status, or last command) than *where they are*.
- Non-terminal panes waste a full row on a "kind badge" (e.g. "Code", "Notebook") that duplicates information already conveyed by the icon.
- Path text clips from the end, hiding the most distinguishing part (the filename) when paths are long.
- The expand/collapse chevron on tab group headers is no longer needed and occupies space that would be better used for a close button.
- When a shell's terminal title is just the working directory (the default for most shells), both the primary and secondary lines show the same information.
## Goals
- Make the most task-relevant information (terminal title, conversation status, or last command) the primary line for terminal panes.
- Reduce visual noise for non-terminal panes by inlining the kind icon next to the title and removing the standalone badge row.
- Show the most distinguishing portion of paths by clipping from the start.
- Provide a direct way to close a tab from the vertical tabs panel.
- Eliminate redundant information when the terminal title matches the working directory.
## Non-goals
- Implementing a "compact" single-row rendering mode (shown in the Figma mock but out of scope for this iteration).
- Adding new pane types or changing how pane titles/subtitles are set in `PaneConfiguration`.
- Changing the tab group header title, count label, or drag behavior.
- Search functionality in the control bar (already non-functional/placeholder).
## Figma / design references
Figma: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7079-22324&m=dev
Note: The Figma mock shows a compact single-row rendering that is not in scope. The close button (X) design on the tab group header and the inline kind icons next to titles are relevant references.
## User experience
### Terminal pane rows
Currently the terminal row layout is:
1. **Primary line** (main text color): working directory • git branch
2. **Secondary line** (sub text color): conversation title/status, or terminal title if it differs from working directory
3. **Tertiary line**: kind badge ("Terminal" or "Oz") + right-side badges (diff stats, PR)
The new layout reverses lines 1 and 2:
1. **Primary line** (main text color): terminal title or conversation status (see rules below)
2. **Secondary line** (sub text color): working directory • git branch
3. **Tertiary line**: unchanged (kind badge + right-side badges)
#### Primary line rules for terminal panes
The primary line content is determined by, in order of precedence:
1. If the pane has an active agent conversation with a display title: show the conversation status indicator followed by the conversation display title.
2. If the terminal title differs from the displayed working directory: show the terminal title.
3. If the terminal title matches the displayed working directory exactly (trimmed comparison) and there is a last completed user command: show the last completed user command string.
4. If the terminal title matches the displayed working directory and there is no completed user command (brand new session): show "New session" in the UI font.
The primary line always uses main text color.
#### Secondary line rules for terminal panes
The secondary line shows the working directory and git branch (same content as the current primary line) in sub text color. This line is always shown when a working directory is available.
#### Terminal title / working directory deduplication detail
"Matches exactly" means a case-sensitive, trimmed string comparison between `terminal_title_from_shell()` and `display_working_directory()`. The deduplication only applies when there is no active agent conversation display title.
When deduplication triggers and we show the last completed command:
- The command string should be rendered in the monospace font family currently used for terminal titles.
- If the last completed command is empty or unavailable (brand new session with no commands run), show the text "New session" in the UI font (not monospace).
### Non-terminal pane rows
Currently non-terminal panes render:
1. **Title line**: title text (main text color)
2. **Subtitle line** (if non-empty): subtitle text (sub text color)
3. **Meta row**: kind badge (icon + label like "Code", "Notebook") on the left, optional badge on the right
The new layout removes the standalone meta row and inlines the kind icon:
1. **Primary line**: kind icon (12px, sub text color) followed by title text (main text color). For code panes, the icon is the programming language icon for the active file (falling back to the generic `Code2` icon if no language icon exists).
2. **Subtitle line** (if non-empty): subtitle text (sub text color)
No separate badge row is rendered. The kind badge row is removed entirely.
#### Code panes with multiple tabs
For code panes with more than one open tab:
- **Primary line**: kind icon + first file's path (the active tab's path from `PaneConfiguration.title()`).
- **Secondary line**: "and X more" where X is `tab_count - 1`, rendered in sub text color. This replaces the current `(+N)` secondary title set by `CodeView::set_title`.
For code panes with a single tab, render normally using the title from `PaneConfiguration`.
#### Kind icon for code panes
Use `crate::code::icon_from_file_path` to attempt to get a language-specific icon (Rust, TypeScript, Python, etc.) from the active file's path. If `icon_from_file_path` returns `None`, fall back to the generic `WarpIcon::Code2` icon rendered as a `to_warpui_icon` in sub text color.
For non-code, non-terminal panes (Notebook, Settings, Workflow, etc.), use the existing `TypedPane::icon()` value.
### Path clipping
All path text in pane rows should clip from the start instead of the end. This applies to:
- Working directory text in terminal pane secondary lines
- File path text in code pane primary lines
- Any other path-like text rendered in pane rows
Use `ClipConfig::start()` instead of `ClipConfig::end()`. The `ClipConfig::start()` variant already exists in the codebase and fades text at the leading edge.
Git branch text should continue to clip from the end (branch names are most distinguishing at the start).
### Close button on tab group headers
Replace the expand/collapse chevron button in the tab group header with a close button (X icon):
- The close button uses an X icon instead of `ChevronDown`/`ChevronRight`.
- Clicking the close button dispatches `WorkspaceAction::CloseTab(tab_index)` to close the entire tab.
- The close button has the same hover styling as the current collapse button (background highlight on hover, pointing hand cursor).
- The close button is always visible in the header (not only on hover).
Remove all expand/collapse state and behavior:
- Remove the `collapsed_tab_groups` set from `VerticalTabsPanelState`.
- Remove the `toggle_group_collapsed`, `toggle_all_groups_collapsed`, and `is_group_collapsed` methods.
- Remove the `ToggleVerticalTabsGroupCollapsed` action handling.
- Tab groups are always expanded (pane rows are always visible).
### No behavioral changes
- Clicking a pane row still focuses that pane (dispatches `WorkspaceAction::FocusPane`).
- Clicking the tab group header title still activates the tab.
- Double-clicking the header still triggers rename.
- Right-clicking still opens the tab context menu.
- Drag-and-drop tab reordering is unchanged.
- The pane count label next to the close button is unchanged.
## Success criteria
1. **Terminal primary line**: For a terminal pane with no agent conversation and a non-default terminal title (e.g. `vim`, `htop`, a custom title), the primary line shows that terminal title in main text color.
2. **Terminal primary line (agent)**: For a terminal pane with an active agent conversation, the primary line shows the conversation status indicator + conversation display title.
3. **Terminal secondary line**: The working directory and git branch are always shown on the second line in sub text color, with the same layout as the current primary line.
4. **Terminal title dedup**: For a terminal pane where `terminal_title_from_shell()` trims to the same string as `display_working_directory()`, the primary line shows the last completed user command (if available) instead of the terminal title.
5. **New session fallback**: For a brand new terminal session with no completed commands and a default terminal title matching the working directory, the primary line shows "New session" in the UI font.
6. **Non-terminal kind icon inline**: For a Notebook pane, the primary line shows the Notebook icon followed by the title — no separate "Notebook" badge row.
7. **Code pane language icon**: For a code pane with a `.rs` file open, the primary line shows the Rust language icon (not the generic Code2 icon).
8. **Code pane language fallback**: For a code pane with a `.txt` file (no language icon), the primary line shows the generic Code2 icon.
9. **Code pane multi-tab**: For a code pane with 3 tabs open, the primary line shows the active file path and the secondary line shows "and 2 more".
10. **No badge row**: Non-terminal pane rows have no standalone kind badge or badge row beneath the title/subtitle.
11. **Path start-clipping**: A long working directory path like `~/very/long/path/to/my-project` clips from the left (showing `…my-project`) rather than from the right.
12. **Git branch end-clipping**: A long git branch name clips from the right (showing `feature/my-long-br…`).
13. **Close button**: Clicking the X button on a tab group header closes the tab (equivalent to `CloseTab`). The close button shows a hover highlight.
14. **No collapse**: There is no expand/collapse chevron. All pane rows in a tab group are always visible.
15. **Pane row click**: Clicking a pane row within a group still focuses that pane.
## Validation
- **Manual testing**: Open Warp with vertical tabs enabled. Create terminal tabs with various states (default shell, running `vim`, agent conversations, multiple directories). Verify primary/secondary line content matches the rules above.
- **New session**: Open a brand new terminal tab. Before running any commands, verify the primary line says "New session" and the secondary line shows the working directory.
- **Code pane testing**: Open code panes with single and multiple files of various languages. Verify language icons appear for supported extensions and fall back to Code2 for unsupported ones. Verify multi-tab "and X more" rendering.
- **Path clipping**: Resize the vertical tabs panel to a narrow width and verify that long paths clip from the start (filename visible) and git branches clip from the end.
- **Close button**: Click the X on a tab group header and verify the tab closes. Verify no collapse/expand behavior remains.
- **Terminal dedup**: In a shell where the terminal title defaults to the working directory, run a command and verify the primary line shows the last command rather than the (redundant) working directory.
- **Regression**: Verify tab group header click (activate tab), double-click (rename), right-click (context menu), and drag reorder still work. Verify pane row click still focuses the pane.
## Open questions
1. **Last completed command source**: The terminal model tracks block metadata, but the exact API to retrieve "the last completed user command string" from `TerminalView` needs to be identified or added. The current codebase doesn't expose a simple `last_completed_command_text()` accessor — this will need to be addressed in the tech spec.
2. **"and X more" click behavior**: Should clicking the "and X more" secondary line text do anything special (e.g. cycle to the next tab in the code pane), or should it behave the same as clicking anywhere else on the pane row (focus the code pane)?
+199
View File
@@ -0,0 +1,199 @@
# 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.