# 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`, where `OnboardingCompleted` is handled - `app/src/workspace/view/onboarding.rs` — `OnboardingTutorial` enum and `start_agent_onboarding_tutorial` - `app/src/workspace/view.rs:8157` — `add_tab_with_pane_layout` - `app/src/workspace/view.rs:4688-4734` — `open_tab_config_with_params` and `open_tab_config` - `app/src/workspace/view.rs:4739` — `create_and_open_new_tab_config` - `app/src/tab_configs/tab_config.rs` — `TabConfig`, `TabConfigPaneNode`, `TabConfigPaneType`, `render_tab_config` - `app/src/user_config/mod.rs:173` — `find_unused_tab_config_path` - `app/src/settings/onboarding.rs:122` — `apply_agent_settings`, where `DefaultSessionMode` is set - `app/src/settings/ai.rs:252` — `DefaultSessionMode` enum - `app/src/terminal/cli_agent.rs:94` — `CLIAgent::command_prefix()` - `app/src/modal.rs` — `Modal` and `ModalViewState` pattern - `app/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 TOML - `TabConfigPaneNode` — so pane nodes can be serialized - `TabConfig` — 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`: ```rust 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 to `CLIAgent::command_prefix()` for CLI agents, `None` for Terminal/Oz. - `icon() -> Icon` — delegates to `CLIAgent::icon()`, with `Icon::Terminal` for Terminal and `Icon::Oz` for Oz. - `display_name() -> &str` — delegates to `CLIAgent::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`: ```rust 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"`, `cwd` set to the absolute directory path - Sets `pane_type` to `TabConfigPaneType::Agent` for Oz, `TabConfigPaneType::Terminal` for Terminal and CLI agents - Appends worktree commands + `worktree_branch_name` param when `enable_worktree` is true, with `worktree_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: ```rust fn write_tab_config(config: &TabConfig, dir: &Path) -> Result ``` 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 `~` via `warp_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 `ActionButton` with `PrimaryTheme` and `with_full_width(true)`, includes Enter keystroke badge via `with_keybinding()` The modal always saves a tab config — there is no "Save as tab config" checkbox. **State:** The modal holds: - `selected_session_type: SessionType` - `selected_directory: PathBuf` (default: home dir) - `is_git_repo: bool` (recomputed on directory change via `std::path::Path::join(".git").is_dir()`) - `enable_worktree: bool` - `MouseStateHandle` for each interactive element **Output struct:** The modal collects its inputs into a plain struct: ```rust pub struct SessionConfigSelection { pub session_type: SessionType, pub directory: PathBuf, pub enable_worktree: bool, } ``` **Event:** The modal emits: ```rust 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`: ```rust session_config_modal: ModalViewState>, ``` 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 1. User completes onboarding slides → `OnboardingCompleted` fires. 2. `root_view` applies settings, transitions to `Terminal` state with the workspace. 3. `root_view` dispatches `WorkspaceAction::ShowSessionConfigModal` (flag-gated). 4. Workspace opens `session_config_modal` as a centered overlay. 5. User selects session type, picks directory, optionally toggles worktree, clicks "Get warping". 6. Modal emits `SessionConfigModalEvent::Completed(selection)`. 7. Workspace calls `handle_session_config_completed`: - Sets `DefaultSessionMode` if Oz. - Calls `build_tab_config` to produce a `TabConfig`. - Calls `write_tab_config` then `open_tab_config` (always saves). - Closes the old empty tab. 8. 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 → `TabConfig` with `cwd` set, 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_name` with default `"my-feature-branch"`, title = `"{{worktree_branch_name}}"`. - CLI agent (Gemini) + directory + worktree → commands include worktree creation + cd + `"gemini"` (in that order), params contain `worktree_branch_name`. - Oz + directory, no worktree → `cwd` set, `pane_type = Agent`, no commands, no params. - Oz + directory + worktree → `pane_type = Agent` with worktree commands, no agent CLI command. - Directory path is always absolute in `panes[0].cwd`. ### TOML round-trip (unit tests) - For each `build_tab_config` output, serialize via `toml::to_string_pretty`, deserialize back as `TabConfig`, verify all fields match. - Validates that `Serialize` on `TabConfig` produces TOML that the existing `Deserialize` path 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 `TabConfig` matching 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 → `PaneTemplate` with correct `cwd`, empty commands. - CLI agent + directory → `PaneTemplate` with correct `cwd`, 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_repo` returns true. - Temp dir without `.git/` → returns false. - Switching from a git dir to a non-git dir forces `enable_worktree` to false. ### DefaultSessionMode (unit test or integration) - Selecting Oz sets `DefaultSessionMode::Agent`. - Selecting Terminal sets `DefaultSessionMode::Terminal`. - Selecting a CLI agent sets `DefaultSessionMode::Terminal`. - When `OpenWarpNewSettingsModes` is off, `DefaultSessionMode` is not touched by this code path. ### Feature flag gating (integration) - When either `OpenWarpNewSettingsModes` or `TabConfigs` is off, `OnboardingCompleted` follows the old tutorial path — modal is never shown. - When both `OpenWarpNewSettingsModes` and `TabConfigs` are on, `OnboardingCompleted` dispatches `ShowSessionConfigModal`. ### 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 `.git` detection async. - **Programmatic tab config editing:** With `Serialize` on `TabConfig`, future features could read → modify → write tab configs (e.g., a tab config editor UI).