24 KiB
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
PaneConfigurationand terminal metadata View as = Tabsfocused-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)—WorkspaceStateand the existingtab_being_renamedstate pattern.app/src/workspace/view.rs (1000-1260)— construction and event handling fortab_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 viaTabData::menu_items.app/src/tab.rs (248-320)— tab context menu items, includingRename tabandReset 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 wherePane title asis 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 andPaneConfigurationEvent::TitleUpdatedpropagation to workspace.app/src/pane_group/mod.rs (4596-4735)— tab-leveldisplay_title,set_title, andclear_title.app/src/pane_group/mod.rs (3995-4138)— pane move APIs; moved pane contents preserve theirPaneConfiguration.app/src/pane_group/mod.rs (1450-1955)— restore eachLeafSnapshotinto a concrete pane andPaneId.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)— savingLeafSnapshottopane_leaves.app/src/persistence/sqlite.rs (2355-2675)— readingpane_leavesback intoLeafSnapshot.crates/persistence/src/schema.rs (300-304)— currentpane_leavesschema.crates/persistence/src/model.rs (371-393)—PaneLeaf/NewPanepersistence 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 editingWorkspaceState::tab_being_renamedto track the active editor target- one
tab_rename_editor: ViewHandle<EditorView>onWorkspace PaneGroup::custom_title: Option<String>for persisted tab-level title stateTabSnapshot.custom_titleand thetabs.custom_titleSQLite 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:
titlesubtitletyped- focus/selection state
- optional
display_title_overridefor 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:
custom_vertical_tabs_title: Option<String>,
Add APIs:
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
Nonefor 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:
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:
pub struct LeafSnapshot {
pub is_focused: bool,
pub custom_vertical_tabs_title: Option<String>,
pub contents: LeafContents,
}
Snapshot flow:
- In
PaneGroup::snapshot_for_node, readpane.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_leafcreates a real pane andPaneId, applyleaf.custom_vertical_tabs_titleto the restored pane'sPaneConfiguration. - For deferred AI document panes, preserve the title in the deferred
LeafSnapshotand 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 TEXTtopane_leaves. - Update
crates/persistence/src/schema.rsfor the new nullable column. - Update
crates/persistence/src/model.rs:PaneLeafgetscustom_vertical_tabs_title: Option<String>NewPanegetscustom_vertical_tabs_title: Option<String>
- Update
save_pane_stateto writesnapshot.custom_vertical_tabs_title.clone(). - Update
read_nodeto populateLeafSnapshot.custom_vertical_tabs_title. - Update tests that construct
LeafSnapshotliterals.
3. Add pane rename workspace state and editor
Add pane rename state alongside tab rename state:
pane_being_renamed: Option<PaneViewLocator>,
Add methods on WorkspaceState mirroring the tab helpers:
is_pane_being_renamedset_pane_being_renamedclear_pane_being_renamedpane_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:
EnterorBlurredcommitsEscapecancels- 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_modalsclears both states
4. Add pane rename actions
Add a new action:
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:
- find the target
PaneGroupbypane_group_id - verify the pane still exists in that group
- activate the tab containing the pane if needed
- focus the target pane
- prefill
pane_rename_editorwith:- existing custom pane name, if present
- otherwise the currently displayed generated pane-row title
- set
pane_being_renamed - 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_buttonsstill dispatchesToggleTabRightClickMenuwithTabContextMenuAnchor::VerticalTabsKebabTabData::menu_itemsstill exposes tab-levelRename tab
For pane rows in View as = Panes, add row-specific right-click handling:
- add
on_right_clicktorender_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:
WorkspaceAction::ToggleVerticalTabsPaneContextMenu {
tab_index: usize,
pane: PaneViewLocator,
position: Vector2F,
}
Implementation options:
- Reuse
tab_right_click_menuandshow_tab_right_click_menuby extending the stored context with an optional pane target. - Or add a separate
vertical_tabs_pane_context_menuview 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 toWorkspaceAction::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:
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 withon_double_clickdispatchingRenamePane - 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_elementcontinues usingrenamable_tab_indexforView as = Tabsrender_group_headercontinues dispatchingRenameTab- horizontal
TabComponentremains unchanged
7. Add custom-title precedence to row rendering only
Extend PaneProps with:
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:
- active tab rename editor, when
props.is_tab_being_renamed - active pane rename editor, when
props.is_pane_being_renamed props.custom_vertical_tabs_title- tab-level
props.display_title_override - 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_overlayrender_detail_sectionbuild_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_titlewhen 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.titlegenerated - store custom pane name in a separate field
- ensure
render_detail_sectionand 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
- User right-clicks a pane row.
- Row-level right-click handler dispatches
ToggleVerticalTabsPaneContextMenuwith the targetPaneViewLocator. - Workspace builds a context menu whose rename item dispatches
RenamePane(locator). RenamePaneverifies the pane still exists, focuses it, computes the editor prefill, and setspane_being_renamed.- Vertical Tabs re-renders the target row title slot as
pane_rename_editor. - User presses
Enteror blurs. - Workspace commits the trimmed name into the target pane's
PaneConfiguration. - Vertical Tabs re-renders the row with
custom_vertical_tabs_title. - App state is saved with the custom name on that pane's
LeafSnapshot.
View as = Tabs display after a pane was named
- User previously named pane A in
View as = Panes. - User switches to
View as = TabswithTab item = Focused session. pane_ids_for_display_granularityselects the focused pane for the tab.PaneProps::newreads pane A'scustom_vertical_tabs_title.- The focused-session row renders pane A's custom name as the main title.
- Double-clicking the title still dispatches
RenameTab, because the row is tab-scoped for rename actions in Tabs mode.
Diagram
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:
RenamePaneshould 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
LeafSnapshotwithcustom_vertical_tabs_title: Some("server")round-trips through SQLite Noneround-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, chooseRename 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 asto 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:
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.