Files
galaxy/specs/APP-3680/TECH.md
T

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:2062handle_agent_onboarding_event, where OnboardingCompleted is handled
  • app/src/workspace/view/onboarding.rsOnboardingTutorial enum and start_agent_onboarding_tutorial
  • app/src/workspace/view.rs:8157add_tab_with_pane_layout
  • app/src/workspace/view.rs:4688-4734open_tab_config_with_params and open_tab_config
  • app/src/workspace/view.rs:4739create_and_open_new_tab_config
  • app/src/tab_configs/tab_config.rsTabConfig, TabConfigPaneNode, TabConfigPaneType, render_tab_config
  • app/src/user_config/mod.rs:173find_unused_tab_config_path
  • app/src/settings/onboarding.rs:122apply_agent_settings, where DefaultSessionMode is set
  • app/src/settings/ai.rs:252DefaultSessionMode enum
  • app/src/terminal/cli_agent.rs:94CLIAgent::command_prefix()
  • app/src/modal.rsModal<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:

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:

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:

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:

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

  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_configrender_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).