15 KiB
Onboarding Tab Config Modal — Tech Spec
Product spec: specs/APP-3680/PRODUCT.md
Current State
Onboarding completion: handle_agent_onboarding_event (app/src/root_view.rs:2062) handles OnboardingCompleted. It applies settings, then calls start_agent_onboarding_tutorial on the workspace, which dispatches the legacy guided-tour flow to the terminal view.
Tab replacement: There is no existing "replace current tab" API. add_tab_with_pane_layout (app/src/workspace/view.rs:8157) always adds a new tab. close_tab (app/src/workspace/view.rs:7805) removes by index.
Tab config TOML writing: create_and_open_new_tab_config (app/src/workspace/view.rs:4739) writes the template to ~/.warp/tab_configs/ via find_unused_tab_config_path (app/src/user_config/mod.rs:173). The filesystem watcher (app/src/user_config/native.rs:266) auto-reloads tab configs.
DefaultSessionMode: DefaultSessionMode (app/src/settings/ai.rs:252) has Terminal and Agent variants. Set during onboarding via apply_agent_settings (app/src/settings/onboarding.rs:122).
Feature flags: TabConfigs, AgentOnboarding, OpenWarpNewSettingsModes, and AgentView are the relevant flags (warp_core/src/features.rs). We'll add a new flag for this modal.
Relevant Code
app/src/root_view.rs:2062—handle_agent_onboarding_event, whereOnboardingCompletedis handledapp/src/workspace/view/onboarding.rs—OnboardingTutorialenum andstart_agent_onboarding_tutorialapp/src/workspace/view.rs:8157—add_tab_with_pane_layoutapp/src/workspace/view.rs:4688-4734—open_tab_config_with_paramsandopen_tab_configapp/src/workspace/view.rs:4739—create_and_open_new_tab_configapp/src/tab_configs/tab_config.rs—TabConfig,TabConfigPaneNode,TabConfigPaneType,render_tab_configapp/src/user_config/mod.rs:173—find_unused_tab_config_pathapp/src/settings/onboarding.rs:122—apply_agent_settings, whereDefaultSessionModeis setapp/src/settings/ai.rs:252—DefaultSessionModeenumapp/src/terminal/cli_agent.rs:94—CLIAgent::command_prefix()app/src/modal.rs—Modal<T>andModalViewState<T>patternapp/src/workspace/one_time_modal_model.rs— one-time modal tracking pattern
Proposed Changes
1. New feature flag
No new feature flag needed. Gate the modal behind both FeatureFlag::OpenWarpNewSettingsModes (this is the new onboarding path) and FeatureFlag::TabConfigs (the modal produces a tab config, so the tab config system must be enabled). Both flags must be on for the modal to appear. When either is off, the old onboarding flow runs unchanged.
2. Add Serialize to tab config types
TabConfigParamType already derives both Serialize and Deserialize. Add Serialize to:
TabConfigPaneType— so pane type is included in serialized TOMLTabConfigPaneNode— so pane nodes can be serializedTabConfig— so full configs can be written to disk
These are simple data structs — adding Serialize is a natural extension that enables writing tab configs programmatically (not just reading them from TOML).
3. SessionType enum
Add a small enum in app/src/tab_configs/mod.rs (or a new submodule) that reuses CLIAgent:
pub enum SessionType {
Terminal,
Oz,
CliAgent(CLIAgent),
}
This wraps the existing CLIAgent (app/src/terminal/cli_agent.rs:82) and adds Terminal/Oz as first-class variants. SessionType provides helpers:
command_prefix() -> Option<&str>— delegates toCLIAgent::command_prefix()for CLI agents,Nonefor Terminal/Oz.icon() -> Icon— delegates toCLIAgent::icon(), withIcon::Terminalfor Terminal andIcon::Ozfor Oz.display_name() -> &str— delegates toCLIAgent::display_name()for CLI agents.pill_label() -> &str— short label for the modal pills (e.g., "Claude" instead of "Claude Code").
4. TabConfig builder: build_tab_config
Add a function in app/src/tab_configs/session_config.rs:
fn build_tab_config(
session_type: &SessionType,
directory: &Path,
enable_worktree: bool,
) -> TabConfig
This builds a TabConfig with a single TabConfigPaneNode using the new flat [[panes]] schema. The logic:
- Sets
name = "Startup Config" - Creates a single pane with
id = "main",cwdset to the absolute directory path - Sets
pane_typetoTabConfigPaneType::Agentfor Oz,TabConfigPaneType::Terminalfor Terminal and CLI agents - Appends worktree commands +
worktree_branch_nameparam whenenable_worktreeis true, withworktree_name_autogenerated = true - Appends
session_type.command_prefix()to commands when it's a CLI agent - Sets
title = "{{worktree_branch_name}}"when worktree is enabled
Pure function, easily unit-tested. The existing render_tab_config and TabConfig::default_param_values work on its output unchanged.
5. write_tab_config
Add a function in app/src/tab_configs/ to serialize and write:
fn write_tab_config(config: &TabConfig, dir: &Path) -> Result<PathBuf>
Uses toml::to_string_pretty(config) (now possible with Serialize), finds an unused path via the shared find_unused_toml_path(dir, "startup_config") helper (generalized from find_unused_tab_config_path in user_config/mod.rs), and writes. Returns the path. The filesystem watcher auto-reloads.
6. Modal view: SessionConfigModal
Create app/src/tab_configs/session_config_modal.rs. This is a self-contained View that renders the Figma layout:
- Session type pill buttons using
Wrap::row()for flex-wrap (hardcoded list in order: Built in agent (Oz), Claude, Codex, Gemini, Terminal) - Directory picker button (opens native
FilePickerConfiguration::folders_only()), displays~viawarp_util::path::user_friendly_path(), left-aligned text with semibold weight, no folder icon - "Enable worktree support" checkbox (disabled when directory is not a git repo)
- "Get warping" button using
ActionButtonwithPrimaryThemeandwith_full_width(true), includes Enter keystroke badge viawith_keybinding()
The modal always saves a tab config — there is no "Save as tab config" checkbox.
State: The modal holds:
selected_session_type: SessionTypeselected_directory: PathBuf(default: home dir)is_git_repo: bool(recomputed on directory change viastd::path::Path::join(".git").is_dir())enable_worktree: boolMouseStateHandlefor each interactive element
Output struct: The modal collects its inputs into a plain struct:
pub struct SessionConfigSelection {
pub session_type: SessionType,
pub directory: PathBuf,
pub enable_worktree: bool,
}
Event: The modal emits:
pub enum SessionConfigModalEvent {
Completed(SessionConfigSelection),
Dismissed,
}
The caller converts the selection into a TabConfig via build_tab_config when needed. The modal does not know what the caller does with the selection.
Git repo detection: When the directory changes, check if selected_directory.join(".git").is_dir() or walk up parents looking for .git. If not a git repo, set is_git_repo = false, force enable_worktree = false, and render the worktree checkbox as disabled with a tooltip.
7. Hosting the modal in Workspace
Add to Workspace:
session_config_modal: ModalViewState<Modal<SessionConfigModal>>,
Follow the same pattern as tab_config_params_modal (app/src/workspace/view.rs:4723). The workspace subscribes to SessionConfigModalEvent and handles both variants.
8. Handling SessionConfigModalEvent::Completed
The workspace handler in a new method handle_session_config_completed:
Step 1: Apply DefaultSessionMode. If session_type == Oz, set DefaultSessionMode::Agent. Otherwise, set DefaultSessionMode::Terminal. (Only when the feature flag is on — when off, the existing onboarding path handles this.)
Step 2: Build a TabConfig. Call build_tab_config(&selection.session_type, &selection.directory, selection.enable_worktree). This produces the canonical TabConfig regardless of the save path.
Step 3: Open the tab. Always save: call write_tab_config(&config, &tab_configs_dir()) to persist the TOML, then call open_tab_config(config), which handles the params modal flow for worktree configs (user gets to pick branch name). If write fails, fall back to open_tab_config_with_params without persisting.
Agent view entry for Oz is handled automatically by PaneMode::Agent in the tab config pane node — pane_tree_from_template enters agent view when it sees PaneMode::Agent. No manual enter_agent_view_on_active_tab() call is needed.
Step 4: Replace current tab.
Step 4: Replace current tab. After adding the new tab, use remove_tab directly (not close_tab) to remove the old empty tab. close_tab would trigger a window close when it's the last tab, but by this point there are always 2+ tabs since the new one was just added. The old tab is at old_tab_index (captured before step 3).
9. Triggering the modal after onboarding
In handle_agent_onboarding_event (app/src/root_view.rs:2080), after the existing OnboardingCompleted handling, when both FeatureFlag::OpenWarpNewSettingsModes.is_enabled() and FeatureFlag::TabConfigs.is_enabled():
Instead of calling start_agent_onboarding_tutorial directly, dispatch a new WorkspaceAction::ShowSessionConfigModal. The workspace opens the modal. On Completed, the workspace replaces the tab and applies settings. On Dismissed, fall through to the existing tutorial path (or just leave the empty tab).
When either flag is off (old onboarding), the existing path (start_agent_onboarding_tutorial) runs unchanged.
End-to-End Flow
- User completes onboarding slides →
OnboardingCompletedfires. root_viewapplies settings, transitions toTerminalstate with the workspace.root_viewdispatchesWorkspaceAction::ShowSessionConfigModal(flag-gated).- Workspace opens
session_config_modalas a centered overlay. - User selects session type, picks directory, optionally toggles worktree, clicks "Get warping".
- Modal emits
SessionConfigModalEvent::Completed(selection). - Workspace calls
handle_session_config_completed:- Sets
DefaultSessionModeif Oz. - Calls
build_tab_configto produce aTabConfig. - Calls
write_tab_configthenopen_tab_config(always saves). - Closes the old empty tab.
- Sets
- Modal is dismissed. User is in their configured session.
Risks and Mitigations
Risk: Breaking existing onboarding. All new behavior is gated behind both FeatureFlag::OpenWarpNewSettingsModes and FeatureFlag::TabConfigs. When either is off, handle_agent_onboarding_event follows the identical code path as today. No changes to OnboardingTutorial, SelectedSettings, or apply_onboarding_settings.
Risk: Tab index math when replacing. Closing the wrong tab index would lose user work. Mitigated by: the old tab is always empty (just created by onboarding), and we close with skip_confirmation = true. We also use the tab index arithmetic described above, which can be validated in tests.
Risk: Git repo detection on directory change. Checking .git is synchronous I/O. For the onboarding modal (called once), this is acceptable. If reused in a hot path later, it should be made async.
Risk: Adding Serialize to TabConfig. Low risk — these are plain data structs with simple fields. Adding Serialize alongside existing Deserialize is a standard pattern. No behavioral change to existing deserialization paths.
Testing and Validation
build_tab_config (unit tests)
These enforce the TOML generation rules from the product spec:
- Terminal + directory, no worktree →
TabConfigwithcwdset, empty commands, no params. - CLI agent (Claude) + directory, no worktree → commands =
["claude"], no params. - Terminal + directory + worktree → commands include worktree creation + cd, params contain
worktree_branch_namewith default"my-feature-branch", title ="{{worktree_branch_name}}". - CLI agent (Gemini) + directory + worktree → commands include worktree creation + cd +
"gemini"(in that order), params containworktree_branch_name. - Oz + directory, no worktree →
cwdset,pane_type = Agent, no commands, no params. - Oz + directory + worktree →
pane_type = Agentwith worktree commands, no agent CLI command. - Directory path is always absolute in
panes[0].cwd.
TOML round-trip (unit tests)
- For each
build_tab_configoutput, serialize viatoml::to_string_pretty, deserialize back asTabConfig, verify all fields match. - Validates that
SerializeonTabConfigproduces TOML that the existingDeserializepath can read — catches any drift between the two.
write_tab_config (unit tests with temp dir)
- Write to an empty temp dir → file is
startup_config.toml. - Write again → file is
startup_config_1.toml. - Write a third time → file is
startup_config_2.toml. - Written file content deserializes to a valid
TabConfigmatching the input. - Directory is created if it doesn't exist.
SessionType helpers (unit tests)
SessionType::Terminal.command_prefix()→None.SessionType::Oz.command_prefix()→None.SessionType::CliAgent(CLIAgent::Claude).command_prefix()→Some("claude").- Display names and icons return the expected values for each variant.
render_tab_config integration (unit tests)
These verify the full pipeline from build_tab_config → render_tab_config produces the correct PaneTemplateType:
- Terminal + directory →
PaneTemplatewith correctcwd, empty commands. - CLI agent + directory →
PaneTemplatewith correctcwd, commands =["claude"]. - Worktree config with default param values → commands have
"my-feature-branch"substituted in.
Git repo detection (unit tests with temp dir)
- Create a temp dir with
.git/→is_git_reporeturns true. - Temp dir without
.git/→ returns false. - Switching from a git dir to a non-git dir forces
enable_worktreeto false.
DefaultSessionMode (unit test or integration)
- Selecting Oz sets
DefaultSessionMode::Agent. - Selecting Terminal sets
DefaultSessionMode::Terminal. - Selecting a CLI agent sets
DefaultSessionMode::Terminal. - When
OpenWarpNewSettingsModesis off,DefaultSessionModeis not touched by this code path.
Feature flag gating (integration)
- When either
OpenWarpNewSettingsModesorTabConfigsis off,OnboardingCompletedfollows the old tutorial path — modal is never shown. - When both
OpenWarpNewSettingsModesandTabConfigsare on,OnboardingCompleteddispatchesShowSessionConfigModal.
UI verification
- Compare rendered modal against Figma mock.
- Verify worktree checkbox is visually disabled when directory is not a git repo.
Follow-ups
- Worktree name generation: Replace hardcoded
"my-feature-branch"once Moira's worktree name generation is ready. - Reusability: Surface the modal from the + tab menu or command palette.
- Async git detection: If the modal is reused in hot paths, make
.gitdetection async. - Programmatic tab config editing: With
SerializeonTabConfig, future features could read → modify → write tab configs (e.g., a tab config editor UI).