From 88dfe1a01fe1c18be1a92037011aa3076701f0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 17:20:50 -0400 Subject: [PATCH] Restore onboarding shell grammar --- .../crates/app_models/src/state.rs | 1 + .../crates/app_ui/src/app_root/onboarding.rs | 287 +++++++++++++----- AgentCompany-Rust/crates/app_ui/src/theme.rs | 4 - AgentCompany-Rust/prd.json | 40 +++ AgentCompany-Rust/progress.txt | 2 + 5 files changed, 255 insertions(+), 79 deletions(-) diff --git a/AgentCompany-Rust/crates/app_models/src/state.rs b/AgentCompany-Rust/crates/app_models/src/state.rs index 46a42b4b..a0d32975 100644 --- a/AgentCompany-Rust/crates/app_models/src/state.rs +++ b/AgentCompany-Rust/crates/app_models/src/state.rs @@ -3,6 +3,7 @@ use domain::SectionId; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum OnboardingStep { #[default] + Welcome, NodeCheck, Providers, Company, diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs index 287d717c..8bd0e353 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs @@ -6,98 +6,159 @@ use std::time::Duration; impl AppRoot { pub(super) fn render_onboarding(&self, cx: &mut Context) -> 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)), - ) + if step == OnboardingStep::Welcome { + return self.render_onboarding_welcome(cx).into_any_element(); + } + + div() + .size_full() + .bg(theme::window_bg()) + .child( + div() + .size_full() + .flex() + .child(self.render_onboarding_hero(step)) + .child(self.render_onboarding_shell(step, cx)), + ) + .into_any_element() } - fn render_onboarding_story( - &self, - step: OnboardingStep, - readiness: &[ConnectorReadiness<'_>], - ) -> impl IntoElement + '_ { + fn render_onboarding_welcome(&self, cx: &mut Context) -> impl IntoElement + '_ { + div() + .size_full() + .bg(theme::window_bg()) + .flex() + .items_center() + .justify_center() + .relative() + .child( + div() + .absolute() + .top_0() + .left_0() + .right_0() + .h(px(160.0)) + .bg(theme::window_bg()), + ) + .child( + div() + .w(px(680.0)) + .px_8() + .flex() + .flex_col() + .items_center() + .text_center() + .child( + div() + .text_size(px(64.0)) + .line_height(px(68.0)) + .text_color(theme::text_primary()) + .child("AgentCompany"), + ) + .child( + div() + .mt(px(16.0)) + .max_w(px(480.0)) + .text_size(px(16.0)) + .line_height(px(28.0)) + .text_color(theme::text_muted()) + .child("Set up your company, connect a runtime, choose an execution mode, and launch the first native operating loop."), + ) + .child(div().mt(px(40.0)).child(primary_button("Get Started", cx, |this, cx| { + this.state.onboarding_step = OnboardingStep::NodeCheck; + cx.notify(); + }))) + .child( + div() + .mt(px(24.0)) + .flex() + .gap_3() + .items_center() + .child(div().text_sm().text_color(theme::text_muted()).child("Language")) + .child(selectable_chip("EN", self.snapshot.locale == "en", cx, |this, cx| { + this.update_locale("en", cx); + })) + .child(selectable_chip("ZH", self.snapshot.locale == "zh", cx, |this, cx| { + this.update_locale("zh", cx); + })), + ), + ) + } + + fn render_onboarding_hero(&self, step: OnboardingStep) -> impl IntoElement + '_ { div() .flex_1() .flex() .items_center() .justify_center() + .px_8() + .py_12() .child( div() - .w(px(620.0)) - .gap_8() + .w(px(460.0)) .child(onboarding_eyebrow("AGENTCOMPANY DESKTOP")) .child( div() + .mt(px(24.0)) + .text_size(px(44.0)) + .line_height(px(46.0)) .text_color(theme::text_primary()) - .child( - 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("Start a real local-first\nagent company."), ) .child( div() - .max_w(px(560.0)) - .text_size(px(18.0)) - .line_height(px(30.0)) + .mt(px(16.0)) + .max_w(px(400.0)) + .text_sm() + .line_height(px(24.0)) .text_color(theme::text_muted()) .child("Set up your company, connect a runtime, choose an execution mode, and launch the first native operating loop."), ) - .child(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"))), + .child(div().mt(px(40.0)).child(onboarding_hero_stepper(step))), ) } - fn render_onboarding_panel( + fn render_onboarding_shell( &self, step: OnboardingStep, cx: &mut Context, ) -> impl IntoElement + '_ { div() - .w(px(620.0)) - .flex() - .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(), - }) + .w(px(540.0)) + .border_l_1() + .border_color(theme::panel_border()) + .bg(theme::window_bg()) + .child( + div() + .px_10() + .py_10() + .child(onboarding_tab_strip(step, cx)) + .child(div().mt(px(24.0)).flex().items_center().child(match step { + OnboardingStep::Welcome => div().into_any_element(), + OnboardingStep::NodeCheck => { + self.render_nodecheck_panel(cx).into_any_element() + } + OnboardingStep::Providers => { + self.render_providers_panel(cx).into_any_element() + } + OnboardingStep::Company => { + self.render_company_setup_panel(cx).into_any_element() + } + OnboardingStep::ModelPool => { + self.render_model_pool_panel(cx).into_any_element() + } + OnboardingStep::ExecutionMode => { + self.render_execution_mode_panel(cx).into_any_element() + } + OnboardingStep::Mission => { + self.render_mission_setup_panel(cx).into_any_element() + } + OnboardingStep::Launch => { + self.render_launch_summary_panel(cx).into_any_element() + } + })), + ) } fn render_nodecheck_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { @@ -149,7 +210,11 @@ impl AppRoot { div() .mt(px(28.0)) .flex() - .justify_end() + .justify_between() + .child(secondary_button("Back", cx, |this, cx| { + this.state.onboarding_step = OnboardingStep::Welcome; + cx.notify(); + })) .child(primary_button("Continue to providers", cx, |this, cx| { this.state.onboarding_step = OnboardingStep::Providers; cx.notify(); @@ -616,20 +681,79 @@ fn onboarding_eyebrow(label: &'static str) -> impl IntoElement { .child(div().text_xs().text_color(theme::text_muted()).child(label)) } -fn onboarding_stepper(step: OnboardingStep) -> impl IntoElement { +fn onboarding_tab_strip(step: OnboardingStep, cx: &mut Context) -> impl IntoElement { let steps = [ - ("Node check", OnboardingStep::NodeCheck), + ("Node Check", OnboardingStep::NodeCheck), ("Providers", OnboardingStep::Providers), ("Company", OnboardingStep::Company), - ("Model pool", OnboardingStep::ModelPool), - ("Execution mode", OnboardingStep::ExecutionMode), + ("Model Pool", OnboardingStep::ModelPool), + ("Mode", OnboardingStep::ExecutionMode), + ("Mission", OnboardingStep::Mission), + ]; + + div() + .flex() + .gap_1() + .border_b_1() + .border_color(theme::panel_border()) + .children(steps.into_iter().map(|(label, target_step)| { + let active = step == target_step; + let completed = onboarding_step_index(step) > onboarding_step_index(target_step); + onboarding_tab_button(label, target_step, active, completed, cx).into_any_element() + })) +} + +fn onboarding_tab_button( + label: &'static str, + target_step: OnboardingStep, + active: bool, + completed: bool, + cx: &mut Context, +) -> impl IntoElement { + div() + .id(SharedString::from(format!("onboarding-tab-{label}"))) + .relative() + .px_3() + .py_2() + .cursor_pointer() + .text_xs() + .text_color(if active { + theme::text_primary() + } else if completed { + theme::accent_teal() + } else { + theme::text_muted() + }) + .hover(|style| style.text_color(theme::text_primary())) + .on_click(cx.listener(move |this, _, _, cx| { + this.state.onboarding_step = target_step; + cx.notify(); + })) + .child(label) + .when(active, |element| { + element.child( + div() + .absolute() + .left(px(8.0)) + .right(px(8.0)) + .bottom(px(-1.0)) + .h(px(2.0)) + .bg(theme::primary_action()), + ) + }) +} + +fn onboarding_hero_stepper(step: OnboardingStep) -> impl IntoElement { + let steps = [ + ("Node Check", OnboardingStep::NodeCheck), + ("Providers", OnboardingStep::Providers), + ("Company", OnboardingStep::Company), + ("Model Pool", OnboardingStep::ModelPool), + ("Mode", OnboardingStep::ExecutionMode), ("Mission", OnboardingStep::Mission), ("Launch", OnboardingStep::Launch), ]; - let active_index = steps - .iter() - .position(|(_, current)| *current == step) - .unwrap_or_default(); + let active_index = onboarding_step_index(step); let mut children = Vec::new(); for (index, (label, current_step)) in steps.into_iter().enumerate() { @@ -649,6 +773,19 @@ fn onboarding_stepper(step: OnboardingStep) -> impl IntoElement { .children(children) } +fn onboarding_step_index(step: OnboardingStep) -> usize { + match step { + OnboardingStep::Welcome => 0, + OnboardingStep::NodeCheck => 0, + OnboardingStep::Providers => 1, + OnboardingStep::Company => 2, + OnboardingStep::ModelPool => 3, + OnboardingStep::ExecutionMode => 4, + OnboardingStep::Mission => 5, + OnboardingStep::Launch => 6, + } +} + fn major_step_chip(label: &'static str, current: bool, complete: bool) -> impl IntoElement { let bg = if complete { theme::accent_teal_soft() @@ -753,8 +890,8 @@ fn readiness_dot(tone: ReadinessTone) -> impl IntoElement { fn onboarding_card() -> gpui::Div { div() - .w(px(650.0)) - .p_10() + .w_full() + .p_8() .rounded_lg() .border_1() .border_color(theme::panel_border()) diff --git a/AgentCompany-Rust/crates/app_ui/src/theme.rs b/AgentCompany-Rust/crates/app_ui/src/theme.rs index f2490bc7..db1a9d27 100644 --- a/AgentCompany-Rust/crates/app_ui/src/theme.rs +++ b/AgentCompany-Rust/crates/app_ui/src/theme.rs @@ -32,10 +32,6 @@ pub fn panel_muted() -> Hsla { rgb(0xf1f4f8).into() } -pub fn split_divider() -> Hsla { - rgb(0xdbe2ea).into() -} - pub fn text_primary() -> Hsla { rgb(0x171c25).into() } diff --git a/AgentCompany-Rust/prd.json b/AgentCompany-Rust/prd.json index 9ad6c0e3..8e6da5c6 100644 --- a/AgentCompany-Rust/prd.json +++ b/AgentCompany-Rust/prd.json @@ -198,6 +198,46 @@ "priority": 14, "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." + }, + { + "id": "US-015", + "title": "Restore onboarding shell topology to original wizard grammar", + "description": "As a first-time operator, I want the native onboarding shell to mirror the original Electron wizard structure so that setup feels identical page-to-page.", + "acceptanceCriteria": [ + "Native onboarding uses original wizard-like shell semantics instead of the custom story/panel split", + "Step progress and CTA placement align with the original topography", + "Typecheck passes", + "cargo test --workspace passes" + ], + "priority": 15, + "passes": true, + "notes": "Passed: native onboarding now restores the original shell grammar with a dedicated welcome screen, left hero plus fixed right panel topology, top step tabs, and source-aligned back/continue flow while preserving the real Rust bootstrap logic." + }, + { + "id": "US-016", + "title": "Restore dashboard topology to original banner-chart-attention grammar", + "description": "As an operator, I want the native dashboard structure to mirror the original Electron dashboard so that information hierarchy is unchanged.", + "acceptanceCriteria": [ + "Dashboard restores original banner, metrics, chart, attention, active work, and recent activity section topology", + "Typecheck passes", + "cargo test --workspace passes" + ], + "priority": 16, + "passes": false, + "notes": "Reopened after strict source audit: current overview is closer than before but still custom in grammar and component topology." + }, + { + "id": "US-017", + "title": "Restore runs detail and queue shell to original operational grammar", + "description": "As an operator, I want the native runs page to preserve the original RunQueue and RunDetail shell structure so that run inspection feels identical.", + "acceptanceCriteria": [ + "Runs page restores original queue/detail shell semantics and hierarchy", + "Typecheck passes", + "cargo test --workspace passes" + ], + "priority": 17, + "passes": false, + "notes": "Reopened after strict source audit: current runs page keeps real runtime data but still uses custom card grammar." } ] } diff --git a/AgentCompany-Rust/progress.txt b/AgentCompany-Rust/progress.txt index 09953468..41153470 100644 --- a/AgentCompany-Rust/progress.txt +++ b/AgentCompany-Rust/progress.txt @@ -16,3 +16,5 @@ Started: 2026-04-19 - 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- 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 - Reopened strict source-parity remediation and corrected the Settings shell topology. Native settings no longer use the off-source left sidebar; they now render as a top tab strip with a single active content panel, matching the original Electron SettingsShell grammar while preserving real theme/locale/execution-mode mutations. + +- Completed US-015 onboarding shell remediation. Native onboarding now restores the original high-level wizard grammar: dedicated welcome screen, left hero region, fixed right-side step panel, top tab strip, and source-aligned back/continue flow while keeping the real storage-backed bootstrap path intact.