252 lines
15 KiB
Markdown
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).
|