use super::*; use app_models::OnboardingStep; use gpui::{Animation, AnimationExt, Hsla, SharedString, pulsating_between, rgb}; use std::time::Duration; impl AppRoot { pub(super) fn render_onboarding(&self, cx: &mut Context) -> impl IntoElement + '_ { let step = self.state.onboarding_step; if step == OnboardingStep::Welcome { return self.render_onboarding_welcome(cx).into_any_element(); } div() .size_full() .bg(theme::window_bg()) .child( div() .size_full() .flex() .child(self.render_onboarding_hero(step)) .child(self.render_onboarding_shell(step, cx)), ) .into_any_element() } fn render_onboarding_welcome(&self, cx: &mut Context) -> impl IntoElement + '_ { div() .size_full() .bg(theme::window_bg()) .flex() .items_center() .justify_center() .relative() .child( div() .absolute() .top_0() .left_0() .right_0() .h(px(160.0)) .bg(theme::window_bg()), ) .child( div() .w(px(680.0)) .px_8() .flex() .flex_col() .items_center() .text_center() .child( div() .text_size(px(64.0)) .line_height(px(68.0)) .text_color(theme::text_primary()) .child("AgentCompany"), ) .child( div() .mt(px(16.0)) .max_w(px(480.0)) .text_size(px(16.0)) .line_height(px(28.0)) .text_color(theme::text_muted()) .child("Set up your company, connect a runtime, choose an execution mode, and launch the first native operating loop."), ) .child(div().mt(px(40.0)).child(primary_button("Get Started", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::NodeCheck; cx.notify(); }))) .child( div() .mt(px(24.0)) .flex() .gap_3() .items_center() .child(div().text_sm().text_color(theme::text_muted()).child("Language")) .child(selectable_chip("EN", self.snapshot.locale == "en", cx, |this, cx| { this.update_locale("en", cx); })) .child(selectable_chip("ZH", self.snapshot.locale == "zh", cx, |this, cx| { this.update_locale("zh", cx); })), ), ) } fn render_onboarding_hero(&self, step: OnboardingStep) -> impl IntoElement + '_ { div() .flex_1() .flex() .items_center() .justify_center() .px_8() .py_12() .child( div() .w(px(460.0)) .child(onboarding_eyebrow("AGENTCOMPANY DESKTOP")) .child( div() .mt(px(24.0)) .text_size(px(44.0)) .line_height(px(46.0)) .text_color(theme::text_primary()) .child("Start a real local-first\nagent company."), ) .child( div() .mt(px(16.0)) .max_w(px(400.0)) .text_sm() .line_height(px(24.0)) .text_color(theme::text_muted()) .child("Set up your company, connect a runtime, choose an execution mode, and launch the first native operating loop."), ) .child(div().mt(px(40.0)).child(onboarding_hero_stepper(step))), ) } fn render_onboarding_shell( &self, step: OnboardingStep, cx: &mut Context, ) -> impl IntoElement + '_ { div() .w(px(540.0)) .border_l_1() .border_color(theme::panel_border()) .bg(theme::window_bg()) .child( div() .px_10() .py_10() .child(onboarding_tab_strip(step, cx)) .child(div().mt(px(24.0)).flex().items_center().child(match step { OnboardingStep::Welcome => div().into_any_element(), OnboardingStep::NodeCheck => { self.render_nodecheck_panel(cx).into_any_element() } OnboardingStep::Providers => { self.render_providers_panel(cx).into_any_element() } OnboardingStep::Company => { self.render_company_setup_panel(cx).into_any_element() } OnboardingStep::ModelPool => { self.render_model_pool_panel(cx).into_any_element() } OnboardingStep::ExecutionMode => { self.render_execution_mode_panel(cx).into_any_element() } OnboardingStep::Mission => { self.render_mission_setup_panel(cx).into_any_element() } OnboardingStep::Launch => { self.render_launch_summary_panel(cx).into_any_element() } })), ) } fn render_nodecheck_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { let ready_connectors = self .snapshot .connectors .iter() .filter(|connector| connector.status == "ready" || connector.auth_state == "ready") .count(); onboarding_card() .child(step_counter("NODECHECK", 1, 7)) .child(card_title("Verify the local runtime boundary")) .child(card_divider()) .child( div() .gap_4() .child(onboarding_metric_row( "Connector inventory", format!("{} configured", self.snapshot.connectors.len()), )) .child(onboarding_metric_row( "Ready runtimes", format!("{} ready", ready_connectors), )) .child(onboarding_metric_row( "Workspace binding", if self.workspace_input.read(cx).value().trim().is_empty() { "Set in company step".to_string() } else { "Path captured".to_string() }, )) .child( div() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel_inset()) .p_4() .text_sm() .line_height(px(22.0)) .text_color(theme::text_secondary()) .child( "The native desktop path boots a real local-first company profile. This step keeps the original wizard contract honest before provider and mission setup.", ), ), ) .child( div() .mt(px(28.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Welcome; cx.notify(); })) .child(primary_button("Continue to providers", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Providers; cx.notify(); })), ) } fn render_providers_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { let connector_options = primary_connector_options(&self.snapshot); let selected_connector_id = self .state .onboarding_connector_id .as_deref() .unwrap_or("anthropic"); onboarding_card() .child(step_counter("PROVIDERS", 2, 7)) .child(card_title("Choose the founding runtime")) .child(card_divider()) .child( div() .gap_4() .child(div().flex().flex_wrap().gap_2().children( connector_options.into_iter().map(|connector| { let active = connector.id == selected_connector_id; let connector_id = connector.id.clone(); selectable_chip( connector.label.as_str(), active, cx, move |this, cx| { this.state.onboarding_connector_id = Some(connector_id.clone()); cx.notify(); }, ) .into_any_element() }), )) .child( div() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel_inset()) .p_4() .text_sm() .line_height(px(22.0)) .text_color(theme::text_secondary()) .child(onboarding_connector_note( &self.snapshot, selected_connector_id, )), ) .child( div() .gap_3() .child( div() .text_xs() .text_color(theme::text_muted()) .child("READINESS"), ) .children( onboarding_readiness( &self.snapshot, self.state.onboarding_connector_id.as_deref(), ) .iter() .map(readiness_row), ), ), ) .child( div() .mt(px(28.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::NodeCheck; cx.notify(); })) .child(primary_button("Continue to company", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Company; cx.notify(); })), ) } fn render_company_setup_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { onboarding_card() .child(step_counter("COMPANY", 3, 7)) .child(card_title("Set up the company shell")) .child(card_divider()) .child( div() .gap_4() .child(self.company_name_input.clone()) .child(self.mission_input.clone()) .child(self.workspace_input.clone()) .child( div() .mt(px(6.0)) .text_xs() .line_height(px(20.0)) .text_color(theme::text_muted()) .child("The workspace path becomes the default execution boundary for the first operating loop."), ), ) .child( div() .mt(px(28.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Providers; cx.notify(); })) .child(primary_button("Continue to model pool", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::ModelPool; cx.notify(); })), ) } fn render_model_pool_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { let connector = self .state .onboarding_connector_id .as_deref() .and_then(|connector_id| { self.snapshot .connectors .iter() .find(|connector| connector.id == connector_id) }); onboarding_card() .child(step_counter("MODELPOOL", 4, 7)) .child(card_title("Review the active model pool")) .child(card_divider()) .child( div() .gap_4() .child(onboarding_metric_row( "Selected runtime", connector .map(|item| item.label.clone()) .unwrap_or_else(|| "No connector selected".to_string()), )) .child(onboarding_metric_row( "Default model", connector .and_then(|item| item.model.clone()) .unwrap_or_else(|| "Use connector default".to_string()), )) .child(onboarding_metric_row( "Status", connector .map(|item| { if item.status.is_empty() { "Unknown".to_string() } else { item.status.clone() } }) .unwrap_or_else(|| "Pending".to_string()), )) .child( div() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel_inset()) .p_4() .text_sm() .line_height(px(22.0)) .text_color(theme::text_secondary()) .child("The original flow paused here to confirm the provider/model surface before execution mode. Native onboarding now preserves that pause with live connector data instead of a collapsed shortcut."), ), ) .child( div() .mt(px(28.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Company; cx.notify(); })) .child(primary_button("Continue to execution mode", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::ExecutionMode; cx.notify(); })), ) } fn render_execution_mode_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { onboarding_card() .child(step_counter("EXECUTIONMODE", 5, 7)) .child(card_title("Choose how the company executes")) .child(card_divider()) .child( div() .gap_4() .child( div() .flex() .gap_2() .child(selectable_chip( "AI CEO", self.state.onboarding_execution_mode == "ai_ceo", cx, |this, cx| { this.state.onboarding_execution_mode = "ai_ceo"; cx.notify(); }, )) .child(selectable_chip( "AI Co-Founder", self.state.onboarding_execution_mode == "ai_cofounder", cx, |this, cx| { this.state.onboarding_execution_mode = "ai_cofounder"; cx.notify(); }, )), ) .child( div() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel_inset()) .p_4() .text_sm() .line_height(px(22.0)) .text_color(theme::text_secondary()) .child(onboarding_execution_mode_note( self.state.onboarding_execution_mode, )), ), ) .child( div() .mt(px(28.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::ModelPool; cx.notify(); })) .child(primary_button("Continue to mission", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Mission; cx.notify(); })), ) } fn render_mission_setup_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { onboarding_card() .child(step_counter("MISSION", 6, 7)) .child(card_title("Define the founding mission")) .child(card_divider()) .child( div() .gap_4() .child(self.onboarding_goal_input.clone()) .child(self.onboarding_goal_detail_input.clone()) .child( div() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel_inset()) .p_4() .text_sm() .line_height(px(22.0)) .text_color(theme::text_secondary()) .child("This mission becomes the first goal, kickoff project brief, and the queued launch run summary inside the local profile."), ), ) .child( div() .mt(px(28.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::ExecutionMode; cx.notify(); })) .child(primary_button("Review launch", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Launch; cx.notify(); })), ) } fn render_launch_summary_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { let connector_id = self .state .onboarding_connector_id .as_deref() .unwrap_or("anthropic"); let connector_label = self .snapshot .connectors .iter() .find(|connector| connector.id == connector_id) .map(|connector| connector.label.clone()) .unwrap_or_else(|| connector_id.to_string()); let company_name = self.company_name_input.read(cx).value(); let workspace_path = self.workspace_input.read(cx).value(); let workspace_value = if workspace_path.trim().is_empty() { "No workspace selected".to_string() } else { "Registered".to_string() }; let goal_title = self.onboarding_goal_input.read(cx).value(); onboarding_card() .child(success_badge()) .child(step_counter("LAUNCH", 7, 7)) .child( div() .mt(px(20.0)) .text_size(px(32.0)) .line_height(px(38.0)) .text_color(theme::text_primary()) .child(if company_name.trim().is_empty() { "Company profile is ready to launch.".to_string() } else { format!("{} is ready to launch.", company_name.trim()) }), ) .child( div() .mt(px(16.0)) .text_size(px(16.0)) .line_height(px(26.0)) .text_color(theme::text_muted()) .child("Company, workspace, goal, lead agent, kickoff project, task, and the initial queued run will be created in the local profile."), ) .child( div() .mt(px(28.0)) .grid() .grid_cols(2) .gap_4() .child(summary_tile("CONNECTOR", connector_label)) .child(summary_tile("LEAD AGENT", "CEO agent created".to_string())) .child(summary_tile("PRIMARY WORKSPACE", workspace_value)) .child(summary_tile("KICKOFF PROJECT", "Created".to_string())) .child(summary_tile( "INITIAL RUN", if goal_title.trim().is_empty() { "Queued".to_string() } else { format!("Queued for {}", goal_title.trim()) }, )), ) .child( div() .mt(px(32.0)) .flex() .justify_between() .child(secondary_button("Back", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Mission; cx.notify(); })) .child(primary_button("Open Control Center", cx, |this, cx| { this.create_company(cx); })), ) } } struct ConnectorReadiness<'a> { label: &'a str, state: &'a str, help: Option<&'a str>, tone: ReadinessTone, } #[derive(Clone, Copy)] enum ReadinessTone { Ready, Warning, Neutral, } fn onboarding_readiness<'a>( snapshot: &'a domain::ProfileSnapshot, selected_connector_id: Option<&'a str>, ) -> Vec> { let mut rows = Vec::new(); let priority = [ selected_connector_id.unwrap_or("anthropic"), "openai-codex", "gemini-cli", ]; for connector_id in priority { if let Some(connector) = snapshot .connectors .iter() .find(|item| item.id == connector_id) { let tone = if connector.status == "ready" || connector.auth_state == "ready" { ReadinessTone::Ready } else if connector.last_error.is_some() || connector.auth_state == "auth_required" { ReadinessTone::Warning } else { ReadinessTone::Neutral }; let state = if connector.status == "ready" { "ready" } else if connector.auth_state == "ready" { "authenticated" } else if connector.auth_state == "auth_required" { "auth required" } else if connector.status.is_empty() { "unknown" } else { connector.status.as_str() }; let help = connector.last_error.as_deref().or_else(|| { (!connector.notes.trim().is_empty()).then_some(connector.notes.as_str()) }); rows.push(ConnectorReadiness { label: connector.label.as_str(), state, help, tone, }); } } rows } fn primary_connector_options(snapshot: &domain::ProfileSnapshot) -> Vec<&domain::Connector> { let preferred = ["anthropic", "openai-codex", "claude-pro", "gemini-cli"]; let mut options = Vec::new(); for connector_id in preferred { if let Some(connector) = snapshot .connectors .iter() .find(|item| item.id == connector_id) { options.push(connector); } } if options.is_empty() { snapshot.connectors.iter().take(4).collect() } else { options } } fn onboarding_eyebrow(label: &'static str) -> impl IntoElement { div() .flex() .gap_3() .items_center() .child(div().w(px(44.0)).h(px(1.0)).bg(theme::text_subtle())) .child(div().text_xs().text_color(theme::text_muted()).child(label)) } fn onboarding_tab_strip(step: OnboardingStep, cx: &mut Context) -> impl IntoElement { let steps = [ ("Node Check", OnboardingStep::NodeCheck), ("Providers", OnboardingStep::Providers), ("Company", OnboardingStep::Company), ("Model Pool", OnboardingStep::ModelPool), ("Mode", OnboardingStep::ExecutionMode), ("Mission", OnboardingStep::Mission), ]; div() .flex() .gap_1() .border_b_1() .border_color(theme::panel_border()) .children(steps.into_iter().map(|(label, target_step)| { let active = step == target_step; let completed = onboarding_step_index(step) > onboarding_step_index(target_step); onboarding_tab_button(label, target_step, active, completed, cx).into_any_element() })) } fn onboarding_tab_button( label: &'static str, target_step: OnboardingStep, active: bool, completed: bool, cx: &mut Context, ) -> impl IntoElement { div() .id(SharedString::from(format!("onboarding-tab-{label}"))) .relative() .px_3() .py_2() .cursor_pointer() .text_xs() .text_color(if active { theme::text_primary() } else if completed { theme::accent_teal() } else { theme::text_muted() }) .hover(|style| style.text_color(theme::text_primary())) .on_click(cx.listener(move |this, _, _, cx| { this.state.onboarding_step = target_step; cx.notify(); })) .child(label) .when(active, |element| { element.child( div() .absolute() .left(px(8.0)) .right(px(8.0)) .bottom(px(-1.0)) .h(px(2.0)) .bg(theme::primary_action()), ) }) } fn onboarding_hero_stepper(step: OnboardingStep) -> impl IntoElement { let steps = [ ("Node Check", OnboardingStep::NodeCheck), ("Providers", OnboardingStep::Providers), ("Company", OnboardingStep::Company), ("Model Pool", OnboardingStep::ModelPool), ("Mode", OnboardingStep::ExecutionMode), ("Mission", OnboardingStep::Mission), ("Launch", OnboardingStep::Launch), ]; let active_index = onboarding_step_index(step); let mut children = Vec::new(); for (index, (label, current_step)) in steps.into_iter().enumerate() { if index > 0 { children.push(step_connector(index <= active_index).into_any_element()); } children.push( major_step_chip(label, step == current_step, index < active_index).into_any_element(), ); } div() .mt(px(12.0)) .flex() .gap_3() .items_center() .children(children) } fn onboarding_step_index(step: OnboardingStep) -> usize { match step { OnboardingStep::Welcome => 0, OnboardingStep::NodeCheck => 0, OnboardingStep::Providers => 1, OnboardingStep::Company => 2, OnboardingStep::ModelPool => 3, OnboardingStep::ExecutionMode => 4, OnboardingStep::Mission => 5, OnboardingStep::Launch => 6, } } fn major_step_chip(label: &'static str, current: bool, complete: bool) -> impl IntoElement { let bg = if complete { theme::accent_teal_soft() } else if current { theme::panel_muted() } else { rgb(0xf1f3f7).into() }; let chip = div() .px_4() .py_2() .rounded_full() .bg(bg) .text_sm() .text_color(if complete { theme::accent_teal() } else { theme::text_secondary() }) .child(label); if current { chip.with_animation( ("onboarding-major-step", label.len()), Animation::new(Duration::from_secs(2)) .repeat() .with_easing(pulsating_between(0.84, 1.0)), |element, delta| element.opacity(delta), ) .into_any_element() } else { chip.into_any_element() } } fn step_connector(active: bool) -> impl IntoElement { div().w(px(42.0)).h(px(1.0)).bg(if active { theme::accent_teal_soft() } else { theme::panel_border() }) } fn onboarding_metric_row(label: &'static str, value: String) -> impl IntoElement { div() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel_inset()) .p_4() .child(div().text_xs().text_color(theme::text_muted()).child(label)) .child( div() .mt(px(8.0)) .text_sm() .text_color(theme::text_primary()) .child(value), ) } fn readiness_row(row: &ConnectorReadiness<'_>) -> impl IntoElement { div() .gap_2() .child( div() .flex() .gap_3() .items_center() .child(readiness_dot(row.tone)) .child( div() .text_base() .text_color(theme::text_primary()) .child(row.label.to_string()), ) .child( div() .text_base() .text_color(theme::text_muted()) .child(row.state.to_string()), ), ) .when_some(row.help, |container, help| { container.child( div() .pl(px(28.0)) .text_sm() .line_height(px(22.0)) .text_color(theme::text_muted()) .child(help.to_string()), ) }) } fn readiness_dot(tone: ReadinessTone) -> impl IntoElement { let color: Hsla = match tone { ReadinessTone::Ready => theme::accent_teal(), ReadinessTone::Warning => theme::accent_orange(), ReadinessTone::Neutral => theme::badge_dark(), }; div().w(px(16.0)).h(px(16.0)).rounded_full().bg(color) } fn onboarding_card() -> gpui::Div { div() .w_full() .p_8() .rounded(px(0.0)) .border_1() .border_color(theme::panel_border()) .bg(theme::panel()) } fn step_counter(label: &'static str, index: usize, total: usize) -> impl IntoElement { div() .text_xs() .text_color(theme::text_muted()) .child(format!("{} {} / {}", label, index, total)) } fn card_title(label: &'static str) -> impl IntoElement { div() .mt(px(8.0)) .text_size(px(24.0)) .line_height(px(30.0)) .text_color(theme::text_primary()) .child(label) } fn card_divider() -> impl IntoElement { div() .mt(px(12.0)) .mb(px(24.0)) .h(px(1.0)) .bg(theme::panel_border()) } fn selectable_chip( label: &str, active: bool, cx: &mut Context, on_click: impl Fn(&mut V, &mut Context) + 'static, ) -> impl IntoElement where V: 'static, { div() .id(SharedString::from(format!("onboarding-chip-{label}"))) .px_4() .py_2() .rounded_full() .cursor_pointer() .bg(if active { theme::primary_action() } else { theme::panel_muted() }) .text_color(if active { theme::primary_action_text() } else { theme::text_secondary() }) .hover(|style| { style.bg(if active { theme::primary_action_hover() } else { theme::panel_hover() }) }) .on_click( cx.listener(move |this, _: &gpui::ClickEvent, _: &mut Window, cx| on_click(this, cx)), ) .child(label.to_string()) } fn onboarding_connector_note(snapshot: &domain::ProfileSnapshot, connector_id: &str) -> String { snapshot .connectors .iter() .find(|connector| connector.id == connector_id) .map(|connector| { if !connector.notes.trim().is_empty() { connector.notes.clone() } else if !connector.description.trim().is_empty() { connector.description.clone() } else { format!( "{} will be bound to the founding CEO agent.", connector.label ) } }) .unwrap_or_else(|| "The selected connector becomes the default CEO runtime.".to_string()) } fn onboarding_execution_mode_note(mode: &str) -> &'static str { match mode { "ai_cofounder" => { "AI Co-Founder keeps human review tighter on deliverables while preserving a shared planning loop." } _ => { "AI CEO starts the company autonomously, boots the lead agent, creates the first project, and queues the kickoff run." } } } fn success_badge() -> impl IntoElement { div() .w(px(60.0)) .h(px(60.0)) .rounded_full() .bg(theme::success_bg()) .child( div() .m(px(16.0)) .w(px(28.0)) .h(px(28.0)) .rounded_full() .bg(theme::accent_teal()) .with_animation( "onboarding-success-badge", Animation::new(Duration::from_secs(2)) .repeat() .with_easing(pulsating_between(0.76, 1.0)), |element, delta| element.opacity(delta), ), ) } fn summary_tile(label: &'static str, value: String) -> impl IntoElement { div() .min_h(px(92.0)) .p_5() .rounded(px(0.0)) .border_1() .border_color(theme::panel_inset_border()) .bg(theme::panel_inset()) .child(div().text_xs().text_color(theme::text_muted()).child(label)) .child( div() .mt(px(10.0)) .text_base() .text_color(theme::text_primary()) .line_height(px(22.0)) .child(value), ) }