Files

252 lines
15 KiB
Markdown

# 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<T>` and `ModalViewState<T>` 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<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 `~` 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<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
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).