Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
# APP-4114: Vertical Tabs — Custom Pane Names
|
||||
|
||||
## Summary
|
||||
|
||||
Add support for custom names on individual panes in Vertical Tabs. This gives users a way to label split panes independently from the tab name, so a multi-pane tab can show meaningful pane labels like `server`, `tests`, or `review` without changing the tab header or the underlying terminal title.
|
||||
|
||||
The feature is scoped to pane items in the Vertical Tabs panel. Custom pane names should replace the main pane item title wherever pane rows are shown, but they should not change the pane header itself or any tab-level naming behavior.
|
||||
|
||||
## Problem
|
||||
|
||||
Warp already supports renaming tabs, but vertical tabs expose pane-level items. When a tab has multiple panes, the existing tab rename feature is too coarse: it names the whole tab rather than the individual split panes inside it.
|
||||
|
||||
Users need a pane-level naming affordance so they can distinguish related panes at a glance while keeping the tab title and pane header behavior unchanged.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a pane-level rename entry point in the Vertical Tabs right-click menu.
|
||||
- Make right-click rename apply to the pane that was right-clicked.
|
||||
- Keep existing overflow / kebab menus tab-scoped, including their existing tab rename behavior.
|
||||
- In `View as = Panes`, allow double-clicking pane title text to start renaming that pane.
|
||||
- Make a custom pane name take precedence over the `Pane title as` setting for the pane item's main title text.
|
||||
- Scope custom pane names to pane items in Vertical Tabs only.
|
||||
- Preserve existing tab rename behavior for tab group headers.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing or removing the existing tab rename feature.
|
||||
- Applying custom pane names to the pane header, terminal title, shell title, window title, or tab group header.
|
||||
- Adding a global pane-name management UI outside Vertical Tabs.
|
||||
- Adding rename affordances to the horizontal tab strip or pane header.
|
||||
- Changing the available `Pane title as`, `Additional metadata`, `Show`, `View as`, or `Tab item` settings.
|
||||
- Changing how terminal titles, working directories, branches, or summary cards are computed when no custom pane name exists.
|
||||
|
||||
## Figma / design references
|
||||
|
||||
Figma: none provided.
|
||||
|
||||
## User experience
|
||||
|
||||
### Conceptual model
|
||||
|
||||
Custom pane names are user-authored labels for pane rows in Vertical Tabs.
|
||||
|
||||
A custom pane name is distinct from:
|
||||
|
||||
- the tab name shown in the tab group header
|
||||
- the pane header title shown inside the main workspace area
|
||||
- terminal title text reported by the shell
|
||||
- generated pane titles derived from command, conversation, working directory, branch, file, notebook, workflow, or other pane metadata
|
||||
|
||||
When a custom pane name exists, it controls only the main title text of the corresponding pane item in Vertical Tabs.
|
||||
|
||||
### Menu entry point
|
||||
|
||||
Vertical Tabs should expose pane rename from the right-click context menu on pane rows. Overflow / kebab menus remain tab-group-level menus in this iteration.
|
||||
|
||||
#### Right-click menu
|
||||
|
||||
When the user right-clicks a pane item in Vertical Tabs:
|
||||
|
||||
- the context menu includes a rename action for the pane
|
||||
- choosing the rename action starts editing the pane that was right-clicked
|
||||
- the action does not rename the tab group header, even if the pane is the only pane in the tab
|
||||
- the action is available for pane rows in `View as = Panes`
|
||||
- the action is not exposed in `View as = Tabs`; tab-level rename remains the available rename action there
|
||||
|
||||
If a right-click occurs on a tab group header, existing tab-level context menu behavior remains unchanged.
|
||||
|
||||
#### Overflow / kebab menu
|
||||
|
||||
Vertical Tabs currently supports tab-group-level overflow menus only; individual pane rows do not have their own overflow menu.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- overflow / kebab menus continue to expose tab-level actions only
|
||||
- the rename action in an overflow / kebab menu renames the tab, not a pane
|
||||
- `View as = Tabs` should expose only tab rename actions, because the rendered item is being used as a tab-level representative row
|
||||
- a custom pane name created while `View as = Panes` was enabled can still affect `View as = Tabs` focused-session display later, according to focused-session semantics
|
||||
|
||||
### Double-click entry point
|
||||
|
||||
When `View as = Panes`, double-clicking the pane title text starts editing that pane's custom name.
|
||||
|
||||
Behavior rules:
|
||||
|
||||
- Double-clicking the title text of a pane row opens an inline editor in place of that title text.
|
||||
- The editor is scoped to the pane whose title text was double-clicked.
|
||||
- Double-clicking whitespace, metadata, badges, or the row background should continue to behave according to existing row behavior unless the implementation already treats those regions as part of the title hit target.
|
||||
- Double-clicking the tab group header should continue to trigger tab rename, not pane rename.
|
||||
- Double-clicking title text in `View as = Tabs` should continue to open the existing inline editor for renaming the tab, not the pane.
|
||||
- Double-clicking a Summary tab item is not a pane rename entry point, because Summary represents the tab as a whole rather than a single pane title.
|
||||
|
||||
### Inline editor behavior
|
||||
|
||||
The pane rename editor should behave consistently with existing inline rename interactions in Warp.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- The editor is prefilled with the current custom pane name if one exists.
|
||||
- If no custom pane name exists, the editor is prefilled with the currently displayed generated title for that pane item.
|
||||
- Pressing `Enter` commits the edit.
|
||||
- Pressing `Escape` cancels the edit and restores the previous displayed title.
|
||||
- Blurring the editor commits the edit if the value changed.
|
||||
- Submitting an unchanged value should leave state unchanged.
|
||||
- Submitting only whitespace should clear the custom pane name and restore generated title behavior.
|
||||
- Leading and trailing whitespace should be trimmed before saving.
|
||||
|
||||
Only one inline rename editor should be active in Vertical Tabs at a time. Starting a different rename action should close or replace the previous editor according to the existing rename-editor pattern.
|
||||
|
||||
### Custom-name display precedence
|
||||
|
||||
If a pane has a custom name, that name is always used as the pane item's main title text in Vertical Tabs.
|
||||
|
||||
This precedence applies regardless of the `Pane title as` setting:
|
||||
|
||||
- `Pane title as = Command / Conversation`
|
||||
- `Pane title as = Working Directory`
|
||||
- `Pane title as = Branch`
|
||||
|
||||
The generated `Pane title as` value should remain available as secondary or metadata text only where existing layouts already render it outside the main title position. The custom pane name should not erase the underlying command, conversation, working directory, or branch metadata from other configured metadata slots.
|
||||
|
||||
If the custom pane name is cleared, the pane item immediately returns to the title generated by the current `Pane title as` setting and pane metadata.
|
||||
|
||||
### Display scope
|
||||
|
||||
Custom pane names apply only to pane items in Vertical Tabs.
|
||||
|
||||
They should affect:
|
||||
|
||||
- pane rows in `View as = Panes`
|
||||
- focused-session rows in `View as = Tabs`, because those rows are derived from an active pane
|
||||
- search text for pane rows, so searching for the custom pane name can find the pane item
|
||||
|
||||
They should not affect:
|
||||
|
||||
- the pane header inside the main workspace area
|
||||
- the tab group header title
|
||||
- the horizontal tab strip
|
||||
- details shown in the hover sidecar
|
||||
- terminal titles reported by the shell
|
||||
- shell integration metadata
|
||||
- notebook, workflow, code, or file object titles outside the Vertical Tabs pane item
|
||||
- Summary mode's tab-level generated primary line
|
||||
|
||||
### Relationship to `View as` modes
|
||||
|
||||
#### `View as = Panes`
|
||||
|
||||
Each visible pane row can have its own custom name.
|
||||
|
||||
When a custom name exists:
|
||||
|
||||
- that row's main title text shows the custom name
|
||||
- the row's icon, badges, secondary text, metadata rows, hover state, selection state, and click-to-focus behavior are unchanged
|
||||
- changing the `Pane title as` setting does not change the row's main title text
|
||||
- clearing the custom name makes the row follow `Pane title as` again
|
||||
|
||||
#### `View as = Tabs` with `Tab item = Focused session`
|
||||
|
||||
The representative row is derived from the tab's active pane. If that active pane has a custom name, the representative row's main title text shows the custom pane name.
|
||||
|
||||
When the active pane changes:
|
||||
|
||||
- the representative row updates to the newly active pane
|
||||
- if the newly active pane has a custom name, that custom name is shown
|
||||
- if the newly active pane does not have a custom name, the row uses generated title behavior
|
||||
|
||||
The tab group header title is still the tab name, not the custom pane name.
|
||||
|
||||
Rename entry points in this mode remain tab-scoped:
|
||||
|
||||
- double-clicking title text opens the existing inline tab rename editor
|
||||
- overflow / kebab rename actions rename the tab
|
||||
- right-click menus expose tab rename, not pane rename
|
||||
|
||||
#### `View as = Tabs` with `Tab item = Summary`
|
||||
|
||||
Summary mode is a tab-level representation and is not primarily a pane item. Custom pane names should not replace the Summary card's generated tab-level primary line.
|
||||
|
||||
Summary mode should continue to summarize the tab based on its existing summary rules.
|
||||
|
||||
### Pane lifecycle behavior
|
||||
|
||||
Custom pane names should remain associated with the pane they were set on.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Switching focus between panes does not move custom names between panes.
|
||||
- Moving a pane within the same tab preserves its custom name.
|
||||
- Moving a pane to another tab preserves its custom name.
|
||||
- Splitting a pane creates a new pane without automatically copying the source pane's custom name unless existing split behavior already clones all pane metadata.
|
||||
- Closing a pane removes its custom name with the pane.
|
||||
- Closing a tab removes custom names for panes in that tab.
|
||||
- Reopening Warp should preserve custom pane names for restored panes if those panes are part of persisted workspace state.
|
||||
|
||||
### Search behavior
|
||||
|
||||
Search in Vertical Tabs should include custom pane names.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- In `View as = Panes`, searching for a custom pane name matches and shows that pane row.
|
||||
- In `View as = Tabs` with `Tab item = Focused session`, searching for the active pane's custom name matches that tab's representative row.
|
||||
- Clearing a custom name removes that custom name from future search matching.
|
||||
- Generated metadata should remain searchable according to existing rules.
|
||||
|
||||
### Empty states and errors
|
||||
|
||||
- If the user opens rename for a pane that no longer exists, the editor should not appear and no unrelated pane should be renamed.
|
||||
- If the targeted pane closes while the editor is open, the editor should close without applying the edit to another pane.
|
||||
- If saving fails, the UI should keep the previous title and avoid showing a stale custom name as if it succeeded.
|
||||
- A blank or whitespace-only submitted name should clear the custom name rather than showing an empty pane title.
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. Right-clicking a pane row in Vertical Tabs exposes a pane rename action.
|
||||
2. Choosing the right-click pane rename action starts editing the pane that was right-clicked.
|
||||
3. Overflow / kebab menus remain tab-group-level menus and do not expose pane rename.
|
||||
4. The rename action in an overflow / kebab menu renames the tab, not a pane.
|
||||
5. In `View as = Tabs`, rename entry points expose tab rename only.
|
||||
6. In `View as = Panes`, double-clicking pane title text opens an inline editor for that pane's custom name.
|
||||
7. Double-clicking the tab group header still renames the tab, not a pane.
|
||||
8. The inline editor is prefilled with the current custom name when one exists.
|
||||
9. The inline editor is prefilled with the currently displayed generated title when no custom name exists.
|
||||
10. Pressing `Enter` commits a trimmed custom pane name.
|
||||
11. Pressing `Escape` cancels without changing the displayed title or stored custom name.
|
||||
12. Submitting an empty or whitespace-only name clears the custom pane name.
|
||||
13. A pane with a custom name shows that name as its main title text in Vertical Tabs.
|
||||
14. A custom pane name takes precedence over `Pane title as = Command / Conversation`.
|
||||
15. A custom pane name takes precedence over `Pane title as = Working Directory`.
|
||||
16. A custom pane name takes precedence over `Pane title as = Branch`.
|
||||
17. Clearing a custom pane name immediately restores generated title behavior based on the current `Pane title as` setting.
|
||||
18. Custom pane names do not change the pane header shown in the main workspace area.
|
||||
19. Custom pane names do not change the tab group header or horizontal tab title.
|
||||
20. In `View as = Tabs` with `Tab item = Focused session`, a representative row shows the active pane's custom name when that pane has one.
|
||||
21. In `View as = Tabs` with `Tab item = Focused session`, changing the active pane updates the representative row to the newly active pane's custom name or generated title as appropriate.
|
||||
22. In Summary mode, custom pane names do not replace the generated summary primary line.
|
||||
23. Searching for a custom pane name finds the corresponding pane item in `View as = Panes`.
|
||||
24. Searching for the active pane's custom name finds the focused-session representative row in `View as = Tabs`.
|
||||
25. Closing a pane with a custom name does not leave a stale custom name visible on any other pane.
|
||||
26. Details shown in the hover sidecar continue to use generated detail content and are not replaced by custom pane names.
|
||||
|
||||
## Validation
|
||||
|
||||
- **Right-click rename**: In `View as = Panes`, create a tab with at least two panes. Right-click the second pane row, choose pane rename, enter a custom name, and verify only that pane row changes.
|
||||
- **Overflow rename regression**: Open the tab-group overflow / kebab menu, choose rename, and verify the tab is renamed rather than any pane.
|
||||
- **Double-click rename**: In `View as = Panes`, double-click pane title text, enter a custom name, and verify the row title updates after commit.
|
||||
- **Tab rename regression**: Double-click the tab group header and verify existing tab rename behavior still works. In `View as = Tabs`, double-click title text and verify it opens the tab rename editor. Verify pane rename does not change the tab group header title.
|
||||
- **Precedence**: Set a custom pane name, then switch `Pane title as` between `Command / Conversation`, `Working Directory`, and `Branch`. Verify the pane row's main title remains the custom name.
|
||||
- **Clear custom name**: Rename a pane to whitespace or otherwise clear the name. Verify the row returns to generated title behavior and updates when `Pane title as` changes.
|
||||
- **Pane header and sidecar scope**: Rename a pane from Vertical Tabs and verify the pane header in the main workspace area and details shown in the hover sidecar do not show the custom name.
|
||||
- **Focused-session mode**: Switch to `View as = Tabs` and `Tab item = Focused session`. Focus panes with and without custom names and verify the representative row updates correctly.
|
||||
- **Summary mode**: Switch to `View as = Tabs` and `Tab item = Summary`. Verify the summary primary line is not replaced by a custom pane name.
|
||||
- **Search**: Search for the custom pane name in Panes mode and focused-session Tabs mode and verify the expected row is matched.
|
||||
- **Lifecycle**: Move a named pane to another tab, close named panes, and relaunch Warp with restored workspace state. Verify names stay attached to the intended panes and disappear when panes are gone.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. Should the pane rename menu item be labeled `Rename Pane`, `Rename pane`, or reuse the existing tab rename label style with pane-specific wording?
|
||||
2. If saving custom pane names requires persistence beyond existing workspace restoration, what persistence boundary should apply for panes created by ephemeral workflows that are not normally restored?
|
||||
@@ -0,0 +1,488 @@
|
||||
# APP-4114: Tech Spec — Custom Pane Names in Vertical Tabs
|
||||
|
||||
## Problem
|
||||
|
||||
APP-4114 adds user-authored names for individual panes in Vertical Tabs. The implementation needs to introduce pane-scoped naming without reusing tab-scoped state too broadly:
|
||||
|
||||
- tab rename already exists and is stored on `PaneGroup`
|
||||
- pane rows already derive generated titles from each pane's `PaneConfiguration` and terminal metadata
|
||||
- `View as = Tabs` focused-session rows reuse pane-row renderers but their rename entry points remain tab-scoped
|
||||
- hover sidecar and pane headers must continue showing generated pane detail, not custom pane names
|
||||
|
||||
The technical problem is to add a pane-scoped title override that follows pane ownership and persistence boundaries, then thread it only into Vertical Tabs row title/search rendering.
|
||||
|
||||
## Relevant code
|
||||
|
||||
- `specs/APP-4114/PRODUCT.md` — agreed user-facing behavior.
|
||||
- `app/src/workspace/action.rs (90-116)` — tab rename actions and tab context menu action shape.
|
||||
- `app/src/workspace/action.rs (650-710)` — `should_save_app_state_on_action`, where rename-style actions are treated as workspace-state changes.
|
||||
- `app/src/workspace/util.rs (88-236)` — `WorkspaceState` and the existing `tab_being_renamed` state pattern.
|
||||
- `app/src/workspace/view.rs (1000-1260)` — construction and event handling for `tab_rename_editor`.
|
||||
- `app/src/workspace/view.rs (4775-4832)` — `rename_tab`, `finish_tab_rename`, `cancel_tab_rename`, and editor prefill behavior.
|
||||
- `app/src/workspace/view.rs (6024-6048)` — tab context menu population via `TabData::menu_items`.
|
||||
- `app/src/tab.rs (248-320)` — tab context menu items, including `Rename tab` and `Reset tab name`.
|
||||
- `app/src/workspace/view/vertical_tabs.rs (319-425)` — shared pane-row wrapper for click, hover sidecar targeting, and tab double-click rename in Tabs mode.
|
||||
- `app/src/workspace/view/vertical_tabs.rs (1455-1778)` — render/search selection for Panes, Focused session, and Summary modes.
|
||||
- `app/src/workspace/view/vertical_tabs.rs (1778-2015)` — tab-group right-click and kebab action wiring.
|
||||
- `app/src/workspace/view/vertical_tabs.rs (3123-3224)` — `render_title_override`, which currently handles tab title overrides and inline tab rename.
|
||||
- `app/src/workspace/view/vertical_tabs.rs (5468-5755)` — compact row title rendering where `Pane title as` is applied.
|
||||
- `app/src/pane_group/pane/mod.rs (661-815)` — `PaneConfiguration`, the pane-owned model for title-like pane metadata.
|
||||
- `app/src/pane_group/mod.rs (6228-6382)` — pane attachment and `PaneConfigurationEvent::TitleUpdated` propagation to workspace.
|
||||
- `app/src/pane_group/mod.rs (4596-4735)` — tab-level `display_title`, `set_title`, and `clear_title`.
|
||||
- `app/src/pane_group/mod.rs (3995-4138)` — pane move APIs; moved pane contents preserve their `PaneConfiguration`.
|
||||
- `app/src/pane_group/mod.rs (1450-1955)` — restore each `LeafSnapshot` into a concrete pane and `PaneId`.
|
||||
- `app/src/pane_group/mod.rs (2032-2072)` — snapshot each leaf pane for app-state persistence.
|
||||
- `app/src/app_state.rs (108-135)` — `LeafSnapshot`, the pane-leaf snapshot boundary.
|
||||
- `app/src/persistence/sqlite.rs (801-842)` — saving `LeafSnapshot` to `pane_leaves`.
|
||||
- `app/src/persistence/sqlite.rs (2355-2675)` — reading `pane_leaves` back into `LeafSnapshot`.
|
||||
- `crates/persistence/src/schema.rs (300-304)` — current `pane_leaves` schema.
|
||||
- `crates/persistence/src/model.rs (371-393)` — `PaneLeaf` / `NewPane` persistence models.
|
||||
- `app/src/workspace/view/vertical_tabs_tests.rs (1-420)` — current home for vertical-tabs pure helper tests.
|
||||
- `app/src/persistence/sqlite_tests.rs (117-260)` — existing SQLite round-trip tests for pane snapshots.
|
||||
|
||||
## Current state
|
||||
|
||||
### Tab rename is tab-scoped
|
||||
|
||||
Tab rename uses:
|
||||
|
||||
- `WorkspaceAction::RenameTab(usize)` to start editing
|
||||
- `WorkspaceState::tab_being_renamed` to track the active editor target
|
||||
- one `tab_rename_editor: ViewHandle<EditorView>` on `Workspace`
|
||||
- `PaneGroup::custom_title: Option<String>` for persisted tab-level title state
|
||||
- `TabSnapshot.custom_title` and the `tabs.custom_title` SQLite column for restoration
|
||||
|
||||
This flow is intentionally tab-level. In Vertical Tabs, group headers and kebab/overflow menus dispatch the same tab rename action. `View as = Tabs` also passes a `renamable_tab_index` into pane-row rendering so double-clicking the representative row opens the tab rename editor.
|
||||
|
||||
### Pane row titles are generated
|
||||
|
||||
Vertical Tabs builds `PaneProps` from a `PaneId` and the pane's `PaneConfiguration`. `PaneProps::new` computes:
|
||||
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `typed`
|
||||
- focus/selection state
|
||||
- optional `display_title_override` for tab-level override behavior in flat Tabs mode
|
||||
|
||||
The row renderers then decide which generated value to show based on `Pane title as`, density, and pane type. Terminal rows use terminal metadata and `VerticalTabsPrimaryInfo`; non-terminal rows generally use `props.displayed_title()`.
|
||||
|
||||
### Focused-session Tabs mode reuses pane rows
|
||||
|
||||
`View as = Tabs` with `Tab item = Focused session` still renders a pane-derived row for the tab's focused pane. The row is tab-scoped for rename interactions, but pane-scoped for metadata. This is why an existing custom pane name should be visible there while rename actions remain tab rename actions.
|
||||
|
||||
### Hover sidecar uses pane detail data
|
||||
|
||||
The hover sidecar is driven by `VerticalTabsDetailTarget` and detail render helpers. It should keep using generated pane detail data. Custom pane names must not be threaded into detail-sidecar section titles or summary content.
|
||||
|
||||
### Pane ownership and persistence
|
||||
|
||||
Live pane content owns a `PaneConfiguration` model. When a pane is moved between tabs, `remove_pane_for_move` returns the pane content and `add_pane_as_hidden` inserts the same content into the destination group, so data stored in the pane's `PaneConfiguration` naturally moves with the pane.
|
||||
|
||||
Persistence is separate. App-state snapshots store pane leaves in `LeafSnapshot`; SQLite persists those leaves through `pane_leaves` and kind-specific pane tables. Because `PaneId` is recreated during restoration, persisted pane custom names should live on each `LeafSnapshot`, not in a persisted `HashMap<PaneId, String>`.
|
||||
|
||||
## Proposed changes
|
||||
|
||||
### 1. Store live custom names on `PaneConfiguration`
|
||||
|
||||
Add a pane-row-only custom title field to `PaneConfiguration`:
|
||||
|
||||
```rust
|
||||
custom_vertical_tabs_title: Option<String>,
|
||||
```
|
||||
|
||||
Add APIs:
|
||||
|
||||
```rust
|
||||
pub fn custom_vertical_tabs_title(&self) -> Option<&str>;
|
||||
pub fn set_custom_vertical_tabs_title(&mut self, title: impl Into<String>, ctx: &mut ModelContext<Self>);
|
||||
pub fn clear_custom_vertical_tabs_title(&mut self, ctx: &mut ModelContext<Self>);
|
||||
```
|
||||
|
||||
Setter behavior:
|
||||
|
||||
- trim leading/trailing whitespace
|
||||
- store `None` for an empty string
|
||||
- no-op if the normalized value did not change
|
||||
- emit a pane-configuration event that causes Vertical Tabs to re-render
|
||||
- do not emit `HeaderContentChanged`, because pane headers must not show this title
|
||||
|
||||
Recommended event shape:
|
||||
|
||||
```rust
|
||||
pub enum PaneConfigurationEvent {
|
||||
// existing variants...
|
||||
VerticalTabsTitleUpdated,
|
||||
}
|
||||
```
|
||||
|
||||
Then update `PaneGroup::attach_pane` to treat `TitleUpdated` and `VerticalTabsTitleUpdated` as `Event::PaneTitleUpdated`. This keeps the workspace and Vertical Tabs repaint path simple without refreshing pane headers.
|
||||
|
||||
This design is preferable to storing a `HashMap<PaneId, String>` on `PaneGroup` because it preserves names when pane contents move between tabs.
|
||||
|
||||
### 2. Persist custom names on `LeafSnapshot`
|
||||
|
||||
Extend `LeafSnapshot`:
|
||||
|
||||
```rust
|
||||
pub struct LeafSnapshot {
|
||||
pub is_focused: bool,
|
||||
pub custom_vertical_tabs_title: Option<String>,
|
||||
pub contents: LeafContents,
|
||||
}
|
||||
```
|
||||
|
||||
Snapshot flow:
|
||||
|
||||
- In `PaneGroup::snapshot_for_node`, read `pane.as_pane().pane_configuration().as_ref(app).custom_vertical_tabs_title()`.
|
||||
- Store that value on the `LeafSnapshot`.
|
||||
- Do not put this field on `LeafContents`; it is pane-leaf metadata that applies uniformly to all pane types.
|
||||
|
||||
Restore flow:
|
||||
|
||||
- When `restore_pane_leaf` creates a real pane and `PaneId`, apply `leaf.custom_vertical_tabs_title` to the restored pane's `PaneConfiguration`.
|
||||
- For deferred AI document panes, preserve the title in the deferred `LeafSnapshot` and apply it after the placeholder is replaced with the real pane.
|
||||
- For invalid/unrestorable leaves, the title is naturally dropped with the pane.
|
||||
|
||||
SQLite flow:
|
||||
|
||||
- Add a migration that adds `custom_vertical_tabs_title TEXT` to `pane_leaves`.
|
||||
- Update `crates/persistence/src/schema.rs` for the new nullable column.
|
||||
- Update `crates/persistence/src/model.rs`:
|
||||
- `PaneLeaf` gets `custom_vertical_tabs_title: Option<String>`
|
||||
- `NewPane` gets `custom_vertical_tabs_title: Option<String>`
|
||||
- Update `save_pane_state` to write `snapshot.custom_vertical_tabs_title.clone()`.
|
||||
- Update `read_node` to populate `LeafSnapshot.custom_vertical_tabs_title`.
|
||||
- Update tests that construct `LeafSnapshot` literals.
|
||||
|
||||
### 3. Add pane rename workspace state and editor
|
||||
|
||||
Add pane rename state alongside tab rename state:
|
||||
|
||||
```rust
|
||||
pane_being_renamed: Option<PaneViewLocator>,
|
||||
```
|
||||
|
||||
Add methods on `WorkspaceState` mirroring the tab helpers:
|
||||
|
||||
- `is_pane_being_renamed`
|
||||
- `set_pane_being_renamed`
|
||||
- `clear_pane_being_renamed`
|
||||
- `pane_being_renamed`
|
||||
|
||||
Add a separate `pane_rename_editor: ViewHandle<EditorView>` to `Workspace` rather than reusing `tab_rename_editor`. The pane editor should use the 12px row-title size used by compact/expanded pane rows, while `tab_rename_editor_font_size` already varies between header-like and row-like tab rename states.
|
||||
|
||||
Editor event handling should mirror tab rename:
|
||||
|
||||
- `Enter` or `Blurred` commits
|
||||
- `Escape` cancels
|
||||
- commit trims and saves through `PaneConfiguration::set_custom_vertical_tabs_title`
|
||||
- unchanged value leaves state unchanged
|
||||
- empty value clears the custom pane name
|
||||
- after commit/cancel, clear the editor buffer and focus the active tab/pane
|
||||
|
||||
Ensure only one inline rename editor is active:
|
||||
|
||||
- starting tab rename clears any pane rename state
|
||||
- starting pane rename clears any tab rename state
|
||||
- `WorkspaceState::close_all_modals` clears both states
|
||||
|
||||
### 4. Add pane rename actions
|
||||
|
||||
Add a new action:
|
||||
|
||||
```rust
|
||||
WorkspaceAction::RenamePane(PaneViewLocator)
|
||||
```
|
||||
|
||||
Optionally add `WorkspaceAction::ResetPaneName(PaneViewLocator)` only if the UI includes an explicit reset item. The current product spec only requires clearing via whitespace submission, so this can be deferred unless needed for parity with tab reset.
|
||||
|
||||
Handle `RenamePane` in `Workspace::handle_action`:
|
||||
|
||||
1. find the target `PaneGroup` by `pane_group_id`
|
||||
2. verify the pane still exists in that group
|
||||
3. activate the tab containing the pane if needed
|
||||
4. focus the target pane
|
||||
5. prefill `pane_rename_editor` with:
|
||||
- existing custom pane name, if present
|
||||
- otherwise the currently displayed generated pane-row title
|
||||
6. set `pane_being_renamed`
|
||||
7. focus the editor and notify
|
||||
|
||||
The prefill value should be computed from the same display helper used by `PaneProps` / row rendering. If the first implementation cannot easily get the exact generated title for every `Pane title as` branch, add a small helper in `vertical_tabs.rs` that returns the current main title text for a `PaneProps` in Panes mode, and unit-test the pure cases.
|
||||
|
||||
`RenamePane` and any commit path must save app state. Follow the existing rename-tab persistence pattern, but verify the final implementation saves after the committed pane name is applied. If action-level saving only captures the pre-edit state, emit `pane_group::Event::AppStateChanged` or dispatch the workspace save action from the commit path.
|
||||
|
||||
### 5. Route right-click pane rename by row location
|
||||
|
||||
Keep tab-group kebab behavior unchanged:
|
||||
|
||||
- `render_group_action_buttons` still dispatches `ToggleTabRightClickMenu` with `TabContextMenuAnchor::VerticalTabsKebab`
|
||||
- `TabData::menu_items` still exposes tab-level `Rename tab`
|
||||
|
||||
For pane rows in `View as = Panes`, add row-specific right-click handling:
|
||||
|
||||
- add `on_right_click` to `render_pane_row_element`, or pass a right-click callback into it
|
||||
- only enable the pane-targeted context menu when `props.display_granularity == VerticalTabsDisplayGranularity::Panes`
|
||||
- dispatch a new action such as:
|
||||
|
||||
```rust
|
||||
WorkspaceAction::ToggleVerticalTabsPaneContextMenu {
|
||||
tab_index: usize,
|
||||
pane: PaneViewLocator,
|
||||
position: Vector2F,
|
||||
}
|
||||
```
|
||||
|
||||
Implementation options:
|
||||
|
||||
- Reuse `tab_right_click_menu` and `show_tab_right_click_menu` by extending the stored context with an optional pane target.
|
||||
- Or add a separate `vertical_tabs_pane_context_menu` view if keeping target-specific menu state separate is cleaner.
|
||||
|
||||
Prefer reusing the existing `Menu<WorkspaceAction>` instance unless the state shape becomes awkward. The menu is still a workspace-level context menu; only the target and first rename item differ.
|
||||
|
||||
Pane-row context menu contents:
|
||||
|
||||
- include `Rename pane`, wired to `WorkspaceAction::RenamePane(locator)`
|
||||
- keep the rest of the existing tab-level menu items only if product/review expectations allow it
|
||||
- do not show pane rename from tab group header right-click
|
||||
- do not show pane rename from kebab / overflow
|
||||
- do not show pane rename in `View as = Tabs`
|
||||
|
||||
Be careful with event propagation: the tab group currently has an `on_right_click` that opens the tab context menu. The row-level handler must prevent the parent group handler from also opening the tab menu, or the parent handler must ignore right-clicks already handled by children.
|
||||
|
||||
### 6. Wire double-click on pane title text in Panes mode
|
||||
|
||||
Do not attach pane rename double-click to the whole row. The product spec scopes it to title text.
|
||||
|
||||
Refactor title rendering into a title-slot helper that can wrap the rendered title element:
|
||||
|
||||
```rust
|
||||
fn render_pane_title_slot(props: &PaneProps<'_>, generated_title: Box<dyn Element>, ...) -> Box<dyn Element>
|
||||
```
|
||||
|
||||
The helper should:
|
||||
|
||||
- render the pane rename editor if `props.is_pane_being_renamed`
|
||||
- otherwise render custom pane name if one exists
|
||||
- otherwise render the generated title
|
||||
- in `View as = Panes`, wrap the title text element with `on_double_click` dispatching `RenamePane`
|
||||
- not add pane rename double-click in `View as = Tabs`
|
||||
- not interfere with the existing row click-to-focus behavior
|
||||
|
||||
Keep existing tab rename double-click intact:
|
||||
|
||||
- `render_pane_row_element` continues using `renamable_tab_index` for `View as = Tabs`
|
||||
- `render_group_header` continues dispatching `RenameTab`
|
||||
- horizontal `TabComponent` remains unchanged
|
||||
|
||||
### 7. Add custom-title precedence to row rendering only
|
||||
|
||||
Extend `PaneProps` with:
|
||||
|
||||
```rust
|
||||
custom_vertical_tabs_title: Option<String>,
|
||||
is_pane_being_renamed: bool,
|
||||
pane_rename_editor: Option<ViewHandle<EditorView>>,
|
||||
```
|
||||
|
||||
Do not overwrite `PaneProps.title`. The generated title should remain available for:
|
||||
|
||||
- pane header behavior
|
||||
- hover sidecar details
|
||||
- Summary mode aggregation
|
||||
- generated metadata/search fragments
|
||||
|
||||
Recommended title precedence for row main-title rendering:
|
||||
|
||||
1. active tab rename editor, when `props.is_tab_being_renamed`
|
||||
2. active pane rename editor, when `props.is_pane_being_renamed`
|
||||
3. `props.custom_vertical_tabs_title`
|
||||
4. tab-level `props.display_title_override`
|
||||
5. generated title from `Pane title as` / pane metadata
|
||||
|
||||
This precedence allows focused-session `View as = Tabs` to display an active pane's custom name while preserving tab rename interactions. When the tab rename editor is open in Tabs mode, the tab editor should take visual precedence.
|
||||
|
||||
Apply this helper anywhere the main pane row title is rendered:
|
||||
|
||||
- expanded terminal first line
|
||||
- compact terminal title line
|
||||
- compact non-terminal title
|
||||
- expanded non-terminal title
|
||||
- focused-session row in Tabs mode
|
||||
|
||||
Do not apply it to:
|
||||
|
||||
- `render_detail_sidecar_overlay`
|
||||
- `render_detail_section`
|
||||
- `build_vertical_tabs_summary_data`
|
||||
- pane header code under `pane_group/pane/view/header`
|
||||
|
||||
### 8. Update search fragments
|
||||
|
||||
Search should match custom pane names without losing generated metadata search.
|
||||
|
||||
Update `PaneProps::rendered_search_text_fragments`:
|
||||
|
||||
- prepend `custom_vertical_tabs_title` when present
|
||||
- keep existing generated fragments
|
||||
- avoid duplicate generated/custom fragments if they are identical after trim
|
||||
|
||||
For terminal panes, do not pass the custom title as the terminal primary text replacement into `terminal_pane_search_text_fragments`; that would hide generated command/directory/branch metadata from search. Instead, add the custom title as an additional fragment around the existing generated fragments.
|
||||
|
||||
Focused-session `View as = Tabs` already searches through representative-pane `PaneProps`, so it will inherit this behavior when the active pane has a custom name.
|
||||
|
||||
Summary mode should not include custom pane names unless the product spec changes. Keep `summary_search_text_fragments` derived from summary data and tab custom title only.
|
||||
|
||||
### 9. Keep hover sidecar generated
|
||||
|
||||
The sidecar should use generated detail content. To avoid accidental leakage:
|
||||
|
||||
- keep `PaneProps.title` generated
|
||||
- store custom pane name in a separate field
|
||||
- ensure `render_detail_section` and detail helpers use generated fields, not the row-title helper
|
||||
- add a unit test or focused assertion for the relevant pure helper if practical
|
||||
|
||||
This is the main reason not to mutate `PaneConfiguration.title()` or `PaneProps.title` with the custom pane name.
|
||||
|
||||
### 10. Telemetry
|
||||
|
||||
If product wants rename-pane usage tracked, add a telemetry event using the `add-telemetry` skill before implementation.
|
||||
|
||||
If telemetry is added, track:
|
||||
|
||||
- rename editor opened
|
||||
- custom pane name committed
|
||||
- custom pane name cleared
|
||||
- entry point (`right_click`, `double_click`)
|
||||
|
||||
Do not block implementation on telemetry unless PM/review requests it.
|
||||
|
||||
## End-to-end flow
|
||||
|
||||
### Right-click rename in `View as = Panes`
|
||||
|
||||
1. User right-clicks a pane row.
|
||||
2. Row-level right-click handler dispatches `ToggleVerticalTabsPaneContextMenu` with the target `PaneViewLocator`.
|
||||
3. Workspace builds a context menu whose rename item dispatches `RenamePane(locator)`.
|
||||
4. `RenamePane` verifies the pane still exists, focuses it, computes the editor prefill, and sets `pane_being_renamed`.
|
||||
5. Vertical Tabs re-renders the target row title slot as `pane_rename_editor`.
|
||||
6. User presses `Enter` or blurs.
|
||||
7. Workspace commits the trimmed name into the target pane's `PaneConfiguration`.
|
||||
8. Vertical Tabs re-renders the row with `custom_vertical_tabs_title`.
|
||||
9. App state is saved with the custom name on that pane's `LeafSnapshot`.
|
||||
|
||||
### `View as = Tabs` display after a pane was named
|
||||
|
||||
1. User previously named pane A in `View as = Panes`.
|
||||
2. User switches to `View as = Tabs` with `Tab item = Focused session`.
|
||||
3. `pane_ids_for_display_granularity` selects the focused pane for the tab.
|
||||
4. `PaneProps::new` reads pane A's `custom_vertical_tabs_title`.
|
||||
5. The focused-session row renders pane A's custom name as the main title.
|
||||
6. Double-clicking the title still dispatches `RenameTab`, because the row is tab-scoped for rename actions in Tabs mode.
|
||||
|
||||
## Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User[User right-clicks pane row] --> RowHandler[Vertical Tabs row right-click handler]
|
||||
RowHandler --> Menu[Workspace pane-targeted context menu]
|
||||
Menu --> RenamePane[WorkspaceAction::RenamePane]
|
||||
RenamePane --> Editor[pane_rename_editor active]
|
||||
Editor --> Commit[Enter / blur commits]
|
||||
Commit --> PaneConfig[PaneConfiguration.custom_vertical_tabs_title]
|
||||
PaneConfig --> RowTitle[Vertical Tabs row title]
|
||||
PaneConfig --> Snapshot[LeafSnapshot.custom_vertical_tabs_title]
|
||||
Snapshot --> SQLite[pane_leaves.custom_vertical_tabs_title]
|
||||
```
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
### Accidentally renaming tabs from pane rows
|
||||
|
||||
Risk: The existing tab-group right-click handler may also fire after a pane-row right-click.
|
||||
|
||||
Mitigation: Add row-level right-click handling that stops propagation, or gate the group handler so handled child events do not open the tab context menu. Verify by right-clicking different pane rows in a multi-pane tab.
|
||||
|
||||
### Custom pane name leaking into pane headers or sidecar
|
||||
|
||||
Risk: Reusing `PaneConfiguration.title` or mutating `PaneProps.title` would affect headers and detail sidecar content.
|
||||
|
||||
Mitigation: Store custom pane title in a separate `PaneConfiguration` field, keep `PaneProps.title` generated, and only apply custom names in row title/search helpers.
|
||||
|
||||
### Losing names when panes move
|
||||
|
||||
Risk: A `PaneGroup`-level `HashMap<PaneId, String>` would not naturally move with pane content when a pane is dragged to another tab.
|
||||
|
||||
Mitigation: Store live custom names on `PaneConfiguration`, which is owned by pane content and moves with it.
|
||||
|
||||
### Persistence migration churn
|
||||
|
||||
Risk: Adding a column to `pane_leaves` touches central app-state persistence.
|
||||
|
||||
Mitigation: Keep the field nullable, add a focused migration, update Diesel schema/models, and add a SQLite round-trip test for a named pane.
|
||||
|
||||
### Saving pre-edit rather than committed state
|
||||
|
||||
Risk: If app-state persistence runs only when `RenamePane` opens the editor, the committed title may not be saved.
|
||||
|
||||
Mitigation: Verify the commit path triggers app-state persistence after setting `custom_vertical_tabs_title`; explicitly emit `AppStateChanged` or dispatch the save action if needed.
|
||||
|
||||
### `View as = Tabs` precedence confusion
|
||||
|
||||
Risk: Focused-session rows are tab-scoped for rename but pane-scoped for display metadata.
|
||||
|
||||
Mitigation: Keep title rendering precedence explicit and add tests/manual validation for the edge case where a named active pane appears in `View as = Tabs` while double-click still opens tab rename.
|
||||
|
||||
## Testing and validation
|
||||
|
||||
### Unit tests
|
||||
|
||||
Add/update tests in `app/src/workspace/view/vertical_tabs_tests.rs` for pure helpers:
|
||||
|
||||
- custom pane title search fragments include the custom name and generated metadata
|
||||
- duplicate custom/generated search fragments are not duplicated
|
||||
- title precedence chooses custom pane title over generated `Pane title as`
|
||||
- Summary search fragments do not include custom pane names
|
||||
|
||||
Add/update workspace action tests in `app/src/workspace/action_tests.rs`:
|
||||
|
||||
- `RenamePane` should save workspace state, or add coverage for the chosen post-commit save path
|
||||
- vertical-tabs settings actions remain unaffected
|
||||
|
||||
Add persistence coverage in `app/src/persistence/sqlite_tests.rs`:
|
||||
|
||||
- a `LeafSnapshot` with `custom_vertical_tabs_title: Some("server")` round-trips through SQLite
|
||||
- `None` round-trips for existing unnamed panes
|
||||
|
||||
If snapshot literal churn is large, add small helper constructors for test snapshots rather than duplicating the new field everywhere.
|
||||
|
||||
### Manual validation
|
||||
|
||||
- In `View as = Panes`, right-click a non-focused pane row, choose `Rename pane`, and verify the right-clicked pane gets the editor.
|
||||
- In `View as = Panes`, double-click title text and verify pane rename starts; double-clicking tab header still renames the tab.
|
||||
- Set `Pane title as` to each option and verify a custom pane name remains the main title.
|
||||
- Clear a custom pane name by submitting whitespace and verify generated title behavior returns.
|
||||
- Switch to `View as = Tabs` / `Focused session`; focus a named pane and verify the representative row shows the custom name.
|
||||
- In `View as = Tabs`, double-click the representative row title and verify it opens tab rename, not pane rename.
|
||||
- Hover a named pane row and verify details sidecar content remains generated.
|
||||
- Move a named pane to another tab and verify the name moves with it.
|
||||
- Restart Warp and verify restored panes keep their custom names.
|
||||
|
||||
### Suggested command validation
|
||||
|
||||
After implementation, run targeted checks before broader presubmit:
|
||||
|
||||
```bash
|
||||
cargo test -p warp_app vertical_tabs
|
||||
cargo test -p warp_app persistence::sqlite_tests
|
||||
cargo test -p warp_app workspace::action_tests
|
||||
```
|
||||
|
||||
Before opening or updating a PR, follow repository rules for full validation with the prescribed fmt/clippy commands or presubmit flow. Do not run `cargo fmt --all` or file-scoped `cargo fmt`.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Add a reset pane name menu item if users need parity with `Reset tab name`.
|
||||
- Add telemetry for pane rename usage if product analytics needs it.
|
||||
- Consider keyboard-accessible pane rename entry points if a keyboard navigation audit identifies a gap.
|
||||
- Consider whether tab configs should encode custom pane names separately from session restoration. This spec only requires persisted workspace restoration.
|
||||
Reference in New Issue
Block a user