Wire native settings and workspace mutations

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 14:28:17 -04:00
1 parent 0a5be5ea97
commit a291e93608
7 files changed
+562 -74

No files matched your search

@@ -136,6 +136,62 @@ impl AppRoot {
}
}
fn update_theme(&mut self, theme: &'static str, cx: &mut Context<Self>) {
let result = {
let mut store = self.store.lock();
store.update_theme(theme)
};
match result {
Ok(_) => self.reload(format!("Theme updated to {theme}."), cx),
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn update_locale(&mut self, locale: &'static str, cx: &mut Context<Self>) {
let result = {
let mut store = self.store.lock();
store.update_locale(locale)
};
match result {
Ok(_) => self.reload(format!("Locale updated to {locale}."), cx),
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn update_execution_mode(&mut self, execution_mode: &'static str, cx: &mut Context<Self>) {
let Some(company_id) = self.current_company_id().map(str::to_string) else {
self.set_status("Create a company first.", cx);
return;
};
let result = {
let mut store = self.store.lock();
store.update_execution_mode(&company_id, execution_mode)
};
match result {
Ok(_) => self.reload(format!("Execution mode updated to {execution_mode}."), cx),
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn set_primary_selected_workspace(&mut self, cx: &mut Context<Self>) {
let Some(company_id) = self.current_company_id().map(str::to_string) else {
self.set_status("Create a company first.", cx);
return;
};
let Some(workspace_id) = self.state.selected_workspace_id.clone() else {
self.set_status("Choose a workspace first.", cx);
return;
};
let result = {
let mut store = self.store.lock();
store.set_primary_workspace(&company_id, &workspace_id)
};
match result {
Ok(_) => self.reload("Primary workspace updated.", cx),
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn decide_selected_approval(
&mut self,
state: ApprovalState,
@@ -314,7 +370,9 @@ impl Render for AppRoot {
SectionId::Workspaces => {
self.render_workspaces(cx).into_any_element()
}
SectionId::Settings => self.render_settings().into_any_element(),
SectionId::Settings => {
self.render_settings(cx).into_any_element()
}
}),
),
)
@@ -1,7 +1,7 @@
use super::*;
impl AppRoot {
pub(super) fn render_settings(&self) -> impl IntoElement + '_ {
pub(super) fn render_settings(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let current = self.snapshot.current_company();
let execution_mode = current
.map(|item| item.execution_mode.clone())
@@ -30,30 +30,97 @@ impl AppRoot {
div()
.flex_1()
.gap_6()
.child(settings_section(
.child(settings_panel(
"General",
vec![
settings_row("Profile path", self.snapshot.profile_path.clone()),
settings_row("Theme", self.snapshot.theme.clone()),
settings_row("Locale", self.snapshot.locale.clone()),
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_section(
.child(settings_panel(
"Execution mode",
vec![settings_row("Company mode", 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_section(
.child(settings_panel(
"Providers",
vec![settings_row(
vec![settings_readonly_row(
"Configured connectors",
self.snapshot.connectors.len().to_string(),
)],
))
.child(settings_section(
.child(settings_panel(
"Data",
vec![
settings_row("Stored profile", "Local SQLite".to_string()),
settings_row("Current company", current_company_name(current)),
settings_readonly_row("Stored profile", "Local SQLite".to_string()),
settings_readonly_row("Current company", current_company_name(current)),
],
)),
)
@@ -78,7 +145,7 @@ fn settings_nav_row(label: &'static str, active: bool) -> impl IntoElement {
.child(label)
}
fn settings_section(label: &'static str, rows: Vec<gpui::Div>) -> impl IntoElement {
fn settings_panel(label: &'static str, rows: Vec<gpui::AnyElement>) -> impl IntoElement {
div()
.p_5()
.rounded_lg()
@@ -94,7 +161,7 @@ fn settings_section(label: &'static str, rows: Vec<gpui::Div>) -> impl IntoEleme
.child(div().mt(px(16.0)).children(rows))
}
fn settings_row(label: &'static str, value: String) -> gpui::Div {
fn settings_readonly_row(label: &'static str, value: String) -> gpui::AnyElement {
div()
.flex()
.justify_between()
@@ -109,6 +176,65 @@ fn settings_row(label: &'static str, value: String) -> gpui::Div {
.text_color(theme::text_primary())
.child(value),
)
.into_any_element()
}
fn settings_action_row(
label: &'static str,
value: String,
actions: Vec<gpui::AnyElement>,
) -> gpui::AnyElement {
div()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.flex()
.justify_between()
.items_center()
.child(div().text_sm().text_color(theme::text_muted()).child(label))
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(value),
),
)
.child(div().mt(px(10.0)).flex().gap_2().children(actions))
.into_any_element()
}
fn settings_action_button<V>(
label: &'static str,
active: bool,
cx: &mut Context<V>,
on_click: impl Fn(&mut V, &mut Context<V>) + 'static,
) -> impl IntoElement
where
V: 'static,
{
div()
.id(SharedString::from(format!("settings-action-{label}")))
.px_3()
.py_2()
.rounded_lg()
.cursor_pointer()
.bg(if active {
theme::primary_action()
} else {
theme::panel_muted()
})
.text_color(if active {
theme::primary_action_text()
} else {
theme::text_secondary()
})
.hover(|style| style.bg(theme::panel_hover()))
.on_click(
cx.listener(move |this, _: &gpui::ClickEvent, _: &mut Window, cx| on_click(this, cx)),
)
.child(label)
}
fn current_company_name(company: Option<&domain::Company>) -> String {
@@ -2,6 +2,17 @@ use super::*;
impl AppRoot {
pub(super) fn render_workspaces(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_workspace_id
.as_ref()
.and_then(|workspace_id| {
self.snapshot
.workspaces
.iter()
.find(|workspace| &workspace.id == workspace_id)
});
div()
.flex()
.flex_col()
@@ -26,7 +37,16 @@ impl AppRoot {
.text_color(theme::text_muted())
.child(format!("{} linked workspaces", self.snapshot.workspaces.len())),
),
),
)
.when_some(selected, |container, workspace| {
if workspace.is_primary {
container
} else {
container.child(primary_button("Set primary", cx, |this, cx| {
this.set_primary_selected_workspace(cx)
}))
}
}),
)
.child(div().h(px(1.0)).bg(theme::panel_border()))
.when(self.snapshot.workspaces.is_empty(), |container| {
@@ -62,64 +82,63 @@ impl AppRoot {
})
.when(!self.snapshot.workspaces.is_empty(), |container| {
container.child(
div()
.children(self.snapshot.workspaces.iter().map(|workspace| {
let active = self.state.selected_workspace_id.as_deref()
== Some(workspace.id.as_str());
let workspace_id = workspace.id.clone();
div()
.id(SharedString::from(format!("workspace-{}", workspace.id)))
.flex()
.justify_between()
.items_center()
.px_6()
.py_5()
.border_b_1()
.border_color(theme::panel_border())
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::window_bg()
})
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_workspace_id = Some(workspace_id.clone());
cx.notify();
}))
.child(
div()
.flex_1()
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(workspace.name.clone()),
)
.child(
div()
.mt(px(8.0))
.text_sm()
.text_color(theme::text_muted())
.child(workspace.local_path.clone()),
)
.child(
div()
.mt(px(8.0))
.text_xs()
.text_color(theme::text_subtle())
.child(format!(
"{} · {}",
workspace.state,
workspace_kind(workspace.is_primary)
)),
),
)
.child(workspace_state_badge(
workspace.state.as_str(),
workspace.is_primary,
))
})),
div().children(self.snapshot.workspaces.iter().map(|workspace| {
let active =
self.state.selected_workspace_id.as_deref() == Some(workspace.id.as_str());
let workspace_id = workspace.id.clone();
div()
.id(SharedString::from(format!("workspace-{}", workspace.id)))
.flex()
.justify_between()
.items_center()
.px_6()
.py_5()
.border_b_1()
.border_color(theme::panel_border())
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::window_bg()
})
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_workspace_id = Some(workspace_id.clone());
cx.notify();
}))
.child(
div()
.flex_1()
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(workspace.name.clone()),
)
.child(
div()
.mt(px(8.0))
.text_sm()
.text_color(theme::text_muted())
.child(workspace.local_path.clone()),
)
.child(
div()
.mt(px(8.0))
.text_xs()
.text_color(theme::text_subtle())
.child(format!(
"{} · {}",
workspace.state,
workspace_kind(workspace.is_primary)
)),
),
)
.child(workspace_state_badge(
workspace.state.as_str(),
workspace.is_primary,
))
})),
)
})
}
@@ -154,6 +154,85 @@ impl ProfileStore {
Ok(())
}
pub fn update_theme(&mut self, theme: &str) -> Result<()> {
let normalized = match theme {
"system" | "light" | "dark" => theme,
_ => return Err(anyhow!("unsupported theme value: {theme}")),
};
self.conn.execute(
"insert into settings (key, value) values ('theme', ?1) on conflict(key) do update set value = excluded.value",
params![normalized],
)?;
Ok(())
}
pub fn update_locale(&mut self, locale: &str) -> Result<()> {
let normalized = match locale {
"en" | "zh" => locale,
_ => return Err(anyhow!("unsupported locale value: {locale}")),
};
self.conn.execute(
"insert into settings (key, value) values ('locale', ?1) on conflict(key) do update set value = excluded.value",
params![normalized],
)?;
Ok(())
}
pub fn update_execution_mode(&mut self, company_id: &str, execution_mode: &str) -> Result<()> {
let normalized = normalized_execution_mode(execution_mode);
let ts = now_iso()?;
self.conn.execute(
"update companies set execution_mode = ?1, auto_approve_hires = ?2, review_deliverables = ?3, updated_at = ?4 where id = ?5",
params![
normalized,
if normalized == "ai_ceo" { 1 } else { 0 },
if normalized == "ai_cofounder" { 1 } else { 0 },
ts,
company_id
],
)?;
self.add_activity(
company_id,
BOARD_READER_ID,
"updated_execution_mode",
"companies",
company_id,
normalized,
)?;
Ok(())
}
pub fn set_primary_workspace(&mut self, company_id: &str, workspace_id: &str) -> Result<()> {
let ts = now_iso()?;
let tx = self.conn.transaction()?;
let belongs = tx.query_row(
"select count(1) from workspaces where id = ?1 and company_id = ?2",
params![workspace_id, company_id],
|row| row.get::<_, i64>(0),
)?;
if belongs == 0 {
return Err(anyhow!("workspace {workspace_id} was not found"));
}
tx.execute(
"update workspaces set is_primary = 0, updated_at = ?1 where company_id = ?2",
params![ts, company_id],
)?;
tx.execute(
"update workspaces set is_primary = 1, updated_at = ?1 where id = ?2 and company_id = ?3",
params![ts, workspace_id, company_id],
)?;
tx.commit()?;
self.add_activity(
company_id,
BOARD_READER_ID,
"updated_primary_workspace",
"workspaces",
workspace_id,
"primary workspace changed",
)?;
Ok(())
}
pub fn decide_approval(
&mut self,
company_id: &str,
@@ -0,0 +1,62 @@
use storage::{NewCompanyInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn settings_and_workspace_mutations_round_trip_through_snapshot() {
let sandbox = tempdir().expect("temp dir");
let mut store = ProfileStore::open(sandbox.path()).expect("open store");
let company_id = store
.create_company(NewCompanyInput {
name: "Orbit Settings".to_string(),
description: "Native control plane".to_string(),
workspace_path: "/tmp/orbit-settings".to_string(),
execution_mode: "ai_ceo".to_string(),
})
.expect("create company");
let ts = support::time::now_iso().expect("timestamp");
let secondary_workspace_id = uuid::Uuid::new_v4().to_string();
let conn = rusqlite::Connection::open(store.db_path()).expect("open raw db");
conn.execute(
"insert into workspaces (id, company_id, name, local_path, repo_url, repo_ref, project_id, state, is_primary, created_at, updated_at) values (?1, ?2, 'Secondary Workspace', '/tmp/orbit-settings/secondary', '', '', null, 'blocked', 0, ?3, ?3)",
rusqlite::params![secondary_workspace_id, company_id, ts],
)
.expect("insert secondary workspace");
store.update_theme("dark").expect("update theme");
store.update_locale("zh").expect("update locale");
store
.update_execution_mode(&company_id, "ai_cofounder")
.expect("update execution mode");
store
.set_primary_workspace(&company_id, &secondary_workspace_id)
.expect("set primary workspace");
let snapshot = store.snapshot().expect("snapshot");
assert_eq!(snapshot.theme, "dark");
assert_eq!(snapshot.locale, "zh");
let company = snapshot
.companies
.iter()
.find(|item| item.id == company_id)
.expect("company present");
assert_eq!(company.execution_mode, "ai_cofounder");
let primary_workspace = snapshot
.workspaces
.iter()
.find(|workspace| workspace.is_primary)
.expect("primary workspace present");
assert_eq!(primary_workspace.id, secondary_workspace_id);
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("reopened snapshot");
assert_eq!(reopened_snapshot.theme, "dark");
assert_eq!(reopened_snapshot.locale, "zh");
assert!(
reopened_snapshot
.workspaces
.iter()
.any(|workspace| workspace.id == secondary_workspace_id && workspace.is_primary)
);
}
+133
View File
@@ -0,0 +1,133 @@
{
"project": "AgentCompany-Rust",
"branchName": "ralph/agentcompany-rust-native-refactor",
"description": "Complete the Electron-to-native Rust+GPUI rewrite with UI/UX parity, runtime contract completion, and native verification.",
"userStories": [
{
"id": "US-001",
"title": "Rebuild native shell, onboarding, dashboard, sidebar, and messages surfaces",
"description": "As an operator, I want the core shell surfaces to match the original product grammar so that the native app feels familiar and production-ready.",
"acceptanceCriteria": [
"Native shell, sidebar, onboarding, dashboard, and messages surfaces are implemented in GPUI",
"UI hierarchy follows the original Electron product surfaces",
"Typecheck passes"
],
"priority": 1,
"passes": true,
"notes": "Completed in commits 301934a and earlier parity work."
},
{
"id": "US-002",
"title": "Rebuild approvals and documents native surfaces",
"description": "As an operator, I want approvals and documents to match the original information architecture so that review and document workflows stay intuitive.",
"acceptanceCriteria": [
"Approvals surface matches the original sectioned layout",
"Documents surface matches the original document center grammar",
"Typecheck passes",
"Verify in native app"
],
"priority": 2,
"passes": true,
"notes": "Completed in commit 0f30772."
},
{
"id": "US-003",
"title": "Rebuild runs and inbox native surfaces",
"description": "As an operator, I want run inspection and attention queues to match the original app so that active work remains legible at a glance.",
"acceptanceCriteria": [
"Runs queue and detail layout are implemented in GPUI",
"Inbox attention groups are implemented in GPUI",
"Typecheck passes",
"Verify in native app"
],
"priority": 3,
"passes": true,
"notes": "Completed in commit 7fe5d6f."
},
{
"id": "US-004",
"title": "Rebuild settings and workspaces native surfaces",
"description": "As an operator, I want settings and workspace management to match the original desktop structure so that control-plane tasks remain consistent.",
"acceptanceCriteria": [
"Settings shell is represented as a sectioned native surface",
"Workspaces are represented as a native list surface",
"Typecheck passes",
"Verify in native app"
],
"priority": 4,
"passes": true,
"notes": "Completed in commit 5cb212c."
},
{
"id": "US-005",
"title": "Extend native persistence verification for aligned surfaces",
"description": "As a maintainer, I want native storage-backed verification so that rebuilt surfaces rest on stable persisted state.",
"acceptanceCriteria": [
"Storage-backed end-to-end test covers reviews, approvals, runs, and documents",
"cargo test --workspace passes",
"Typecheck passes"
],
"priority": 5,
"passes": true,
"notes": "Completed with native_surface_e2e.rs in commit 0f30772."
},
{
"id": "US-006",
"title": "Implement native settings mutations and workspace interactions",
"description": "As an operator, I want settings and workspace surfaces to perform real actions so that the native app controls production state end-to-end.",
"acceptanceCriteria": [
"Settings theme/locale/execution mode/provider/data actions are wired to real Rust-side state transitions or bridge actions",
"Workspace selection and primary workspace interactions are backed by real state changes",
"No fallback behavior is introduced",
"Typecheck passes",
"Verify in native app"
],
"priority": 6,
"passes": true,
"notes": "Completed with real settings/workspace mutations and snapshot-backed verification."
},
{
"id": "US-007",
"title": "Add native animation and interaction-state parity",
"description": "As an operator, I want the native app to preserve the original motion, hover, selection, and attention behavior so that the product feels identical in use.",
"acceptanceCriteria": [
"Core surfaces implement matching hover, selection, and emphasis states",
"Onboarding, inbox, messages, and dashboard include native motion/state treatment aligned with Electron references",
"No placeholder animation remains",
"Typecheck passes",
"Verify in native app"
],
"priority": 7,
"passes": false,
"notes": "High-value remaining parity layer."
},
{
"id": "US-008",
"title": "Complete runtime contract integration into native app flows",
"description": "As an operator, I want Pi Mono run, steer, follow-up, approval, and artifact events to drive the native UI so that the app is a complete replacement for Electron.",
"acceptanceCriteria": [
"Run lifecycle events are surfaced in native UI from real runtime data",
"Approval gates participate in real runtime flows",
"Message and artifact updates reflect live runtime contract events",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 8,
"passes": false,
"notes": "Core completion story for full replacement."
},
{
"id": "US-009",
"title": "Add final native end-to-end verification matrix",
"description": "As a maintainer, I want a broader native verification matrix so that the rewritten app can be trusted as the production desktop path.",
"acceptanceCriteria": [
"Native tests cover onboarding bootstrap, run inspection, approval visibility, document visibility, settings/workspace state, and inbox attention derivation",
"cargo clippy --workspace --all-targets --all-features -- -D warnings passes",
"cargo test --workspace passes"
],
"priority": 9,
"passes": false,
"notes": "Final confidence layer before calling the rewrite complete."
}
]
}
+11
View File
@@ -0,0 +1,11 @@
# Ralph Progress Log
Started: 2026-04-19
---
- Established native GPUI shell parity direction by reading PRD.md, Electron source surfaces, and visual screenshots before editing.
- Completed native shell, onboarding, overview, sidebar, and messages parity work.
- Completed approvals, documents, runs, inbox, settings, and workspaces parity passes.
- Added native storage-backed verification covering reviews, approvals, runs, and documents.
- Remaining work is concentrated in interaction depth, animation/state parity, and full runtime contract completion.
- Standing constraint: all code is written directly in the current context; no subagents or agent teams are used.
- Completed real settings/workspace state mutations in native Rust path: theme, locale, execution mode, and primary workspace.
- Added native_settings_workspace_e2e.rs to verify mutation persistence across reopen.