Close final native parity audit gaps

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 16:23:28 -04:00
1 parent d4c6056f5e
commit 7ef60d82bb
10 files changed
+555 -190

No files matched your search

@@ -2,4 +2,4 @@ pub mod actions;
pub mod state; pub mod state;
pub use actions::AppAction; pub use actions::AppAction;
pub use state::{AppState, InboxFilter, OnboardingStep}; pub use state::{AppState, InboxFilter, OnboardingStep, RunFilter};
@@ -3,7 +3,11 @@ use domain::SectionId;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum OnboardingStep { pub enum OnboardingStep {
#[default] #[default]
NodeCheck,
Providers,
Company, Company,
ModelPool,
ExecutionMode,
Mission, Mission,
Launch, Launch,
} }
@@ -18,6 +22,15 @@ pub enum InboxFilter {
Messages, Messages,
} }
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum RunFilter {
#[default]
All,
Running,
NeedsReview,
Finished,
}
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct AppState { pub struct AppState {
pub section: SectionId, pub section: SectionId,
@@ -30,6 +43,8 @@ pub struct AppState {
pub onboarding_connector_id: Option<String>, pub onboarding_connector_id: Option<String>,
pub onboarding_execution_mode: &'static str, pub onboarding_execution_mode: &'static str,
pub inbox_filter: InboxFilter, pub inbox_filter: InboxFilter,
pub run_filter: RunFilter,
pub message_unread_only: bool,
pub status_line: Option<String>, pub status_line: Option<String>,
} }
@@ -5,7 +5,7 @@ use crate::{
text_input::TextInput, text_input::TextInput,
}, },
}; };
use app_models::{AppState, OnboardingStep}; use app_models::{AppState, OnboardingStep, RunFilter};
use domain::{ApprovalState, ProfileSnapshot, SectionId}; use domain::{ApprovalState, ProfileSnapshot, SectionId};
use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px}; use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px};
use storage::OnboardingBootstrapInput; use storage::OnboardingBootstrapInput;
@@ -33,6 +33,8 @@ pub struct AppRoot {
onboarding_goal_input: Entity<TextInput>, onboarding_goal_input: Entity<TextInput>,
onboarding_goal_detail_input: Entity<TextInput>, onboarding_goal_detail_input: Entity<TextInput>,
message_input: Entity<TextInput>, message_input: Entity<TextInput>,
run_search_input: Entity<TextInput>,
message_search_input: Entity<TextInput>,
document_title_input: Entity<TextInput>, document_title_input: Entity<TextInput>,
document_body_input: Entity<TextInput>, document_body_input: Entity<TextInput>,
runtime_steer_input: Entity<TextInput>, runtime_steer_input: Entity<TextInput>,
@@ -52,6 +54,8 @@ impl AppRoot {
onboarding_goal_input: TextInput::new(cx, "Primary goal"), onboarding_goal_input: TextInput::new(cx, "Primary goal"),
onboarding_goal_detail_input: TextInput::new(cx, "Goal acceptance criteria"), onboarding_goal_detail_input: TextInput::new(cx, "Goal acceptance criteria"),
message_input: TextInput::new(cx, "Board message"), message_input: TextInput::new(cx, "Board message"),
run_search_input: TextInput::new(cx, "Search run queue"),
message_search_input: TextInput::new(cx, "Search messages"),
document_title_input: TextInput::new(cx, "Document title"), document_title_input: TextInput::new(cx, "Document title"),
document_body_input: TextInput::new(cx, "Document body"), document_body_input: TextInput::new(cx, "Document body"),
runtime_steer_input: TextInput::new(cx, "Steer the active run"), runtime_steer_input: TextInput::new(cx, "Steer the active run"),
@@ -158,7 +162,12 @@ impl AppRoot {
.update(cx, |input, _| input.clear()); .update(cx, |input, _| input.clear());
self.onboarding_goal_detail_input self.onboarding_goal_detail_input
.update(cx, |input, _| input.clear()); .update(cx, |input, _| input.clear());
self.state.onboarding_step = OnboardingStep::Company; self.state.onboarding_step = OnboardingStep::NodeCheck;
self.state.run_filter = RunFilter::All;
self.state.message_unread_only = false;
self.run_search_input.update(cx, |input, _| input.clear());
self.message_search_input
.update(cx, |input, _| input.clear());
self.reload("Company bootstrapped from native onboarding.", cx); self.reload("Company bootstrapped from native onboarding.", cx);
} }
Err(error) => self.set_status(error.to_string(), cx), Err(error) => self.set_status(error.to_string(), cx),
@@ -67,7 +67,7 @@ impl AppRoot {
.items_center() .items_center()
.justify_center() .justify_center()
.text_color(theme::text_muted()) .text_color(theme::text_muted())
.child("No documents have been created yet."), .child("No documents yet. Create the first company document to start the knowledge base."),
) )
}) })
.when(!self.snapshot.documents.is_empty(), |container| { .when(!self.snapshot.documents.is_empty(), |container| {
@@ -1,7 +1,7 @@
use super::{ use super::{
messages_parts::{ messages_parts::{
compact_message_card, composer_chip, composer_subject, filter_shell, message_section_label, compact_message_card, composer_chip, composer_subject, message_section_label,
search_shell, secondary_message, selected_message_card, selected_recipient, secondary_message, selected_message_card, selected_recipient,
}, },
*, *,
}; };
@@ -23,10 +23,17 @@ impl AppRoot {
.first() .first()
.and_then(|group| group.rows.first().map(|row| row.key.clone())) .and_then(|group| group.rows.first().map(|row| row.key.clone()))
}); });
let message_query = self.message_search_input.read(cx).value().to_lowercase();
let visible_messages = selected_channel let visible_messages = selected_channel
.as_ref() .as_ref()
.map(|channel_key| messages_for_channel(&self.snapshot, channel_key)) .map(|channel_key| messages_for_channel(&self.snapshot, channel_key))
.unwrap_or_default(); .unwrap_or_default()
.into_iter()
.filter(|message| {
message_matches_query(message, message_query.as_str())
&& (!self.state.message_unread_only || message.read_at.is_none())
})
.collect::<Vec<_>>();
div() div()
.flex() .flex()
@@ -45,8 +52,10 @@ impl AppRoot {
.border_color(theme::panel_border()) .border_color(theme::panel_border())
.bg(theme::panel()) .bg(theme::panel())
.child(render_message_header( .child(render_message_header(
self,
selected_channel.as_deref(), selected_channel.as_deref(),
visible_messages.len(), visible_messages.len(),
cx,
)) ))
.child( .child(
div() div()
@@ -319,7 +328,12 @@ fn render_message_sidebar<'a>(
) )
} }
fn render_message_header(active_channel: Option<&str>, visible_count: usize) -> impl IntoElement { fn render_message_header(
root: &AppRoot,
active_channel: Option<&str>,
visible_count: usize,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div() div()
.flex() .flex()
.justify_between() .justify_between()
@@ -348,8 +362,9 @@ fn render_message_header(active_channel: Option<&str>, visible_count: usize) ->
div() div()
.flex() .flex()
.gap_3() .gap_3()
.child(search_shell("Search messages")) .items_center()
.child(filter_shell()), .child(root.message_search_input.clone())
.child(message_filter_toggle(root.state.message_unread_only, cx)),
) )
} }
@@ -427,6 +442,52 @@ fn render_message_inspector(selected: Option<&domain::AgentMessage>) -> impl Int
}) })
} }
fn message_filter_toggle(unread_only: bool, cx: &mut Context<AppRoot>) -> impl IntoElement {
div()
.id("message-filter-toggle")
.px_3()
.py_2()
.rounded_full()
.cursor_pointer()
.bg(if unread_only {
theme::primary_action()
} else {
theme::panel_muted()
})
.text_xs()
.text_color(if unread_only {
theme::primary_action_text()
} else {
theme::text_secondary()
})
.hover(|style| {
style.bg(if unread_only {
theme::primary_action_hover()
} else {
theme::panel_hover()
})
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.message_unread_only = !this.state.message_unread_only;
cx.notify();
}))
.child(if unread_only {
"Unread only"
} else {
"All messages"
})
}
fn message_matches_query(message: &domain::AgentMessage, query: &str) -> bool {
if query.trim().is_empty() {
return true;
}
let query = query.trim();
message.subject.to_lowercase().contains(query)
|| message.body.to_lowercase().contains(query)
|| message.channel.to_lowercase().contains(query)
}
fn message_channel_key(message: &domain::AgentMessage) -> String { fn message_channel_key(message: &domain::AgentMessage) -> String {
match message.channel.as_str() { match message.channel.as_str() {
"company" => "company-all".to_string(), "company" => "company-all".to_string(),
@@ -7,7 +7,7 @@ pub(super) fn message_section_label(label: &'static str) -> impl IntoElement {
pub(super) fn selected_message_card(message: Option<&domain::AgentMessage>) -> impl IntoElement { pub(super) fn selected_message_card(message: Option<&domain::AgentMessage>) -> impl IntoElement {
let subject = message let subject = message
.map(|item| item.subject.clone()) .map(|item| item.subject.clone())
.unwrap_or_else(|| "No message selected".to_string()); .unwrap_or_else(|| "Select a message thread".to_string());
let body = message.map(|item| item.body.clone()).unwrap_or_else(|| { let body = message.map(|item| item.body.clone()).unwrap_or_else(|| {
"Choose a company channel, direct thread, or task discussion from the left rail." "Choose a company channel, direct thread, or task discussion from the left rail."
.to_string() .to_string()
@@ -152,33 +152,6 @@ fn trim_sender(sender: &str) -> &str {
sender.get(..2).unwrap_or(sender) sender.get(..2).unwrap_or(sender)
} }
pub(super) fn search_shell(label: &'static str) -> impl IntoElement {
div()
.w(px(240.0))
.px_4()
.py_3()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.text_sm()
.text_color(theme::text_subtle())
.child(label)
}
pub(super) fn filter_shell() -> impl IntoElement {
div()
.px_4()
.py_3()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.text_sm()
.text_color(theme::text_secondary())
.child("Filters")
}
pub(super) fn composer_chip(label: &str) -> impl IntoElement { pub(super) fn composer_chip(label: &str) -> impl IntoElement {
div() div()
.px_4() .px_4()
@@ -88,13 +88,76 @@ impl AppRoot {
.items_center() .items_center()
.justify_center() .justify_center()
.child(match step { .child(match step {
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::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::Mission => self.render_mission_setup_panel(cx).into_any_element(),
OnboardingStep::Launch => self.render_launch_summary_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 + '_ { fn render_nodecheck_panel(&self, cx: &mut Context<Self>) -> 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_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(
"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_end()
.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<Self>) -> impl IntoElement + '_ {
let connector_options = primary_connector_options(&self.snapshot); let connector_options = primary_connector_options(&self.snapshot);
let selected_connector_id = self let selected_connector_id = self
.state .state
@@ -103,7 +166,81 @@ impl AppRoot {
.unwrap_or("anthropic"); .unwrap_or("anthropic");
onboarding_card() onboarding_card()
.child(step_counter("COMPANY", 1, 3)) .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_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()
.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<Self>) -> impl IntoElement + '_ {
onboarding_card()
.child(step_counter("COMPANY", 3, 7))
.child(card_title("Set up the company shell")) .child(card_title("Set up the company shell"))
.child(card_divider()) .child(card_divider())
.child( .child(
@@ -119,116 +256,77 @@ impl AppRoot {
.line_height(px(20.0)) .line_height(px(20.0))
.text_color(theme::text_muted()) .text_color(theme::text_muted())
.child("The workspace path becomes the default execution boundary for the first operating loop."), .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( .child(
div() div()
.mt(px(28.0)) .mt(px(28.0))
.flex() .flex()
.justify_end() .justify_between()
.child(primary_button("Continue to mission", cx, |this, cx| { .child(secondary_button("Back", cx, |this, cx| {
this.state.onboarding_step = OnboardingStep::Mission; 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(); cx.notify();
})), })),
) )
} }
fn render_mission_setup_panel(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ { fn render_model_pool_panel(&self, cx: &mut Context<Self>) -> 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() onboarding_card()
.child(step_counter("MISSION", 2, 3)) .child(step_counter("MODELPOOL", 4, 7))
.child(card_title("Define the founding mission")) .child(card_title("Review the active model pool"))
.child(card_divider()) .child(card_divider())
.child( .child(
div() div()
.gap_4() .gap_4()
.child(self.onboarding_goal_input.clone()) .child(onboarding_metric_row(
.child(self.onboarding_goal_detail_input.clone()) "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( .child(
div() div()
.mt(px(12.0)) .rounded_lg()
.gap_3() .border_1()
.child( .border_color(theme::panel_border())
div() .bg(theme::panel_inset())
.text_xs() .p_4()
.text_color(theme::text_muted()) .text_sm()
.child("EXECUTION MODE"), .line_height(px(22.0))
) .text_color(theme::text_secondary())
.child( .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."),
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_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_execution_mode_note(
self.state.onboarding_execution_mode,
)),
),
), ),
) )
.child( .child(
@@ -240,6 +338,107 @@ impl AppRoot {
this.state.onboarding_step = OnboardingStep::Company; this.state.onboarding_step = OnboardingStep::Company;
cx.notify(); 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<Self>) -> 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_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_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<Self>) -> 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_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("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| { .child(primary_button("Review launch", cx, |this, cx| {
this.state.onboarding_step = OnboardingStep::Launch; this.state.onboarding_step = OnboardingStep::Launch;
cx.notify(); cx.notify();
@@ -271,14 +470,15 @@ impl AppRoot {
onboarding_card() onboarding_card()
.child(success_badge()) .child(success_badge())
.child(step_counter("LAUNCH", 7, 7))
.child( .child(
div() div()
.mt(px(28.0)) .mt(px(20.0))
.text_size(px(32.0)) .text_size(px(32.0))
.line_height(px(38.0)) .line_height(px(38.0))
.text_color(theme::text_primary()) .text_color(theme::text_primary())
.child(if company_name.trim().is_empty() { .child(if company_name.trim().is_empty() {
"Native control center is ready.".to_string() "Company profile is ready to launch.".to_string()
} else { } else {
format!("{} is ready to launch.", company_name.trim()) format!("{} is ready to launch.", company_name.trim())
}), }),
@@ -417,28 +617,36 @@ fn onboarding_eyebrow(label: &'static str) -> impl IntoElement {
} }
fn onboarding_stepper(step: OnboardingStep) -> impl IntoElement { fn onboarding_stepper(step: OnboardingStep) -> impl IntoElement {
let steps = [
("Node check", OnboardingStep::NodeCheck),
("Providers", OnboardingStep::Providers),
("Company", OnboardingStep::Company),
("Model pool", OnboardingStep::ModelPool),
("Execution mode", OnboardingStep::ExecutionMode),
("Mission", OnboardingStep::Mission),
("Launch", OnboardingStep::Launch),
];
let active_index = steps
.iter()
.position(|(_, current)| *current == step)
.unwrap_or_default();
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() div()
.mt(px(12.0)) .mt(px(12.0))
.flex() .flex()
.gap_3() .gap_3()
.items_center() .items_center()
.child(major_step_chip( .children(children)
"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 major_step_chip(label: &'static str, current: bool, complete: bool) -> impl IntoElement { fn major_step_chip(label: &'static str, current: bool, complete: bool) -> impl IntoElement {
@@ -475,8 +683,29 @@ fn major_step_chip(label: &'static str, current: bool, complete: bool) -> impl I
} }
} }
fn step_connector() -> impl IntoElement { fn step_connector(active: bool) -> impl IntoElement {
div().w(px(42.0)).h(px(1.0)).bg(theme::panel_border()) 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_lg()
.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 { fn readiness_row(row: &ConnectorReadiness<'_>) -> impl IntoElement {
@@ -4,6 +4,14 @@ use std::time::Duration;
impl AppRoot { impl AppRoot {
pub(super) fn render_runs(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ { pub(super) fn render_runs(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let search_query = self.run_search_input.read(cx).value().to_lowercase();
let filtered_runs = self
.snapshot
.runs
.iter()
.filter(|item| run_matches_filter(&self.snapshot, item, self.state.run_filter))
.filter(|item| run_matches_query(&self.snapshot, item, search_query.as_str()))
.collect::<Vec<_>>();
let selected = self let selected = self
.state .state
.selected_run_id .selected_run_id
@@ -42,13 +50,13 @@ impl AppRoot {
.mt(px(6.0)) .mt(px(6.0))
.text_sm() .text_sm()
.text_color(theme::text_muted()) .text_color(theme::text_muted())
.child(format!("{} runs", self.snapshot.runs.len())), .child(format!("{} visible runs", filtered_runs.len())),
), ),
) )
.child(run_filter_shell()), .child(run_filter_controls(self.state.run_filter, cx)),
) )
.child(run_search_shell("Search runs")) .child(self.run_search_input.clone())
.children(self.snapshot.runs.iter().map(|item| { .children(filtered_runs.iter().map(|item| {
let active = let active =
self.state.selected_run_id.as_deref() == Some(item.id.as_str()); self.state.selected_run_id.as_deref() == Some(item.id.as_str());
let run_id = item.id.clone(); let run_id = item.id.clone();
@@ -287,14 +295,14 @@ impl AppRoot {
div() div()
.text_xl() .text_xl()
.text_color(theme::text_primary()) .text_color(theme::text_primary())
.child("No run selected"), .child("Run queue is clear"),
) )
.child( .child(
div() div()
.mt(px(10.0)) .mt(px(10.0))
.text_base() .text_base()
.text_color(theme::text_muted()) .text_color(theme::text_muted())
.child("Choose a run from the queue to inspect its status and execution context."), .child("Choose a run from the queue to inspect execution context, approvals, runtime events, and artifacts."),
), ),
), ),
) )
@@ -547,7 +555,7 @@ fn run_live_controls_panel<'a>(
div() div()
.text_lg() .text_lg()
.text_color(theme::text_primary()) .text_color(theme::text_primary())
.child("Live runtime controls"), .child("Operator actions"),
) )
.child( .child(
div() div()
@@ -618,32 +626,102 @@ fn timeline_row(label: &'static str, value: String) -> impl IntoElement {
) )
} }
fn run_filter_shell() -> impl IntoElement { fn run_filter_controls(cx_filter: RunFilter, cx: &mut Context<AppRoot>) -> impl IntoElement {
div() div()
.px_4() .flex()
.py_3() .gap_2()
.rounded_lg() .child(run_filter_chip(
.border_1() "All",
.border_color(theme::panel_border()) cx_filter == RunFilter::All,
.bg(theme::panel()) RunFilter::All,
.text_sm() cx,
.text_color(theme::text_secondary()) ))
.child("All") .child(run_filter_chip(
"Running",
cx_filter == RunFilter::Running,
RunFilter::Running,
cx,
))
.child(run_filter_chip(
"Needs review",
cx_filter == RunFilter::NeedsReview,
RunFilter::NeedsReview,
cx,
))
.child(run_filter_chip(
"Finished",
cx_filter == RunFilter::Finished,
RunFilter::Finished,
cx,
))
} }
fn run_search_shell(label: &'static str) -> impl IntoElement { fn run_filter_chip(
label: &'static str,
active: bool,
next: RunFilter,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div() div()
.px_4() .id(SharedString::from(format!("run-filter-{label}")))
.py_3() .px_3()
.rounded_lg() .py_2()
.border_1() .rounded_full()
.border_color(theme::panel_border()) .cursor_pointer()
.bg(theme::panel()) .bg(if active {
.text_sm() theme::primary_action()
.text_color(theme::text_subtle()) } else {
theme::panel_muted()
})
.text_xs()
.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, _, _, cx| {
this.state.run_filter = next;
cx.notify();
}))
.child(label) .child(label)
} }
fn run_matches_filter(
snapshot: &domain::ProfileSnapshot,
run: &domain::Run,
filter: RunFilter,
) -> bool {
match filter {
RunFilter::All => true,
RunFilter::Running => matches!(run.status.as_str(), "queued" | "running"),
RunFilter::NeedsReview => run.task_id.as_ref().is_some_and(|task_id| {
snapshot.approvals.iter().any(|approval| {
approval.related_task_id.as_deref() == Some(task_id.as_str())
&& approval.state == "pending"
})
}),
RunFilter::Finished => matches!(run.status.as_str(), "completed" | "failed" | "cancelled"),
}
}
fn run_matches_query(snapshot: &domain::ProfileSnapshot, run: &domain::Run, query: &str) -> bool {
if query.trim().is_empty() {
return true;
}
let query = query.trim();
let title = run_task_title(snapshot, run).to_lowercase();
let subtitle = run_subtitle(snapshot, run).to_lowercase();
let summary = run.summary.to_lowercase();
title.contains(query) || subtitle.contains(query) || summary.contains(query)
}
fn short_id(value: &str) -> &str { fn short_id(value: &str) -> &str {
value.get(..8).unwrap_or(value) value.get(..8).unwrap_or(value)
} }
+10 -10
View File
@@ -140,8 +140,8 @@
"cargo test --workspace passes" "cargo test --workspace passes"
], ],
"priority": 10, "priority": 10,
"passes": false, "passes": true,
"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." "notes": "Passed: native onboarding now restores the original staged wizard grammar much more faithfully with explicit nodecheck/providers/company/modelpool/executionmode/mission/launch steps, while keeping the real Rust bootstrap transaction intact."
}, },
{ {
"id": "US-011", "id": "US-011",
@@ -154,8 +154,8 @@
"cargo test --workspace passes" "cargo test --workspace passes"
], ],
"priority": 11, "priority": 11,
"passes": false, "passes": true,
"notes": "In progress: dashboard now restores banners, attention rows, performance cards, active work, and activity feed. Inbox now has real interactive filters and a unified attention stream instead of the previous static grouped placeholder. Remaining gap is finer visual parity and final audit." "notes": "Passed: dashboard and inbox parity remediation is closed for this cycle; dashboard grammar was already restored earlier, and the remaining communication/search shell mismatches have been eliminated with real native filtering/search state."
}, },
{ {
"id": "US-012", "id": "US-012",
@@ -168,8 +168,8 @@
"cargo test --workspace passes" "cargo test --workspace passes"
], ],
"priority": 12, "priority": 12,
"passes": false, "passes": true,
"notes": "In progress: approvals now restore board/user/manager review grouping semantics and runs demote native runtime controls out of the main header hierarchy into a secondary live-controls section. Remaining gap is finer run queue/detail grammar parity." "notes": "Passed: runs now use real native queue filtering/search state, the empty-state copy is aligned, and the non-source runtime slab has been reduced to a lower-weight operator actions panel without distorting the main operational hierarchy."
}, },
{ {
"id": "US-013", "id": "US-013",
@@ -182,8 +182,8 @@
"cargo test --workspace passes" "cargo test --workspace passes"
], ],
"priority": 13, "priority": 13,
"passes": false, "passes": true,
"notes": "In progress: native Document Center now separates documents prose and work products using persisted artifacts. Messages now restores a more source-like communication shell with grouped channels, unread badges, a channel header, a message stream, a composer area, and a side inspector. Remaining work is the final source-to-native parity audit and any last micro-gaps it surfaces." "notes": "Passed: document-center empty/editor grammar is aligned for this remediation cycle, and communication shell parity is closed with real message search/unread filtering and source-aligned empty states."
}, },
{ {
"id": "US-014", "id": "US-014",
@@ -196,8 +196,8 @@
"cargo test --workspace passes" "cargo test --workspace passes"
], ],
"priority": 14, "priority": 14,
"passes": false, "passes": true,
"notes": "Audit not yet finalized. The post-completion remediation cycle has now rebuilt onboarding, dashboard, inbox, approvals, runs, documents, and messages toward source grammar. Next step is a final explicit audit pass to decide which remaining deviations are still material." "notes": "Passed: final source-to-native parity re-audit completed. Remaining hard-coded source mismatches identified in runs/documents/onboarding/messages were eliminated, and the workspace passes cargo clippy and cargo test."
} }
] ]
} }
+1 -1
View File
@@ -14,4 +14,4 @@ Started: 2026-04-19
- Added runtime_contract_snapshot_e2e.rs to verify runtime event and artifact persistence across reopen. - 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. - 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- 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. - 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- Rebuilt the native dashboard away from the placeholder metrics/activity shell. It now has real recovery and budget banners, attention rows, a performance section, active work, and recent activity, all driven by persisted local data. Remaining US-011 work is the inbox-side parity and finer dashboard detail matching.\n- Restored more source grammar across inbox, approvals, and runs. Inbox now has real interactive filters driving a unified attention stream; approvals again distinguish board/user/manager review groupings; and run detail no longer lets live runtime controls dominate the main header region.\n- Restored document-center grammar toward the source surface: the native page now separates documents prose from work products and uses persisted artifacts as the work-product list instead of hiding them behind the runs inspector only.\n- Restored the communication shell toward the source product grammar: grouped channels, unread badges, channel header, message stream, composer, and side inspector. Combined with the earlier document-center rewrite, the remaining work is now the final parity audit rather than major page-grammar gaps.\n - 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- Rebuilt the native dashboard away from the placeholder metrics/activity shell. It now has real recovery and budget banners, attention rows, a performance section, active work, and recent activity, all driven by persisted local data. Remaining US-011 work is the inbox-side parity and finer dashboard detail matching.\n- Restored more source grammar across inbox, approvals, and runs. Inbox now has real interactive filters driving a unified attention stream; approvals again distinguish board/user/manager review groupings; and run detail no longer lets live runtime controls dominate the main header region.\n- Restored document-center grammar toward the source surface: the native page now separates documents prose from work products and uses persisted artifacts as the work-product list instead of hiding them behind the runs inspector only.\n- Restored the communication shell toward the source product grammar: grouped channels, unread badges, channel header, message stream, composer, and side inspector. Combined with the earlier document-center rewrite, the remaining work is now the final parity audit rather than major page-grammar gaps.\n- Closed the final parity remediation slice. Native onboarding now restores explicit wizard-tab semantics (nodecheck/providers/company/modelpool/executionmode/mission/launch), runs now use real queue filtering/search state instead of static shells, messages now use real search plus unread filtering instead of static filter chrome, and the remaining off-source empty-state copy in runs/documents/messages/onboarding was eliminated. Final cargo clippy and cargo test both pass.\n