Rebuild native onboarding bootstrap flow
This commit is contained in:
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)
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user