20 KiB
Sidecars for Tab Config Menu — Tech Spec
Linear: APP-3886
Product spec: specs/APP-3886/PRODUCT.md
Problem
The tab configs menu has no per-item management actions. Users cannot set a default, edit, or remove tab configs from within the menu. The DefaultSessionMode setting only supports Terminal | Agent, with no way to select a tab config. The sidecar infrastructure exists for worktree/shell submenus but not for action panels.
Relevant code
Tab configs menu & sidecar:
app/src/workspace/view.rs:5159—unified_new_session_menu_items()builds the menu (shared by both horizontal and vertical tabs)app/src/workspace/view.rs:5280—open_tab_configs_menu()opens the menu, takesis_vertical_tabsparam for widthapp/src/workspace/view.rs:7838—update_new_session_sidecar()dispatches sidecar content based on hovered itemapp/src/workspace/view.rs:7675—configure_terminal_new_session_sidecar()(Windows-only, to be removed)app/src/workspace/view.rs:7731—configure_worktree_new_session_sidecar()app/src/workspace/view.rs:20291— dropdown menu overlay rendering (different anchoring for vertical vs horizontal)app/src/workspace/view.rs:20331— sidecar overlay rendering (shared by both modes, anchored to hovered item label)app/src/workspace/view.rs:966—new_session_sidecar_menufield (existingMenu-based sidecar)app/src/workspace/view.rs:1650— sidecar menu construction inbuild_menus()app/src/workspace/view/vertical_tabs.rs:1031—render_new_tab_button()for vertical tabs (dispatches sameToggleNewSessionMenuaction)app/src/workspace/view/vertical_tabs.rs:766—VERTICAL_TABS_ADD_TAB_POSITION_ID
Tab config data model:
app/src/tab_configs/tab_config.rs:128—TabConfigstruct (nosource_pathfield)app/src/user_config/util.rs:167—parse_tab_config_dir_entry()(path known here, only stored for errors)app/src/user_config/mod.rs:105—WarpConfig::tab_configs()accessorapp/src/user_config/native.rs:248—load_tab_configs()and filesystem watcher
DefaultSessionMode setting:
app/src/settings/ai.rs:253—DefaultSessionModeenum (Terminal | Agent, derivesCopy,EnumIter)app/src/settings/ai.rs:1087— stored asdefault_session_mode_internalinAISettingsapp/src/settings/ai.rs:1227—AISettings::default_session_mode()accessor (gates on AI enabled)app/src/settings_view/features_page.rs:3273—update_default_session_mode_dropdown()builds dropdown fromiter()
Cmd+T flow:
app/src/workspace/view.rs:18317—AddDefaultTabhandler (the Cmd+T entry point; routes based onDefaultSessionMode)app/src/app_menus.rs:1084—open_new_default_tab_or_window()(macOS native menu callback; always dispatchesCustomAction::NewTab→AddDefaultTab)app/src/workspace/view.rs:9637—add_new_session_tab_with_default_mode()(checksDefaultSessionMode)app/src/workspace/view.rs:5430—open_tab_config()(opens a tab config, shows params modal if needed)
Editor setting for opening files:
app/src/util/openable_file_type.rs:97—resolve_file_target()(usesopen_file_editor)app/src/util/openable_file_type.rs:112—resolve_file_target_with_editor_choice()(accepts explicit editor choice)app/src/util/file/external_editor/settings.rs:66—open_code_panels_file_editorsettingapp/src/workspace/view.rs:5469—create_and_open_new_tab_config()(currently uses wrong setting)
Confirmation dialog prior art:
app/src/workspace/close_session_confirmation_dialog.rs—CloseSessionConfirmationDialogpattern
Windows shell listing:
app/src/workspace/view.rs:5191—#[cfg(target_os = "windows")]Terminal submenu parentapp/src/workspace/view.rs:7675—configure_terminal_new_session_sidecar()(lists shells)
Current state
Menu structure
unified_new_session_menu_items() builds the menu. This function is shared across both horizontal and vertical tab bar modes — the same items appear regardless of layout. The menu is opened via open_tab_configs_menu() which accepts is_vertical_tabs only to adjust the menu width (268px for vertical, default for horizontal).
Current order:
- Agent (with Cmd+T shortcut label if default is Agent)
- Terminal (submenu on Windows, regular item elsewhere; Cmd+T shortcut if default is Terminal)
- Cloud Oz
- User tab configs (from
WarpConfig::tab_configs()) - Separator + "New worktree config" (submenu) + "New Tab Config"
Sidecar positioning (horizontal vs vertical)
The dropdown menu is positioned differently in each mode:
- Horizontal tabs: anchored to
NEW_TAB_BUTTON_POSITION_ID(lower-left of the + button) - Vertical tabs: anchored to
VERTICAL_TABS_ADD_TAB_POSITION_ID(below the + button)
The sidecar overlay anchors to the hovered menu item's label text, so it's positioned the same way in both modes. The new action sidecar will use the same anchoring mechanism and work in both modes without special handling.
Note: The vertical tabs panel has its own separate "detail sidecar" (render_detail_sidecar in vertical_tabs.rs) that shows pane details when hovering rows in the panel. This is a completely different system from the new-session menu sidecar and is unrelated to this feature.
DefaultSessionMode
A Copy + EnumIter enum stored in AISettings. The settings dropdown iterates it. default_session_mode() returns Terminal when AI is disabled, otherwise returns the stored value.
TabConfig
Has no source_path field. The file path is available during parsing but discarded for successfully parsed configs.
Proposed changes
1. Add source_path to TabConfig
File: app/src/tab_configs/tab_config.rs
Add a skipped field:
#[serde(skip)]
pub source_path: Option<PathBuf>,
File: app/src/user_config/util.rs
In parse_tab_config_dir_entry(), populate source_path on successfully parsed configs:
Some(parsed.map(|mut config| {
config.source_path = Some(item.path().into());
config
}).map_err(...))
2. Extend DefaultSessionMode with TabConfig and CloudAgent variants + companion path setting
File: app/src/settings/ai.rs
Add CloudAgent and TabConfig variants to DefaultSessionMode:
pub enum DefaultSessionMode {
#[default]
Terminal,
Agent,
CloudAgent,
TabConfig,
}
This preserves Copy and EnumIter. No Shell variant is needed — shell-specific defaults are handled by the existing NewSessionShell setting (see "Setting interaction" below).
Add a companion setting in AISettings to store the tab config file path:
default_tab_config_path: DefaultTabConfigPath {
type: String,
default: String::new(),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
hierarchy: "general",
}
SyncToCloud::Never because tab config file paths are machine-local.
The companion is only read when mode is TabConfig. For all other modes it's ignored.
Add a helper on AISettings:
fn resolved_default_tab_config(&self, app: &AppContext) -> Option<TabConfig>
Reads default_tab_config_path, finds the matching TabConfig in WarpConfig::tab_configs() by source_path, and returns it. Returns None if the path is empty, the file doesn't exist, or the config isn't loaded (triggers fallback to Terminal).
Setting interaction: DefaultSessionMode × NewSessionShell
Two settings, two concerns:
DefaultSessionMode(Terminal | Agent | CloudAgent | TabConfig) — controls what kind of thing Cmd+T opens.NewSessionShell(existing, inSessionSettings) — controls which shell binary a terminal session uses. Only relevant when the mode resolves to opening a terminal.
How they interact on Cmd+T:
Terminal→ opens a terminal tab using whateverNewSessionShellis set to.Agent→ opens agent view.NewSessionShellirrelevant.CloudAgent→ opens ambient agent tab.NewSessionShellirrelevant.TabConfig→ opens the stored tab config.NewSessionShellirrelevant (config defines its own panes/commands).
"Make default" from the sidecar:
- Terminal item → sets
DefaultSessionMode::Terminal. Doesn't touchNewSessionShell. - A specific shell (Windows, e.g., PowerShell) → sets
DefaultSessionMode::Terminaland updatesNewSessionShellto that shell. Now Cmd+T → terminal → PowerShell. - Agent / Cloud Oz → sets
DefaultSessionModetoAgent/CloudAgent. Doesn't touchNewSessionShell. - A user tab config → sets
DefaultSessionMode::TabConfig+ stores config path indefault_tab_config_path.
Key invariant: NewSessionShell is always the authority for which shell a terminal uses. DefaultSessionMode never duplicates that.
3. Create action sidecar render function
New file: app/src/tab_configs/action_sidecar.rs
A free function render_action_sidecar() (not a View) that returns Box<dyn Element>. Called directly from the Workspace render method. This is simpler than a View since the sidecar has no internal state — it just reads the current item and settings to produce an element tree.
The function takes a struct describing what to show:
enum SidecarItemKind {
BuiltIn { name: String, default_mode: DefaultSessionMode, shell: Option<AvailableShell> },
UserTabConfig { config: TabConfig },
}
Button clicks dispatch WorkspaceAction variants directly (TabConfigSidecarMakeDefault, TabConfigSidecarEditConfig, TabConfigSidecarRemoveConfig), which the Workspace handles inline in handle_action.
File: app/src/tab_configs/mod.rs — add pub(crate) mod action_sidecar;
4. Add RemoveTabConfigConfirmationDialog
New file: app/src/tab_configs/remove_confirmation_dialog.rs
Follows the CloseSessionConfirmationDialog pattern (app/src/workspace/close_session_confirmation_dialog.rs):
- Title: "Remove tab config?"
- Body: "This will permanently delete {config_name} ({file_path})."
- Buttons: Cancel / Remove (destructive)
Events: RemoveTabConfigConfirmationEvent::Confirm { path } | Cancel
On confirm, delete the file from disk. The filesystem watcher handles menu refresh. If the removed config was the default, clear default_tab_config_path and set DefaultSessionMode back to Terminal.
5. Wire up action sidecar in Workspace
File: app/src/workspace/view.rs
Add new fields to Workspace:
tab_config_action_sidecar_item: Option<SidecarItemKind>,
tab_config_action_sidecar_mouse_states: SidecarMouseStates,
remove_tab_config_confirmation_dialog: ViewHandle<RemoveTabConfigConfirmationDialog>,
The sidecar is shown when tab_config_action_sidecar_item is Some. No separate bool is needed.
In update_new_session_sidecar(): Extend the match to handle all actionable items (Terminal, shell variants, Agent, Cloud Oz, user tab configs). For these, set tab_config_action_sidecar_item to Some(item_kind). For "New worktree config", keep existing behavior. For "New Tab Config" and separators, set it to None.
In render(): Add a second positioned overlay that calls render_action_sidecar() when tab_config_action_sidecar_item is Some (same positioning logic as the existing sidecar). This overlay uses the same OffsetPositioning::offset_from_save_position_element anchored to the hovered menu item label, so it works identically in both horizontal and vertical tabs modes.
Event handlers: WorkspaceAction::TabConfigSidecar* variants are handled directly in handle_action. Subscribe to RemoveTabConfigConfirmationEvent.
6. Rename AddTab → AddDefaultTab and route Cmd+T through it
Files: app/src/workspace/action.rs, app/src/workspace/view.rs, app/src/workspace/mod.rs, app/src/app_menus.rs
Rename WorkspaceAction::AddTab to WorkspaceAction::AddDefaultTab to clearly distinguish it from the explicit AddTerminalTab and AddAgentTab actions:
AddDefaultTab= "open whatever the user's default is" (the Cmd+T action). ChecksDefaultSessionModeand routes accordingly.AddTerminalTab= "always open a terminal, ignoring default" (explicit override, has its own keybinding).AddAgentTab= "always open an agent tab" (explicit override).
The AddDefaultTab handler checks the effective DefaultSessionMode:
TabConfig→ callresolved_default_tab_config(). If found, callopen_tab_config(). If missing, clear toTerminaland fall through.CloudAgent→ calladd_ambient_agent_tab().Agent/Terminal→ existing behavior (add_terminal_tabinternally respects Agent mode).
macOS native menu (Cmd+T routing):
On macOS, Cmd+T is handled by the native menu system, not the WarpUI keybinding system. The native menu's "New Terminal Tab" item holds Cmd+T for non-Agent modes; "New Agent Tab" holds it for Agent mode. Both callbacks ultimately dispatch through CustomAction::NewTab → AddDefaultTab.
The callback open_new_default_tab_or_window (app/src/app_menus.rs) always dispatches CustomAction::NewTab, which the binding system maps to WorkspaceAction::AddDefaultTab. This means Cmd+T always goes through the AddDefaultTab handler regardless of the current mode — the handler is the single place that routes based on DefaultSessionMode.
7. Flatten Windows Terminal shell items
File: app/src/workspace/view.rs
In unified_new_session_menu_items(), replace the #[cfg(target_os = "windows")] block (view.rs:5191) that creates a submenu parent with code that lists each AvailableShell as an individual top-level MenuItem with AddTabWithShell action.
Remove NewSessionSidecarKind::Terminal, configure_terminal_new_session_sidecar(), and related dead code.
8. Fix editor setting for tab config file opens
File: app/src/workspace/view.rs
In create_and_open_new_tab_config() (view.rs:5469), change:
let target = resolve_file_target(&path, settings, None);
to:
let target = resolve_file_target_with_editor_choice(
&path,
*settings.open_code_panels_file_editor,
*settings.prefer_markdown_viewer,
*settings.open_file_layout,
None,
);
Apply the same fix to save_current_tab_as_new_config() (view.rs:5500) and the OpenTabConfigErrorFile handler (view.rs:18250).
The "Edit config" button in the action sidecar will also use resolve_file_target_with_editor_choice with open_code_panels_file_editor.
9. Update settings dropdown
File: app/src/settings_view/features_page.rs
Replace the default_session_mode_dropdown: ViewHandle<Dropdown<FeaturesPageAction>> (features_page.rs:1222) with a FilterableDropdown<FeaturesPageAction>. The FilterableDropdown component (app/src/view_components/filterable_dropdown.rs) already supports search/filter, arrow key navigation, and the same DropdownItem API — it wraps a Menu with a search editor, so users can type to narrow down the list when many tab configs are present.
In update_default_session_mode_dropdown() (features_page.rs:3273), after the DefaultSessionMode::iter() items (Terminal, Agent), append an item for each loaded tab config from WarpConfig::tab_configs(), using the config name as the display label and dispatching a new FeaturesPageAction variant that sets both DefaultSessionMode::TabConfig and default_tab_config_path.
Subscribe to WarpConfigUpdateEvent::TabConfigs to rebuild the dropdown when configs change.
10. Cmd+T keybinding indicator in menu
File: app/src/workspace/view.rs
In unified_new_session_menu_items(), the Cmd+T shortcut label is currently assigned to either the Agent or Terminal item based on default_is_agent. Replace this with a comprehensive check of the effective default:
DefaultSessionMode::TabConfig→ attach the shortcut label to the matching tab config's menu item (matched bysource_path).DefaultSessionMode::Agent→ attach to the Agent item (existing behavior).DefaultSessionMode::CloudAgent→ attach to the Cloud Oz item.DefaultSessionMode::Terminal→ attach to the "Terminal" item.
Note: Per-shell shortcut label logic (i.e., showing Cmd+T on the specific shell item when a shell is made default on Windows) is not implemented in v1. The shortcut label always appears on the "Terminal" item when mode is Terminal, regardless of which shell was selected via "Make default".
End-to-end flow
Hovering a tab config in the menu
- User hovers over a tab config item in the dropdown (works identically in horizontal and vertical tabs).
handle_new_session_menu_event→ItemHovered→update_new_session_sidecar().- Match identifies the item as a user tab config (by checking if the action is
SelectTabConfig). - Populates
tab_config_action_sidecarwith the config's name, path, and all three buttons. - Sets
show_tab_config_action_sidecar = true, hidesshow_new_session_sidecar. - Render places the sidecar overlay anchored to the hovered item.
"Make default" for a tab config
- User clicks "Make default" in the action sidecar.
- Sidecar dispatches
WorkspaceAction::TabConfigSidecarMakeDefault { mode: TabConfig, tab_config_path: Some(path), shell: None }. - Workspace handler sets
default_session_mode_internaltoTabConfiganddefault_tab_config_pathto the file path. - Menu closes. Next time it opens, the Cmd+T shortcut label appears on that config's menu item.
- Settings dropdown updates via the
AISettingsChangedEventsubscription.
Cmd+T with tab config default
- User presses Cmd+T → native menu dispatches
CustomAction::NewTab→AddDefaultTabaction. - Handler checks
DefaultSessionMode::TabConfig. - Reads
default_tab_config_path, finds matching config inWarpConfig::tab_configs()viaresolved_default_tab_config(). - Calls
open_tab_config()→ shows params modal if needed, else opens directly. - If config file is missing, clears settings to
Terminaland opens a normal terminal tab.
"Remove" flow
- User clicks "Remove" in the sidecar.
- Sidecar dispatches
WorkspaceAction::TabConfigSidecarRemoveConfig { name, path }. - Workspace opens
RemoveTabConfigConfirmationDialogwith the config name and path. - User confirms → dialog emits
Confirm { path }. - Handler deletes the file. If removed config was the default, clears
default_tab_config_pathand sets mode toTerminal. - Filesystem watcher reloads configs; menu updates on next open.
Risks and mitigations
- Backward compat for
DefaultSessionModeserialization: Adding aTabConfigvariant changes serialized values. Old clients reading aTabConfigvalue will fail to deserialize and fall back to the default (Terminal). This is acceptable — the worst case is losing the default preference on downgrade. - Race between file deletion and watcher: After "Remove" deletes the file, there's a brief window where the config is still in
WarpConfig::tab_configs(). The watcher debounce handles this. The sidecar closes the menu on removal, so the user won't see a stale entry. - Large number of tab configs: The settings dropdown and menu will list all configs. No pagination is needed for v1, but configs with identical names are disambiguated by file path in the sidecar subtitle.
Testing and validation
- Unit tests:
TabConfig.source_pathis populated after parsing.DefaultSessionMode::TabConfig+default_tab_config_pathround-trips through serialization.resolved_default_tab_config()returnsNonewhen path is empty, missing, or not in loaded configs.resolve_file_target_with_editor_choiceis used withopen_code_panels_file_editorfor all tab config file opens.
- Integration / computer-use:
- Both horizontal and vertical tabs: Open the tab configs menu in each mode, hover items, verify sidecar appears and is positioned correctly.
- Make default → verify Cmd+T behavior and settings dropdown sync.
- Edit config → verify correct editor opens.
- Remove → verify confirmation dialog, file deletion, menu update, and default fallback.
- Keyboard navigation through menu → sidecar updates.
- Vertical tabs: verify the action sidecar doesn't conflict with the vertical tabs detail sidecar (they are independent systems).
- Regression:
- "New worktree config" repo-list sidecar unchanged.
- Existing
DefaultSessionMode::TerminalandAgentbehavior unchanged. - Vertical tabs detail sidecar (hover-to-preview pane details) unaffected.
Follow-ups
- Potential for tab config reordering in the menu.
- Richer sidecar content (preview of pane layout, param summary).