Close final native parity audit gaps
This commit is contained in:
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 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)]
|
||||
pub enum OnboardingStep {
|
||||
#[default]
|
||||
NodeCheck,
|
||||
Providers,
|
||||
Company,
|
||||
ModelPool,
|
||||
ExecutionMode,
|
||||
Mission,
|
||||
Launch,
|
||||
}
|
||||
@@ -18,6 +22,15 @@ pub enum InboxFilter {
|
||||
Messages,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum RunFilter {
|
||||
#[default]
|
||||
All,
|
||||
Running,
|
||||
NeedsReview,
|
||||
Finished,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppState {
|
||||
pub section: SectionId,
|
||||
@@ -30,6 +43,8 @@ pub struct AppState {
|
||||
pub onboarding_connector_id: Option<String>,
|
||||
pub onboarding_execution_mode: &'static str,
|
||||
pub inbox_filter: InboxFilter,
|
||||
pub run_filter: RunFilter,
|
||||
pub message_unread_only: bool,
|
||||
pub status_line: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
text_input::TextInput,
|
||||
},
|
||||
};
|
||||
use app_models::{AppState, OnboardingStep};
|
||||
use app_models::{AppState, OnboardingStep, RunFilter};
|
||||
use domain::{ApprovalState, ProfileSnapshot, SectionId};
|
||||
use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px};
|
||||
use storage::OnboardingBootstrapInput;
|
||||
@@ -33,6 +33,8 @@ pub struct AppRoot {
|
||||
onboarding_goal_input: Entity<TextInput>,
|
||||
onboarding_goal_detail_input: Entity<TextInput>,
|
||||
message_input: Entity<TextInput>,
|
||||
run_search_input: Entity<TextInput>,
|
||||
message_search_input: Entity<TextInput>,
|
||||
document_title_input: Entity<TextInput>,
|
||||
document_body_input: Entity<TextInput>,
|
||||
runtime_steer_input: Entity<TextInput>,
|
||||
@@ -52,6 +54,8 @@ impl AppRoot {
|
||||
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"),
|
||||
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_body_input: TextInput::new(cx, "Document body"),
|
||||
runtime_steer_input: TextInput::new(cx, "Steer the active run"),
|
||||
@@ -158,7 +162,12 @@ impl AppRoot {
|
||||
.update(cx, |input, _| input.clear());
|
||||
self.onboarding_goal_detail_input
|
||||
.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);
|
||||
}
|
||||
Err(error) => self.set_status(error.to_string(), cx),
|
||||
|
||||
@@ -67,7 +67,7 @@ impl AppRoot {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.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| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{
|
||||
messages_parts::{
|
||||
compact_message_card, composer_chip, composer_subject, filter_shell, message_section_label,
|
||||
search_shell, secondary_message, selected_message_card, selected_recipient,
|
||||
compact_message_card, composer_chip, composer_subject, message_section_label,
|
||||
secondary_message, selected_message_card, selected_recipient,
|
||||
},
|
||||
*,
|
||||
};
|
||||
@@ -23,10 +23,17 @@ impl AppRoot {
|
||||
.first()
|
||||
.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
|
||||
.as_ref()
|
||||
.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()
|
||||
.flex()
|
||||
@@ -45,8 +52,10 @@ impl AppRoot {
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(render_message_header(
|
||||
self,
|
||||
selected_channel.as_deref(),
|
||||
visible_messages.len(),
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
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()
|
||||
.flex()
|
||||
.justify_between()
|
||||
@@ -348,8 +362,9 @@ fn render_message_header(active_channel: Option<&str>, visible_count: usize) ->
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(search_shell("Search messages"))
|
||||
.child(filter_shell()),
|
||||
.items_center()
|
||||
.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 {
|
||||
match message.channel.as_str() {
|
||||
"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 {
|
||||
let subject = message
|
||||
.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(|| {
|
||||
"Choose a company channel, direct thread, or task discussion from the left rail."
|
||||
.to_string()
|
||||
@@ -152,33 +152,6 @@ fn trim_sender(sender: &str) -> &str {
|
||||
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 {
|
||||
div()
|
||||
.px_4()
|
||||
|
||||
@@ -88,13 +88,76 @@ impl AppRoot {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.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::ModelPool => self.render_model_pool_panel(cx).into_any_element(),
|
||||
OnboardingStep::ExecutionMode => {
|
||||
self.render_execution_mode_panel(cx).into_any_element()
|
||||
}
|
||||
OnboardingStep::Mission => self.render_mission_setup_panel(cx).into_any_element(),
|
||||
OnboardingStep::Launch => self.render_launch_summary_panel(cx).into_any_element(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_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 selected_connector_id = self
|
||||
.state
|
||||
@@ -103,7 +166,81 @@ impl AppRoot {
|
||||
.unwrap_or("anthropic");
|
||||
|
||||
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_divider())
|
||||
.child(
|
||||
@@ -119,116 +256,77 @@ impl AppRoot {
|
||||
.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;
|
||||
.justify_between()
|
||||
.child(secondary_button("Back", cx, |this, cx| {
|
||||
this.state.onboarding_step = OnboardingStep::Providers;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(primary_button("Continue to model pool", cx, |this, cx| {
|
||||
this.state.onboarding_step = OnboardingStep::ModelPool;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_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()
|
||||
.child(step_counter("MISSION", 2, 3))
|
||||
.child(card_title("Define the founding mission"))
|
||||
.child(step_counter("MODELPOOL", 4, 7))
|
||||
.child(card_title("Review the active model pool"))
|
||||
.child(card_divider())
|
||||
.child(
|
||||
div()
|
||||
.gap_4()
|
||||
.child(self.onboarding_goal_input.clone())
|
||||
.child(self.onboarding_goal_detail_input.clone())
|
||||
.child(onboarding_metric_row(
|
||||
"Selected runtime",
|
||||
connector
|
||||
.map(|item| item.label.clone())
|
||||
.unwrap_or_else(|| "No connector selected".to_string()),
|
||||
))
|
||||
.child(onboarding_metric_row(
|
||||
"Default model",
|
||||
connector
|
||||
.and_then(|item| item.model.clone())
|
||||
.unwrap_or_else(|| "Use connector default".to_string()),
|
||||
))
|
||||
.child(onboarding_metric_row(
|
||||
"Status",
|
||||
connector
|
||||
.map(|item| {
|
||||
if item.status.is_empty() {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
item.status.clone()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "Pending".to_string()),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.mt(px(12.0))
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(theme::text_muted())
|
||||
.child("EXECUTION MODE"),
|
||||
)
|
||||
.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,
|
||||
)),
|
||||
),
|
||||
.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 original flow paused here to confirm the provider/model surface before execution mode. Native onboarding now preserves that pause with live connector data instead of a collapsed shortcut."),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -240,6 +338,107 @@ impl AppRoot {
|
||||
this.state.onboarding_step = OnboardingStep::Company;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(primary_button("Continue to execution mode", cx, |this, cx| {
|
||||
this.state.onboarding_step = OnboardingStep::ExecutionMode;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_execution_mode_panel(&self, cx: &mut Context<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| {
|
||||
this.state.onboarding_step = OnboardingStep::Launch;
|
||||
cx.notify();
|
||||
@@ -271,14 +470,15 @@ impl AppRoot {
|
||||
|
||||
onboarding_card()
|
||||
.child(success_badge())
|
||||
.child(step_counter("LAUNCH", 7, 7))
|
||||
.child(
|
||||
div()
|
||||
.mt(px(28.0))
|
||||
.mt(px(20.0))
|
||||
.text_size(px(32.0))
|
||||
.line_height(px(38.0))
|
||||
.text_color(theme::text_primary())
|
||||
.child(if company_name.trim().is_empty() {
|
||||
"Native control center is ready.".to_string()
|
||||
"Company profile is ready to launch.".to_string()
|
||||
} else {
|
||||
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 {
|
||||
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()
|
||||
.mt(px(12.0))
|
||||
.flex()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.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,
|
||||
))
|
||||
.children(children)
|
||||
}
|
||||
|
||||
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 {
|
||||
div().w(px(42.0)).h(px(1.0)).bg(theme::panel_border())
|
||||
fn step_connector(active: bool) -> impl IntoElement {
|
||||
div().w(px(42.0)).h(px(1.0)).bg(if active {
|
||||
theme::accent_teal_soft()
|
||||
} else {
|
||||
theme::panel_border()
|
||||
})
|
||||
}
|
||||
|
||||
fn onboarding_metric_row(label: &'static str, value: String) -> impl IntoElement {
|
||||
div()
|
||||
.rounded_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 {
|
||||
|
||||
@@ -4,6 +4,14 @@ use std::time::Duration;
|
||||
|
||||
impl AppRoot {
|
||||
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
|
||||
.state
|
||||
.selected_run_id
|
||||
@@ -42,13 +50,13 @@ impl AppRoot {
|
||||
.mt(px(6.0))
|
||||
.text_sm()
|
||||
.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"))
|
||||
.children(self.snapshot.runs.iter().map(|item| {
|
||||
.child(self.run_search_input.clone())
|
||||
.children(filtered_runs.iter().map(|item| {
|
||||
let active =
|
||||
self.state.selected_run_id.as_deref() == Some(item.id.as_str());
|
||||
let run_id = item.id.clone();
|
||||
@@ -287,14 +295,14 @@ impl AppRoot {
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(theme::text_primary())
|
||||
.child("No run selected"),
|
||||
.child("Run queue is clear"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.text_base()
|
||||
.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()
|
||||
.text_lg()
|
||||
.text_color(theme::text_primary())
|
||||
.child("Live runtime controls"),
|
||||
.child("Operator actions"),
|
||||
)
|
||||
.child(
|
||||
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()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.text_sm()
|
||||
.text_color(theme::text_secondary())
|
||||
.child("All")
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(run_filter_chip(
|
||||
"All",
|
||||
cx_filter == RunFilter::All,
|
||||
RunFilter::All,
|
||||
cx,
|
||||
))
|
||||
.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()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.text_sm()
|
||||
.text_color(theme::text_subtle())
|
||||
.id(SharedString::from(format!("run-filter-{label}")))
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded_full()
|
||||
.cursor_pointer()
|
||||
.bg(if active {
|
||||
theme::primary_action()
|
||||
} 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
value.get(..8).unwrap_or(value)
|
||||
}
|
||||
|
||||
+10
-10
@@ -140,8 +140,8 @@
|
||||
"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."
|
||||
"passes": true,
|
||||
"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",
|
||||
@@ -154,8 +154,8 @@
|
||||
"cargo test --workspace passes"
|
||||
],
|
||||
"priority": 11,
|
||||
"passes": false,
|
||||
"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."
|
||||
"passes": true,
|
||||
"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",
|
||||
@@ -168,8 +168,8 @@
|
||||
"cargo test --workspace passes"
|
||||
],
|
||||
"priority": 12,
|
||||
"passes": false,
|
||||
"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."
|
||||
"passes": true,
|
||||
"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",
|
||||
@@ -182,8 +182,8 @@
|
||||
"cargo test --workspace passes"
|
||||
],
|
||||
"priority": 13,
|
||||
"passes": false,
|
||||
"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."
|
||||
"passes": true,
|
||||
"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",
|
||||
@@ -196,8 +196,8 @@
|
||||
"cargo test --workspace passes"
|
||||
],
|
||||
"priority": 14,
|
||||
"passes": false,
|
||||
"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."
|
||||
"passes": true,
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,4 +14,4 @@ Started: 2026-04-19
|
||||
- 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- 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
|
||||
Reference in new issue
Block a user