refactor: split native app root by screen

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 01:32:40 -04:00
1 parent 6508e75a62
commit 5097591725
12 files changed
+964 -712

No files matched your search

+11 -712
View File
@@ -10,6 +10,17 @@ use domain::{ApprovalState, ProfileSnapshot, SectionId};
use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px, rgb};
use storage::NewCompanyInput;
mod approvals;
mod documents;
mod inbox;
mod messages;
mod onboarding;
mod overview;
mod runs;
mod settings;
mod sidebar;
mod workspaces;
pub struct AppRoot {
store: SharedStore,
snapshot: ProfileSnapshot,
@@ -229,718 +240,6 @@ impl AppRoot {
.child(div().text_3xl().text_color(rgb(0x171411)).child(value))
.child(div().text_sm().text_color(rgb(0x6f665d)).child(detail))
}
fn render_sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let sections = [
SectionId::Overview,
SectionId::Runs,
SectionId::Inbox,
SectionId::Messages,
SectionId::Approvals,
SectionId::Documents,
SectionId::Workspaces,
SectionId::Settings,
];
div()
.w(px(208.0))
.h_full()
.p_4()
.gap_4()
.flex()
.flex_col()
.bg(rgb(0x161514))
.text_color(rgb(0xf5eee4))
.child(div().text_lg().child("AgentCompany"))
.children(self.snapshot.companies.iter().map(|company| {
let active =
self.snapshot.current_company_id.as_deref() == Some(company.id.as_str());
let company_id = company.id.clone();
div()
.id(SharedString::from(format!("company-{}", company.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0x9e3d2d) } else { rgb(0x211d1a) })
.hover(|style| style.bg(rgb(0x362c27)))
.on_click(cx.listener(move |this, _, _, cx| {
this.switch_company(company_id.clone(), cx)
}))
.child(company.name.clone())
}))
.child(div().h(px(1.0)).bg(rgb(0x2f2924)))
.children(sections.into_iter().map(|section| {
let active = self.state.section == section;
div()
.id(SharedString::from(format!(
"section-{}",
section.label().to_lowercase()
)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0x161514) })
.text_color(if active { rgb(0x171411) } else { rgb(0xf5eee4) })
.hover(|style| style.bg(rgb(0x2c2825)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.section = section;
cx.notify();
}))
.child(section.label())
}))
}
fn render_overview(&self) -> impl IntoElement + '_ {
let company = self.snapshot.current_company();
let company_name = company
.map(|item| item.name.clone())
.unwrap_or_else(|| "No company".to_string());
div()
.flex()
.flex_col()
.gap_4()
.child(
div()
.flex()
.gap_4()
.child(self.metric_card(
"Active tasks",
self.snapshot.active_tasks().to_string(),
"Tasks in todo, review, or execution",
))
.child(self.metric_card(
"Pending approvals",
self.snapshot.pending_approvals().to_string(),
"Human review gates",
))
.child(self.metric_card(
"Unread messages",
self.snapshot.unread_messages().to_string(),
"Company and direct channels",
))
.child(self.metric_card(
"Active runs",
self.snapshot.active_runs().to_string(),
"Queued, running, and waiting runs",
)),
)
.child(
div()
.flex()
.gap_4()
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child(company_name))
.children(self.snapshot.tasks.iter().take(6).map(|task| {
div()
.py_2()
.border_b_1()
.border_color(rgb(0xeee3d7))
.child(format!("{} · {}", task.status, task.title))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Recent activity"))
.children(self.snapshot.activity.iter().take(6).map(|item| {
div()
.py_2()
.border_b_1()
.border_color(rgb(0xeee3d7))
.child(format!("{} · {}", item.action, item.detail))
})),
),
)
}
fn render_runs(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_run_id
.as_ref()
.and_then(|id| self.snapshot.runs.iter().find(|item| &item.id == id));
let selected_status = selected
.map(|item| item.status.clone())
.unwrap_or_else(|| "Select a run".to_string());
let selected_summary = selected
.map(|item| item.summary.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "No run selected.".to_string());
let selected_model = selected
.and_then(|item| item.model.clone())
.unwrap_or_else(|| "model pending".to_string());
let selected_agent = selected
.and_then(|run| {
self.snapshot
.agents
.iter()
.find(|agent| agent.id == run.agent_id)
.map(|agent| agent.name.clone())
})
.unwrap_or_else(|| "Agent unavailable".to_string());
let selected_workspace = selected
.and_then(|run| {
run.workspace_id.as_ref().and_then(|workspace_id| {
self.snapshot
.workspaces
.iter()
.find(|workspace| &workspace.id == workspace_id)
.map(|workspace| workspace.name.clone())
})
})
.unwrap_or_else(|| "Workspace pending".to_string());
let selected_error = selected
.and_then(|item| item.error_message.clone())
.unwrap_or_default();
div()
.flex()
.gap_4()
.child(
div()
.id("runs-list")
.w(px(360.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Run queue"))
.children(self.snapshot.runs.iter().map(|item| {
let active =
self.state.selected_run_id.as_deref() == Some(item.id.as_str());
let run_id = item.id.clone();
let headline = if item.summary.trim().is_empty() {
format!("{} · {}", item.status, item.id)
} else {
format!("{} · {}", item.status, item.summary)
};
div()
.id(SharedString::from(format!("run-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_run_id = Some(run_id.clone());
cx.notify();
}))
.child(headline)
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Run inspector"))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(selected_status),
)
.child(
div()
.flex()
.gap_2()
.child(self.metric_card("Agent", selected_agent, "Assigned operator"))
.child(self.metric_card(
"Model",
selected_model,
"Runtime-selected model",
)),
)
.child(
div()
.flex()
.gap_2()
.child(self.metric_card(
"Workspace",
selected_workspace,
"Bound workspace",
))
.child(
self.metric_card(
"Connector",
selected
.map(|item| item.connector_id.clone())
.unwrap_or_else(|| "connector pending".to_string()),
"Execution provider",
),
),
)
.child(
div()
.min_h(px(180.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(selected_summary),
)
.when(!selected_error.is_empty(), |container| {
container.child(
div()
.p_3()
.border_1()
.border_color(rgb(0x9e3d2d))
.rounded_md()
.bg(rgb(0xffefe8))
.child(selected_error),
)
}),
)
}
fn render_inbox(&self) -> impl IntoElement + '_ {
div()
.flex()
.gap_4()
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Unread messages"))
.children(
self.snapshot
.agent_messages
.iter()
.filter(|item| item.read_at.is_none())
.take(8)
.map(|item| {
div()
.py_2()
.border_b_1()
.border_color(rgb(0xeee3d7))
.child(format!("{} · {}", item.subject, item.body))
}),
),
)
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Pending approvals"))
.children(
self.snapshot
.approvals
.iter()
.filter(|item| item.state == "pending")
.take(8)
.map(|item| {
div().py_2().border_b_1().border_color(rgb(0xeee3d7)).child(
format!("{} · {}", item.approval_type, item.payload_summary),
)
}),
),
)
}
fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self.state.selected_message_id.as_ref().and_then(|id| {
self.snapshot
.agent_messages
.iter()
.find(|item| &item.id == id)
});
let selected_subject = selected
.map(|item| item.subject.clone())
.unwrap_or_else(|| "Select a thread".to_string());
let selected_body = selected
.map(|item| item.body.clone())
.unwrap_or_else(|| "No message selected.".to_string());
div()
.flex()
.gap_4()
.child(
div()
.id("messages-list")
.w(px(320.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Channels"))
.children(self.snapshot.agent_messages.iter().map(|item| {
let active =
self.state.selected_message_id.as_deref() == Some(item.id.as_str());
let message_id = item.id.clone();
div()
.id(SharedString::from(format!("message-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_message_id = Some(message_id.clone());
cx.notify();
}))
.child(format!("{} · {}", item.subject, item.priority))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Conversation"))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(selected_subject),
)
.child(
div()
.min_h(px(220.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(selected_body),
)
.child(self.message_input.clone())
.child(
div()
.flex()
.gap_2()
.child(primary_button("Send update", cx, |this, cx| {
this.send_message(cx)
}))
.child(secondary_button("Mark read", cx, |this, cx| {
this.mark_selected_message_read(cx)
})),
),
)
}
fn render_approvals(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_approval_id
.as_ref()
.and_then(|id| self.snapshot.approvals.iter().find(|item| &item.id == id));
let approval_type = selected
.map(|item| item.approval_type.clone())
.unwrap_or_else(|| "Select an approval".to_string());
let approval_summary = selected
.map(|item| item.impact_summary.clone())
.unwrap_or_else(|| "No approval selected.".to_string());
div()
.flex()
.gap_4()
.child(
div()
.id("approvals-list")
.w(px(340.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Approval queue"))
.children(self.snapshot.approvals.iter().map(|item| {
let active =
self.state.selected_approval_id.as_deref() == Some(item.id.as_str());
let approval_id = item.id.clone();
div()
.id(SharedString::from(format!("approval-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_approval_id = Some(approval_id.clone());
cx.notify();
}))
.child(format!("{} · {}", item.state, item.payload_summary))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Decision"))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(approval_type),
)
.child(
div()
.min_h(px(160.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(approval_summary),
)
.child(
div()
.flex()
.gap_2()
.child(primary_button("Approve", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Approved,
"Approved from native shell.",
cx,
)
}))
.child(secondary_button("Reject", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Rejected,
"Rejected from native shell.",
cx,
)
})),
),
)
}
fn render_documents(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_document_id
.as_ref()
.and_then(|id| self.snapshot.documents.iter().find(|item| &item.id == id));
let document_title = selected
.map(|item| item.title.clone())
.unwrap_or_else(|| "Select a document".to_string());
let document_content = selected
.map(|item| item.content.clone())
.unwrap_or_else(|| "No document selected.".to_string());
div()
.flex()
.gap_4()
.child(
div()
.id("documents-list")
.w(px(340.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Document center"))
.children(self.snapshot.documents.iter().map(|item| {
let active =
self.state.selected_document_id.as_deref() == Some(item.id.as_str());
let document_id = item.id.clone();
div()
.id(SharedString::from(format!("document-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_document_id = Some(document_id.clone());
cx.notify();
}))
.child(format!("{} · {}", item.status, item.title))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Quick draft"))
.child(self.document_title_input.clone())
.child(self.document_body_input.clone())
.child(primary_button("Save draft", cx, |this, cx| {
this.create_document(cx)
}))
.child(div().h(px(1.0)).bg(rgb(0xeee3d7)))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(document_title),
)
.child(
div()
.min_h(px(200.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(document_content),
),
)
}
fn render_workspaces(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
div()
.flex()
.flex_col()
.gap_3()
.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)))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(if active { rgb(0xf7f0e7) } else { rgb(0xfffbf5) })
.cursor_pointer()
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_workspace_id = Some(workspace_id.clone());
cx.notify();
}))
.child(div().text_lg().child(workspace.name.clone()))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(workspace.local_path.clone()),
)
.child(div().text_sm().text_color(rgb(0x6f665d)).child(format!(
"{} · {}",
workspace.state,
if workspace.is_primary {
"primary"
} else {
"linked"
}
)))
}))
}
fn render_settings(&self) -> 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_4()
.child(self.metric_card(
"Profile path",
self.snapshot.profile_path.clone(),
"Electron-compatible local profile",
))
.child(self.metric_card(
"Theme",
self.snapshot.theme.clone(),
"Stored in the shared settings table",
))
.child(self.metric_card(
"Locale",
self.snapshot.locale.clone(),
"Read from settings or the OS locale",
))
.child(self.metric_card(
"Connectors",
self.snapshot.connectors.len().to_string(),
execution_mode,
))
}
fn render_onboarding(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
div()
.size_full()
.p_8()
.flex()
.gap_8()
.bg(rgb(0xf3efe8))
.child(
div()
.flex_1()
.gap_4()
.child(div().text_3xl().text_color(rgb(0x171411)).child("Start a native AgentCompany profile"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Create the first company, point it at a workspace, and keep the SQLite profile local."))
.child(self.company_name_input.clone())
.child(self.mission_input.clone())
.child(self.workspace_input.clone())
.child(primary_button("Create company", cx, |this, cx| this.create_company(cx))),
)
.child(
div()
.w(px(380.0))
.p_5()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("What lands in SQLite"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Company"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Primary workspace"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Connector catalog"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Shared profile settings")),
)
}
}
impl Render for AppRoot {
@@ -0,0 +1,97 @@
use super::*;
impl AppRoot {
pub(super) fn render_approvals(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_approval_id
.as_ref()
.and_then(|id| self.snapshot.approvals.iter().find(|item| &item.id == id));
let approval_type = selected
.map(|item| item.approval_type.clone())
.unwrap_or_else(|| "Select an approval".to_string());
let approval_summary = selected
.map(|item| item.impact_summary.clone())
.unwrap_or_else(|| "No approval selected.".to_string());
div()
.flex()
.gap_4()
.child(
div()
.id("approvals-list")
.w(px(340.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Approval queue"))
.children(self.snapshot.approvals.iter().map(|item| {
let active =
self.state.selected_approval_id.as_deref() == Some(item.id.as_str());
let approval_id = item.id.clone();
div()
.id(SharedString::from(format!("approval-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_approval_id = Some(approval_id.clone());
cx.notify();
}))
.child(format!("{} · {}", item.state, item.payload_summary))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Decision"))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(approval_type),
)
.child(
div()
.min_h(px(160.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(approval_summary),
)
.child(
div()
.flex()
.gap_2()
.child(primary_button("Approve", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Approved,
"Approved from native shell.",
cx,
)
}))
.child(secondary_button("Reject", cx, |this, cx| {
this.decide_selected_approval(
ApprovalState::Rejected,
"Rejected from native shell.",
cx,
)
})),
),
)
}
}
@@ -0,0 +1,84 @@
use super::*;
impl AppRoot {
pub(super) fn render_documents(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_document_id
.as_ref()
.and_then(|id| self.snapshot.documents.iter().find(|item| &item.id == id));
let document_title = selected
.map(|item| item.title.clone())
.unwrap_or_else(|| "Select a document".to_string());
let document_content = selected
.map(|item| item.content.clone())
.unwrap_or_else(|| "No document selected.".to_string());
div()
.flex()
.gap_4()
.child(
div()
.id("documents-list")
.w(px(340.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Document center"))
.children(self.snapshot.documents.iter().map(|item| {
let active =
self.state.selected_document_id.as_deref() == Some(item.id.as_str());
let document_id = item.id.clone();
div()
.id(SharedString::from(format!("document-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_document_id = Some(document_id.clone());
cx.notify();
}))
.child(format!("{} · {}", item.status, item.title))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Quick draft"))
.child(self.document_title_input.clone())
.child(self.document_body_input.clone())
.child(primary_button("Save draft", cx, |this, cx| {
this.create_document(cx)
}))
.child(div().h(px(1.0)).bg(rgb(0xeee3d7)))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(document_title),
)
.child(
div()
.min_h(px(200.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(document_content),
),
)
}
}
@@ -0,0 +1,57 @@
use super::*;
impl AppRoot {
pub(super) fn render_inbox(&self) -> impl IntoElement + '_ {
div()
.flex()
.gap_4()
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Unread messages"))
.children(
self.snapshot
.agent_messages
.iter()
.filter(|item| item.read_at.is_none())
.take(8)
.map(|item| {
div()
.py_2()
.border_b_1()
.border_color(rgb(0xeee3d7))
.child(format!("{} · {}", item.subject, item.body))
}),
),
)
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Pending approvals"))
.children(
self.snapshot
.approvals
.iter()
.filter(|item| item.state == "pending")
.take(8)
.map(|item| {
div().py_2().border_b_1().border_color(rgb(0xeee3d7)).child(
format!("{} · {}", item.approval_type, item.payload_summary),
)
}),
),
)
}
}
@@ -0,0 +1,91 @@
use super::*;
impl AppRoot {
pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self.state.selected_message_id.as_ref().and_then(|id| {
self.snapshot
.agent_messages
.iter()
.find(|item| &item.id == id)
});
let selected_subject = selected
.map(|item| item.subject.clone())
.unwrap_or_else(|| "Select a thread".to_string());
let selected_body = selected
.map(|item| item.body.clone())
.unwrap_or_else(|| "No message selected.".to_string());
div()
.flex()
.gap_4()
.child(
div()
.id("messages-list")
.w(px(320.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Channels"))
.children(self.snapshot.agent_messages.iter().map(|item| {
let active =
self.state.selected_message_id.as_deref() == Some(item.id.as_str());
let message_id = item.id.clone();
div()
.id(SharedString::from(format!("message-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_message_id = Some(message_id.clone());
cx.notify();
}))
.child(format!("{} · {}", item.subject, item.priority))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Conversation"))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(selected_subject),
)
.child(
div()
.min_h(px(220.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(selected_body),
)
.child(self.message_input.clone())
.child(
div()
.flex()
.gap_2()
.child(primary_button("Send update", cx, |this, cx| {
this.send_message(cx)
}))
.child(secondary_button("Mark read", cx, |this, cx| {
this.mark_selected_message_read(cx)
})),
),
)
}
}
@@ -0,0 +1,38 @@
use super::*;
impl AppRoot {
pub(super) fn render_onboarding(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
div()
.size_full()
.p_8()
.flex()
.gap_8()
.bg(rgb(0xf3efe8))
.child(
div()
.flex_1()
.gap_4()
.child(div().text_3xl().text_color(rgb(0x171411)).child("Start a native AgentCompany profile"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Create the first company, point it at a workspace, and keep the SQLite profile local."))
.child(self.company_name_input.clone())
.child(self.mission_input.clone())
.child(self.workspace_input.clone())
.child(primary_button("Create company", cx, |this, cx| this.create_company(cx))),
)
.child(
div()
.w(px(380.0))
.p_5()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("What lands in SQLite"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Company"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Primary workspace"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Connector catalog"))
.child(div().text_sm().text_color(rgb(0x6f665d)).child("Shared profile settings")),
)
}
}
@@ -0,0 +1,80 @@
use super::*;
impl AppRoot {
pub(super) fn render_overview(&self) -> impl IntoElement + '_ {
let company = self.snapshot.current_company();
let company_name = company
.map(|item| item.name.clone())
.unwrap_or_else(|| "No company".to_string());
div()
.flex()
.flex_col()
.gap_4()
.child(
div()
.flex()
.gap_4()
.child(self.metric_card(
"Active tasks",
self.snapshot.active_tasks().to_string(),
"Tasks in todo, review, or execution",
))
.child(self.metric_card(
"Pending approvals",
self.snapshot.pending_approvals().to_string(),
"Human review gates",
))
.child(self.metric_card(
"Unread messages",
self.snapshot.unread_messages().to_string(),
"Company and direct channels",
))
.child(self.metric_card(
"Active runs",
self.snapshot.active_runs().to_string(),
"Queued, running, and waiting runs",
)),
)
.child(
div()
.flex()
.gap_4()
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child(company_name))
.children(self.snapshot.tasks.iter().take(6).map(|task| {
div()
.py_2()
.border_b_1()
.border_color(rgb(0xeee3d7))
.child(format!("{} · {}", task.status, task.title))
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Recent activity"))
.children(self.snapshot.activity.iter().take(6).map(|item| {
div()
.py_2()
.border_b_1()
.border_color(rgb(0xeee3d7))
.child(format!("{} · {}", item.action, item.detail))
})),
),
)
}
}
@@ -0,0 +1,152 @@
use super::*;
impl AppRoot {
pub(super) fn render_runs(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let selected = self
.state
.selected_run_id
.as_ref()
.and_then(|id| self.snapshot.runs.iter().find(|item| &item.id == id));
let selected_status = selected
.map(|item| item.status.clone())
.unwrap_or_else(|| "Select a run".to_string());
let selected_summary = selected
.map(|item| item.summary.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "No run selected.".to_string());
let selected_model = selected
.and_then(|item| item.model.clone())
.unwrap_or_else(|| "model pending".to_string());
let selected_agent = selected
.and_then(|run| {
self.snapshot
.agents
.iter()
.find(|agent| agent.id == run.agent_id)
.map(|agent| agent.name.clone())
})
.unwrap_or_else(|| "Agent unavailable".to_string());
let selected_workspace = selected
.and_then(|run| {
run.workspace_id.as_ref().and_then(|workspace_id| {
self.snapshot
.workspaces
.iter()
.find(|workspace| &workspace.id == workspace_id)
.map(|workspace| workspace.name.clone())
})
})
.unwrap_or_else(|| "Workspace pending".to_string());
let selected_error = selected
.and_then(|item| item.error_message.clone())
.unwrap_or_default();
div()
.flex()
.gap_4()
.child(
div()
.id("runs-list")
.w(px(360.0))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.overflow_scroll()
.child(div().text_lg().child("Run queue"))
.children(self.snapshot.runs.iter().map(|item| {
let active =
self.state.selected_run_id.as_deref() == Some(item.id.as_str());
let run_id = item.id.clone();
let headline = if item.summary.trim().is_empty() {
format!("{} · {}", item.status, item.id)
} else {
format!("{} · {}", item.status, item.summary)
};
div()
.id(SharedString::from(format!("run-{}", item.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0xfffbf5) })
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_run_id = Some(run_id.clone());
cx.notify();
}))
.child(headline)
})),
)
.child(
div()
.flex_1()
.p_4()
.gap_3()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(rgb(0xfffbf5))
.child(div().text_lg().child("Run inspector"))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(selected_status),
)
.child(
div()
.flex()
.gap_2()
.child(self.metric_card("Agent", selected_agent, "Assigned operator"))
.child(self.metric_card(
"Model",
selected_model,
"Runtime-selected model",
)),
)
.child(
div()
.flex()
.gap_2()
.child(self.metric_card(
"Workspace",
selected_workspace,
"Bound workspace",
))
.child(
self.metric_card(
"Connector",
selected
.map(|item| item.connector_id.clone())
.unwrap_or_else(|| "connector pending".to_string()),
"Execution provider",
),
),
)
.child(
div()
.min_h(px(180.0))
.p_3()
.border_1()
.border_color(rgb(0xeee3d7))
.rounded_md()
.bg(rgb(0xf7f0e7))
.child(selected_summary),
)
.when(!selected_error.is_empty(), |container| {
container.child(
div()
.p_3()
.border_1()
.border_color(rgb(0x9e3d2d))
.rounded_md()
.bg(rgb(0xffefe8))
.child(selected_error),
)
}),
)
}
}
@@ -0,0 +1,34 @@
use super::*;
impl AppRoot {
pub(super) fn render_settings(&self) -> 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_4()
.child(self.metric_card(
"Profile path",
self.snapshot.profile_path.clone(),
"Electron-compatible local profile",
))
.child(self.metric_card(
"Theme",
self.snapshot.theme.clone(),
"Stored in the shared settings table",
))
.child(self.metric_card(
"Locale",
self.snapshot.locale.clone(),
"Read from settings or the OS locale",
))
.child(self.metric_card(
"Connectors",
self.snapshot.connectors.len().to_string(),
execution_mode,
))
}
}
@@ -0,0 +1,64 @@
use super::*;
impl AppRoot {
pub(super) fn render_sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let sections = [
SectionId::Overview,
SectionId::Runs,
SectionId::Inbox,
SectionId::Messages,
SectionId::Approvals,
SectionId::Documents,
SectionId::Workspaces,
SectionId::Settings,
];
div()
.w(px(208.0))
.h_full()
.p_4()
.gap_4()
.flex()
.flex_col()
.bg(rgb(0x161514))
.text_color(rgb(0xf5eee4))
.child(div().text_lg().child("AgentCompany"))
.children(self.snapshot.companies.iter().map(|company| {
let active =
self.snapshot.current_company_id.as_deref() == Some(company.id.as_str());
let company_id = company.id.clone();
div()
.id(SharedString::from(format!("company-{}", company.id)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0x9e3d2d) } else { rgb(0x211d1a) })
.hover(|style| style.bg(rgb(0x362c27)))
.on_click(cx.listener(move |this, _, _, cx| {
this.switch_company(company_id.clone(), cx)
}))
.child(company.name.clone())
}))
.child(div().h(px(1.0)).bg(rgb(0x2f2924)))
.children(sections.into_iter().map(|section| {
let active = self.state.section == section;
div()
.id(SharedString::from(format!(
"section-{}",
section.label().to_lowercase()
)))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(if active { rgb(0xefe2d4) } else { rgb(0x161514) })
.text_color(if active { rgb(0x171411) } else { rgb(0xf5eee4) })
.hover(|style| style.bg(rgb(0x2c2825)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.section = section;
cx.notify();
}))
.child(section.label())
}))
}
}
@@ -0,0 +1,45 @@
use super::*;
impl AppRoot {
pub(super) fn render_workspaces(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
div()
.flex()
.flex_col()
.gap_3()
.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)))
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(rgb(0xd9cdbf))
.bg(if active { rgb(0xf7f0e7) } else { rgb(0xfffbf5) })
.cursor_pointer()
.hover(|style| style.bg(rgb(0xf4ede4)))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_workspace_id = Some(workspace_id.clone());
cx.notify();
}))
.child(div().text_lg().child(workspace.name.clone()))
.child(
div()
.text_sm()
.text_color(rgb(0x6f665d))
.child(workspace.local_path.clone()),
)
.child(div().text_sm().text_color(rgb(0x6f665d)).child(format!(
"{} · {}",
workspace.state,
if workspace.is_primary {
"primary"
} else {
"linked"
}
)))
}))
}
}
@@ -0,0 +1,211 @@
# AgentCompany-Rust Native GPUI Rewrite Plan
Goal: rebuild AgentCompany as a native Rust + GPUI desktop app that preserves the current Electron product surface, UI/UX structure, and core execution semantics while replacing the entire application stack with Rust crates and a typed Pi Mono sidecar boundary.
Architecture: keep the PRD four-layer split intact. UI lives in `crates/app_ui`, page/application state in `crates/app_models`, business entities and rules in `crates/domain`, persistence and sidecar orchestration in `crates/storage`, `crates/runtime`, and `crates/pi_bridge`. Preserve Electron parity by reading the Electron renderer, screenshots, and e2e flows as the truth source for layout and workflow behavior.
Tech Stack: Rust 2024, GPUI, rusqlite, anyhow/thiserror, Pi Mono sidecar over typed JSON RPC, cargo fmt/clippy/test.
---
## Phase 0: Ground truth and guardrails
### Task 0.1: Lock the rewrite truth sources into the repo
Objective: make the migration inputs explicit so implementation never guesses.
Files:
- Create: `docs/reference/electron-parity.md`
- Reference: `PRD.md`, `../README.md`, `../问题.md`, `../screenshots/*`, `../src/renderer/**/*`, `../tests/e2e/desktop.smoke.spec.ts`
Verification:
- Document section-to-section mapping for onboarding, dashboard, communication, approvals, documents, and settings.
- Confirm every item in PRD section 5 and 14 has a Rust destination.
### Task 0.2: Stabilize the Rust baseline
Objective: restore and verify the existing Rust workspace before structural changes.
Files:
- Existing workspace only
Verification commands:
- `cargo fmt --check`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace`
Status at plan creation: complete.
---
## Phase 1: UI shell parity foundation
### Task 1.1: Split the monolithic `app_root.rs`
Objective: move each page renderer into a dedicated module so the UI can grow toward parity without a god file.
Files:
- Modify: `crates/app_ui/src/app_root.rs`
- Create: `crates/app_ui/src/app_root/sidebar.rs`
- Create: `crates/app_ui/src/app_root/overview.rs`
- Create: `crates/app_ui/src/app_root/runs.rs`
- Create: `crates/app_ui/src/app_root/inbox.rs`
- Create: `crates/app_ui/src/app_root/messages.rs`
- Create: `crates/app_ui/src/app_root/approvals.rs`
- Create: `crates/app_ui/src/app_root/documents.rs`
- Create: `crates/app_ui/src/app_root/workspaces.rs`
- Create: `crates/app_ui/src/app_root/settings.rs`
- Create: `crates/app_ui/src/app_root/onboarding.rs`
Verification:
- `cargo fmt --check`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace`
### Task 1.2: Introduce explicit shell/theme tokens
Objective: replace ad-hoc colors and spacing with a tokenized shell that matches the Electron visual hierarchy.
Files:
- Create: `crates/app_ui/src/theme.rs`
- Create: `crates/app_ui/src/shell.rs`
- Modify: `crates/app_ui/src/lib.rs`
- Modify: `crates/app_ui/src/app_root.rs`
Verification:
- main shell matches screenshot structure: left sidebar, top title bar, content canvas, onboarding split screen.
- `cargo fmt --check`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace`
### Task 1.3: Add parity notes per major screen
Objective: each Rust screen states which Electron screen and screenshot it is matching.
Files:
- Create: `docs/reference/dashboard-parity.md`
- Create: `docs/reference/onboarding-parity.md`
- Create: `docs/reference/communication-parity.md`
- Create: `docs/reference/approvals-parity.md`
Verification:
- Each note names source files and screenshots.
---
## Phase 2: Data model and storage completion
### Task 2.1: Extend domain IDs and typed entities
Objective: replace stringly-typed cross-domain references with PRD-aligned newtypes and explicit enums.
Files:
- Modify: `crates/domain/src/lib.rs`
- Modify: `crates/domain/src/models.rs`
- Modify: `crates/domain/src/enums.rs`
- Create as needed under `crates/domain/src/`
Verification:
- repository and runtime APIs accept semantic IDs instead of free-form strings where feasible.
- `cargo test --workspace`
### Task 2.2: Expand SQLite schema toward PRD entities
Objective: support thread state, artifacts, workspace lease state, audit events, and richer approvals.
Files:
- Modify: `crates/storage/src/schema.rs`
- Modify: `crates/storage/src/store.rs`
- Create: `migrations/*` when schema versioning is introduced
- Create tests under `crates/storage/tests/`
Verification:
- migration tests cover schema bootstrap and round-trip persistence for company, run, approval, message, document, workspace, and artifact slices.
---
## Phase 3: Runtime contract completion
### Task 3.1: Expand Pi bridge protocol to PRD contract
Objective: cover handshake, run lifecycle, tool lifecycle, approvals, steer, follow-up, and compaction events.
Files:
- Modify: `crates/pi_bridge/src/protocol.rs`
- Modify: `crates/pi_bridge/src/client.rs`
- Modify: `crates/runtime/src/service.rs`
- Modify: `crates/runtime/tests/sidecar_contract.rs`
Verification:
- sidecar contract tests assert handshake, start run, tool approval, steer, follow-up, and run finished flow.
### Task 3.2: Add runtime projection into storage/app state
Objective: runtime events must land in durable state and visible UI state.
Files:
- Modify: `crates/runtime/src/lib.rs`
- Modify: `crates/storage/src/store.rs`
- Modify: `crates/app_models/src/state.rs`
- Add tests where needed
Verification:
- a synthetic runtime event sequence updates run status, approvals, messages, and artifacts in one end-to-end test.
---
## Phase 4: Vertical workflow parity
### Task 4.1: Onboarding parity
Objective: reproduce the Electron onboarding flow, step structure, and persistence semantics.
### Task 4.2: Dashboard parity
Objective: reproduce metrics, active work, and recent activity layout.
### Task 4.3: Communication parity
Objective: reproduce three-column message layout, message detail, and composer flow.
### Task 4.4: Approval parity
Objective: reproduce approval queue/detail/actions and connect decisions to runtime.
### Task 4.5: Documents/workspaces parity
Objective: reproduce document center and workspace/artifact visibility.
Verification for each task:
- UI-visible proof in Rust layout
- persisted state proof in SQLite
- runtime behavior proof where applicable
- automated test proof
---
## Phase 5: End-to-end verification
### Task 5.1: Native smoke harness
Objective: add a Rust-native smoke path that boots the app against a temp profile and exercises the primary first-run flow.
Files:
- Create: `crates/app/tests/native_smoke.rs` or equivalent supported harness
- Add any test support modules needed under `crates/app/tests/`
Verification:
- first-run onboarding bootstrap is exercised against a temp profile
- smoke test is part of `cargo test --workspace`
### Task 5.2: Runtime + storage end-to-end flow
Objective: one test drives onboarding bootstrap, run creation, tool approval, message writeback, and artifact registration through Rust crates.
Files:
- Create: `crates/runtime/tests/runtime_storage_e2e.rs`
- Modify storage/runtime code as needed
Verification:
- `cargo test --workspace runtime_storage_e2e -- --nocapture`
---
## Phase 6: Hardening and release readiness
### Task 6.1: Cross-platform shell and path abstraction
### Task 6.2: crash recovery and session restore
### Task 6.3: packaging/release pipeline
Verification baseline:
- `cargo fmt --check`
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
- `cargo test --workspace`
---
Execution rule: every completed slice must leave the workspace green and in a committable state before the next slice starts.