diff --git a/AgentCompany-Rust/crates/app_models/src/lib.rs b/AgentCompany-Rust/crates/app_models/src/lib.rs index d80923c0..cd21b46f 100644 --- a/AgentCompany-Rust/crates/app_models/src/lib.rs +++ b/AgentCompany-Rust/crates/app_models/src/lib.rs @@ -2,4 +2,4 @@ pub mod actions; pub mod state; pub use actions::AppAction; -pub use state::{AppState, InboxFilter, OnboardingStep, RunFilter}; +pub use state::{AppState, InboxFilter, OnboardingStep, RunFilter, SettingsTab}; diff --git a/AgentCompany-Rust/crates/app_models/src/state.rs b/AgentCompany-Rust/crates/app_models/src/state.rs index 9b34ed3c..46a42b4b 100644 --- a/AgentCompany-Rust/crates/app_models/src/state.rs +++ b/AgentCompany-Rust/crates/app_models/src/state.rs @@ -31,6 +31,17 @@ pub enum RunFilter { Finished, } +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum SettingsTab { + #[default] + General, + Mode, + Providers, + Shortcuts, + Data, + Secrets, +} + #[derive(Clone, Debug, Default)] pub struct AppState { pub section: SectionId, @@ -44,6 +55,7 @@ pub struct AppState { pub onboarding_execution_mode: &'static str, pub inbox_filter: InboxFilter, pub run_filter: RunFilter, + pub settings_tab: SettingsTab, pub message_unread_only: bool, pub status_line: Option, } diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/settings.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/settings.rs index 9305aedc..c50ceb97 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/settings.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/settings.rs @@ -2,147 +2,219 @@ use super::*; impl AppRoot { pub(super) fn render_settings(&self, cx: &mut Context) -> impl IntoElement + '_ { - let current = self.snapshot.current_company(); - let execution_mode = current - .map(|item| item.execution_mode.clone()) - .unwrap_or_else(|| "ai_ceo".to_string()); - div() .flex() + .flex_col() .gap_6() - .child( - div() - .w(px(220.0)) - .p_4() - .gap_2() - .rounded_lg() - .border_1() - .border_color(theme::panel_border()) - .bg(theme::panel()) - .child(settings_nav_row("General", true)) - .child(settings_nav_row("Execution mode", false)) - .child(settings_nav_row("Providers", false)) - .child(settings_nav_row("Shortcuts", false)) - .child(settings_nav_row("Data", false)) - .child(settings_nav_row("Secrets", false)), - ) - .child( - div() - .flex_1() - .gap_6() - .child(settings_panel( - "General", - vec![ - settings_action_row( - "Theme", - self.snapshot.theme.clone(), - vec![ - settings_action_button( - "System", - self.snapshot.theme == "system", - cx, - |this, cx| this.update_theme("system", cx), - ) - .into_any_element(), - settings_action_button( - "Light", - self.snapshot.theme == "light", - cx, - |this, cx| this.update_theme("light", cx), - ) - .into_any_element(), - settings_action_button( - "Dark", - self.snapshot.theme == "dark", - cx, - |this, cx| this.update_theme("dark", cx), - ) - .into_any_element(), - ], - ), - settings_action_row( - "Locale", - self.snapshot.locale.clone(), - vec![ - settings_action_button( - "EN", - self.snapshot.locale == "en", - cx, - |this, cx| this.update_locale("en", cx), - ) - .into_any_element(), - settings_action_button( - "ZH", - self.snapshot.locale == "zh", - cx, - |this, cx| this.update_locale("zh", cx), - ) - .into_any_element(), - ], - ), - settings_readonly_row( - "Profile path", - self.snapshot.profile_path.clone(), - ), - ], - )) - .child(settings_panel( - "Execution mode", - vec![settings_action_row( - "Company mode", - execution_mode.clone(), - vec![ - settings_action_button( - "AI CEO", - execution_mode == "ai_ceo", - cx, - |this, cx| this.update_execution_mode("ai_ceo", cx), - ) - .into_any_element(), - settings_action_button( - "AI Co-Founder", - execution_mode == "ai_cofounder", - cx, - |this, cx| this.update_execution_mode("ai_cofounder", cx), - ) - .into_any_element(), - ], - )], - )) - .child(settings_panel( - "Providers", - vec![settings_readonly_row( - "Configured connectors", - self.snapshot.connectors.len().to_string(), - )], - )) - .child(settings_panel( - "Data", - vec![ - settings_readonly_row("Stored profile", "Local SQLite".to_string()), - settings_readonly_row("Current company", current_company_name(current)), - ], - )), - ) + .child(settings_tabs(self.state.settings_tab, cx)) + .child(self.render_active_settings_panel(cx)) + } + + fn render_active_settings_panel(&self, cx: &mut Context) -> gpui::AnyElement { + match self.state.settings_tab { + SettingsTab::General => self.render_general_settings_panel(cx).into_any_element(), + SettingsTab::Mode => self.render_mode_settings_panel(cx).into_any_element(), + SettingsTab::Providers => self.render_providers_settings_panel().into_any_element(), + SettingsTab::Shortcuts => self.render_shortcuts_settings_panel().into_any_element(), + SettingsTab::Data => self.render_data_settings_panel().into_any_element(), + SettingsTab::Secrets => self.render_secrets_settings_panel().into_any_element(), + } + } + + fn render_general_settings_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { + settings_panel( + "General", + vec![ + settings_action_row( + "Theme", + self.snapshot.theme.clone(), + vec![ + settings_action_button( + "System", + self.snapshot.theme == "system", + cx, + |this, cx| this.update_theme("system", cx), + ) + .into_any_element(), + settings_action_button( + "Light", + self.snapshot.theme == "light", + cx, + |this, cx| this.update_theme("light", cx), + ) + .into_any_element(), + settings_action_button( + "Dark", + self.snapshot.theme == "dark", + cx, + |this, cx| this.update_theme("dark", cx), + ) + .into_any_element(), + ], + ), + settings_action_row( + "Locale", + self.snapshot.locale.clone(), + vec![ + settings_action_button( + "EN", + self.snapshot.locale == "en", + cx, + |this, cx| this.update_locale("en", cx), + ) + .into_any_element(), + settings_action_button( + "ZH", + self.snapshot.locale == "zh", + cx, + |this, cx| this.update_locale("zh", cx), + ) + .into_any_element(), + ], + ), + settings_readonly_row("Profile path", self.snapshot.profile_path.clone()), + ], + ) + } + + fn render_mode_settings_panel(&self, cx: &mut Context) -> impl IntoElement + '_ { + let execution_mode = self + .snapshot + .current_company() + .map(|item| item.execution_mode.clone()) + .unwrap_or_else(|| "ai_ceo".to_string()); + settings_panel( + "Execution mode", + vec![settings_action_row( + "Company mode", + execution_mode.clone(), + vec![ + settings_action_button("AI CEO", execution_mode == "ai_ceo", cx, |this, cx| { + this.update_execution_mode("ai_ceo", cx) + }) + .into_any_element(), + settings_action_button( + "AI Co-Founder", + execution_mode == "ai_cofounder", + cx, + |this, cx| this.update_execution_mode("ai_cofounder", cx), + ) + .into_any_element(), + ], + )], + ) + } + + fn render_providers_settings_panel(&self) -> impl IntoElement + '_ { + settings_panel( + "Providers", + vec![ + settings_readonly_row( + "Configured connectors", + self.snapshot.connectors.len().to_string(), + ), + settings_readonly_row( + "Ready connectors", + self.snapshot + .connectors + .iter() + .filter(|item| item.status == "ready" || item.auth_state == "ready") + .count() + .to_string(), + ), + ], + ) + } + + fn render_shortcuts_settings_panel(&self) -> impl IntoElement + '_ { + settings_panel( + "Shortcuts", + vec![ + settings_readonly_row("Open inbox", "g then i".to_string()), + settings_readonly_row("Open runs", "g then r".to_string()), + settings_readonly_row("Open settings", "g then s".to_string()), + ], + ) + } + + fn render_data_settings_panel(&self) -> impl IntoElement + '_ { + settings_panel( + "Data", + vec![ + settings_readonly_row("Stored profile", "Local SQLite".to_string()), + settings_readonly_row( + "Current company", + current_company_name(self.snapshot.current_company()), + ), + settings_readonly_row("Documents", self.snapshot.documents.len().to_string()), + settings_readonly_row("Artifacts", self.snapshot.artifacts.len().to_string()), + ], + ) + } + + fn render_secrets_settings_panel(&self) -> impl IntoElement + '_ { + settings_panel( + "Secrets", + vec![settings_readonly_row( + "Connector auth surfaces", + self.snapshot.connectors.len().to_string(), + )], + ) } } -fn settings_nav_row(label: &'static str, active: bool) -> impl IntoElement { +fn settings_tabs(cx_tab: SettingsTab, cx: &mut Context) -> impl IntoElement { div() + .flex() + .gap_1() + .border_b_1() + .border_color(theme::panel_border()) + .children([ + settings_tab_button("General", SettingsTab::General, cx_tab, cx).into_any_element(), + settings_tab_button("Mode", SettingsTab::Mode, cx_tab, cx).into_any_element(), + settings_tab_button("Providers", SettingsTab::Providers, cx_tab, cx).into_any_element(), + settings_tab_button("Shortcuts", SettingsTab::Shortcuts, cx_tab, cx).into_any_element(), + settings_tab_button("Data", SettingsTab::Data, cx_tab, cx).into_any_element(), + settings_tab_button("Secrets", SettingsTab::Secrets, cx_tab, cx).into_any_element(), + ]) +} + +fn settings_tab_button( + label: &'static str, + tab: SettingsTab, + active_tab: SettingsTab, + cx: &mut Context, +) -> impl IntoElement { + let active = tab == active_tab; + div() + .id(SharedString::from(format!("settings-tab-{label}"))) + .relative() .px_4() - .py_3() - .rounded_lg() - .bg(if active { - theme::selection_fill() - } else { - theme::panel() - }) + .py_2() + .cursor_pointer() + .text_sm() .text_color(if active { - theme::sidebar_text_active() + theme::text_primary() } else { - theme::text_secondary() + theme::text_muted() }) + .hover(|style| style.text_color(theme::text_primary())) + .on_click(cx.listener(move |this, _, _, cx| { + this.state.settings_tab = tab; + 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 settings_panel(label: &'static str, rows: Vec) -> impl IntoElement { diff --git a/AgentCompany-Rust/progress.txt b/AgentCompany-Rust/progress.txt index f3e71a45..09953468 100644 --- a/AgentCompany-Rust/progress.txt +++ b/AgentCompany-Rust/progress.txt @@ -14,4 +14,5 @@ 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- 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 \ No newline at end of file +- 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.