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
+109
View File
@@ -0,0 +1,109 @@
# APP-3655: Vertical Tabs — Search Functionality + Control Bar UI Polish
## 1. Summary
Implement search/filter functionality for the vertical tabs panel search bar, and update the search bar's visual appearance to match the design mocks. The search bar will filter the visible pane items and tab groups in real time as the user types.
## 2. Problem
The search bar in the vertical tabs panel was implemented as a non-functional placeholder (APP-3648). Users with many tabs and tab groups have no quick way to navigate to a specific pane. The control bar's current visual treatment (background, border, padding) also diverges from the design mocks.
## 3. Goals
- Make the search bar fully functional: it accepts text input and filters the list of pane items and tab groups in real time.
- Match the design mocks for the control bar: correct horizontal padding and a visually "uncontainerized" search input (no background, no border).
- Preserve tab/group ordering in filtered results.
- Omit tab groups that have no matching panes.
## 4. Non-goals
- Fuzzy matching or ranked/sorted results — simple substring matching is sufficient.
- Searching across window sessions or workspaces beyond the current panel.
- Keyboard-navigable filtered results (arrow-key selection of filtered items) — out of scope for this iteration.
- Persisting or restoring the search query across panel closes or app restarts.
- Highlighting matched text within pane item rows.
## 5. Figma / Design References
Figma: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7080-26025&m=dev
## 6. User Experience
### Search bar activation
- The search input is now fully focusable and accepts keyboard input.
- Clicking anywhere on the search bar focuses it and shows a cursor.
- The "Not yet implemented" hover tooltip is removed.
### Filtering behavior
- As the user types into the search bar, the pane list is filtered in real time — no submit/enter required.
- Filtering is **case-insensitive** substring matching.
- **Search domain**: all visible text within a pane item row is considered, including:
- Primary title (e.g. working directory or pane title)
- Secondary title / subtitle (e.g. git branch, terminal title)
- Pane kind label (e.g. "Terminal", "Code", "Settings")
- Any badge text visible in the row (e.g. "Unsaved", PR name, diff stats)
- **Ordering**: panes that pass the filter are shown in their original display order — no reranking.
- **Tab groups**: a tab group header is shown only if at least one of its panes matches the query. If no panes in a group match, the group header is omitted entirely.
- **Empty state**: if the query matches no panes across all groups, the list area shows an empty state message, e.g. "No tabs match your search."
### Clearing the search
- Pressing **Escape** while the search bar is focused clears the query and returns focus to the previously active pane (existing behavior for focus return is preserved).
- Deleting all characters from the input restores the full, unfiltered list.
- Clicking outside the search bar (e.g. clicking a pane item) does **not** clear the query — the filter remains active.
### Control bar visual changes
- **Horizontal padding**: The control bar's left and right padding matches the content area of the panel (12px on each side, consistent with `GROUP_HORIZONTAL_PADDING`).
- **Search input container**: The background fill and border of the search input container are removed. The search input appears as a plain, unstyled text field with the magnifying glass icon — visually integrated into the control bar rather than enclosed in a box.
- All other control bar elements (split new-tab button, icon sizes, vertical padding) are unchanged.
### Tab switching while filtered
- When a search query is active, the "next tab" and "previous tab" actions (keyboard shortcuts) cycle through only the **filtered** pane list — not the full natural order. The user steps through exactly the panes that are currently visible.
- Once the query is cleared, next/previous tab navigation resumes using the full natural order.
### Interaction with non-search actions
- Creating a new tab (plus button) while a search query is active does not clear the search.
- Clicking a pane item to navigate to it does not clear the search query.
- The filtered view reflects the current query at all times while the query is non-empty.
## 7. Success Criteria
1. Typing into the search bar visibly filters the pane list to only panes whose row text contains the query (case-insensitive) without pressing Enter.
2. Tab groups with no matching panes are hidden entirely, including their group header row.
3. Tab groups with at least one matching pane are shown, and only their matching panes appear beneath them.
4. Original display ordering of both groups and panes within groups is preserved in filtered results.
5. Clearing the query (by deleting all text) restores the full unfiltered list identically to how it appeared before searching.
6. Pressing Escape while focused on the search bar clears the query and returns focus to the active pane.
7. While a search query is active, next/previous tab keyboard shortcuts cycle through only the filtered (visible) panes, not the full natural order.
8. An empty state is displayed when the query matches no panes.
9. The control bar has 12px horizontal padding on each side, matching the panel content area.
10. The search input has no visible background or border — it is visually "uncontainerized."
11. Clicking on the search bar focuses it (cursor visible, text input accepted) with no tooltip shown.
12. The search domain includes all visible text in the pane row: primary title, secondary title, pane kind label, and badge text.
13. Collapsing and re-opening the panel preserves the active search query and filtered view.
14. Clicking a pane item while a query is active does not clear the query.
## 8. Validation
- **Manual**: Type partial strings that match: only the primary title, only a subtitle, only a kind label (e.g. "code"), only badge text. Confirm only matching panes appear.
- **Manual**: Confirm tab groups with no matches are completely hidden.
- **Manual**: Confirm ordering of surviving results matches the original order.
- **Manual**: Confirm empty state message appears when no panes match.
- **Manual**: Press Escape — query clears, full list restores, focus returns to active pane.
- **Visual**: Screenshot the control bar with the search input focused and unfocused; confirm no background or border on the search input, and 12px horizontal padding aligning with pane item rows.
- **Regression**: Confirm the plus/new-tab button still creates tabs and opens the dropdown while a query is active.
- **Manual**: With a filtered list active, use next/previous tab shortcuts and confirm navigation skips non-matching panes.
- **Manual**: Collapse and re-open the panel; confirm the query and filtered view are preserved.
- **Manual**: Click a pane item in the filtered list; confirm the query is not cleared.
## 9. Open Questions
None — all open questions resolved:
- Search query persists across panel collapse/re-open.
- Clicking a pane item to navigate does not clear the query.
- All visible text in the pane row is searchable; no fields are excluded.
+208
View File
@@ -0,0 +1,208 @@
# APP-3655: Vertical Tabs — Search Functionality + Control Bar UI Polish (Tech Spec)
## 1. Problem
The vertical tabs panel has a search input that is fully inert — it cannot be focused or typed into. This spec covers:
1. Making the search input functional: read query text, filter the rendered pane list and tab group headers in real time.
2. Updating tab cycling (`activate_next_tab` / `activate_prev_tab`) to skip tab groups that have no matching panes when a filter is active.
3. Visual polish of the control bar: remove the search input container background/border, and align the control bar's horizontal padding with the rest of the panel content.
## 2. Relevant Code
- `app/src/workspace/view/vertical_tabs.rs` — all vertical tabs rendering; the files that change most
- `render_control_bar` (line 185) — search bar UI
- `render_groups` (line 380) — tab group iteration, filter logic goes here
- `render_tab_group` (line 408) — renders one group header + its pane rows
- `PaneProps::new` (line ~882) — assembles the searchable text fields per pane
- `TypedPane::kind_label`, `TypedPane::badge` (lines ~820–860)
- `app/src/workspace/view.rs`
- `Workspace` struct (line ~760) — add `vertical_tabs_search_query: String`
- `vertical_tabs_search_input` constructor (line 890) — add `Edited` subscription
- `activate_next_tab` / `activate_prev_tab` (lines 7481–7497) — filter-aware cycling
- `app/src/editor/view/mod.rs`
- `EditorView::buffer_text(&self, ctx: &AppContext) -> String` (line 3675) — the public API to read query text from an `EditorView`
## 3. Current State
**Search input**: The `EditorView` for the search bar is created and rendered but is fully passive. No subscription listens to text changes, and no query string is read or used anywhere.
**Control bar layout** (`render_control_bar`, line 232–242):
- The outer `Container` uses `Padding::uniform(CONTROL_BAR_VERTICAL_PADDING)` (4px all sides) — no left/right padding to match the content area.
- The search bar `Container` has `with_background(internal_colors::fg_overlay_1(theme))` and `with_corner_radius(...)`, giving it a visible box appearance.
**Tab cycling** (`activate_next_tab` / `activate_prev_tab`, lines 7481–7497):
- Cycles through `self.tabs` (all tab groups) unconditionally by index with wrap-around.
## 4. Proposed Changes
### 4.1 Store the search query on `VerticalTabsPanelState`
Add a `String` field to `VerticalTabsPanelState` (vertical_tabs.rs):
```rust
pub(super) struct VerticalTabsPanelState {
// ... existing fields ...
search_query: String,
}
```
Initialize it as `String::new()`. Keeping the query here (rather than on `Workspace`) co-locates it with all other panel-specific state, and `render_groups` already receives `state: &VerticalTabsPanelState` directly — no additional threading needed to read it during render. `activate_next_tab`/`activate_prev_tab` access it via `self.vertical_tabs_panel.search_query` (the field on `Workspace` is `vertical_tabs_panel: VerticalTabsPanelState`, view.rs line 862).
### 4.2 Subscribe to `EditorEvent::Edited` in `vertical_tabs_search_input`
In `Workspace::vertical_tabs_search_input` (view.rs line 890), add a second subscription alongside the existing `Escape` handler:
```rust
ctx.subscribe_to_view(&editor, |me, editor_view, event, ctx| {
if matches!(event, EditorEvent::Edited(_)) {
me.vertical_tabs_panel.search_query = editor_view.as_ref(ctx).buffer_text(ctx);
ctx.notify();
}
});
```
When `Escape` clears the editor, also clear the query. Merge this into the existing `Escape` handler rather than adding a separate subscription:
```rust
ctx.subscribe_to_view(&editor, |me, _, event, ctx| {
if matches!(event, EditorEvent::Escape) {
me.vertical_tabs_panel.search_query.clear();
me.focus_active_tab(ctx);
}
});
```
### 4.3 Apply the filter in `render_groups`
`render_groups` (line 380) already receives `state: &VerticalTabsPanelState`. Read the query from `state.search_query` directly — no signature change needed.
If the query is non-empty:
1. For each `tab` in `workspace.tabs`, compute the list of `PaneId`s from `pane_group.visible_pane_ids()` that satisfy `pane_matches_query`.
2. If no panes match → skip the entire tab group (no call to `render_tab_group`).
3. If ≥1 panes match → call `render_tab_group`, passing the filtered `Vec<PaneId>` so only matching rows are rendered.
4. If no groups survive and the query is non-empty → render the empty-state message: `"No tabs match your search."` styled the same as the existing `"No tabs open"` message.
If the query is empty → preserve the existing behavior (pass all pane IDs, no change).
**Add a helper function:**
```rust
fn pane_matches_query(props: &PaneProps<'_>, query_lower: &str) -> bool {
props.title.to_lowercase().contains(query_lower)
|| props.subtitle.to_lowercase().contains(query_lower)
|| props.kind_label.to_lowercase().contains(query_lower)
|| props.typed.badge().map_or(false, |b| b.to_lowercase().contains(query_lower))
}
```
The caller lowercases the query once before the loop: `let query_lower = state.search_query.to_lowercase();`. This avoids re-allocating the lowercased query string for every pane check. Each pane field is still lowercased per check; that is fine given that pane titles are short strings and tab counts are in the tens to low hundreds. No caching is needed at this scale.
`PaneProps` already aggregates `title`, `subtitle`, `kind_label`, and `badge` — no need to reach into lower-level types.
### 4.4 Update `render_tab_group` to accept a filtered pane list
Change the signature to accept an optional filtered pane ID list:
```rust
fn render_tab_group(
state: &VerticalTabsPanelState,
workspace: &Workspace,
tab_index: usize,
tab: &TabData,
filtered_pane_ids: Option<&[PaneId]>, // None = render all
app: &AppContext,
) -> Box<dyn Element>
```
Inside the function, where `visible_pane_ids` is currently read from `pane_group.visible_pane_ids()`, replace with the filtered list when `filtered_pane_ids` is `Some`. The group header always renders when this function is called (skipping was already handled in `render_groups`).
### 4.5 Filter-aware tab cycling
In `activate_next_tab` and `activate_prev_tab` (view.rs lines 7481–7497), when `self.vertical_tabs_search_query` is non-empty, compute the set of tab indices that have at least one matching pane, then find the next/previous in that set (with wrap-around) relative to `self.active_tab_index`.
```rust
pub fn activate_next_tab(&mut self, ctx: &mut ViewContext<Self>) {
if self.vertical_tabs_panel.search_query.is_empty() {
// existing logic
let index = if self.active_tab_index + 1 < self.tabs.len() {
self.active_tab_index + 1
} else {
0
};
self.activate_tab(index, ctx);
} else {
let matching: Vec<usize> = self.matching_tab_indices(ctx);
if let Some(next) = next_in_cycle(&matching, self.active_tab_index) {
self.activate_tab(next, ctx);
}
}
}
```
Add a private helper:
```rust
fn matching_tab_indices(&self, ctx: &AppContext) -> Vec<usize> {
// returns tab indices (in original order) where ≥1 pane matches the query
}
```
The `next_in_cycle` / `prev_in_cycle` helpers find the next/previous element in a sorted index list relative to a current value, wrapping around.
### 4.6 Control bar visual changes
In `render_control_bar` (line 209–227), change the `search_bar` container:
- Remove `.with_background(internal_colors::fg_overlay_1(theme))`.
- Remove `.with_corner_radius(...)`.
- Remove (or simplify) the fixed `with_padding(Padding::uniform(4.).with_left(8.).with_right(8.))` inner padding — replace with minimal padding that aligns the icon visually without a box.
In the outer `Container` (line 232–242), change its padding to add 12px left and right:
```rust
.with_padding(
Padding::uniform(CONTROL_BAR_VERTICAL_PADDING)
.with_left(GROUP_HORIZONTAL_PADDING)
.with_right(GROUP_HORIZONTAL_PADDING),
)
```
Remove the `SEARCH_BAR_HEIGHT` constraint on the search bar's `ConstrainedBox` if it conflicts with the new unboxed layout, or keep it for consistent height — check against the Figma mock.
## 5. End-to-End Flow
1. User types "rust" into the search bar.
2. `EditorView` emits `EditorEvent::Edited(_)`.
3. The subscription in `vertical_tabs_search_input` fires: `workspace.vertical_tabs_search_query = "rust"`, then `ctx.notify()`.
4. The workspace re-renders. `render_groups` reads `"rust"` from `workspace.vertical_tabs_search_query`.
5. For each tab group, `visible_pane_ids()` is retrieved, each pane is tested via `pane_matches_query`, and the filtered ID list is passed to `render_tab_group`. Groups with no matches are skipped.
6. User presses next-tab shortcut. `activate_next_tab` reads the non-empty query, computes `matching_tab_indices`, and steps to the next matching tab index, skipping groups with no matches.
7. User presses Escape. The `Escape` subscription clears `vertical_tabs_search_query` and calls `focus_active_tab`. The workspace re-renders with the full unfiltered list.
## 6. Risks and Mitigations
- **Performance**: `pane_matches_query` runs on every render for each visible pane. For realistic Warp usage (tens to low hundreds of tabs), this is O(n) with cheap string operations and is not a concern. The query is lowercased once before the loop; pane fields are lowercased per check on short strings. No caching is needed unless profiling shows otherwise.
- **`PaneProps::new` returns `Option`**: The filter must handle the `None` case (pane no longer exists) gracefully — consistent with how the existing render loop handles it via `continue`.
- **Collapse state**: Collapsed tab groups still participate in filtering. A group whose header is collapsed but whose panes match should be shown with its header visible (collapsed state preserved). The group header is always rendered when `render_tab_group` is called; only the pane rows are hidden when collapsed. This is unchanged behavior.
- **Query not cleared on new tab**: Creating a new tab appends to `self.tabs`; the query remains. The new tab group will appear only if one of its panes matches the query (it likely won't until it has content), which is acceptable. No special handling needed.
- **Escape event double-clear**: Subscribing to `Escape` in two places (old handler + new query-clear) needs to be merged into one subscription to avoid double-firing. Merge both effects into the single `Escape` handler.
## 7. Testing and Validation
- **Manual**: Type a partial query that matches only a subset of panes — confirm only matching rows and their group headers are visible.
- **Manual**: Type a query that matches no panes — confirm the empty state message appears.
- **Manual**: Clear the query — confirm the full list is restored exactly.
- **Manual**: Press Escape — confirm the query clears, the full list restores, and focus returns to the active pane.
- **Manual**: With a filter active, use next/prev tab shortcuts — confirm navigation lands only on tab groups with matching panes.
- **Manual**: Collapse a tab group that has matching panes, then search — confirm the group header is visible and collapsed (not omitted).
- **Visual**: Screenshot the control bar; confirm no background or border on the search input, and left/right padding aligns with pane row text.
- **Regression**: Confirm next/prev tab cycling without a query is unchanged (all tabs cycle in order).
## 8. Follow-ups
- Highlighted matched text within pane rows (out of scope for this iteration).
- Keyboard navigation through filtered results via arrow keys (out of scope).
- Fuzzy or ranked matching if substring search proves insufficient.
- Persisting the query across app restarts (currently out of scope per PRODUCT.md).