opencrabs 0.3.79

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
use super::types::*;
use super::wizard::OnboardingWizard;

impl OnboardingWizard {
    /// Write down where the user is, so leaving halfway is resumable (#919).
    ///
    /// Only for a genuine first-time run. `quick_jump` opens a single step from
    /// a slash command and is not a pass through the wizard, and a user
    /// re-running `/onboard` after finishing has nothing to resume — the gate
    /// reads the completion marker and never looks at the step.
    fn persist_progress(&self) {
        if self.quick_jump || !self.is_first_time {
            return;
        }
        super::state::OnboardingState::record_step(self.step.as_key(), self.mode.as_key());
    }

    /// Advance to the next step
    pub fn next_step(&mut self) {
        // Wrapped so every path out of the step machine below — including the
        // early returns on validation errors — records where the user ended up.
        let prev_step = self.step;
        self.next_step_inner();
        // Step-scoped save (#926): a successful transition (the step actually
        // changed, no validation error) commits the config section owned by
        // the step just completed, so an interrupted wizard keeps everything
        // already confirmed instead of losing it to the single end-of-wizard
        // write. On failure, surface it and stay put: nothing was persisted,
        // so the user retries the same step rather than moving on with an
        // unsaved foundation.
        if self.step != prev_step
            && self.error_message.is_none()
            && let Err(e) = self.apply_step_config(prev_step)
        {
            self.error_message = Some(e);
            self.step = prev_step;
        }
        self.persist_progress();
    }

    fn next_step_inner(&mut self) {
        self.error_message = None;
        self.focused_field = 0;
        // Reset manual scroll so each new step starts at the top —
        // a Page Down on the previous step shouldn't leave the user
        // mid-scroll on a step they haven't seen yet.
        self.user_scroll_offset = 0;

        // In quick_jump mode, completing a step exits back to chat
        // instead of advancing through the wizard flow.
        if self.quick_jump {
            self.quick_jump_done = true;
            return;
        }

        match self.step {
            OnboardingStep::ModeSelect => {
                self.step = OnboardingStep::Workspace;
            }
            OnboardingStep::Workspace => {
                // Create config files in the workspace directory
                if let Err(e) = self.ensure_config_files() {
                    self.error_message = Some(format!("Failed to create config files: {}", e));
                    return;
                }
                self.step = OnboardingStep::ProviderAuth;
                self.auth_field = AuthField::Provider;
                self.ps.detect_existing_key();
            }
            OnboardingStep::ProviderAuth => {
                // Keyless providers need no API key: CLI subprocesses (Claude
                // CLI, OpenCode CLI) AND any API provider whose onboarding entry
                // has an empty key_label. Only demand a key when the provider
                // actually has a key field.
                let keyless = self.ps.is_keyless();
                if self.ps.api_key_input.is_empty() && !self.ps.is_custom() && !keyless {
                    self.error_message = Some("API key is required".to_string());
                    return;
                }
                if self.ps.is_custom()
                    && (self.ps.base_url.is_empty()
                        || self.ps.custom_model.is_empty()
                        || self.ps.custom_name.is_empty())
                {
                    self.error_message = Some(
                        "Base URL, model name, and provider name are required for custom provider"
                            .to_string(),
                    );
                    return;
                }
                // QuickStart: skip channels, go straight to daemon
                if self.mode == WizardMode::QuickStart {
                    self.step = OnboardingStep::Daemon;
                } else {
                    tracing::debug!("[next_step] ProviderAuth → Channels");
                    self.step = OnboardingStep::Channels;
                    self.focused_field = 0;
                }
            }
            OnboardingStep::Channels => {
                // Handled by handle_channels_key — Enter on focused channel or Continue
                self.step = OnboardingStep::VoiceSetup;
                self.voice_field = VoiceField::SttModeSelect;
                self.detect_existing_groq_key();
            }
            OnboardingStep::TelegramSetup
            | OnboardingStep::DiscordSetup
            | OnboardingStep::WhatsAppSetup
            | OnboardingStep::SlackSetup
            | OnboardingStep::TrelloSetup => {
                // Return to channel list after completing a channel setup
                self.step = OnboardingStep::Channels;
            }
            OnboardingStep::VoiceSetup => {
                self.step = OnboardingStep::ImageSetup;
                self.image_field = ImageField::VisionToggle;
                self.detect_existing_image_key();
            }
            OnboardingStep::ImageSetup => {
                self.step = OnboardingStep::Daemon;
            }
            OnboardingStep::Daemon => {
                self.step = OnboardingStep::HealthCheck;
                self.start_health_check();
            }
            OnboardingStep::HealthCheck => {
                self.step = OnboardingStep::BrainSetup;
                self.brain_field = BrainField::AboutMe;
            }
            OnboardingStep::BrainSetup => {
                if self.brain_generated || self.brain_error.is_some() {
                    self.step = OnboardingStep::Complete;
                }
                // Otherwise wait for generation to finish or user to trigger it
            }
            OnboardingStep::Complete => {
                // Already complete
            }
        }
    }

    /// Go back to the previous step
    pub fn prev_step(&mut self) -> bool {
        let cancelled = self.prev_step_inner();
        self.persist_progress();
        cancelled
    }

    fn prev_step_inner(&mut self) -> bool {
        self.error_message = None;
        self.focused_field = 0;
        self.user_scroll_offset = 0;

        match self.step {
            OnboardingStep::ModeSelect => {
                // Can't go back further — return true to signal "cancel wizard"
                return true;
            }
            OnboardingStep::Workspace => {
                self.step = OnboardingStep::ModeSelect;
            }
            OnboardingStep::ProviderAuth => {
                self.step = OnboardingStep::Workspace;
            }
            OnboardingStep::Channels => {
                self.step = OnboardingStep::ProviderAuth;
                self.auth_field = AuthField::Provider;
            }
            OnboardingStep::TelegramSetup => {
                self.step = OnboardingStep::Channels;
            }
            OnboardingStep::DiscordSetup
            | OnboardingStep::WhatsAppSetup
            | OnboardingStep::SlackSetup
            | OnboardingStep::TrelloSetup => {
                self.step = OnboardingStep::Channels;
            }
            OnboardingStep::VoiceSetup => {
                self.step = OnboardingStep::Channels;
            }
            OnboardingStep::ImageSetup => {
                self.step = OnboardingStep::VoiceSetup;
                self.voice_field = VoiceField::SttModeSelect;
            }
            OnboardingStep::Daemon => {
                // QuickStart: go back to ProviderAuth, Advanced: go back to ImageSetup
                if self.mode == WizardMode::QuickStart {
                    self.step = OnboardingStep::ProviderAuth;
                    self.auth_field = AuthField::Provider;
                } else {
                    self.step = OnboardingStep::ImageSetup;
                    self.image_field = ImageField::VisionToggle;
                }
            }
            OnboardingStep::HealthCheck => {
                self.step = OnboardingStep::Daemon;
            }
            OnboardingStep::BrainSetup => {
                self.step = OnboardingStep::HealthCheck;
                self.brain_generating = false;
                self.brain_error = None;
            }
            OnboardingStep::Complete => {
                self.step = OnboardingStep::BrainSetup;
                self.brain_field = BrainField::AboutMe;
            }
        }
        false
    }
}