Rebuild native onboarding bootstrap flow

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 15:44:35 -04:00
1 parent d9b35d73ab
commit 213837af1d
9 files changed
+997 -191

No files matched your search

@@ -2,4 +2,4 @@ pub mod actions;
pub mod state;
pub use actions::AppAction;
pub use state::AppState;
pub use state::{AppState, OnboardingStep};
@@ -1,5 +1,13 @@
use domain::SectionId;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum OnboardingStep {
#[default]
Company,
Mission,
Launch,
}
#[derive(Clone, Debug, Default)]
pub struct AppState {
pub section: SectionId,
@@ -8,6 +16,9 @@ pub struct AppState {
pub selected_message_id: Option<String>,
pub selected_document_id: Option<String>,
pub selected_workspace_id: Option<String>,
pub onboarding_step: OnboardingStep,
pub onboarding_connector_id: Option<String>,
pub onboarding_execution_mode: &'static str,
pub status_line: Option<String>,
}
@@ -5,10 +5,10 @@ use crate::{
text_input::TextInput,
},
};
use app_models::AppState;
use app_models::{AppState, OnboardingStep};
use domain::{ApprovalState, ProfileSnapshot, SectionId};
use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px};
use storage::NewCompanyInput;
use storage::OnboardingBootstrapInput;
mod approvals;
mod documents;
@@ -30,6 +30,8 @@ pub struct AppRoot {
company_name_input: Entity<TextInput>,
mission_input: Entity<TextInput>,
workspace_input: Entity<TextInput>,
onboarding_goal_input: Entity<TextInput>,
onboarding_goal_detail_input: Entity<TextInput>,
message_input: Entity<TextInput>,
document_title_input: Entity<TextInput>,
document_body_input: Entity<TextInput>,
@@ -47,6 +49,8 @@ impl AppRoot {
company_name_input: TextInput::new(cx, "Company name"),
mission_input: TextInput::new(cx, "Mission or thesis direction"),
workspace_input: TextInput::new(cx, "Primary workspace path"),
onboarding_goal_input: TextInput::new(cx, "Primary goal"),
onboarding_goal_detail_input: TextInput::new(cx, "Goal acceptance criteria"),
message_input: TextInput::new(cx, "Board message"),
document_title_input: TextInput::new(cx, "Document title"),
document_body_input: TextInput::new(cx, "Document body"),
@@ -59,6 +63,15 @@ impl AppRoot {
}
fn sync_selection(&mut self) {
if self.state.onboarding_connector_id.is_none() {
self.state.onboarding_connector_id = self
.snapshot
.connectors
.iter()
.find(|connector| connector.id == "anthropic")
.or_else(|| self.snapshot.connectors.first())
.map(|connector| connector.id.clone());
}
if self.state.selected_run_id.is_none() {
self.state.selected_run_id = self.snapshot.runs.first().map(|item| item.id.clone());
}
@@ -111,22 +124,42 @@ impl AppRoot {
}
fn create_company(&mut self, cx: &mut Context<Self>) {
let input = NewCompanyInput {
name: self.company_name_input.read(cx).value(),
description: self.mission_input.read(cx).value(),
let input = OnboardingBootstrapInput {
company_name: self.company_name_input.read(cx).value(),
company_description: self.mission_input.read(cx).value(),
workspace_path: self.workspace_input.read(cx).value(),
execution_mode: "ai_ceo".to_string(),
execution_mode: self.state.onboarding_execution_mode.to_string(),
goal_title: self.onboarding_goal_input.read(cx).value(),
goal_description: self.onboarding_goal_detail_input.read(cx).value(),
connector_id: self
.state
.onboarding_connector_id
.clone()
.unwrap_or_else(|| "anthropic".to_string()),
connector_model: self
.snapshot
.connectors
.iter()
.find(|connector| {
Some(connector.id.as_str()) == self.state.onboarding_connector_id.as_deref()
})
.and_then(|connector| connector.model.clone()),
};
let result = {
let mut store = self.store.lock();
store.create_company(input)
store.bootstrap_onboarding(input)
};
match result {
Ok(_) => {
self.company_name_input.update(cx, |input, _| input.clear());
self.mission_input.update(cx, |input, _| input.clear());
self.workspace_input.update(cx, |input, _| input.clear());
self.reload("Company created in the native profile.", cx);
self.onboarding_goal_input
.update(cx, |input, _| input.clear());
self.onboarding_goal_detail_input
.update(cx, |input, _| input.clear());
self.state.onboarding_step = OnboardingStep::Company;
self.reload("Company bootstrapped from native onboarding.", cx);
}
Err(error) => self.set_status(error.to_string(), cx),
}
@@ -1,167 +1,459 @@
use super::*;
use gpui::{Animation, AnimationExt, pulsating_between};
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<Self>) -> impl IntoElement + '_ {
let step = self.state.onboarding_step;
let readiness = onboarding_readiness(
&self.snapshot,
self.state.onboarding_connector_id.as_deref(),
);
div().size_full().bg(theme::window_bg()).child(
div()
.size_full()
.flex()
.child(self.render_onboarding_story(step, &readiness))
.child(div().w(px(12.0)).bg(theme::split_divider()))
.child(self.render_onboarding_panel(step, cx)),
)
}
fn render_onboarding_story(
&self,
step: OnboardingStep,
readiness: &[ConnectorReadiness<'_>],
) -> impl IntoElement + '_ {
div()
.size_full()
.bg(theme::window_bg())
.flex_1()
.flex()
.items_center()
.justify_center()
.child(
div()
.size_full()
.flex()
.w(px(620.0))
.gap_8()
.child(onboarding_eyebrow("AGENTCOMPANY DESKTOP"))
.child(
div()
.flex_1()
.p_8()
.gap_8()
.text_color(theme::text_primary())
.child(
div()
.text_size(px(68.0))
.line_height(px(68.0))
.child("Start a real local-first"),
)
.child(
div()
.text_size(px(68.0))
.line_height(px(68.0))
.child("agent company."),
),
)
.child(
div()
.max_w(px(560.0))
.text_size(px(18.0))
.line_height(px(30.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(onboarding_stepper(step))
.child(
div()
.mt(px(44.0))
.gap_5()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("CONNECTOR READINESS"),
)
.children(readiness.iter().map(|item| readiness_row(item))),
)
.child(div().mt(px(56.0)).child(onboarding_eyebrow("BY ELYDORA"))),
)
}
fn render_onboarding_panel(
&self,
step: OnboardingStep,
cx: &mut Context<Self>,
) -> impl IntoElement + '_ {
div()
.w(px(620.0))
.flex()
.items_center()
.justify_center()
.child(match step {
OnboardingStep::Company => self.render_company_setup_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_company_setup_panel(&self, cx: &mut Context<Self>) -> 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("COMPANY", 1, 3))
.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(12.0))
.gap_3()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("CEO RUNTIME"),
)
.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_lg()
.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()
.mt(px(28.0))
.flex()
.justify_end()
.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<Self>) -> impl IntoElement + '_ {
onboarding_card()
.child(step_counter("MISSION", 2, 3))
.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()
.mt(px(12.0))
.gap_3()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("EXECUTION MODE"),
)
.child(
div()
.flex()
.gap_2()
.child(traffic_light(theme::danger()))
.child(traffic_light(rgb_hex(0xfebc2e)))
.child(traffic_light(rgb_hex(0x28c840))),
)
.child(div().h(px(80.0)))
.child(eyebrow("AGENTCOMPANY DESKTOP"))
.child(
div()
.text_3xl()
.text_color(theme::text_primary())
.child("Start a real local-first")
.child(div().child("agent company.")),
.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()
.text_lg()
.text_color(theme::text_muted())
.child("Set up your company, point it at a workspace, and launch")
.child(div().child("the first native Rust control center.")),
)
.child(onboarding_steps())
.child(
div()
.w(px(520.0))
.p_5()
.gap_4()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("COMPANY SETUP"),
)
.child(self.company_name_input.clone())
.child(self.mission_input.clone())
.child(self.workspace_input.clone())
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("The native shell writes directly into SQLite with no cloud fallback."),
),
)
.child(readiness_panel())
.child(div().flex_1())
.child(eyebrow("BY ELYDORA")),
)
.child(
div()
.w(px(14.0))
.bg(theme::split_divider()),
)
.child(
div()
.w(px(520.0))
.p_8()
.child(
div()
.mt(px(220.0))
.p_8()
.gap_5()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(success_badge())
.child(
div()
.text_2xl()
.text_color(theme::text_primary())
.child("Native shell ready."),
)
.child(
div()
.text_lg()
.text_color(theme::text_muted())
.child("Create the company record, register the workspace, and")
.child(div().child("open the GPUI control center.")),
)
.child(
div()
.flex()
.gap_3()
.child(launch_tile("CONNECTOR", "SQLite local profile"))
.child(launch_tile("LEAD AGENT", "CEO shell created")),
)
.child(
div()
.flex()
.gap_3()
.child(launch_tile("PRIMARY WORKSPACE", "Registered on create"))
.child(launch_tile("KICKOFF PROJECT", "Created from first task")),
)
.child(launch_tile("INITIAL RUN", "Queued once company state lands"))
.child(primary_button("Create company", cx, |this, cx| {
this.create_company(cx)
})),
.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::Company;
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<Self>) -> 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(
div()
.mt(px(28.0))
.text_size(px(32.0))
.line_height(px(38.0))
.text_color(theme::text_primary())
.child(if company_name.trim().is_empty() {
"Native control center is ready.".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);
})),
)
}
}
fn eyebrow(label: &'static str) -> impl IntoElement {
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<ConnectorReadiness<'a>> {
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(48.0)).h(px(1.0)).bg(theme::text_subtle()))
.child(
div()
.text_xs()
.text_color(theme::text_subtle())
.child(label),
)
.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_steps() -> impl IntoElement {
fn onboarding_stepper(step: OnboardingStep) -> impl IntoElement {
div()
.mt(px(12.0))
.flex()
.gap_3()
.items_center()
.child(step_chip("Company", true))
.child(div().w(px(32.0)).h(px(1.0)).bg(theme::text_subtle()))
.child(step_chip("Mission", true))
.child(div().w(px(32.0)).h(px(1.0)).bg(theme::text_subtle()))
.child(step_chip("Launch", false))
.child(major_step_chip(
"Company",
matches!(step, OnboardingStep::Company),
true,
))
.child(step_connector())
.child(major_step_chip(
"Mission",
matches!(step, OnboardingStep::Mission),
!matches!(step, OnboardingStep::Company),
))
.child(step_connector())
.child(major_step_chip(
"Launch",
matches!(step, OnboardingStep::Launch),
false,
))
}
fn step_chip(label: &'static str, complete: bool) -> impl IntoElement {
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(if complete {
theme::success_bg()
} else {
theme::panel_muted()
})
.bg(bg)
.text_sm()
.text_color(if complete {
theme::accent_teal()
@@ -169,12 +461,12 @@ fn step_chip(label: &'static str, complete: bool) -> impl IntoElement {
theme::text_secondary()
})
.child(label);
if complete {
if current {
chip.with_animation(
("step-chip", label.len()),
Animation::new(Duration::from_secs(3))
("onboarding-major-step", label.len()),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.78, 1.0)),
.with_easing(pulsating_between(0.84, 1.0)),
|element, delta| element.opacity(delta),
)
.into_any_element()
@@ -183,90 +475,194 @@ fn step_chip(label: &'static str, complete: bool) -> impl IntoElement {
}
}
fn readiness_panel() -> impl IntoElement {
div()
.w(px(520.0))
.gap_3()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("CONNECTOR READINESS"),
)
.child(readiness_row("Claude Code", "ready", true))
.child(readiness_row("Codex", "ready", true))
.child(readiness_row("Gemini CLI", "auth required", false))
.child(
div()
.pl(px(28.0))
.text_xs()
.text_color(theme::text_subtle())
.child("Gemini CLI requires authentication before the runtime can use it."),
)
fn step_connector() -> impl IntoElement {
div().w(px(42.0)).h(px(1.0)).bg(theme::panel_border())
}
fn readiness_row(label: &'static str, state: &'static str, ready: bool) -> impl IntoElement {
fn readiness_row(row: &ConnectorReadiness<'_>) -> impl IntoElement {
div()
.flex()
.gap_3()
.items_center()
.child(div().w(px(16.0)).h(px(16.0)).rounded_full().bg(if ready {
theme::accent_teal()
} else {
theme::accent_orange()
}))
.gap_2()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(label),
.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()),
),
)
.child(div().text_sm().text_color(theme::text_muted()).child(state))
.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(px(650.0))
.p_10()
.rounded_lg()
.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<V>(
label: &str,
active: bool,
cx: &mut Context<V>,
on_click: impl Fn(&mut V, &mut Context<V>) + '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(72.0))
.h(px(72.0))
.w(px(60.0))
.h(px(60.0))
.rounded_full()
.bg(theme::success_bg())
.child(
div()
.m(px(20.0))
.w(px(32.0))
.h(px(32.0))
.m(px(16.0))
.w(px(28.0))
.h(px(28.0))
.rounded_full()
.bg(theme::accent_teal())
.with_animation(
"success-badge-inner",
"onboarding-success-badge",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.72, 1.0)),
.with_easing(pulsating_between(0.76, 1.0)),
|element, delta| element.opacity(delta),
),
)
}
fn launch_tile(label: &'static str, value: &'static str) -> impl IntoElement {
fn summary_tile(label: &'static str, value: String) -> impl IntoElement {
div()
.flex_1()
.p_4()
.gap_2()
.min_h(px(92.0))
.p_5()
.rounded_lg()
.bg(theme::panel_muted())
.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()
.text_sm()
.mt(px(10.0))
.text_base()
.text_color(theme::text_primary())
.line_height(px(22.0))
.child(value),
)
}
fn rgb_hex(hex: u32) -> gpui::Hsla {
gpui::rgb(hex).into()
}
fn traffic_light(color: gpui::Hsla) -> impl IntoElement {
div().w(px(12.0)).h(px(12.0)).rounded_full().bg(color)
}
+3 -1
View File
@@ -1,4 +1,6 @@
mod schema;
mod store;
pub use store::{NewCompanyInput, ProfileStore};
pub use store::{
NewCompanyInput, OnboardingBootstrapInput, OnboardingBootstrapSummary, ProfileStore,
};
@@ -27,6 +27,29 @@ pub struct NewCompanyInput {
pub execution_mode: String,
}
#[derive(Clone, Debug, Default)]
pub struct OnboardingBootstrapInput {
pub company_name: String,
pub company_description: String,
pub workspace_path: String,
pub execution_mode: String,
pub goal_title: String,
pub goal_description: String,
pub connector_id: String,
pub connector_model: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct OnboardingBootstrapSummary {
pub company_id: String,
pub company_name: String,
pub workspace_id: Option<String>,
pub lead_agent_id: String,
pub project_id: String,
pub task_id: String,
pub run_id: String,
}
pub struct ProfileStore {
profile_dir: PathBuf,
db_path: PathBuf,
@@ -149,6 +172,160 @@ impl ProfileStore {
Ok(company_id)
}
pub fn bootstrap_onboarding(
&mut self,
input: OnboardingBootstrapInput,
) -> Result<OnboardingBootstrapSummary> {
let company_name = input.company_name.trim();
if company_name.len() < 2 {
return Err(anyhow!("company name must be at least 2 characters"));
}
let connector_id = self.resolve_onboarding_connector(input.connector_id.trim())?;
let goal_title = input.goal_title.trim();
if goal_title.is_empty() {
return Err(anyhow!("goal title cannot be empty"));
}
let company_id = Uuid::new_v4().to_string();
let workspace_id = if input.workspace_path.trim().is_empty() {
None
} else {
Some(Uuid::new_v4().to_string())
};
let goal_id = Uuid::new_v4().to_string();
let lead_agent_id = Uuid::new_v4().to_string();
let project_id = Uuid::new_v4().to_string();
let task_id = Uuid::new_v4().to_string();
let run_id = Uuid::new_v4().to_string();
let ts = now_iso()?;
let execution_mode = normalized_execution_mode(&input.execution_mode);
let tx = self.conn.transaction()?;
tx.execute(
r#"
insert into companies (
id, name, description, status, auto_approve_hires,
review_deliverables, execution_mode, created_at, updated_at
) values (?1, ?2, ?3, 'active', ?4, ?5, ?6, ?7, ?7)
"#,
params![
company_id,
company_name,
input.company_description.trim(),
if execution_mode == "ai_ceo" { 1 } else { 0 },
if execution_mode == "ai_cofounder" {
1
} else {
0
},
execution_mode,
ts,
],
)?;
tx.execute(
"insert into settings (key, value) values ('current_company_id', ?1) on conflict(key) do update set value = excluded.value",
params![company_id],
)?;
if let Some(workspace_id) = workspace_id.as_ref() {
tx.execute(
r#"
insert into workspaces (
id, company_id, name, local_path, repo_url, repo_ref,
project_id, state, is_primary, created_at, updated_at
) values (?1, ?2, 'Default Workspace', ?3, '', '', null, 'ready', 1, ?4, ?4)
"#,
params![workspace_id, company_id, input.workspace_path.trim(), ts],
)?;
}
tx.execute(
"insert into goals (id, company_id, title, description, parent_id, owner_agent_id, status, created_at, updated_at) values (?1, ?2, ?3, ?4, null, null, 'active', ?5, ?5)",
params![
goal_id,
company_id,
goal_title,
if input.goal_description.trim().is_empty() {
format!("Achieve: {goal_title}")
} else {
input.goal_description.trim().to_string()
},
ts,
],
)?;
tx.execute(
"insert into agents (id, company_id, name, role, title, department, status, reports_to, connector_id, workspace_id, model, capabilities, budget_monthly_usd, spent_monthly_usd, created_at, updated_at) values (?1, ?2, 'CEO', 'CEO', 'Chief Executive Officer', null, 'idle', null, ?3, ?4, ?5, ?6, 250, 0, ?7, ?7)",
params![
lead_agent_id,
company_id,
connector_id,
workspace_id,
input.connector_model.as_deref().map(str::trim).filter(|value| !value.is_empty()),
"Executive leadership, strategic planning, team building, goal decomposition, delegation, and organizational management.",
ts,
],
)?;
tx.execute(
"insert into projects (id, company_id, goal_id, name, description, lead_agent_id, status, target_date, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, 'in_progress', null, ?7, ?7)",
params![
project_id,
company_id,
goal_id,
format!("Execution plan: {goal_title}"),
format!("Turn the founding goal \"{goal_title}\" into the first real operating plan and delegation map."),
lead_agent_id,
ts,
],
)?;
tx.execute(
"insert into tasks (id, company_id, project_id, goal_id, parent_id, title, description, assignee_agent_id, workspace_id, status, priority, active_run_id, cost_usd, task_type, requires_user_review, started_at, completed_at, cancelled_at, created_at, updated_at) values (?1, ?2, ?3, ?4, null, ?5, ?6, ?7, ?8, 'todo', 'high', ?9, 0, 'execution', 0, null, null, null, ?10, ?10)",
params![
task_id,
company_id,
project_id,
goal_id,
format!("Operationalize goal: {goal_title}"),
kickoff_task_description(company_name, input.company_description.trim(), goal_title),
lead_agent_id,
workspace_id,
run_id,
ts,
],
)?;
tx.execute(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, error_message, model, cost_usd, started_at, finished_at, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, 'queued', ?7, null, ?8, null, null, null, ?9, ?9)",
params![
run_id,
company_id,
task_id,
lead_agent_id,
workspace_id,
connector_id,
"Kickoff execution queued",
input.connector_model.as_deref().map(str::trim).filter(|value| !value.is_empty()),
ts,
],
)?;
tx.execute(
"insert into activity (id, company_id, actor, action, entity_type, entity_id, detail, created_at) values (?1, ?2, ?3, 'company.bootstrapped', 'companies', ?4, ?5, ?6)",
params![
Uuid::new_v4().to_string(),
company_id,
BOARD_READER_ID,
company_id,
format!("Bootstrapped from onboarding with connector {connector_id}."),
ts,
],
)?;
tx.commit()?;
Ok(OnboardingBootstrapSummary {
company_id,
company_name: company_name.to_string(),
workspace_id,
lead_agent_id,
project_id,
task_id,
run_id,
})
}
pub fn set_current_company(&mut self, company_id: &str) -> Result<()> {
self.conn.execute(
"insert into settings (key, value) values ('current_company_id', ?1) on conflict(key) do update set value = excluded.value",
@@ -157,6 +334,18 @@ impl ProfileStore {
Ok(())
}
fn resolve_onboarding_connector(&self, connector_id: &str) -> Result<String> {
let resolved = resolve_onboarding_connector_fallback(connector_id);
self.conn
.query_row(
"select id from connectors where id = ?1",
params![resolved],
|row| row.get(0),
)
.optional()?
.ok_or_else(|| anyhow!("connector {resolved} was not found"))
}
pub fn update_theme(&mut self, theme: &str) -> Result<()> {
let normalized = match theme {
"system" | "light" | "dark" => theme,
@@ -1040,6 +1229,25 @@ fn subject_for(body: &str) -> String {
subject
}
fn kickoff_task_description(
company_name: &str,
company_description: &str,
goal_title: &str,
) -> String {
let description = if company_description.trim().is_empty() {
"No company description yet."
} else {
company_description.trim()
};
[
format!("Read the founding goal \"{goal_title}\" and convert it into the first real operating plan."),
"Explore the workspace and identify the project structure, key files, frameworks, and execution constraints.".to_string(),
format!("Use the company context — name \"{company_name}\" and description \"{description}\" — when deciding the first departments, delegation plan, and operating cadence."),
"Create the first tasks, approval checkpoints, and coordination messages needed to start execution.".to_string(),
]
.join("\n\n")
}
fn normalized_execution_mode(value: &str) -> &str {
match value {
"ai_cofounder" => "ai_cofounder",
@@ -1047,6 +1255,10 @@ fn normalized_execution_mode(value: &str) -> &str {
}
}
fn resolve_onboarding_connector_fallback(value: &str) -> &str {
if value.is_empty() { "anthropic" } else { value }
}
fn default_locale() -> String {
match env::var("LANG") {
Ok(lang) if lang.to_ascii_lowercase().contains("zh") => "zh".to_string(),
@@ -0,0 +1,81 @@
use storage::{OnboardingBootstrapInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn onboarding_bootstrap_creates_company_goal_agent_project_task_and_run() {
let sandbox = tempdir().expect("temp dir");
let mut store = ProfileStore::open(sandbox.path()).expect("open store");
let summary = store
.bootstrap_onboarding(OnboardingBootstrapInput {
company_name: "Orbit Ops".to_string(),
company_description: "Local-first operations company".to_string(),
workspace_path: "/tmp/orbit-ops".to_string(),
execution_mode: "ai_ceo".to_string(),
goal_title: "Ship the native shell".to_string(),
goal_description:
"Rebuild the product in Rust and GPUI without losing product grammar.".to_string(),
connector_id: "anthropic".to_string(),
connector_model: Some("claude-sonnet".to_string()),
})
.expect("bootstrap onboarding");
let snapshot = store.snapshot().expect("snapshot");
assert_eq!(
snapshot.current_company_id.as_deref(),
Some(summary.company_id.as_str())
);
assert!(
snapshot
.companies
.iter()
.any(|company| company.id == summary.company_id && company.name == "Orbit Ops")
);
assert!(
snapshot
.workspaces
.iter()
.any(|workspace| Some(workspace.id.as_str()) == summary.workspace_id.as_deref())
);
assert!(
snapshot
.goals
.iter()
.any(|goal| goal.title == "Ship the native shell" && goal.status == "active")
);
assert!(snapshot.agents.iter().any(|agent| {
agent.id == summary.lead_agent_id
&& agent.name == "CEO"
&& agent.connector_id == "anthropic"
&& agent.model.as_deref() == Some("claude-sonnet")
}));
assert!(
snapshot
.projects
.iter()
.any(|project| project.id == summary.project_id && project.status == "in_progress")
);
assert!(snapshot.tasks.iter().any(|task| {
task.id == summary.task_id
&& task.active_run_id.as_deref() == Some(summary.run_id.as_str())
&& task.assignee_agent_id.as_deref() == Some(summary.lead_agent_id.as_str())
}));
assert!(snapshot.runs.iter().any(|run| {
run.id == summary.run_id
&& run.agent_id == summary.lead_agent_id
&& run.status == "queued"
&& run.task_id.as_deref() == Some(summary.task_id.as_str())
}));
assert!(snapshot.activity.iter().any(|activity| {
activity.entity_id == summary.company_id && activity.action == "company.bootstrapped"
}));
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("reopened snapshot");
assert!(
reopened_snapshot
.runs
.iter()
.any(|run| run.id == summary.run_id && run.status == "queued")
);
}
+70
View File
@@ -128,6 +128,76 @@
"priority": 9,
"passes": true,
"notes": "Completed with the final native verification matrix: profile_store_e2e.rs covers onboarding bootstrap, native_surface_e2e.rs covers run/approval/document visibility, native_settings_workspace_e2e.rs covers settings/workspaces, runtime_live_flow_e2e.rs covers live runtime approval/message/run flows, and native_inbox_attention_e2e.rs covers inbox attention derivation. cargo clippy/test both pass."
},
{
"id": "US-010",
"title": "Restore onboarding parity against original wizard flow",
"description": "As a first-time operator, I want the native onboarding surface to preserve the original multi-step wizard grammar so that setup feels identical to the Electron app.",
"acceptanceCriteria": [
"Onboarding restores the original staged wizard structure instead of the simplified dual-column placeholder shell",
"Native onboarding reflects original step semantics such as readiness, provider setup, mission, and launch summary",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 10,
"passes": false,
"notes": "In progress: native onboarding now uses a real multi-stage company/mission/launch flow with screenshot-aligned left narrative + right summary grammar, real connector readiness from snapshot data, and a real bootstrap transaction that creates company/workspace/goal/CEO/project/task/run. Remaining gap: restore the full original wizard tab semantics (nodecheck/providers/modelpool/executionmode) more precisely."
},
{
"id": "US-011",
"title": "Restore dashboard and inbox information architecture parity",
"description": "As an operator, I want the dashboard and inbox to match the original attention hierarchy, banners, and filtering behavior so that operating cadence feels unchanged.",
"acceptanceCriteria": [
"Dashboard restores original metrics, attention rows, active work, and recent activity grammar instead of simplified placeholder sections",
"Inbox restores original filterable attention list behavior instead of static grouped blocks with hardcoded active filter state",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 11,
"passes": false,
"notes": "Audit found missing dashboard banners/charts/attention structure and inbox filter interaction parity."
},
{
"id": "US-012",
"title": "Restore runs and approvals page grammar to original operational surfaces",
"description": "As an operator, I want runs and approvals to keep the original queue/detail and review grouping semantics so that native review flows feel identical to Electron.",
"acceptanceCriteria": [
"Runs restores original queue/detail grammar and removes non-source control slabs that distort layout hierarchy",
"Approvals restores board/user/manager review grouping and richer approval inspector semantics from the original source",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 12,
"passes": false,
"notes": "Audit found the live runtime controls now dominate the run detail layout and approvals still flatten original review semantics."
},
{
"id": "US-013",
"title": "Restore document center parity including work product grammar",
"description": "As an operator, I want documents and work products to preserve the original center/editor grammar so that company operations remain familiar.",
"acceptanceCriteria": [
"Document Center restores the original document plus work-product information architecture rather than a simplified draft form and flat list",
"Document actions and metadata hierarchy align with original Electron semantics",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 13,
"passes": false,
"notes": "Audit found current native documents surface lacks work-product grouping, modal/editor grammar, and action hierarchy from the original source."
},
{
"id": "US-014",
"title": "Run final UI parity re-audit after remediation",
"description": "As a maintainer, I want a final source-to-native parity audit after remediation so that we can truthfully claim the native UI matches the Electron product grammar.",
"acceptanceCriteria": [
"Key native surfaces are re-audited against original source components and screenshots",
"Remaining parity deviations are documented or eliminated",
"cargo clippy --workspace --all-targets --all-features -- -D warnings passes",
"cargo test --workspace passes"
],
"priority": 14,
"passes": false,
"notes": "Created after post-completion audit found significant remaining UI grammar gaps."
}
]
}
+2 -1
View File
@@ -13,4 +13,5 @@ Started: 2026-04-19
- Added runtime contract persistence entities for run events and artifacts, exposed them in ProfileSnapshot, and rendered them in the native runs inspector.
- Added runtime_contract_snapshot_e2e.rs to verify runtime event and artifact persistence across reopen.
- Runtime contract integration advanced: RuntimeEvent ingestion now persists message/tool/artifact/run-finished events into storage and exposes them in the runs inspector.
- Wired a live native runtime session into the GPUI Runs surface. Selected runs now launch Pi Mono through env-configured stdio sidecar settings, runtime tool calls create approval rows, approval decisions send ToolApproval back into the live session, runtime messages project into agent_messages, and run lifecycle still persists into run_events/artifacts/runs.\n- Closed the final native verification matrix. The workspace now has explicit storage-backed e2e coverage for onboarding bootstrap, approvals/documents/runs visibility, settings/workspace mutations, live runtime contract flows, and inbox attention derivation.\n
- Wired a live native runtime session into the GPUI Runs surface. Selected runs now launch Pi Mono through env-configured stdio sidecar settings, runtime tool calls create approval rows, approval decisions send ToolApproval back into the live session, runtime messages project into agent_messages, and run lifecycle still persists into run_events/artifacts/runs.\n- Closed the final native verification matrix. The workspace now has explicit storage-backed e2e coverage for onboarding bootstrap, approvals/documents/runs visibility, settings/workspace mutations, live runtime contract flows, and inbox attention derivation.\n- Reopened Ralph parity work after a direct source audit. The current native app has real runtime/data flows, but several surfaces still diverge from Electron page grammar: onboarding is too collapsed, dashboard misses banner/attention/chart structure, inbox filters are not real, run detail is dominated by native control slabs not present in source, approvals flatten board/user/manager semantics, and documents still lack work-product/editor hierarchy. Added US-010 through US-014 to close these parity gaps honestly.
- Rebuilt native onboarding away from the old static split shell. It now has a real multi-stage company/mission/launch flow, screenshot-aligned narrative/summary composition, real connector readiness derived from snapshot data, and a storage bootstrap transaction that creates company, workspace, goal, CEO, kickoff project, kickoff task, and queued run. Remaining US-010 work is the finer original wizard-tab parity.\n