Files

20 KiB
Raw Permalink Blame History

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:5159unified_new_session_menu_items() builds the menu (shared by both horizontal and vertical tabs)
  • app/src/workspace/view.rs:5280open_tab_configs_menu() opens the menu, takes is_vertical_tabs param for width
  • app/src/workspace/view.rs:7838update_new_session_sidecar() dispatches sidecar content based on hovered item
  • app/src/workspace/view.rs:7675configure_terminal_new_session_sidecar() (Windows-only, to be removed)
  • app/src/workspace/view.rs:7731configure_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:966new_session_sidecar_menu field (existing Menu-based sidecar)
  • app/src/workspace/view.rs:1650 — sidecar menu construction in build_menus()
  • app/src/workspace/view/vertical_tabs.rs:1031render_new_tab_button() for vertical tabs (dispatches same ToggleNewSessionMenu action)
  • app/src/workspace/view/vertical_tabs.rs:766VERTICAL_TABS_ADD_TAB_POSITION_ID

Tab config data model:

  • app/src/tab_configs/tab_config.rs:128TabConfig struct (no source_path field)
  • app/src/user_config/util.rs:167parse_tab_config_dir_entry() (path known here, only stored for errors)
  • app/src/user_config/mod.rs:105WarpConfig::tab_configs() accessor
  • app/src/user_config/native.rs:248load_tab_configs() and filesystem watcher

DefaultSessionMode setting:

  • app/src/settings/ai.rs:253DefaultSessionMode enum (Terminal | Agent, derives Copy, EnumIter)
  • app/src/settings/ai.rs:1087 — stored as default_session_mode_internal in AISettings
  • app/src/settings/ai.rs:1227AISettings::default_session_mode() accessor (gates on AI enabled)
  • app/src/settings_view/features_page.rs:3273update_default_session_mode_dropdown() builds dropdown from iter()

Cmd+T flow:

  • app/src/workspace/view.rs:18317AddDefaultTab handler (the Cmd+T entry point; routes based on DefaultSessionMode)
  • app/src/app_menus.rs:1084open_new_default_tab_or_window() (macOS native menu callback; always dispatches CustomAction::NewTabAddDefaultTab)
  • app/src/workspace/view.rs:9637add_new_session_tab_with_default_mode() (checks DefaultSessionMode)
  • app/src/workspace/view.rs:5430open_tab_config() (opens a tab config, shows params modal if needed)

Editor setting for opening files:

  • app/src/util/openable_file_type.rs:97resolve_file_target() (uses open_file_editor)
  • app/src/util/openable_file_type.rs:112resolve_file_target_with_editor_choice() (accepts explicit editor choice)
  • app/src/util/file/external_editor/settings.rs:66open_code_panels_file_editor setting
  • app/src/workspace/view.rs:5469create_and_open_new_tab_config() (currently uses wrong setting)

Confirmation dialog prior art:

  • app/src/workspace/close_session_confirmation_dialog.rsCloseSessionConfirmationDialog pattern

Windows shell listing:

  • app/src/workspace/view.rs:5191#[cfg(target_os = "windows")] Terminal submenu parent
  • app/src/workspace/view.rs:7675configure_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:

  1. Agent (with Cmd+T shortcut label if default is Agent)
  2. Terminal (submenu on Windows, regular item elsewhere; Cmd+T shortcut if default is Terminal)
  3. Cloud Oz
  4. User tab configs (from WarpConfig::tab_configs())
  5. 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, in SessionSettings) — 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 whatever NewSessionShell is set to.
  • Agent → opens agent view. NewSessionShell irrelevant.
  • CloudAgent → opens ambient agent tab. NewSessionShell irrelevant.
  • TabConfig → opens the stored tab config. NewSessionShell irrelevant (config defines its own panes/commands).

"Make default" from the sidecar:

  • Terminal item → sets DefaultSessionMode::Terminal. Doesn't touch NewSessionShell.
  • A specific shell (Windows, e.g., PowerShell) → sets DefaultSessionMode::Terminal and updates NewSessionShell to that shell. Now Cmd+T → terminal → PowerShell.
  • Agent / Cloud Oz → sets DefaultSessionMode to Agent / CloudAgent. Doesn't touch NewSessionShell.
  • A user tab config → sets DefaultSessionMode::TabConfig + stores config path in default_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 AddTabAddDefaultTab 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). Checks DefaultSessionMode and 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:

  1. TabConfig → call resolved_default_tab_config(). If found, call open_tab_config(). If missing, clear to Terminal and fall through.
  2. CloudAgent → call add_ambient_agent_tab().
  3. Agent / Terminal → existing behavior (add_terminal_tab internally 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::NewTabAddDefaultTab.

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:

  1. DefaultSessionMode::TabConfig → attach the shortcut label to the matching tab config's menu item (matched by source_path).
  2. DefaultSessionMode::Agent → attach to the Agent item (existing behavior).
  3. DefaultSessionMode::CloudAgent → attach to the Cloud Oz item.
  4. 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

  1. User hovers over a tab config item in the dropdown (works identically in horizontal and vertical tabs).
  2. handle_new_session_menu_eventItemHoveredupdate_new_session_sidecar().
  3. Match identifies the item as a user tab config (by checking if the action is SelectTabConfig).
  4. Populates tab_config_action_sidecar with the config's name, path, and all three buttons.
  5. Sets show_tab_config_action_sidecar = true, hides show_new_session_sidecar.
  6. Render places the sidecar overlay anchored to the hovered item.

"Make default" for a tab config

  1. User clicks "Make default" in the action sidecar.
  2. Sidecar dispatches WorkspaceAction::TabConfigSidecarMakeDefault { mode: TabConfig, tab_config_path: Some(path), shell: None }.
  3. Workspace handler sets default_session_mode_internal to TabConfig and default_tab_config_path to the file path.
  4. Menu closes. Next time it opens, the Cmd+T shortcut label appears on that config's menu item.
  5. Settings dropdown updates via the AISettingsChangedEvent subscription.

Cmd+T with tab config default

  1. User presses Cmd+T → native menu dispatches CustomAction::NewTabAddDefaultTab action.
  2. Handler checks DefaultSessionMode::TabConfig.
  3. Reads default_tab_config_path, finds matching config in WarpConfig::tab_configs() via resolved_default_tab_config().
  4. Calls open_tab_config() → shows params modal if needed, else opens directly.
  5. If config file is missing, clears settings to Terminal and opens a normal terminal tab.

"Remove" flow

  1. User clicks "Remove" in the sidecar.
  2. Sidecar dispatches WorkspaceAction::TabConfigSidecarRemoveConfig { name, path }.
  3. Workspace opens RemoveTabConfigConfirmationDialog with the config name and path.
  4. User confirms → dialog emits Confirm { path }.
  5. Handler deletes the file. If removed config was the default, clears default_tab_config_path and sets mode to Terminal.
  6. Filesystem watcher reloads configs; menu updates on next open.

Risks and mitigations

  • Backward compat for DefaultSessionMode serialization: Adding a TabConfig variant changes serialized values. Old clients reading a TabConfig value 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_path is populated after parsing.
    • DefaultSessionMode::TabConfig + default_tab_config_path round-trips through serialization.
    • resolved_default_tab_config() returns None when path is empty, missing, or not in loaded configs.
    • resolve_file_target_with_editor_choice is used with open_code_panels_file_editor for 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::Terminal and Agent behavior 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).