This commit is contained in:
ZacharyZhang-NY committed 2026-04-21 13:09:45 -04:00
1 parent 97033dfabd
commit 16d4de02c6
62 files changed
-18768

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
/target
.DS_Store
-7433
View File
File diff suppressed because it is too large. Load diff
-42
View File
@@ -1,42 +0,0 @@
[workspace]
members = [
"crates/support",
"crates/domain",
"crates/storage",
"crates/pi_bridge",
"crates/runtime",
"crates/app_models",
"crates/app_ui",
"crates/app",
]
default-members = ["crates/app"]
resolver = "2"
[workspace.package]
edition = "2024"
license = "Apache-2.0"
publish = false
version = "0.1.0"
[workspace.dependencies]
anyhow = "1.0.100"
dirs = "6.0.0"
gpui = "0.2.2"
parking_lot = "0.12.5"
rusqlite = { version = "0.37.0", features = ["bundled", "serde_json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
smallvec = "1.15.1"
tempfile = "3.23.0"
thiserror = "2.0.17"
time = { version = "0.3.44", features = ["formatting", "macros"] }
uuid = { version = "1.18.1", features = ["serde", "v4"] }
unicode-segmentation = "1.12.0"
app_models = { path = "crates/app_models" }
app_ui = { path = "crates/app_ui" }
domain = { path = "crates/domain" }
pi_bridge = { path = "crates/pi_bridge" }
runtime = { path = "crates/runtime" }
storage = { path = "crates/storage" }
support = { path = "crates/support" }
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "agentcompany_app"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[[bin]]
name = "agentcompany"
path = "src/main.rs"
[dependencies]
anyhow.workspace = true
app_ui.workspace = true
parking_lot.workspace = true
storage.workspace = true
support.workspace = true
-13
View File
@@ -1,13 +0,0 @@
use anyhow::Result;
use parking_lot::Mutex;
use std::sync::Arc;
use storage::ProfileStore;
use support::paths::resolve_profile_dir;
fn main() -> Result<()> {
let profile_dir = resolve_profile_dir()?;
let store = Arc::new(Mutex::new(ProfileStore::open(&profile_dir)?));
let snapshot = store.lock().snapshot()?;
app_ui::launch(store, snapshot);
Ok(())
}
@@ -1,9 +0,0 @@
[package]
name = "app_models"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
domain.workspace = true
@@ -1,13 +0,0 @@
use domain::SectionId;
#[derive(Clone, Debug)]
pub enum AppAction {
SelectSection(SectionId),
SelectRun(Option<String>),
SelectApproval(Option<String>),
SelectMessage(Option<String>),
SelectDocument(Option<String>),
SelectWorkspace(Option<String>),
Status(String),
ClearStatus,
}
@@ -1,5 +0,0 @@
pub mod actions;
pub mod state;
pub use actions::AppAction;
pub use state::{AppState, InboxFilter, OnboardingStep, RunFilter, SettingsTab};
@@ -1,72 +0,0 @@
use domain::SectionId;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum OnboardingStep {
#[default]
Welcome,
NodeCheck,
Providers,
Company,
ModelPool,
ExecutionMode,
Mission,
Launch,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum InboxFilter {
#[default]
All,
Approvals,
Reviews,
Failed,
Messages,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum RunFilter {
#[default]
All,
Running,
NeedsReview,
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,
pub selected_run_id: Option<String>,
pub selected_approval_id: Option<String>,
pub selected_message_id: Option<String>,
pub selected_document_id: Option<String>,
pub selected_workspace_id: Option<String>,
pub onboarding_step: OnboardingStep,
pub onboarding_connector_id: Option<String>,
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<String>,
}
impl AppState {
pub fn set_status(&mut self, value: impl Into<String>) {
self.status_line = Some(value.into());
}
pub fn clear_status(&mut self) {
self.status_line = None;
}
}
@@ -1,19 +0,0 @@
[package]
name = "app_ui"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
anyhow.workspace = true
app_models.workspace = true
domain.workspace = true
gpui.workspace = true
parking_lot.workspace = true
pi_bridge.workspace = true
runtime.workspace = true
serde_json.workspace = true
storage.workspace = true
support.workspace = true
unicode-segmentation.workspace = true
@@ -1,425 +0,0 @@
use crate::{
SharedStore, theme,
widgets::{
button::{primary_button, secondary_button},
text_input::TextInput,
},
};
use app_models::{AppState, OnboardingStep, RunFilter, SettingsTab};
use domain::{ApprovalState, ProfileSnapshot, SectionId};
use gpui::{App, Context, Entity, SharedString, Window, div, prelude::*, px};
use storage::OnboardingBootstrapInput;
mod approvals;
mod documents;
mod inbox;
mod messages;
mod messages_parts;
mod onboarding;
mod overview;
mod runs;
mod runtime;
mod settings;
mod sidebar;
mod workspaces;
pub struct AppRoot {
store: SharedStore,
snapshot: ProfileSnapshot,
state: AppState,
company_name_input: Entity<TextInput>,
mission_input: Entity<TextInput>,
workspace_input: Entity<TextInput>,
onboarding_goal_input: Entity<TextInput>,
onboarding_goal_detail_input: Entity<TextInput>,
message_input: Entity<TextInput>,
run_search_input: Entity<TextInput>,
message_search_input: Entity<TextInput>,
document_title_input: Entity<TextInput>,
document_body_input: Entity<TextInput>,
runtime_steer_input: Entity<TextInput>,
runtime_follow_up_input: Entity<TextInput>,
runtime_session: Option<runtime::LiveRuntimeSession>,
}
impl AppRoot {
pub fn new(store: SharedStore, snapshot: ProfileSnapshot, cx: &mut App) -> Self {
let mut root = Self {
store,
snapshot,
state: AppState::default(),
company_name_input: TextInput::new(cx, "Company name"),
mission_input: TextInput::new(cx, "Mission or thesis direction"),
workspace_input: TextInput::new(cx, "Primary workspace path"),
onboarding_goal_input: TextInput::new(cx, "Primary goal"),
onboarding_goal_detail_input: TextInput::new(cx, "Goal acceptance criteria"),
message_input: TextInput::new(cx, "Board message"),
run_search_input: TextInput::new(cx, "Search run queue"),
message_search_input: TextInput::new(cx, "Search messages"),
document_title_input: TextInput::new(cx, "Document title"),
document_body_input: TextInput::new(cx, "Document body"),
runtime_steer_input: TextInput::new(cx, "Steer the active run"),
runtime_follow_up_input: TextInput::new(cx, "Queue a follow-up after the current run"),
runtime_session: None,
};
root.sync_selection();
root
}
fn sync_selection(&mut self) {
if self.state.onboarding_connector_id.is_none() {
self.state.onboarding_connector_id = self
.snapshot
.connectors
.iter()
.find(|connector| connector.id == "anthropic")
.or_else(|| self.snapshot.connectors.first())
.map(|connector| connector.id.clone());
}
if self.state.selected_run_id.is_none() {
self.state.selected_run_id = self.snapshot.runs.first().map(|item| item.id.clone());
}
if self.state.selected_approval_id.is_none() {
self.state.selected_approval_id =
self.snapshot.approvals.first().map(|item| item.id.clone());
}
if self.state.selected_message_id.is_none() {
self.state.selected_message_id = self
.snapshot
.agent_messages
.first()
.map(|item| item.id.clone());
}
if self.state.selected_document_id.is_none() {
self.state.selected_document_id =
self.snapshot.documents.first().map(|item| item.id.clone());
}
if self.state.selected_workspace_id.is_none() {
self.state.selected_workspace_id =
self.snapshot.workspaces.first().map(|item| item.id.clone());
}
}
fn current_company_id(&self) -> Option<&str> {
self.snapshot
.current_company()
.map(|company| company.id.as_str())
}
fn set_status(&mut self, message: impl Into<String>, cx: &mut Context<Self>) {
self.state.set_status(message);
cx.notify();
}
fn reload(&mut self, status: impl Into<String>, cx: &mut Context<Self>) {
let result = {
let store = self.store.lock();
store.snapshot()
};
match result {
Ok(snapshot) => {
self.snapshot = snapshot;
self.sync_selection();
self.state.set_status(status);
cx.notify();
}
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn create_company(&mut self, cx: &mut Context<Self>) {
let input = OnboardingBootstrapInput {
company_name: self.company_name_input.read(cx).value(),
company_description: self.mission_input.read(cx).value(),
workspace_path: self.workspace_input.read(cx).value(),
execution_mode: self.state.onboarding_execution_mode.to_string(),
goal_title: self.onboarding_goal_input.read(cx).value(),
goal_description: self.onboarding_goal_detail_input.read(cx).value(),
connector_id: self
.state
.onboarding_connector_id
.clone()
.unwrap_or_else(|| "anthropic".to_string()),
connector_model: self
.snapshot
.connectors
.iter()
.find(|connector| {
Some(connector.id.as_str()) == self.state.onboarding_connector_id.as_deref()
})
.and_then(|connector| connector.model.clone()),
};
let result = {
let mut store = self.store.lock();
store.bootstrap_onboarding(input)
};
match result {
Ok(_) => {
self.company_name_input.update(cx, |input, _| input.clear());
self.mission_input.update(cx, |input, _| input.clear());
self.workspace_input.update(cx, |input, _| input.clear());
self.onboarding_goal_input
.update(cx, |input, _| input.clear());
self.onboarding_goal_detail_input
.update(cx, |input, _| input.clear());
self.state.onboarding_step = OnboardingStep::NodeCheck;
self.state.run_filter = RunFilter::All;
self.state.message_unread_only = false;
self.run_search_input.update(cx, |input, _| input.clear());
self.message_search_input
.update(cx, |input, _| input.clear());
self.reload("Company bootstrapped from native onboarding.", cx);
}
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn switch_company(&mut self, company_id: String, cx: &mut Context<Self>) {
let result = {
let mut store = self.store.lock();
store.set_current_company(&company_id)
};
match result {
Ok(_) => self.reload("Company switched.", cx),
Err(error) => self.set_status(error.to_string(), cx),
}
}
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,
note: &'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 Some(approval_id) = self.state.selected_approval_id.clone() else {
self.set_status("Choose an approval first.", cx);
return;
};
let selected_approval = self
.snapshot
.approvals
.iter()
.find(|approval| approval.id == approval_id)
.cloned();
let result = {
let mut store = self.store.lock();
store.decide_approval(&company_id, &approval_id, state, note)
};
match result {
Ok(_) => {
self.maybe_dispatch_runtime_approval(selected_approval.as_ref(), state, cx);
self.reload(format!("Approval moved to {}.", state.as_str()), cx);
}
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn send_message(&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 body = self.message_input.read(cx).value();
let result = {
let mut store = self.store.lock();
store.send_board_message(&company_id, &body)
};
match result {
Ok(_) => {
self.message_input.update(cx, |input, _| input.clear());
self.reload("Board message saved.", cx);
}
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn mark_selected_message_read(&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(message_id) = self.state.selected_message_id.clone() else {
self.set_status("Choose a message first.", cx);
return;
};
let result = {
let mut store = self.store.lock();
store.mark_message_read(&company_id, &message_id)
};
match result {
Ok(_) => self.reload("Message marked as read.", cx),
Err(error) => self.set_status(error.to_string(), cx),
}
}
fn create_document(&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 title = self.document_title_input.read(cx).value();
let body = self.document_body_input.read(cx).value();
let result = {
let mut store = self.store.lock();
store.create_document(&company_id, &title, &body)
};
match result {
Ok(_) => {
self.document_title_input
.update(cx, |input, _| input.clear());
self.document_body_input
.update(cx, |input, _| input.clear());
self.reload("Document saved.", cx);
}
Err(error) => self.set_status(error.to_string(), cx),
}
}
}
impl Render for AppRoot {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.snapshot.companies.is_empty() {
return self.render_onboarding(cx).into_any_element();
}
let header = self
.snapshot
.current_company()
.map(|company| format!("{} · {}", company.name, company.execution_mode))
.unwrap_or_else(|| "AgentCompany".to_string());
div()
.size_full()
.flex()
.bg(theme::window_bg())
.child(self.render_sidebar(cx))
.child(
div()
.flex_1()
.flex()
.flex_col()
.child(
div()
.px_6()
.h(px(56.0))
.flex()
.items_center()
.border_b_1()
.border_color(theme::panel_border())
.bg(theme::shell_header())
.child(
div()
.flex()
.items_center()
.justify_between()
.w_full()
.child(
div()
.text_size(px(14.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(self.state.section.label()),
)
.child(
div()
.px_4()
.py_3()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.text_size(px(14.0))
.text_color(theme::text_secondary())
.child(header),
),
),
)
.child(
div()
.id("main-scroll")
.flex_1()
.overflow_scroll()
.p_8()
.child(match self.state.section {
SectionId::Overview => self.render_overview(cx).into_any_element(),
SectionId::Runs => self.render_runs(cx).into_any_element(),
SectionId::Inbox => self.render_inbox(cx).into_any_element(),
SectionId::Messages => self.render_messages(cx).into_any_element(),
SectionId::Approvals => {
self.render_approvals(cx).into_any_element()
}
SectionId::Documents => {
self.render_documents(cx).into_any_element()
}
SectionId::Workspaces => {
self.render_workspaces(cx).into_any_element()
}
SectionId::Settings => {
self.render_settings(cx).into_any_element()
}
}),
),
)
.into_any_element()
}
}
@@ -1,389 +0,0 @@
use super::*;
impl AppRoot {
pub(super) fn render_approvals(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let groups = review_groups(&self.snapshot);
let selected_pending = self
.state
.selected_approval_id
.as_ref()
.and_then(|approval_id| {
self.snapshot
.approvals
.iter()
.find(|item| &item.id == approval_id)
});
div()
.flex()
.flex_col()
.gap_6()
.child(section_label("AGENT REVIEWS IN PROGRESS"))
.child(render_review_groups_panel(&groups))
.child(section_label("PENDING APPROVALS"))
.child(
div()
.grid()
.grid_cols(2)
.gap_4()
.child(self.render_pending_approval_list(cx))
.child(self.render_pending_approval_detail(selected_pending, cx)),
)
}
fn render_pending_approval_list(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let pending = pending_approvals(&self.snapshot).collect::<Vec<_>>();
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(pending.is_empty(), |container| {
container.child(
div()
.min_h(px(280.0))
.flex()
.items_center()
.justify_center()
.child(
div()
.w(px(320.0))
.text_center()
.text_color(theme::text_muted())
.child("No pending approvals"),
),
)
})
.when(!pending.is_empty(), |container| {
container.children(pending.into_iter().map(|approval| {
let active =
self.state.selected_approval_id.as_deref() == Some(approval.id.as_str());
let approval_id = approval.id.clone();
div()
.id(SharedString::from(format!("approval-{}", approval.id)))
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_approval_id = Some(approval_id.clone());
cx.notify();
}))
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(approval.payload_summary.clone()),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
approval.approval_type.replace('_', " "),
approval.state
)),
)
}))
})
}
fn render_pending_approval_detail(
&self,
approval: Option<&domain::Approval>,
cx: &mut Context<Self>,
) -> impl IntoElement + '_ {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.when_some(approval, |container, approval| {
container
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child(approval.approval_type.replace('_', " ")),
)
.child(
div()
.mt(px(10.0))
.text_lg()
.text_color(theme::text_primary())
.child(approval.payload_summary.clone()),
)
.child(
div()
.mt(px(10.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_inset_border())
.bg(theme::panel_inset())
.p_4()
.text_sm()
.line_height(px(22.0))
.text_color(theme::text_secondary())
.child(approval.impact_summary.clone()),
)
.when_some(approval.payload_json.clone(), |inner, payload| {
inner.child(
div()
.mt(px(12.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_4()
.text_xs()
.line_height(px(20.0))
.text_color(theme::text_muted())
.child(payload),
)
})
.child(
div()
.mt(px(18.0))
.flex()
.gap_3()
.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,
)
})),
)
})
.when(approval.is_none(), |container| {
container.child(
div()
.min_h(px(320.0))
.flex()
.items_center()
.justify_center()
.text_color(theme::text_muted())
.child("Select an approval to inspect its impact and decision surface."),
)
})
}
}
struct ReviewGroups<'a> {
board: Vec<ReviewRow<'a>>,
user: Vec<ReviewRow<'a>>,
manager: Vec<ReviewRow<'a>>,
}
struct ReviewRow<'a> {
task: &'a domain::Task,
assignee: Option<&'a domain::Agent>,
reviewer: Option<&'a domain::Agent>,
kind: ReviewKind,
}
#[derive(Clone, Copy)]
enum ReviewKind {
Board,
User,
Manager,
}
fn review_groups(snapshot: &domain::ProfileSnapshot) -> ReviewGroups<'_> {
let pending_approval_task_ids = snapshot
.approvals
.iter()
.filter(|approval| approval.state == "pending" || approval.state == "revision_requested")
.filter_map(|approval| approval.related_task_id.as_deref())
.collect::<Vec<_>>();
let mut board = Vec::new();
let mut user = Vec::new();
let mut manager = Vec::new();
for task in snapshot
.tasks
.iter()
.filter(|task| task.status == "in_review")
{
let assignee = task
.assignee_agent_id
.as_ref()
.and_then(|agent_id| snapshot.agents.iter().find(|agent| &agent.id == agent_id));
let reviewer = assignee
.and_then(|assignee| assignee.reports_to.as_ref())
.and_then(|manager_id| snapshot.agents.iter().find(|agent| &agent.id == manager_id));
let row = if pending_approval_task_ids
.iter()
.any(|task_id| *task_id == task.id)
{
ReviewRow {
task,
assignee,
reviewer: None,
kind: ReviewKind::Board,
}
} else if task.requires_user_review || reviewer.is_none() {
ReviewRow {
task,
assignee,
reviewer: None,
kind: ReviewKind::User,
}
} else {
ReviewRow {
task,
assignee,
reviewer,
kind: ReviewKind::Manager,
}
};
match row.kind {
ReviewKind::Board => board.push(row),
ReviewKind::User => user.push(row),
ReviewKind::Manager => manager.push(row),
}
}
ReviewGroups {
board,
user,
manager,
}
}
fn render_review_groups_panel(groups: &ReviewGroups<'_>) -> impl IntoElement {
let has_rows =
!(groups.board.is_empty() && groups.user.is_empty() && groups.manager.is_empty());
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(has_rows, |container| {
container
.when(!groups.board.is_empty(), |inner| {
inner.child(review_group_section(
"Board review",
groups.board.as_slice(),
))
})
.when(!groups.user.is_empty(), |inner| {
inner.child(review_group_section("Human review", groups.user.as_slice()))
})
.when(!groups.manager.is_empty(), |inner| {
inner.child(review_group_section(
"Manager review",
groups.manager.as_slice(),
))
})
})
.when(!has_rows, |container| {
container.child(
div()
.min_h(px(180.0))
.flex()
.items_center()
.justify_center()
.text_color(theme::text_muted())
.child("No agent reviews are currently waiting on a human."),
)
})
}
fn review_group_section<'a>(
title: &'static str,
rows: &'a [ReviewRow<'a>],
) -> impl IntoElement + 'a {
div()
.child(
div()
.px_4()
.pt_3()
.pb_1()
.text_xs()
.text_color(theme::text_muted())
.child(title),
)
.children(rows.iter().map(|row| {
div()
.flex()
.items_start()
.gap_3()
.px_4()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.w(px(28.0))
.h(px(28.0))
.rounded(px(0.0))
.bg(theme::panel_muted()),
)
.child(
div()
.flex_1()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(row.task.title.clone()),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.text_color(theme::text_muted())
.child(review_row_subtitle(row)),
),
)
}))
}
fn review_row_subtitle(row: &ReviewRow<'_>) -> String {
let assignee = row
.assignee
.map(|agent| agent.name.clone())
.unwrap_or_else(|| "Unassigned".to_string());
match row.kind {
ReviewKind::Board => format!("{} · awaits board review", assignee),
ReviewKind::User => format!("{} · awaits human review", assignee),
ReviewKind::Manager => match row.reviewer {
Some(reviewer) => format!("{} → reviewed by {}", assignee, reviewer.name),
None => format!("{} · awaiting manager review", assignee),
},
}
}
fn section_label(title: &'static str) -> impl IntoElement {
div().text_xs().text_color(theme::text_muted()).child(title)
}
fn pending_approvals(
snapshot: &domain::ProfileSnapshot,
) -> impl Iterator<Item = &domain::Approval> {
snapshot
.approvals
.iter()
.filter(|item| item.state == "pending" || item.state == "revision_requested")
}
@@ -1,286 +0,0 @@
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 work_products = self.snapshot.artifacts.iter().take(12).collect::<Vec<_>>();
let total_count = self.snapshot.documents.len() + work_products.len();
div()
.flex()
.flex_col()
.gap_6()
.child(render_document_header(total_count, cx))
.child(
div()
.grid()
.grid_cols(2)
.gap_4()
.child(
div()
.gap_4()
.child(document_section_label("DOCUMENTS PROSE"))
.child(self.render_document_prose_list(cx, selected))
.when(!work_products.is_empty(), |container| {
container
.child(document_section_label("WORK PRODUCTS"))
.child(render_work_products(work_products.as_slice()))
}),
)
.child(self.render_document_editor_surface(selected)),
)
}
fn render_document_prose_list(
&self,
cx: &mut Context<Self>,
selected: Option<&domain::Document>,
) -> impl IntoElement + '_ {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(self.snapshot.documents.is_empty(), |container| {
container.child(
div()
.min_h(px(180.0))
.flex()
.items_center()
.justify_center()
.text_color(theme::text_muted())
.child("No documents yet. Create the first company document to start the knowledge base."),
)
})
.when(!self.snapshot.documents.is_empty(), |container| {
container.children(self.snapshot.documents.iter().map(|document| {
let active =
selected.map(|item| item.id.as_str()) == Some(document.id.as_str());
let document_id = document.id.clone();
div()
.id(SharedString::from(format!("document-{}", document.id)))
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_document_id = Some(document_id.clone());
cx.notify();
}))
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.flex()
.justify_between()
.items_center()
.gap_3()
.child(
div()
.flex_1()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(document.title.clone()),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.text_color(theme::text_muted())
.child(format!(
"{} · v{} · {}",
document.document_type.replace('_', " "),
document.version,
short_date(document.updated_at.as_str())
)),
),
)
.child(document_status(document.status.as_str())),
)
}))
})
}
fn render_document_editor_surface(
&self,
selected: Option<&domain::Document>,
) -> impl IntoElement + '_ {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div().text_lg().text_color(theme::text_primary()).child(
selected
.map(|item| item.title.clone())
.unwrap_or_else(|| "New document".to_string()),
),
)
.when_some(selected, |container, document| {
container
.child(
div()
.mt(px(8.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!(
"{} · v{} · {}",
document.document_type.replace('_', " "),
document.version,
document.status
)),
)
.child(
div()
.mt(px(16.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_inset_border())
.bg(theme::panel_inset())
.p_4()
.text_sm()
.line_height(px(22.0))
.text_color(theme::text_secondary())
.child(document.content.clone()),
)
})
.child(
div()
.mt(px(20.0))
.text_xs()
.text_color(theme::text_muted())
.child("DRAFT EDITOR"),
)
.child(div().mt(px(10.0)).child(self.document_title_input.clone()))
.child(div().mt(px(12.0)).child(self.document_body_input.clone()))
.child(
div().mt(px(14.0)).flex().justify_end().child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("Use the draft editor to create the next document revision."),
),
)
}
}
fn render_document_header(total_count: usize, cx: &mut Context<AppRoot>) -> impl IntoElement {
div()
.flex()
.justify_between()
.items_center()
.child(
div()
.child(
div()
.text_2xl()
.text_color(theme::text_primary())
.child("Document Center"),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!("{} items", total_count)),
),
)
.child(primary_button("Save draft", cx, |this, cx| {
this.create_document(cx)
}))
}
fn document_section_label(label: &'static str) -> impl IntoElement {
div().text_xs().text_color(theme::text_muted()).child(label)
}
fn render_work_products<'a>(products: &'a [&'a domain::ArtifactRecord]) -> impl IntoElement + 'a {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(products.iter().map(|artifact| {
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.flex()
.justify_between()
.items_center()
.gap_3()
.child(
div()
.flex_1()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(artifact.title.clone()),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.text_color(theme::text_muted())
.child(format!(
"{} · {} · {}",
artifact.provider,
artifact.external_id,
short_date(artifact.created_at.as_str())
)),
),
)
.child(
div()
.px_3()
.py_2()
.rounded_full()
.bg(theme::accent_blue_soft())
.text_xs()
.text_color(theme::accent_blue())
.child("ARTIFACT"),
),
)
}))
}
fn document_status(status: &str) -> impl IntoElement {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w(px(8.0))
.h(px(8.0))
.rounded_full()
.bg(theme::badge_dark()),
)
.child(
div()
.text_xs()
.text_color(theme::text_secondary())
.child(status.to_uppercase()),
)
}
fn short_date(value: &str) -> &str {
value.get(..10).unwrap_or(value)
}
@@ -1,391 +0,0 @@
use super::*;
use app_models::InboxFilter;
use gpui::{Animation, AnimationExt, SharedString, pulsating_between};
use std::time::Duration;
impl AppRoot {
pub(super) fn render_inbox(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let pending_approvals = self
.snapshot
.approvals
.iter()
.filter(|item| item.state == "pending")
.collect::<Vec<_>>();
let unread_messages = self
.snapshot
.agent_messages
.iter()
.filter(|item| item.read_at.is_none())
.collect::<Vec<_>>();
let review_tasks = self
.snapshot
.tasks
.iter()
.filter(|task| task.status == "in_review")
.collect::<Vec<_>>();
let failed_runs = self
.snapshot
.runs
.iter()
.filter(|run| {
matches!(
run.status.as_str(),
"failed" | "timed_out" | "cancelled" | "interrupted"
)
})
.collect::<Vec<_>>();
let total_attention = pending_approvals.len()
+ unread_messages.len()
+ review_tasks.len()
+ failed_runs.len();
let rows = inbox_rows(
&self.snapshot,
self.state.inbox_filter,
pending_approvals.as_slice(),
review_tasks.as_slice(),
failed_runs.as_slice(),
unread_messages.as_slice(),
);
let mut row_elements = Vec::with_capacity(rows.len());
for (index, row) in rows.into_iter().enumerate() {
row_elements.push(row.render(index, cx).into_any_element());
}
div()
.flex()
.flex_col()
.gap_4()
.child(
div()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child("Inbox"),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!("{} items need attention", total_attention)),
),
)
.child(inbox_filters(
self.state.inbox_filter,
pending_approvals.len(),
review_tasks.len(),
failed_runs.len(),
unread_messages.len(),
cx,
))
.child(
div()
.rounded_md()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(total_attention == 0, |container| {
container.child(
div()
.min_h(px(260.0))
.flex()
.items_center()
.justify_center()
.child(
div()
.w(px(420.0))
.text_center()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child("All clear"),
)
.child(
div()
.mt(px(10.0))
.text_base()
.text_color(theme::text_muted())
.child("No approvals, reviews, failed runs, or unread internal messages need board attention right now."),
),
),
)
})
.when(!row_elements.is_empty(), |container| container.children(row_elements)),
)
}
}
#[derive(Clone)]
struct InboxRow {
title: String,
subtitle: String,
accent: gpui::Hsla,
tile_bg: gpui::Hsla,
action: InboxAction,
}
#[derive(Clone)]
enum InboxAction {
Approval(String),
ReviewTask(String),
FailedRun(String),
Message(String),
}
impl InboxRow {
fn render(self, index: usize, cx: &mut Context<AppRoot>) -> impl IntoElement {
let action = self.action.clone();
let title = self.title.clone();
let subtitle = self.subtitle.clone();
let accent = self.accent;
let tile_bg = self.tile_bg;
div()
.id(SharedString::from(format!("inbox-row-{index}")))
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
match &action {
InboxAction::Approval(approval_id) => {
this.state.section = SectionId::Approvals;
this.state.selected_approval_id = Some(approval_id.clone());
}
InboxAction::ReviewTask(task_id) => {
if let Some(run_id) = this
.snapshot
.tasks
.iter()
.find(|task| task.id == *task_id)
.and_then(|task| task.active_run_id.clone())
{
this.state.section = SectionId::Runs;
this.state.selected_run_id = Some(run_id);
} else {
this.state.section = SectionId::Approvals;
}
}
InboxAction::FailedRun(run_id) => {
this.state.section = SectionId::Runs;
this.state.selected_run_id = Some(run_id.clone());
}
InboxAction::Message(message_id) => {
this.state.section = SectionId::Messages;
this.state.selected_message_id = Some(message_id.clone());
}
}
cx.notify();
}))
.px_4()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.flex()
.gap_3()
.items_center()
.child(div().w(px(36.0)).h(px(36.0)).rounded(px(0.0)).bg(tile_bg))
.child(
div()
.flex_1()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(title),
)
.child(
div()
.mt(px(4.0))
.text_sm()
.text_color(theme::text_muted())
.line_clamp(2)
.child(subtitle),
),
)
.child(active_filter_dot(accent, index)),
)
}
}
fn inbox_rows(
snapshot: &domain::ProfileSnapshot,
filter: InboxFilter,
pending_approvals: &[&domain::Approval],
review_tasks: &[&domain::Task],
failed_runs: &[&domain::Run],
unread_messages: &[&domain::AgentMessage],
) -> Vec<InboxRow> {
let mut rows = Vec::new();
if matches!(filter, InboxFilter::All | InboxFilter::Approvals) {
rows.extend(pending_approvals.iter().map(|approval| InboxRow {
title: approval.payload_summary.clone(),
subtitle: format!("{} · pending", approval.approval_type.replace('_', " ")),
accent: theme::accent_orange(),
tile_bg: theme::accent_orange_soft(),
action: InboxAction::Approval(approval.id.clone()),
}));
}
if matches!(filter, InboxFilter::All | InboxFilter::Reviews) {
rows.extend(review_tasks.iter().map(|task| InboxRow {
title: task.title.clone(),
subtitle: format!("{} · awaiting review", task.status),
accent: theme::accent_blue(),
tile_bg: theme::accent_blue_soft(),
action: InboxAction::ReviewTask(task.id.clone()),
}));
}
if matches!(filter, InboxFilter::All | InboxFilter::Failed) {
rows.extend(failed_runs.iter().map(|run| {
InboxRow {
title: inbox_run_title(snapshot, run),
subtitle: run
.error_message
.clone()
.unwrap_or_else(|| run.status.clone()),
accent: theme::danger(),
tile_bg: theme::danger_bg(),
action: InboxAction::FailedRun(run.id.clone()),
}
}));
}
if matches!(filter, InboxFilter::All | InboxFilter::Messages) {
rows.extend(unread_messages.iter().map(|message| InboxRow {
title: message.subject.clone(),
subtitle: message.body.clone(),
accent: theme::accent_teal(),
tile_bg: theme::accent_teal_soft(),
action: InboxAction::Message(message.id.clone()),
}));
}
rows
}
fn inbox_run_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
run.task_id
.as_ref()
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
.map(|task| task.title.clone())
.unwrap_or_else(|| {
if run.summary.trim().is_empty() {
format!("Run {}", run.id.get(..8).unwrap_or(run.id.as_str()))
} else {
run.summary.clone()
}
})
}
fn inbox_filters(
active: InboxFilter,
approvals: usize,
reviews: usize,
failed_runs: usize,
messages: usize,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div()
.flex()
.gap_2()
.child(filter_chip(
"All",
approvals + reviews + failed_runs + messages,
active == InboxFilter::All,
InboxFilter::All,
cx,
))
.child(filter_chip(
"Approvals",
approvals,
active == InboxFilter::Approvals,
InboxFilter::Approvals,
cx,
))
.child(filter_chip(
"Reviews",
reviews,
active == InboxFilter::Reviews,
InboxFilter::Reviews,
cx,
))
.child(filter_chip(
"Failed",
failed_runs,
active == InboxFilter::Failed,
InboxFilter::Failed,
cx,
))
.child(filter_chip(
"Messages",
messages,
active == InboxFilter::Messages,
InboxFilter::Messages,
cx,
))
}
fn filter_chip(
label: &'static str,
count: usize,
active: bool,
next: InboxFilter,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
let chip = div()
.id(SharedString::from(format!("inbox-filter-{label}")))
.px_4()
.py_2()
.rounded_full()
.cursor_pointer()
.bg(if active {
theme::primary_action()
} else {
theme::panel_muted()
})
.text_xs()
.text_color(if active {
theme::primary_action_text()
} else {
theme::text_secondary()
})
.hover(|style| {
style.bg(if active {
theme::primary_action_hover()
} else {
theme::panel_hover()
})
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.inbox_filter = next;
cx.notify();
}))
.child(format!("{} {}", label, count));
if active {
chip.with_animation(
("inbox-filter", label.len()),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.82, 1.0)),
|element, delta| element.opacity(delta),
)
.into_any_element()
} else {
chip.into_any_element()
}
}
fn active_filter_dot(accent: gpui::Hsla, index: usize) -> impl IntoElement {
div()
.w(px(10.0))
.h(px(10.0))
.rounded_full()
.bg(accent)
.with_animation(
("inbox-row-dot", index),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.8, 1.0)),
|element, delta| element.opacity(delta),
)
}
@@ -1,574 +0,0 @@
use super::{
messages_parts::{
compact_message_card, composer_chip, composer_subject, message_section_label,
secondary_message, selected_message_card, selected_recipient,
},
*,
};
impl AppRoot {
pub(super) fn render_messages<'a>(
&'a self,
cx: &'a mut Context<Self>,
) -> impl IntoElement + 'a {
let selected = self.state.selected_message_id.as_ref().and_then(|id| {
self.snapshot
.agent_messages
.iter()
.find(|item| &item.id == id)
});
let channel_groups = message_channel_groups(&self.snapshot);
let selected_channel = selected.map(message_channel_key).or_else(|| {
channel_groups
.first()
.and_then(|group| group.rows.first().map(|row| row.key.clone()))
});
let message_query = self.message_search_input.read(cx).value().to_lowercase();
let visible_messages = selected_channel
.as_ref()
.map(|channel_key| messages_for_channel(&self.snapshot, channel_key))
.unwrap_or_default()
.into_iter()
.filter(|message| {
message_matches_query(message, message_query.as_str())
&& (!self.state.message_unread_only || message.read_at.is_none())
})
.collect::<Vec<_>>();
div()
.flex()
.child(render_message_sidebar(
&channel_groups,
selected_channel.as_deref(),
&self.state.selected_message_id,
cx,
))
.child(
div()
.flex_1()
.flex_col()
.bg(theme::card())
.child(render_message_header(
self,
selected_channel.as_deref(),
visible_messages.len(),
cx,
))
.child(
div()
.id("messages-stream")
.flex_1()
.overflow_scroll()
.px(px(8.0))
.py(px(12.0))
.when(visible_messages.is_empty(), |container| {
container.child(selected_message_card(None))
})
.when(!visible_messages.is_empty(), |container| {
container.children(visible_messages.iter().map(|message| {
let is_selected = self.state.selected_message_id.as_deref()
== Some(message.id.as_str());
let message_id = message.id.clone();
div()
.id(SharedString::from(format!(
"message-stream-{}",
message.id
)))
.cursor_pointer()
.hover(|style| style.bg(theme::muted()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_message_id =
Some(message_id.clone());
cx.notify();
}))
.child(if is_selected {
selected_message_card(Some(message)).into_any_element()
} else {
compact_message_card(message).into_any_element()
})
}))
}),
)
.child(render_message_composer(self, selected, cx)),
)
.child(render_message_inspector(selected))
}
}
#[derive(Clone)]
struct ChannelRow {
key: String,
label: String,
count: usize,
urgent: bool,
rows: Vec<String>,
}
#[derive(Clone)]
struct ChannelGroup {
label: &'static str,
rows: Vec<ChannelRow>,
}
fn message_channel_groups(snapshot: &domain::ProfileSnapshot) -> Vec<ChannelGroup> {
let company = build_channel_group(snapshot, "COMPANY", |message| message.channel == "company");
let department = build_channel_group(snapshot, "DEPARTMENT", |message| {
message.channel == "department"
});
let direct = build_channel_group(snapshot, "DIRECT MESSAGES", |message| {
message.channel == "direct"
});
let task_threads = build_channel_group(snapshot, "TASK DISCUSSIONS", |message| {
matches!(message.channel.as_str(), "project" | "incident" | "run")
});
[company, department, direct, task_threads]
.into_iter()
.filter(|group| !group.rows.is_empty())
.collect()
}
fn build_channel_group(
snapshot: &domain::ProfileSnapshot,
label: &'static str,
predicate: impl Fn(&domain::AgentMessage) -> bool,
) -> ChannelGroup {
let mut channels = std::collections::BTreeMap::<String, Vec<&domain::AgentMessage>>::new();
for message in snapshot
.agent_messages
.iter()
.filter(|message| predicate(message))
{
channels
.entry(message_channel_key(message))
.or_default()
.push(message);
}
let rows = channels
.into_iter()
.map(|(key, messages)| {
let label = channel_label(snapshot, messages.as_slice());
let count = messages
.iter()
.filter(|message| message.read_at.is_none())
.count();
let urgent = messages
.iter()
.any(|message| message.priority == "urgent" && message.read_at.is_none());
let rows = messages
.iter()
.map(|message| message.id.clone())
.collect::<Vec<_>>();
ChannelRow {
key,
label,
count,
urgent,
rows,
}
})
.collect::<Vec<_>>();
ChannelGroup { label, rows }
}
fn channel_label(snapshot: &domain::ProfileSnapshot, messages: &[&domain::AgentMessage]) -> String {
let Some(first) = messages.first() else {
return "#empty".to_string();
};
match first.channel.as_str() {
"company" => "#company-wide".to_string(),
"department" => format!(
"#{}",
first
.channel_target_id
.clone()
.unwrap_or_else(|| "department".to_string())
),
"direct" => first
.to_agent_id
.as_ref()
.and_then(|agent_id| snapshot.agents.iter().find(|agent| &agent.id == agent_id))
.map(|agent| agent.name.clone())
.unwrap_or_else(|| "Direct".to_string()),
"project" => format!(
"#{}",
first
.channel_target_id
.clone()
.unwrap_or_else(|| "project".to_string())
),
"incident" => format!(
"#{}",
first
.channel_target_id
.clone()
.unwrap_or_else(|| "incident".to_string())
),
"run" => format!(
"Run {}",
first
.channel_target_id
.clone()
.unwrap_or_default()
.get(..8)
.unwrap_or("run")
),
other => other.to_string(),
}
}
fn render_message_sidebar<'a>(
groups: &'a [ChannelGroup],
active_channel: Option<&'a str>,
selected_message_id: &'a Option<String>,
cx: &'a mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div()
.w(px(260.0))
.flex_col()
.overflow_hidden()
.border_r_1()
.border_color(theme::border())
.bg(theme::card())
.child(
div()
.id("message-sidebar-scroll")
.flex_1()
.overflow_scroll()
.px(px(8.0))
.py(px(12.0))
.children(groups.iter().map(|group| {
div()
.mb_3()
.child(message_section_label(group.label))
.children(group.rows.iter().map(|channel| {
let active = active_channel == Some(channel.key.as_str());
let next_message_id = channel
.rows
.first()
.cloned()
.or_else(|| selected_message_id.clone())
.unwrap_or_default();
div()
.id(SharedString::from(format!(
"message-channel-{}",
channel.key
)))
.cursor_pointer()
.hover(|style| {
if active {
style
} else {
style.bg(theme::muted())
}
})
.bg(if active {
theme::accent()
} else {
theme::card()
})
.px(px(12.0))
.py(px(8.0))
.on_click(cx.listener(move |this, _, _, cx| {
if !next_message_id.is_empty() {
this.state.selected_message_id = Some(next_message_id.clone());
}
cx.notify();
}))
.child(
div()
.flex()
.items_center()
.gap(px(10.0))
.child(
div()
.w(px(28.0))
.h(px(28.0))
.flex()
.items_center()
.justify_center()
.rounded(px(0.0))
.bg(theme::muted()),
)
.child(
div().flex_1().child(
div()
.text_size(px(13.0))
.font_weight(if active || channel.count > 0 {
gpui::FontWeight::SEMIBOLD
} else {
gpui::FontWeight::MEDIUM
})
.text_color(if active || channel.count > 0 {
theme::foreground()
} else {
theme::muted_foreground()
})
.truncate()
.child(channel.label.clone()),
),
)
.when(channel.urgent, |container| {
container.child(
div()
.w(px(8.0))
.h(px(8.0))
.rounded_full()
.bg(theme::destructive()),
)
})
.when(channel.count > 0, |container| {
container.child(
div()
.h(px(18.0))
.min_w(px(18.0))
.flex()
.items_center()
.justify_center()
.rounded_full()
.bg(theme::primary())
.px(px(4.0))
.text_size(px(10.0))
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme::primary_foreground())
.child(channel.count.to_string()),
)
}),
)
}))
})),
)
.child(
div()
.flex()
.justify_between()
.items_center()
.px(px(16.0))
.py(px(8.0))
.border_t_1()
.border_color(theme::border())
.text_size(px(11.0))
.text_color(theme::muted_foreground())
.child(format!(
"{} channels",
groups.iter().map(|group| group.rows.len()).sum::<usize>()
))
.child("live threads"),
)
}
fn render_message_header(
root: &AppRoot,
active_channel: Option<&str>,
visible_count: usize,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div()
.flex()
.justify_between()
.items_center()
.px(px(16.0))
.py(px(10.0))
.border_b_1()
.border_color(theme::border())
.bg(theme::card())
.child(
div()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::foreground())
.child(active_channel.unwrap_or("Select a channel").to_string()),
)
.child(
div()
.mt(px(4.0))
.text_size(px(11.0))
.text_color(theme::muted_foreground())
.child(format!("{} visible messages", visible_count)),
),
)
.child(
div()
.flex()
.gap_3()
.items_center()
.child(root.message_search_input.clone())
.child(message_filter_toggle(root.state.message_unread_only, cx)),
)
}
fn render_message_composer<'a>(
root: &'a AppRoot,
selected: Option<&'a domain::AgentMessage>,
cx: &'a mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div()
.px(px(16.0))
.py(px(12.0))
.border_t_1()
.border_color(theme::border())
.bg(theme::card())
.child(
div()
.flex()
.gap_3()
.child(composer_chip(selected_recipient(selected).as_str()))
.child(composer_subject(selected)),
)
.child(
div()
.mt(px(12.0))
.p_3()
.rounded(px(0.0))
.bg(theme::muted())
.border_1()
.border_color(theme::border())
.child(root.message_input.clone()),
)
.child(
div()
.mt(px(10.0))
.flex()
.justify_between()
.items_center()
.child(
div()
.text_size(px(11.0))
.text_color(theme::muted_foreground())
.child("Reply, mention, or route an operational update."),
)
.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_message_inspector(selected: Option<&domain::AgentMessage>) -> impl IntoElement {
div()
.w(px(320.0))
.border_1()
.rounded(px(0.0))
.border_color(theme::border())
.bg(theme::card())
.p(px(20.0))
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::foreground())
.child("Conversation details"),
)
.child(selected_message_card(selected))
.when_some(secondary_message(selected), |container, message| {
container.child(compact_message_card(message))
})
}
fn message_filter_toggle(unread_only: bool, cx: &mut Context<AppRoot>) -> impl IntoElement {
div()
.id("message-filter-toggle")
.px(px(10.0))
.py(px(6.0))
.rounded_full()
.cursor_pointer()
.bg(if unread_only {
theme::primary()
} else {
theme::muted()
})
.text_size(px(11.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(if unread_only {
theme::primary_foreground()
} else {
theme::muted_foreground()
})
.hover(|style| {
style.bg(if unread_only {
theme::primary()
} else {
theme::muted()
})
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.message_unread_only = !this.state.message_unread_only;
cx.notify();
}))
.child(if unread_only {
"Unread only"
} else {
"All messages"
})
}
fn message_matches_query(message: &domain::AgentMessage, query: &str) -> bool {
if query.trim().is_empty() {
return true;
}
let query = query.trim();
message.subject.to_lowercase().contains(query)
|| message.body.to_lowercase().contains(query)
|| message.channel.to_lowercase().contains(query)
}
fn message_channel_key(message: &domain::AgentMessage) -> String {
match message.channel.as_str() {
"company" => "company-all".to_string(),
"department" => format!(
"department-{}",
message
.channel_target_id
.clone()
.unwrap_or_else(|| "general".to_string())
),
"direct" => {
let to = message
.to_agent_id
.clone()
.unwrap_or_else(|| "broadcast".to_string());
format!("direct-{}-{}", message.from_agent_id, to)
}
"project" => format!(
"project-{}",
message
.channel_target_id
.clone()
.unwrap_or_else(|| "general".to_string())
),
"incident" => format!(
"incident-{}",
message
.channel_target_id
.clone()
.unwrap_or_else(|| "room".to_string())
),
"run" => format!(
"run-{}",
message
.channel_target_id
.clone()
.unwrap_or_else(|| "run".to_string())
),
other => format!("{}-misc", other),
}
}
fn messages_for_channel<'a>(
snapshot: &'a domain::ProfileSnapshot,
channel_key: &str,
) -> Vec<&'a domain::AgentMessage> {
snapshot
.agent_messages
.iter()
.filter(|message| message_channel_key(message) == channel_key)
.collect::<Vec<_>>()
}
@@ -1,214 +0,0 @@
use super::*;
pub(super) fn message_section_label(label: &'static str) -> impl IntoElement {
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::muted_foreground())
.mb(px(4.0))
.px(px(8.0))
.child(label)
}
pub(super) fn selected_message_card(message: Option<&domain::AgentMessage>) -> impl IntoElement {
let subject = message
.map(|item| item.subject.clone())
.unwrap_or_else(|| "Select a message thread".to_string());
let body = message.map(|item| item.body.clone()).unwrap_or_else(|| {
"Choose a company channel, direct thread, or task discussion from the left rail."
.to_string()
});
div()
.p_4()
.rounded(px(0.0))
.border_1()
.border_color(theme::border())
.bg(theme::card())
.child(message_header(message, false))
.child(
div()
.mt(px(16.0))
.text_size(px(14.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::foreground())
.child(subject),
)
.child(
div()
.mt(px(12.0))
.text_size(px(14.0))
.text_color(theme::muted_foreground())
.child(body),
)
.child(
div()
.mt(px(16.0))
.text_size(px(14.0))
.text_color(theme::muted_foreground())
.child("↩ Reply"),
)
}
pub(super) fn compact_message_card(message: &domain::AgentMessage) -> impl IntoElement {
div()
.mt(px(16.0))
.p(px(12.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::border())
.bg(theme::card())
.child(message_header(Some(message), true))
.child(
div()
.mt(px(12.0))
.text_size(px(14.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::foreground())
.child(message.subject.clone()),
)
.child(
div()
.mt(px(8.0))
.text_size(px(14.0))
.text_color(theme::muted_foreground())
.line_clamp(2)
.child(message.body.clone()),
)
}
fn message_header(message: Option<&domain::AgentMessage>, urgent: bool) -> impl IntoElement {
let sender = message
.map(|item| trim_sender(item.from_agent_id.as_str()))
.unwrap_or("AC");
let timestamp = message
.and_then(|item| item.created_at.get(11..16))
.unwrap_or("--:--")
.to_string();
div()
.flex()
.items_center()
.gap_3()
.child(
div()
.w(px(36.0))
.h(px(36.0))
.rounded_full()
.bg(theme::accent())
.border_1()
.border_color(theme::border())
.text_color(theme::foreground())
.flex()
.items_center()
.justify_center()
.text_size(px(14.0))
.font_weight(gpui::FontWeight::MEDIUM)
.child(sender.to_string()),
)
.child(
div()
.flex()
.flex_1()
.gap_2()
.items_center()
.child(
div()
.text_size(px(14.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::foreground())
.child(
message
.map(|item| item.sender_kind.clone())
.unwrap_or_else(|| "human".to_string()),
),
)
.child(
div()
.text_size(px(12.0))
.text_color(theme::muted_foreground())
.child(
message
.map(|item| item.channel.clone())
.unwrap_or_else(|| "company".to_string()),
),
)
.child(
div()
.px(px(8.0))
.py(px(2.0))
.rounded_full()
.bg(theme::primary())
.text_size(px(10.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::primary_foreground())
.child("OPERATIONS"),
)
.child(
div()
.flex_1()
.text_size(px(12.0))
.text_color(theme::muted_foreground())
.child(timestamp),
)
.when(urgent, |container| {
container.child(
div()
.px(px(8.0))
.py(px(2.0))
.rounded_full()
.bg(theme::destructive())
.text_size(px(10.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::primary_foreground())
.child("URGENT"),
)
}),
)
}
pub(super) fn secondary_message(
selected: Option<&domain::AgentMessage>,
) -> Option<&domain::AgentMessage> {
selected
}
fn trim_sender(sender: &str) -> &str {
sender.get(..2).unwrap_or(sender)
}
pub(super) fn composer_chip(label: &str) -> impl IntoElement {
div()
.px(px(12.0))
.py(px(6.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::border())
.bg(theme::muted())
.text_size(px(12.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::muted_foreground())
.child(label.to_string())
}
pub(super) fn composer_subject(message: Option<&domain::AgentMessage>) -> impl IntoElement {
div()
.flex_1()
.px(px(12.0))
.py(px(6.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::border())
.bg(theme::card())
.text_size(px(14.0))
.text_color(theme::muted_foreground())
.child(
message
.map(|item| item.subject.clone())
.unwrap_or_else(|| "Subject (optional)".to_string()),
)
}
pub(super) fn selected_recipient(message: Option<&domain::AgentMessage>) -> String {
message
.and_then(|item| item.to_agent_id.clone())
.unwrap_or_else(|| "CEO (CEO)".to_string())
}
File diff suppressed because it is too large. Load diff
@@ -1,900 +0,0 @@
use super::*;
impl AppRoot {
pub(super) fn render_overview(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let pending_approvals = self
.snapshot
.approvals
.iter()
.filter(|approval| approval.state == "pending")
.collect::<Vec<_>>();
let failed_runs = self
.snapshot
.runs
.iter()
.filter(|run| {
matches!(
run.status.as_str(),
"failed" | "timed_out" | "cancelled" | "interrupted"
)
})
.collect::<Vec<_>>();
let unread_messages = self
.snapshot
.agent_messages
.iter()
.filter(|message| message.read_at.is_none())
.collect::<Vec<_>>();
let budget_alerts = self
.snapshot
.agents
.iter()
.filter(|agent| {
agent.budget_monthly_usd > 0.0
&& agent.spent_monthly_usd >= agent.budget_monthly_usd * 0.8
})
.collect::<Vec<_>>();
let interrupted_runs = self
.snapshot
.runs
.iter()
.filter(|run| run.status == "interrupted")
.collect::<Vec<_>>();
let active_work = self
.snapshot
.runs
.iter()
.filter(|run| matches!(run.status.as_str(), "queued" | "running"))
.take(8)
.collect::<Vec<_>>();
let total_spend = self
.snapshot
.runs
.iter()
.filter_map(|run| run.cost_usd)
.sum::<f64>();
let approval_rows = pending_approvals
.iter()
.take(5)
.map(|approval| {
attention_row(
approval.payload_summary.clone(),
format!("{} · pending", approval.approval_type.replace('_', " ")),
theme::accent_orange_soft(),
theme::accent_orange(),
SectionId::Approvals,
Some(approval.id.as_str()),
cx,
)
.into_any_element()
})
.collect::<Vec<_>>();
let failed_rows = failed_runs
.iter()
.take(3)
.map(|run| {
attention_row(
overview_run_title(&self.snapshot, run),
run.error_message
.clone()
.unwrap_or_else(|| run.status.clone()),
theme::danger_bg(),
theme::danger(),
SectionId::Runs,
Some(run.id.as_str()),
cx,
)
.into_any_element()
})
.collect::<Vec<_>>();
let unread_rows = unread_messages
.iter()
.take(3)
.map(|message| {
attention_row(
message.subject.clone(),
message.body.clone(),
theme::accent_teal_soft(),
theme::accent_teal(),
SectionId::Messages,
Some(message.id.as_str()),
cx,
)
.into_any_element()
})
.collect::<Vec<_>>();
div()
.flex()
.flex_col()
.gap_6()
.when(!interrupted_runs.is_empty(), |container| {
container.child(recovery_banner(interrupted_runs.as_slice()))
})
.when(!budget_alerts.is_empty(), |container| {
container.child(budget_alert_banner(budget_alerts.as_slice(), cx))
})
.child(
div()
.grid()
.grid_cols(4)
.gap_3()
.child(metric_card(
"ACTIVE TASKS",
self.snapshot.active_tasks().to_string(),
format!(
"{} queued",
self.snapshot
.tasks
.iter()
.filter(|task| task.status == "todo")
.count()
),
theme::accent_blue_soft(),
theme::accent_blue(),
SectionId::Inbox,
cx,
))
.child(metric_card(
"APPROVALS",
pending_approvals.len().to_string(),
if pending_approvals.is_empty() {
"All clear".to_string()
} else {
"Needs attention".to_string()
},
theme::accent_orange_soft(),
theme::accent_orange(),
SectionId::Approvals,
cx,
))
.child(metric_card(
"AGENTS",
self.snapshot.agents.len().to_string(),
format!(
"{} running",
self.snapshot
.agents
.iter()
.filter(|agent| agent.status == "running")
.count()
),
theme::accent_teal_soft(),
theme::accent_teal(),
SectionId::Messages,
cx,
))
.child(metric_card(
"TOTAL SPEND",
format!("${total_spend:.2}"),
"Open settings and costs".to_string(),
theme::panel_muted(),
theme::text_primary(),
SectionId::Settings,
cx,
)),
)
.child(eyebrow_label("PERFORMANCE"))
.child(
div()
.grid()
.grid_cols(2)
.gap_3()
.child(run_activity_card(&self.snapshot))
.child(success_rate_card(&self.snapshot))
.child(cost_trend_card(&self.snapshot))
.child(agent_workload_card(&self.snapshot)),
)
.child(eyebrow_label("BUDGET UTILIZATION"))
.child(budget_utilization_card(&self.snapshot))
.when(
!(pending_approvals.is_empty()
&& failed_runs.is_empty()
&& unread_messages.is_empty()),
|container| {
container.child(needs_attention_label()).child(
div()
.gap_2()
.children(approval_rows)
.children(failed_rows)
.children(unread_rows),
)
},
)
.child(active_work_header(cx))
.child(active_work_card(&self.snapshot, active_work.as_slice(), cx))
.child(eyebrow_label("RECENT ACTIVITY"))
.child(activity_feed_card(&self.snapshot))
}
}
fn recovery_banner(runs: &[&domain::Run]) -> impl IntoElement {
let danger_30 = gpui::Hsla { a: 0.3, ..theme::danger() };
div()
.rounded(px(0.0))
.border_1()
.border_color(danger_30)
.bg(theme::warning_bg())
.px_4()
.py_4()
.mb_4()
.child(
div()
.flex()
.gap_2()
.items_center()
.child(
div()
.w(px(16.0))
.h(px(16.0))
.rounded(px(4.0))
.bg(theme::danger())
.flex()
.items_center()
.justify_center()
.child(div().w(px(10.0)).h(px(10.0)).rounded_full().bg(theme::panel())),
)
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(format!(
"{} interrupted task(s) detected from the previous session",
runs.len()
)),
),
)
.children(runs.iter().map(|run| {
div()
.mt(px(8.0))
.rounded(px(4.0))
.bg(theme::panel())
.px_4()
.py_3()
.child(
div()
.text_size(px(13.0))
.text_color(theme::text_primary())
.child(overview_run_title_from_run(run)),
)
.child(
div()
.mt(px(4.0))
.text_size(px(11.0))
.text_color(theme::text_muted())
.child(run.id.get(..8).unwrap_or(run.id.as_str()).to_string()),
)
}))
}
fn budget_alert_banner(agents: &[&domain::Agent], cx: &mut Context<AppRoot>) -> impl IntoElement {
let danger_30 = gpui::Hsla { a: 0.3, ..theme::danger() };
div().gap_2().children(agents.iter().map(|agent| {
div()
.id(SharedString::from(format!("budget-alert-{}", agent.id)))
.rounded(px(0.0))
.border_1()
.border_color(danger_30)
.bg(theme::danger_bg())
.px_4()
.py_3()
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(|this, _, _, cx| {
this.state.section = SectionId::Settings;
cx.notify();
}))
.child(
div()
.flex()
.gap_2()
.items_center()
.child(
div()
.w(px(16.0))
.h(px(16.0))
.rounded(px(4.0))
.bg(theme::danger())
.flex()
.items_center()
.justify_center()
.child(div().w(px(10.0)).h(px(10.0)).rounded_full().bg(theme::panel())),
)
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(format!(
"{} is at ${:.2} / ${:.2} monthly budget.",
agent.name, agent.spent_monthly_usd, agent.budget_monthly_usd
)),
),
)
}))
}
fn metric_card(
label: &'static str,
value: String,
subtitle: String,
icon_bg: gpui::Hsla,
_tone: gpui::Hsla,
target_section: SectionId,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
let icon_letter = match label {
"ACTIVE TASKS" => "T",
"APPROVALS" => "A",
"RUNS" => "R",
"AGENTS" => "A",
_ => "•",
};
div()
.id(SharedString::from(format!("overview-metric-{label}")))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.px_4()
.py_4()
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.section = target_section;
cx.notify();
}))
.child(
div()
.flex()
.gap_4()
.items_center()
.child(
div()
.w(px(48.0))
.h(px(48.0))
.rounded(px(4.0))
.bg(icon_bg)
.flex()
.items_center()
.justify_center()
.child(
div()
.text_size(px(18.0))
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme::text_muted())
.child(icon_letter),
),
)
.child(
div()
.flex_1()
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::text_muted())
.child(label),
)
.child(
div()
.text_size(px(28.0))
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme::text_primary())
.child(value),
)
.child(
div()
.mt(px(6.0))
.text_size(px(11.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_muted())
.child(subtitle),
),
),
)
}
fn eyebrow_label(label: &'static str) -> impl IntoElement {
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::text_muted())
.child(label)
}
fn needs_attention_label() -> impl IntoElement {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w(px(6.0))
.h(px(6.0))
.rounded_full()
.bg(theme::accent_orange()),
)
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::accent_orange())
.child("NEEDS ATTENTION"),
)
}
fn attention_row(
title: String,
subtitle: String,
tile_bg: gpui::Hsla,
accent: gpui::Hsla,
target_section: SectionId,
target_id: Option<&str>,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
let selected_id = target_id.map(str::to_string);
div()
.id(SharedString::from(format!("overview-attention-{title}")))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.px_4()
.py_3()
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.section = target_section;
match target_section {
SectionId::Runs => this.state.selected_run_id = selected_id.clone(),
SectionId::Approvals => this.state.selected_approval_id = selected_id.clone(),
SectionId::Messages => this.state.selected_message_id = selected_id.clone(),
_ => {}
}
cx.notify();
}))
.child(
div()
.flex()
.gap_3()
.items_center()
.child(
div()
.w(px(36.0))
.h(px(36.0))
.rounded(px(4.0))
.bg(tile_bg)
.flex()
.items_center()
.justify_center()
.child(
div()
.w(px(8.0))
.h(px(8.0))
.rounded_full()
.bg(accent),
),
)
.child(
div()
.flex_1()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(title),
)
.child(
div()
.mt(px(4.0))
.text_size(px(11.0))
.text_color(theme::text_muted())
.line_clamp(2)
.child(subtitle),
),
)
.child(
div()
.text_size(px(14.0))
.text_color(theme::text_muted())
.child(">"),
),
)
}
fn run_activity_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
let queued = snapshot
.runs
.iter()
.filter(|run| run.status == "queued")
.count();
let running = snapshot
.runs
.iter()
.filter(|run| run.status == "running")
.count();
let completed = snapshot
.runs
.iter()
.filter(|run| run.status == "completed")
.count();
mini_chart_card(
"RUN ACTIVITY",
vec![
("queued".to_string(), queued),
("running".to_string(), running),
("completed".to_string(), completed),
],
theme::accent_blue(),
)
}
fn success_rate_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
let finished = snapshot
.runs
.iter()
.filter(|run| matches!(run.status.as_str(), "completed" | "failed" | "timed_out"))
.count();
let completed = snapshot
.runs
.iter()
.filter(|run| run.status == "completed")
.count();
let rate = if finished == 0 {
0.0
} else {
completed as f32 / finished as f32
};
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::text_muted())
.child("SUCCESS RATE"),
)
.child(
div()
.mt(px(10.0))
.text_size(px(28.0))
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme::accent_teal())
.child(format!("{:.0}%", rate * 100.0)),
)
.child(
div()
.mt(px(10.0))
.h(px(10.0))
.rounded_full()
.bg(theme::panel_muted())
.child(
div()
.w(gpui::relative(rate.clamp(0.0, 1.0)))
.h_full()
.rounded_full()
.bg(theme::accent_teal()),
),
)
}
fn cost_trend_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
let costs = snapshot
.runs
.iter()
.filter_map(|run| run.cost_usd)
.collect::<Vec<_>>();
let total = costs.iter().sum::<f64>();
let average = if costs.is_empty() {
0.0
} else {
total / costs.len() as f64
};
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::text_muted())
.child("COST TREND"),
)
.child(
div()
.mt(px(10.0))
.text_size(px(28.0))
.font_weight(gpui::FontWeight::BOLD)
.text_color(theme::text_primary())
.child(format!("${total:.2}")),
)
.child(
div()
.mt(px(8.0))
.text_size(px(11.0))
.text_color(theme::text_muted())
.child(format!("Average ${average:.2} per metered run")),
)
}
fn agent_workload_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
let rows = snapshot
.agents
.iter()
.map(|agent| {
let active_runs = snapshot
.runs
.iter()
.filter(|run| run.agent_id == agent.id)
.count();
(agent.name.clone(), active_runs)
})
.collect::<Vec<_>>();
mini_chart_card("AGENT WORKLOAD", rows, theme::accent_orange())
}
fn mini_chart_card(
label: &'static str,
rows: Vec<(String, usize)>,
accent: gpui::Hsla,
) -> impl IntoElement {
let max = rows
.iter()
.map(|(_, count)| *count)
.max()
.unwrap_or(1)
.max(1) as f32;
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::text_muted())
.child(label),
)
.children(rows.into_iter().map(|(name, count)| {
div()
.mt(px(12.0))
.child(
div()
.flex()
.justify_between()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(name),
)
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_muted())
.child(count.to_string()),
),
)
.child(
div()
.mt(px(6.0))
.h(px(8.0))
.rounded_full()
.bg(theme::panel_muted())
.child(
div()
.w(gpui::relative((count as f32 / max).clamp(0.0, 1.0)))
.h_full()
.rounded_full()
.bg(accent),
),
)
}))
}
fn budget_utilization_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.children(
snapshot
.agents
.iter()
.filter(|agent| agent.budget_monthly_usd > 0.0)
.map(|agent| {
let ratio =
(agent.spent_monthly_usd / agent.budget_monthly_usd).clamp(0.0, 1.0) as f32;
div()
.mt(px(12.0))
.child(
div()
.flex()
.justify_between()
.items_center()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(agent.name.clone()),
)
.child(
div()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_muted())
.child(format!(
"${:.2} / ${:.2}",
agent.spent_monthly_usd, agent.budget_monthly_usd
)),
),
)
.child(
div()
.mt(px(6.0))
.h(px(8.0))
.rounded_full()
.bg(theme::panel_muted())
.child(div().w(gpui::relative(ratio)).h_full().rounded_full().bg(
if ratio >= 1.0 {
theme::danger()
} else if ratio >= 0.8 {
theme::accent_orange()
} else {
theme::accent_teal()
},
)),
)
}),
)
}
fn active_work_header(cx: &mut Context<AppRoot>) -> impl IntoElement {
div()
.flex()
.justify_between()
.items_center()
.child(eyebrow_label("ACTIVE WORK"))
.child(
div()
.id("overview-all-runs")
.cursor_pointer()
.text_size(px(11.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::accent_blue())
.hover(|style| style.text_color(theme::text_primary()))
.on_click(cx.listener(|this, _, _, cx| {
this.state.section = SectionId::Runs;
cx.notify();
}))
.child("All runs"),
)
}
fn active_work_card<'a>(
snapshot: &'a domain::ProfileSnapshot,
runs: &[&'a domain::Run],
cx: &mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(runs.is_empty(), |container| {
container.child(
div()
.p_6()
.text_center()
.text_color(theme::text_muted())
.child("No tasks are running right now."),
)
})
.when(!runs.is_empty(), |container| {
container.children(runs.iter().map(|run| {
let run_id = run.id.clone();
div()
.id(SharedString::from(format!("overview-run-{run_id}")))
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.section = SectionId::Runs;
this.state.selected_run_id = Some(run_id.clone());
cx.notify();
}))
.px_4()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(overview_run_title(snapshot, run)),
)
.child(
div()
.mt(px(4.0))
.text_size(px(11.0))
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
overview_run_agent(snapshot, run),
run.status
)),
)
}))
})
}
fn activity_feed_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(snapshot.activity.iter().take(10).map(|item| {
div()
.px_4()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(format!("{} {}", item.actor, item.action.replace('_', " "))),
)
.child(
div()
.mt(px(4.0))
.text_size(px(11.0))
.text_color(theme::text_muted())
.child(item.detail.clone()),
)
}))
}
fn overview_run_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
run.task_id
.as_ref()
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
.map(|task| task.title.clone())
.unwrap_or_else(|| overview_run_title_from_run(run))
}
fn overview_run_title_from_run(run: &domain::Run) -> String {
if run.summary.trim().is_empty() {
format!("Run {}", run.id.get(..8).unwrap_or(run.id.as_str()))
} else {
run.summary.clone()
}
}
fn overview_run_agent(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
snapshot
.agents
.iter()
.find(|agent| agent.id == run.agent_id)
.map(|agent| agent.name.clone())
.unwrap_or_else(|| run.agent_id.clone())
}
@@ -1,842 +0,0 @@
use super::*;
use gpui::{Animation, AnimationExt, pulsating_between};
use std::time::Duration;
impl AppRoot {
pub(super) fn render_runs(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let search_query = self.run_search_input.read(cx).value().to_lowercase();
let filtered_runs = self
.snapshot
.runs
.iter()
.filter(|item| run_matches_filter(&self.snapshot, item, self.state.run_filter))
.filter(|item| run_matches_query(&self.snapshot, item, search_query.as_str()))
.collect::<Vec<_>>();
let selected = self
.state
.selected_run_id
.as_ref()
.and_then(|id| self.snapshot.runs.iter().find(|item| &item.id == id))
.or_else(|| filtered_runs.first().copied());
if self.snapshot.runs.is_empty() {
return runs_empty_state().into_any_element();
}
div()
.flex()
.gap_8()
.child(render_run_queue_panel(
&self.snapshot,
filtered_runs.as_slice(),
self.state.selected_run_id.as_deref(),
self.state.run_filter,
self.run_search_input.clone(),
cx,
))
.child(render_run_detail_column(self, selected, cx))
.into_any_element()
}
}
fn render_run_queue_panel(
snapshot: &domain::ProfileSnapshot,
runs: &[&domain::Run],
selected_run_id: Option<&str>,
run_filter: RunFilter,
run_search_input: Entity<TextInput>,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div().w(px(420.0)).gap_5().child(
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.flex()
.justify_between()
.items_center()
.child(
div()
.child(
div()
.text_lg()
.text_color(theme::text_primary())
.child("Run queue"),
)
.child(
div()
.mt(px(4.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!("{} visible runs", runs.len())),
),
)
.child(run_filter_controls(run_filter, cx)),
)
.child(div().mt(px(16.0)).child(run_search_input))
.child(div().mt(px(16.0)).gap_2().children(runs.iter().map(|run| {
run_queue_row(snapshot, run, selected_run_id == Some(run.id.as_str()), cx)
.into_any_element()
})))
.when(runs.is_empty(), |container| {
container.child(
div()
.mt(px(16.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel_muted())
.px_4()
.py_6()
.text_center()
.child(
div()
.text_sm()
.text_color(theme::text_muted())
.child("No runs match this filter."),
),
)
}),
)
}
fn run_queue_row(
snapshot: &domain::ProfileSnapshot,
run: &domain::Run,
active: bool,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
let run_id = run.id.clone();
let title = run_task_title(snapshot, run);
let subtitle = run_subtitle(snapshot, run);
let supporting_text = if let Some(error_message) = run.error_message.clone() {
if error_message.trim().is_empty() {
run.summary.clone()
} else {
error_message
}
} else if run.summary.trim().is_empty() {
"No summary yet".to_string()
} else {
run.summary.clone()
};
div()
.id(SharedString::from(format!("run-row-{}", run.id)))
.rounded(px(0.0))
.border_1()
.border_color(if active {
theme::selection_border()
} else {
theme::panel_border()
})
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.cursor_pointer()
.hover(|style| style.bg(theme::panel_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.selected_run_id = Some(run_id.clone());
cx.notify();
}))
.px_4()
.py_3()
.child(
div()
.flex()
.justify_between()
.items_center()
.gap_3()
.child(
div()
.flex_1()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(theme::text_primary())
.truncate()
.child(title),
)
.child(
div()
.mt(px(2.0))
.text_size(px(11.0))
.text_color(theme::text_muted())
.truncate()
.child(subtitle),
),
)
.child(run_status_badge(run.status.as_str())),
)
.child(
div()
.mt(px(6.0))
.flex()
.justify_between()
.gap_3()
.child(
div()
.text_size(px(11.0))
.text_color(theme::text_muted())
.line_clamp(1)
.child(supporting_text),
)
.child(
div()
.text_size(px(11.0))
.text_color(theme::text_muted())
.child(short_date(run.updated_at.as_str()).to_string()),
),
)
}
fn render_run_detail_column<'a>(
root: &'a AppRoot,
selected: Option<&'a domain::Run>,
cx: &'a mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div().flex_1().gap_6().when_some(selected, |container, run| {
container
.child(run_detail_panel(&root.snapshot, run))
.child(run_trace_panel(&root.snapshot, run))
.child(secondary_data_panel(
"Runtime events",
run_events_panel(&root.snapshot, run.id.as_str()).into_any_element(),
))
.child(secondary_data_panel(
"Artifacts",
run_artifacts_panel(&root.snapshot, run.id.as_str()).into_any_element(),
))
.child(run_live_controls_panel(root, cx))
})
.when(selected.is_none(), |container| {
container.child(
div()
.min_h(px(320.0))
.flex()
.items_center()
.justify_center()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.w(px(420.0))
.text_center()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child("Select a run"),
)
.child(
div()
.mt(px(10.0))
.text_base()
.text_color(theme::text_muted())
.child("Choose a run from the queue to inspect details, trace, artifacts, and operator actions."),
),
),
)
})
}
fn runs_empty_state() -> impl IntoElement {
div()
.min_h(px(320.0))
.flex()
.items_center()
.justify_center()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.w(px(420.0))
.text_center()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child("No runs yet"),
)
.child(
div()
.mt(px(10.0))
.text_base()
.text_color(theme::text_muted())
.child("Runs will appear here once your company starts executing work."),
),
)
}
fn run_detail_panel(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> impl IntoElement {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child(run_task_title(snapshot, run)),
)
.child(
div()
.mt(px(8.0))
.text_sm()
.text_color(theme::text_muted())
.child(run_subtitle(snapshot, run)),
)
.child(
div()
.mt(px(20.0))
.child(detail_row(
"Status",
run_status_badge(run.status.as_str()).into_any_element(),
))
.child(detail_row(
"Agent",
text_value(run_agent_name(snapshot, run)).into_any_element(),
))
.child(detail_row(
"Connector",
text_value(run.connector_id.clone()).into_any_element(),
))
.when_some(run.model.clone(), |container, model| {
container.child(detail_row("Model", text_value(model).into_any_element()))
})
.child(detail_row(
"Created",
text_value(short_date(run.created_at.as_str()).to_string()).into_any_element(),
))
.child(detail_row(
"Started",
text_value(
run.started_at
.as_deref()
.map(short_date)
.unwrap_or("Pending")
.to_string(),
)
.into_any_element(),
))
.when_some(run.finished_at.as_deref(), |container, finished_at| {
container.child(detail_row(
"Finished",
text_value(short_date(finished_at).to_string()).into_any_element(),
))
})
.child(detail_row(
"Cost",
text_value(
run.cost_usd
.map(|value| format!("${value:.2}"))
.unwrap_or_else(|| "$0.00".to_string()),
)
.into_any_element(),
)),
)
.child(
div()
.mt(px(16.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_inset_border())
.bg(theme::panel_inset())
.p_4()
.text_sm()
.text_color(theme::text_secondary())
.child(if run.summary.trim().is_empty() {
"No summary yet.".to_string()
} else {
run.summary.clone()
}),
)
.when_some(run.error_message.clone(), |content, error| {
if error.trim().is_empty() {
content
} else {
content.child(
div()
.mt(px(16.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::danger())
.bg(theme::danger_bg())
.px_4()
.py_3()
.text_sm()
.text_color(theme::danger())
.child(error),
)
}
})
}
fn run_trace_panel(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> impl IntoElement {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_lg()
.text_color(theme::text_primary())
.child("Execution trace"),
)
.child(run_timeline(snapshot, run))
}
fn secondary_data_panel(title: &'static str, body: gpui::AnyElement) -> impl IntoElement {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_lg()
.text_color(theme::text_primary())
.child(title),
)
.child(body)
}
fn detail_row(label: &'static str, value: gpui::AnyElement) -> impl IntoElement {
div()
.flex()
.justify_between()
.items_center()
.py_2()
.border_b_1()
.border_color(theme::panel_border())
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.child(value)
}
fn text_value(value: String) -> impl IntoElement {
div()
.text_sm()
.text_color(theme::text_primary())
.child(value)
}
fn run_task_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
run.task_id
.as_ref()
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
.map(|task| task.title.clone())
.unwrap_or_else(|| {
if run.summary.trim().is_empty() {
format!("Run {}", short_id(run.id.as_str()))
} else {
run.summary.clone()
}
})
}
fn run_subtitle(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
format!(
"{} via {}{}",
run_agent_name(snapshot, run),
run.connector_id,
run.workspace_id
.as_ref()
.map(|_| format!(" · {}", run_workspace_name(snapshot, run)))
.unwrap_or_default()
)
}
fn run_agent_name(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
snapshot
.agents
.iter()
.find(|agent| agent.id == run.agent_id)
.map(|agent| agent.name.clone())
.unwrap_or_else(|| run.agent_id.clone())
}
fn run_workspace_name(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
run.workspace_id
.as_ref()
.and_then(|workspace_id| {
snapshot
.workspaces
.iter()
.find(|workspace| &workspace.id == workspace_id)
})
.map(|workspace| workspace.name.clone())
.unwrap_or_else(|| "Workspace pending".to_string())
}
fn run_status_badge(status: &str) -> impl IntoElement {
let badge = div()
.px_3()
.py_2()
.rounded_full()
.bg(match status {
"running" => theme::accent_teal_soft(),
"failed" | "timed_out" | "cancelled" | "interrupted" => theme::danger_bg(),
_ => theme::panel_muted(),
})
.text_xs()
.text_color(match status {
"running" => theme::accent_teal(),
"failed" | "timed_out" | "cancelled" | "interrupted" => theme::danger(),
_ => theme::text_secondary(),
})
.child(status.to_uppercase());
if status == "running" {
badge
.with_animation(
("run-badge", 1usize),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.8, 1.0)),
|element, delta| element.opacity(delta),
)
.into_any_element()
} else {
badge.into_any_element()
}
}
fn run_timeline(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> impl IntoElement {
div()
.mt(px(16.0))
.gap_3()
.child(timeline_row(
"Created",
short_date(run.created_at.as_str()).to_string(),
))
.child(timeline_row(
"Started",
run.started_at
.as_deref()
.map(short_date)
.unwrap_or("Pending")
.to_string(),
))
.child(timeline_row(
"Task",
run.task_id
.as_ref()
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
.map(|task| task.status.clone())
.unwrap_or_else(|| "Unlinked".to_string()),
))
.child(timeline_row(
"Updated",
short_date(run.updated_at.as_str()).to_string(),
))
}
fn run_events_panel(snapshot: &domain::ProfileSnapshot, run_id: &str) -> impl IntoElement {
let events = snapshot
.run_events
.iter()
.filter(|event| event.run_id == run_id)
.take(8)
.collect::<Vec<_>>();
if events.is_empty() {
div()
.mt(px(16.0))
.text_sm()
.text_color(theme::text_muted())
.child("No runtime events recorded yet.")
.into_any_element()
} else {
div()
.mt(px(16.0))
.gap_3()
.children(events.into_iter().map(|event| {
div()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(event.title.clone()),
)
.child(
div()
.mt(px(6.0))
.text_xs()
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
event.event_kind,
short_date(event.created_at.as_str())
)),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_secondary())
.child(event.detail.clone()),
)
}))
.into_any_element()
}
}
fn run_artifacts_panel(snapshot: &domain::ProfileSnapshot, run_id: &str) -> impl IntoElement {
let artifacts = snapshot
.artifacts
.iter()
.filter(|artifact| artifact.run_id == run_id)
.take(8)
.collect::<Vec<_>>();
if artifacts.is_empty() {
div()
.mt(px(16.0))
.text_sm()
.text_color(theme::text_muted())
.child("No artifacts recorded yet.")
.into_any_element()
} else {
div()
.mt(px(16.0))
.gap_3()
.children(artifacts.into_iter().map(|artifact| {
div()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(artifact.title.clone()),
)
.child(
div()
.mt(px(6.0))
.text_xs()
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
artifact.provider,
short_date(artifact.created_at.as_str())
)),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.text_color(theme::text_secondary())
.child(artifact.external_id.clone()),
)
}))
.into_any_element()
}
}
fn run_live_controls_panel<'a>(
root: &'a AppRoot,
cx: &'a mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_lg()
.text_color(theme::text_primary())
.child("Operator actions"),
)
.child(
div()
.mt(px(8.0))
.text_sm()
.text_color(theme::text_muted())
.child("Secondary runtime controls live here so the main queue/detail hierarchy stays aligned with the source product."),
)
.child(
div()
.mt(px(16.0))
.flex()
.gap_3()
.child(primary_button("Launch native runtime", cx, |this, cx| {
this.launch_selected_run(cx);
}))
.child(secondary_button("Approve selected gate", cx, |this, cx| {
this.decide_selected_approval(
domain::ApprovalState::Approved,
"Approved from native runtime controls.",
cx,
);
}))
.child(secondary_button("Reject selected gate", cx, |this, cx| {
this.decide_selected_approval(
domain::ApprovalState::Rejected,
"Rejected from native runtime controls.",
cx,
);
})),
)
.child(div().mt(px(16.0)).child(root.runtime_steer_input.clone()))
.child(
div()
.mt(px(10.0))
.flex()
.justify_end()
.child(primary_button("Send steer", cx, |this, cx| {
this.send_runtime_steer(cx);
})),
)
.child(div().mt(px(16.0)).child(root.runtime_follow_up_input.clone()))
.child(
div()
.mt(px(10.0))
.flex()
.justify_end()
.child(secondary_button("Queue follow-up", cx, |this, cx| {
this.send_runtime_follow_up(cx);
})),
)
}
fn timeline_row(label: &'static str, value: String) -> impl IntoElement {
div()
.flex()
.justify_between()
.items_center()
.py_2()
.border_b_1()
.border_color(theme::panel_border())
.child(div().text_sm().text_color(theme::text_muted()).child(label))
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(value),
)
}
fn run_filter_controls(cx_filter: RunFilter, cx: &mut Context<AppRoot>) -> impl IntoElement {
div()
.flex()
.gap_2()
.child(run_filter_chip(
"All",
cx_filter == RunFilter::All,
RunFilter::All,
cx,
))
.child(run_filter_chip(
"Running",
cx_filter == RunFilter::Running,
RunFilter::Running,
cx,
))
.child(run_filter_chip(
"Needs review",
cx_filter == RunFilter::NeedsReview,
RunFilter::NeedsReview,
cx,
))
.child(run_filter_chip(
"Finished",
cx_filter == RunFilter::Finished,
RunFilter::Finished,
cx,
))
}
fn run_filter_chip(
label: &'static str,
active: bool,
next: RunFilter,
cx: &mut Context<AppRoot>,
) -> impl IntoElement {
div()
.id(SharedString::from(format!("run-filter-{label}")))
.px_3()
.py_2()
.rounded_full()
.cursor_pointer()
.bg(if active {
theme::primary_action()
} else {
theme::panel_muted()
})
.text_xs()
.text_color(if active {
theme::primary_action_text()
} else {
theme::text_secondary()
})
.hover(|style| {
style.bg(if active {
theme::primary_action_hover()
} else {
theme::panel_hover()
})
})
.on_click(cx.listener(move |this, _, _, cx| {
this.state.run_filter = next;
cx.notify();
}))
.child(label)
}
fn run_matches_filter(
snapshot: &domain::ProfileSnapshot,
run: &domain::Run,
filter: RunFilter,
) -> bool {
match filter {
RunFilter::All => true,
RunFilter::Running => matches!(run.status.as_str(), "queued" | "running"),
RunFilter::NeedsReview => run.task_id.as_ref().is_some_and(|task_id| {
snapshot.approvals.iter().any(|approval| {
approval.related_task_id.as_deref() == Some(task_id.as_str())
&& approval.state == "pending"
})
}),
RunFilter::Finished => {
matches!(run.status.as_str(), "completed" | "failed" | "cancelled")
}
}
}
fn run_matches_query(snapshot: &domain::ProfileSnapshot, run: &domain::Run, query: &str) -> bool {
if query.trim().is_empty() {
return true;
}
let query = query.trim();
let title = run_task_title(snapshot, run).to_lowercase();
let subtitle = run_subtitle(snapshot, run).to_lowercase();
let summary = run.summary.to_lowercase();
title.contains(query) || subtitle.contains(query) || summary.contains(query)
}
fn short_id(value: &str) -> &str {
value.get(..8).unwrap_or(value)
}
fn short_date(value: &str) -> &str {
value.get(..10).unwrap_or(value)
}
@@ -1,547 +0,0 @@
use super::*;
use ::runtime::RuntimeConfig;
use anyhow::{Context as _, Result, anyhow};
use pi_bridge::{
ClientRequest, FollowUpRequest, RunBudget, RunRequest, RuntimeEvent, RuntimeHandshake,
SteerRequest, ToolApprovalDecision,
};
use std::{
env,
path::PathBuf,
sync::mpsc::{self, Receiver, Sender, TryRecvError},
time::Duration,
};
use support::paths::{RUNTIME_ARGS_ENV, RUNTIME_COMMAND_ENV, RUNTIME_WORKDIR_ENV};
pub(super) struct LiveRuntimeSession {
pub run_id: String,
pub commands: Sender<RuntimeCommand>,
pub updates: Receiver<RuntimeUpdate>,
}
pub(super) enum RuntimeCommand {
ToolApproval {
run_id: String,
tool_call_id: String,
approved: bool,
note: String,
},
Steer {
run_id: String,
body: String,
},
FollowUp {
run_id: String,
body: String,
},
}
pub(super) enum RuntimeUpdate {
Event(RuntimeEvent),
Failed(String),
Closed,
}
struct RuntimeApprovalPayload {
run_id: String,
tool_call_id: String,
}
impl AppRoot {
pub(super) fn launch_selected_run(&mut self, cx: &mut Context<Self>) {
if self.runtime_session.is_some() {
self.set_status("A native runtime session is already active.", cx);
return;
}
let selected_run_id = match self.state.selected_run_id.clone() {
Some(run_id) => run_id,
None => {
self.set_status("Choose a run first.", cx);
return;
}
};
let request = match self.build_run_request(selected_run_id.as_str()) {
Ok(request) => request,
Err(error) => {
self.set_status(error.to_string(), cx);
return;
}
};
let config = match resolve_runtime_config() {
Ok(config) => config,
Err(error) => {
self.set_status(error.to_string(), cx);
return;
}
};
let result = {
let mut store = self.store.lock();
store.mark_run_started(selected_run_id.as_str())
};
if let Err(error) = result {
self.set_status(error.to_string(), cx);
return;
}
let (command_tx, command_rx) = mpsc::channel();
let (update_tx, update_rx) = mpsc::channel();
spawn_runtime_worker(config, request, command_rx, update_tx);
self.runtime_session = Some(LiveRuntimeSession {
run_id: selected_run_id,
commands: command_tx,
updates: update_rx,
});
self.reload("Native runtime connected.", cx);
self.start_runtime_poll_loop(cx);
}
pub(super) fn send_runtime_steer(&mut self, cx: &mut Context<Self>) {
let Some(run_id) = self.active_runtime_run_id() else {
self.set_status("Launch a native runtime first.", cx);
return;
};
let body = self.runtime_steer_input.read(cx).value();
let trimmed = body.trim().to_string();
if trimmed.is_empty() {
self.set_status("Steer message cannot be empty.", cx);
return;
}
if let Err(error) = self.send_runtime_command(RuntimeCommand::Steer {
run_id,
body: trimmed,
}) {
self.runtime_session = None;
self.set_status(error.to_string(), cx);
return;
}
self.runtime_steer_input
.update(cx, |input, _| input.clear());
self.set_status("Steer queued into the native runtime.", cx);
}
pub(super) fn send_runtime_follow_up(&mut self, cx: &mut Context<Self>) {
let Some(run_id) = self.active_runtime_run_id() else {
self.set_status("Launch a native runtime first.", cx);
return;
};
let body = self.runtime_follow_up_input.read(cx).value();
let trimmed = body.trim().to_string();
if trimmed.is_empty() {
self.set_status("Follow-up message cannot be empty.", cx);
return;
}
if let Err(error) = self.send_runtime_command(RuntimeCommand::FollowUp {
run_id,
body: trimmed,
}) {
self.runtime_session = None;
self.set_status(error.to_string(), cx);
return;
}
self.runtime_follow_up_input
.update(cx, |input, _| input.clear());
self.set_status("Follow-up queued into the native runtime.", cx);
}
pub(super) fn maybe_dispatch_runtime_approval(
&mut self,
approval: Option<&domain::Approval>,
state: ApprovalState,
cx: &mut Context<Self>,
) {
let Some(approval) = approval else {
return;
};
if approval.approval_type != "runtime_tool_call" {
return;
}
let payload = match parse_runtime_approval_payload(approval) {
Ok(payload) => payload,
Err(error) => {
self.set_status(error.to_string(), cx);
return;
}
};
let approved = matches!(state, ApprovalState::Approved);
let note = match state {
ApprovalState::Approved => "Approved from native review.",
ApprovalState::Rejected => "Rejected from native review.",
ApprovalState::Pending => "Pending native review.",
ApprovalState::RevisionRequested => "Revision requested from native review.",
}
.to_string();
if let Err(error) = self.send_runtime_command(RuntimeCommand::ToolApproval {
run_id: payload.run_id,
tool_call_id: payload.tool_call_id,
approved,
note,
}) {
self.runtime_session = None;
self.set_status(error.to_string(), cx);
}
}
fn active_runtime_run_id(&self) -> Option<String> {
let session = self.runtime_session.as_ref()?;
let selected_matches =
self.state.selected_run_id.as_deref() == Some(session.run_id.as_str());
if selected_matches {
Some(session.run_id.clone())
} else {
None
}
}
fn send_runtime_command(&self, command: RuntimeCommand) -> Result<()> {
let session = self
.runtime_session
.as_ref()
.ok_or_else(|| anyhow!("native runtime session is unavailable"))?;
session
.commands
.send(command)
.context("native runtime command channel is closed")
}
fn start_runtime_poll_loop(&self, cx: &mut Context<Self>) {
cx.spawn(
|entity: gpui::WeakEntity<AppRoot>, async_cx: &mut gpui::AsyncApp| {
let entity = entity.clone();
let mut async_cx = async_cx.clone();
async move {
loop {
async_cx
.background_executor()
.timer(Duration::from_millis(120))
.await;
let keep_running = entity
.update(&mut async_cx, |this, cx| this.drain_runtime_updates(cx))
.unwrap_or(false);
if !keep_running {
break;
}
}
}
},
)
.detach();
}
fn drain_runtime_updates(&mut self, cx: &mut Context<Self>) -> bool {
let mut keep_running = self.runtime_session.is_some();
loop {
let next_update = match self.runtime_session.as_mut() {
Some(session) => match session.updates.try_recv() {
Ok(update) => Some(update),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
self.runtime_session = None;
self.reload("Native runtime disconnected.", cx);
keep_running = false;
None
}
},
None => None,
};
let Some(update) = next_update else {
break;
};
self.apply_runtime_update(update, cx);
keep_running = self.runtime_session.is_some();
}
keep_running
}
fn apply_runtime_update(&mut self, update: RuntimeUpdate, cx: &mut Context<Self>) {
match update {
RuntimeUpdate::Event(event) => {
let selected_run_id = self.state.selected_run_id.clone();
let result = {
let mut store = self.store.lock();
store.ingest_runtime_event(event.clone())
};
match result {
Ok(()) => {
self.reload("Native runtime event applied.", cx);
match event {
RuntimeEvent::ToolCall {
run_id,
tool_call_id,
..
} => {
self.state.selected_run_id = Some(run_id.clone());
self.state.selected_approval_id = self
.snapshot
.approvals
.iter()
.find(|approval| {
approval.approval_type == "runtime_tool_call"
&& approval
.payload_json
.as_deref()
.map(|payload| {
payload.contains(run_id.as_str())
&& payload.contains(tool_call_id.as_str())
})
.unwrap_or(false)
})
.map(|approval| approval.id.clone());
cx.notify();
}
RuntimeEvent::RunFinished { run_id, .. } => {
self.state.selected_run_id = Some(run_id);
cx.notify();
}
RuntimeEvent::Message { run_id, .. }
| RuntimeEvent::ToolResult { run_id, .. }
| RuntimeEvent::Artifact { run_id, .. } => {
if selected_run_id.as_deref() != Some(run_id.as_str()) {
self.state.selected_run_id = Some(run_id);
cx.notify();
}
}
RuntimeEvent::HandshakeAck { .. } => {}
}
}
Err(error) => {
self.runtime_session = None;
self.set_status(error.to_string(), cx);
}
}
}
RuntimeUpdate::Failed(message) => {
self.runtime_session = None;
self.set_status(message, cx);
}
RuntimeUpdate::Closed => {
self.runtime_session = None;
self.set_status("Native runtime session closed.", cx);
}
}
}
fn build_run_request(&self, run_id: &str) -> Result<RunRequest> {
let run = self
.snapshot
.runs
.iter()
.find(|run| run.id == run_id)
.cloned()
.ok_or_else(|| anyhow!("run {run_id} was not found in the active snapshot"))?;
let task = run
.task_id
.as_ref()
.and_then(|task_id| self.snapshot.tasks.iter().find(|task| &task.id == task_id))
.cloned()
.ok_or_else(|| anyhow!("selected run must be linked to a task before native launch"))?;
let company_id = self
.current_company_id()
.map(str::to_string)
.ok_or_else(|| anyhow!("create a company first"))?;
let workspace_path = run.workspace_id.as_ref().and_then(|workspace_id| {
self.snapshot
.workspaces
.iter()
.find(|workspace| &workspace.id == workspace_id)
.map(|workspace| workspace.local_path.clone())
});
let prompt = if task.description.trim().is_empty() {
task.title.clone()
} else {
format!("{}\n\n{}", task.title, task.description)
};
Ok(RunRequest {
run_id: run.id.clone(),
session_id: Some(run.id.clone()),
company_id,
task_id: Some(task.id.clone()),
agent_id: run.agent_id.clone(),
workspace_path,
connector_id: run.connector_id.clone(),
model: run.model.clone(),
prompt,
context_json: serde_json::json!({
"task_title": task.title,
"task_description": task.description,
"source": "native_gpui",
}),
budget: RunBudget {
thinking_level: Some("high".to_string()),
token_budget: None,
},
})
}
}
fn resolve_runtime_config() -> Result<RuntimeConfig> {
let command = env::var(RUNTIME_COMMAND_ENV)
.ok()
.filter(|value| !value.trim().is_empty())
.map(PathBuf::from)
.ok_or_else(|| {
anyhow!("set {RUNTIME_COMMAND_ENV} to the Pi Mono sidecar executable path")
})?;
let args = match env::var(RUNTIME_ARGS_ENV) {
Ok(value) if !value.trim().is_empty() => serde_json::from_str::<Vec<String>>(&value)
.with_context(|| format!("{RUNTIME_ARGS_ENV} must be a JSON string array"))?,
_ => Vec::new(),
};
let working_dir = env::var(RUNTIME_WORKDIR_ENV)
.ok()
.filter(|value| !value.trim().is_empty())
.map(PathBuf::from);
Ok(RuntimeConfig {
command,
args,
working_dir,
})
}
fn parse_runtime_approval_payload(approval: &domain::Approval) -> Result<RuntimeApprovalPayload> {
let payload_json = approval
.payload_json
.as_deref()
.ok_or_else(|| anyhow!("runtime approval payload is missing"))?;
let payload = serde_json::from_str::<serde_json::Value>(payload_json)
.context("failed to decode runtime approval payload")?;
Ok(RuntimeApprovalPayload {
run_id: payload
.get("run_id")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.ok_or_else(|| anyhow!("runtime approval payload is missing run_id"))?,
tool_call_id: payload
.get("tool_call_id")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.ok_or_else(|| anyhow!("runtime approval payload is missing tool_call_id"))?,
})
}
fn spawn_runtime_worker(
config: RuntimeConfig,
request: RunRequest,
command_rx: Receiver<RuntimeCommand>,
update_tx: Sender<RuntimeUpdate>,
) {
std::thread::spawn(move || {
if let Err(error) = run_runtime_worker(config, request, command_rx, &update_tx) {
let _ = update_tx.send(RuntimeUpdate::Failed(error.to_string()));
}
let _ = update_tx.send(RuntimeUpdate::Closed);
});
}
fn run_runtime_worker(
config: RuntimeConfig,
request: RunRequest,
command_rx: Receiver<RuntimeCommand>,
update_tx: &Sender<RuntimeUpdate>,
) -> Result<()> {
let run_id = request.run_id.clone();
let (mut writer, mut events) =
pi_bridge::PiBridgeClient::spawn_split(config.command, &config.args, config.working_dir)?;
writer.send(&ClientRequest::Handshake(RuntimeHandshake {
protocol_version: 1,
app_name: "AgentCompany-Rust".to_string(),
app_version: env!("CARGO_PKG_VERSION").to_string(),
}))?;
update_tx
.send(RuntimeUpdate::Event(events.read_event()?))
.context("failed to deliver runtime handshake event")?;
writer.send(&ClientRequest::StartRun(Box::new(request)))?;
loop {
let event = events.read_event()?;
let pending_tool_call = match &event {
RuntimeEvent::ToolCall { tool_call_id, .. } => Some(tool_call_id.clone()),
_ => None,
};
let run_finished = matches!(event, RuntimeEvent::RunFinished { .. });
update_tx
.send(RuntimeUpdate::Event(event))
.context("failed to deliver runtime event")?;
if let Some(expected_tool_call_id) = pending_tool_call {
loop {
match command_rx
.recv()
.context("runtime command channel closed")?
{
RuntimeCommand::ToolApproval {
run_id: command_run_id,
tool_call_id,
approved,
note,
} if command_run_id == run_id && tool_call_id == expected_tool_call_id => {
writer.send(&ClientRequest::ToolApproval(ToolApprovalDecision {
run_id: command_run_id,
tool_call_id,
approved,
note,
}))?;
break;
}
RuntimeCommand::Steer {
run_id: command_run_id,
body,
} if command_run_id == run_id => {
writer.send(&ClientRequest::Steer(SteerRequest {
run_id: command_run_id,
body,
}))?;
}
RuntimeCommand::FollowUp {
run_id: command_run_id,
body,
} if command_run_id == run_id => {
writer.send(&ClientRequest::FollowUp(FollowUpRequest {
run_id: command_run_id,
body,
}))?;
}
_ => {}
}
}
}
if run_finished {
loop {
match command_rx
.recv()
.context("runtime command channel closed")?
{
RuntimeCommand::Steer {
run_id: command_run_id,
body,
} if command_run_id == run_id => {
writer.send(&ClientRequest::Steer(SteerRequest {
run_id: command_run_id,
body,
}))?;
update_tx
.send(RuntimeUpdate::Event(events.read_event()?))
.context("failed to deliver steer runtime event")?;
}
RuntimeCommand::FollowUp {
run_id: command_run_id,
body,
} if command_run_id == run_id => {
writer.send(&ClientRequest::FollowUp(FollowUpRequest {
run_id: command_run_id,
body,
}))?;
update_tx
.send(RuntimeUpdate::Event(events.read_event()?))
.context("failed to deliver follow-up runtime event")?;
}
RuntimeCommand::ToolApproval { .. } => {}
_ => {}
}
}
}
}
}
@@ -1,316 +0,0 @@
use super::*;
impl AppRoot {
pub(super) fn render_settings(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
div()
.flex()
.flex_col()
.gap_6()
.child(settings_tabs(self.state.settings_tab, cx))
.child(self.render_active_settings_panel(cx))
}
fn render_active_settings_panel(&self, cx: &mut Context<Self>) -> 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<Self>) -> 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<Self>) -> 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_tabs(cx_tab: SettingsTab, cx: &mut Context<AppRoot>) -> 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<AppRoot>,
) -> impl IntoElement {
let active = tab == active_tab;
div()
.id(SharedString::from(format!("settings-tab-{label}")))
.relative()
.px_4()
.py_2()
.cursor_pointer()
.text_sm()
.text_color(if active {
theme::text_primary()
} else {
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<gpui::AnyElement>) -> impl IntoElement {
div()
.p_5()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_lg()
.text_color(theme::text_primary())
.child(label),
)
.child(div().mt(px(16.0)).children(rows))
}
fn settings_readonly_row(label: &'static str, value: String) -> gpui::AnyElement {
div()
.flex()
.justify_between()
.items_center()
.py_3()
.border_b_1()
.border_color(theme::panel_border())
.child(div().text_sm().text_color(theme::text_muted()).child(label))
.child(
div()
.text_sm()
.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(px(0.0))
.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 {
company
.map(|item| item.name.clone())
.unwrap_or_else(|| "No company".to_string())
}
@@ -1,234 +0,0 @@
use super::*;
use gpui::Window;
impl AppRoot {
pub(super) fn render_sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let sections = [
(SectionId::Overview, None),
(SectionId::Runs, Some(self.snapshot.active_runs())),
(SectionId::Inbox, Some(self.snapshot.active_tasks())),
(SectionId::Messages, Some(self.snapshot.unread_messages())),
(
SectionId::Approvals,
Some(self.snapshot.pending_approvals()),
),
(SectionId::Documents, None),
(SectionId::Workspaces, None),
];
div()
.w(px(256.0))
.h_full()
.p_4()
.gap_4()
.flex()
.flex_col()
.bg(theme::sidebar_bg())
.border_r_1()
.border_color(theme::sidebar_divider())
.child(
div()
.flex()
.items_center()
.justify_between()
.child(
div()
.flex()
.gap_2()
.child(traffic_light(rgb_hex(0xff5f57)))
.child(traffic_light(rgb_hex(0xfebc2e)))
.child(traffic_light(rgb_hex(0x28c840))),
)
.child(
div()
.text_size(px(11.0))
.text_color(theme::text_subtle())
.child("local-first"),
),
)
.child(
div()
.flex()
.gap_2()
.items_center()
.child(
div()
.flex_1()
.px_3()
.py_2()
.border_1()
.rounded(px(0.0))
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(theme::text_primary())
.child(
self.snapshot
.current_company()
.map(|company| company.name.clone())
.unwrap_or_else(|| "AgentCompany".to_string()),
),
),
)
.child(
div()
.w(px(30.0))
.h(px(30.0))
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.flex()
.items_center()
.justify_center()
.text_size(px(12.0))
.text_color(theme::text_muted())
.child("⌘"),
),
)
.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(px(0.0))
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.hover(|style| style.bg(theme::sidebar_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.switch_company(company_id.clone(), cx)
}))
.child(
div()
.text_size(px(13.0))
.font_weight(if active {
gpui::FontWeight::SEMIBOLD
} else {
gpui::FontWeight::MEDIUM
})
.text_color(if active {
theme::sidebar_text_active()
} else {
theme::sidebar_text()
})
.child(company.name.clone()),
)
}))
.child(div().h(px(1.0)).bg(theme::sidebar_divider()))
.children(sections.into_iter().map(|(section, badge)| {
let active = self.state.section == section;
let label = section.label();
div()
.id(SharedString::from(format!(
"section-{}",
label.to_lowercase()
)))
.flex()
.items_center()
.justify_between()
.px_3()
.py(px(7.0))
.rounded(px(0.0))
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::sidebar_bg()
})
.text_color(if active {
theme::sidebar_text_active()
} else {
theme::sidebar_text()
})
.hover(|style| style.bg(theme::sidebar_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.state.section = section;
cx.notify();
}))
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight::MEDIUM)
.child(label),
)
.when_some(badge.filter(|count| *count > 0), |container, count| {
container.child(
div()
.min_w(px(24.0))
.px_2()
.py_1()
.rounded_full()
.bg(if section == SectionId::Messages {
theme::accent_orange()
} else {
theme::badge_dark()
})
.text_size(px(11.0))
.text_color(theme::panel())
.child(count.to_string()),
)
})
}))
.child(div().flex_1())
.child(div().h(px(1.0)).bg(theme::sidebar_divider()))
.child(sidebar_footer_row(
"Settings",
self.state.section == SectionId::Settings,
cx,
|this, cx| {
this.state.section = SectionId::Settings;
cx.notify();
},
))
}
}
fn sidebar_footer_row<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!("sidebar-footer-{label}")))
.px_4()
.py_3()
.rounded(px(0.0))
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::sidebar_bg()
})
.text_color(if active {
theme::sidebar_text_active()
} else {
theme::sidebar_text()
})
.hover(|style| style.bg(theme::sidebar_hover()))
.on_click(
cx.listener(move |this, _: &gpui::ClickEvent, _: &mut Window, cx| on_click(this, cx)),
)
.child(label)
}
fn rgb_hex(hex: u32) -> gpui::Hsla {
gpui::rgb(hex).into()
}
fn traffic_light(color: gpui::Hsla) -> impl IntoElement {
div().w(px(12.0)).h(px(12.0)).rounded_full().bg(color)
}
@@ -1,171 +0,0 @@
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()
.gap_6()
.child(
div()
.flex()
.justify_between()
.items_start()
.child(
div()
.child(
div()
.text_2xl()
.text_color(theme::text_primary())
.child("Workspaces"),
)
.child(
div()
.mt(px(6.0))
.text_sm()
.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| {
container.child(
div()
.min_h(px(260.0))
.flex()
.items_center()
.justify_center()
.rounded(px(0.0))
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.w(px(420.0))
.text_center()
.child(
div()
.text_xl()
.text_color(theme::text_primary())
.child("No workspaces linked"),
)
.child(
div()
.mt(px(10.0))
.text_base()
.text_color(theme::text_muted())
.child("The active company will show its local workspaces here."),
),
),
)
})
.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,
))
})),
)
})
}
}
fn workspace_kind(is_primary: bool) -> &'static str {
if is_primary { "primary" } else { "linked" }
}
fn workspace_state_badge(state: &str, is_primary: bool) -> impl IntoElement {
div()
.flex()
.items_center()
.gap_2()
.child(div().w(px(8.0)).h(px(8.0)).rounded_full().bg(match state {
"ready" => theme::accent_teal(),
"blocked" | "missing" | "error" => theme::danger(),
_ => theme::accent_blue(),
}))
.child(
div()
.text_xs()
.text_color(theme::text_secondary())
.child(if is_primary {
format!("{} · PRIMARY", state.to_uppercase())
} else {
state.to_uppercase()
}),
)
}
@@ -1,38 +0,0 @@
mod app_root;
mod theme;
mod widgets;
use app_root::AppRoot;
use domain::ProfileSnapshot;
use gpui::{App, AppContext, Application, Bounds, WindowBounds, WindowOptions, px, size};
use parking_lot::Mutex;
use std::sync::Arc;
use storage::ProfileStore;
use widgets::text_input::bind_text_input_keys;
pub type SharedStore = Arc<Mutex<ProfileStore>>;
pub fn launch(store: SharedStore, snapshot: ProfileSnapshot) {
Application::new().run(move |cx: &mut App| {
bind_text_input_keys(cx);
let store = store.clone();
let snapshot = snapshot.clone();
let bounds = Bounds::centered(None, size(px(1380.0), px(920.0)), cx);
let Ok(window) = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
move |_, cx| cx.new(|cx| AppRoot::new(store.clone(), snapshot.clone(), cx)),
) else {
cx.quit();
return;
};
window
.update(cx, |_, window, cx| {
cx.activate(true);
window.activate_window();
})
.ok();
});
}
@@ -1,166 +0,0 @@
use gpui::{Hsla, rgb};
pub fn window_bg() -> Hsla {
rgb(0xf9f9f7).into()
}
pub fn shell_header() -> Hsla {
rgb(0xfcfcfa).into()
}
pub fn panel() -> Hsla {
rgb(0xffffff).into()
}
pub fn panel_border() -> Hsla {
rgb(0xe4e4df).into()
}
pub fn panel_hover() -> Hsla {
rgb(0xf5f7fa).into()
}
pub fn panel_inset() -> Hsla {
rgb(0xf7f7f3).into()
}
pub fn panel_inset_border() -> Hsla {
rgb(0xcbcbc5).into()
}
pub fn panel_muted() -> Hsla {
rgb(0xf1f3f6).into()
}
pub fn text_primary() -> Hsla {
rgb(0x22252b).into()
}
pub fn text_secondary() -> Hsla {
rgb(0x5e6673).into()
}
pub fn text_muted() -> Hsla {
rgb(0x8a9099).into()
}
pub fn text_subtle() -> Hsla {
rgb(0xa2a7af).into()
}
pub fn primary_action() -> Hsla {
rgb(0x5d6f8e).into()
}
pub fn primary_action_hover() -> Hsla {
rgb(0x6c7d99).into()
}
pub fn primary_action_text() -> Hsla {
rgb(0xfafcff).into()
}
pub fn accent_blue() -> Hsla {
rgb(0x35507a).into()
}
pub fn accent_blue_soft() -> Hsla {
rgb(0xeef3fb).into()
}
pub fn accent_teal() -> Hsla {
rgb(0x0d9488).into()
}
pub fn accent_teal_soft() -> Hsla {
rgb(0xecfef8).into()
}
pub fn accent_orange() -> Hsla {
rgb(0xc96a13).into()
}
pub fn accent_orange_soft() -> Hsla {
rgb(0xfff1e7).into()
}
pub fn danger() -> Hsla {
rgb(0xc74a4a).into()
}
pub fn danger_bg() -> Hsla {
rgb(0xfff0f0).into()
}
pub fn success_bg() -> Hsla {
rgb(0xebfbf3).into()
}
pub fn sidebar_bg() -> Hsla {
rgb(0xfbfbf9).into()
}
pub fn sidebar_hover() -> Hsla {
rgb(0xf4f5f2).into()
}
pub fn sidebar_divider() -> Hsla {
rgb(0xecece8).into()
}
pub fn sidebar_text() -> Hsla {
rgb(0x5e6673).into()
}
pub fn sidebar_text_active() -> Hsla {
rgb(0x22252b).into()
}
pub fn selection_fill() -> Hsla {
rgb(0xeef3fb).into()
}
pub fn selection_border() -> Hsla {
rgb(0xd5deeb).into()
}
pub fn badge_dark() -> Hsla {
rgb(0x6b7280).into()
}
// Tailwind v4 theme variable aliases (exact oklch → rgb from original styles.css)
pub fn card() -> Hsla {
rgb(0xffffff).into()
}
pub fn muted() -> Hsla {
rgb(0xf7f7f7).into()
}
pub fn accent() -> Hsla {
rgb(0xf7f7f7).into()
}
pub fn foreground() -> Hsla {
rgb(0x222222).into()
}
pub fn muted_foreground() -> Hsla {
rgb(0x5e5e5e).into()
}
pub fn primary() -> Hsla {
rgb(0x2d2d2d).into()
}
pub fn primary_foreground() -> Hsla {
rgb(0xfafafa).into()
}
pub fn destructive() -> Hsla {
rgb(0xc74a4a).into()
}
pub fn border() -> Hsla {
rgb(0xe8e8e8).into()
}
@@ -1,50 +0,0 @@
use crate::theme;
use gpui::{Context, SharedString, Window, div, prelude::*};
pub fn primary_button<V>(
label: &'static str,
cx: &mut Context<V>,
on_click: impl Fn(&mut V, &mut Context<V>) + 'static,
) -> impl IntoElement
where
V: 'static,
{
div()
.id(SharedString::from(format!("primary-button-{label}")))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(theme::primary_action())
.text_color(theme::primary_action_text())
.hover(|style| style.bg(theme::primary_action_hover()))
.on_click(
cx.listener(move |this, _: &gpui::ClickEvent, _: &mut Window, cx| on_click(this, cx)),
)
.child(label)
}
pub fn secondary_button<V>(
label: &'static str,
cx: &mut Context<V>,
on_click: impl Fn(&mut V, &mut Context<V>) + 'static,
) -> impl IntoElement
where
V: 'static,
{
div()
.id(SharedString::from(format!("secondary-button-{label}")))
.px_3()
.py_2()
.rounded_md()
.cursor_pointer()
.bg(theme::panel())
.border_1()
.border_color(theme::panel_border())
.text_color(theme::text_primary())
.hover(|style| style.bg(theme::panel_hover()))
.on_click(
cx.listener(move |this, _: &gpui::ClickEvent, _: &mut Window, cx| on_click(this, cx)),
)
.child(label)
}
@@ -1,2 +0,0 @@
pub mod button;
pub mod text_input;
@@ -1,447 +0,0 @@
use crate::theme;
use gpui::{
App, Bounds, Context, CursorStyle, Element, ElementId, ElementInputHandler, Entity,
EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, LayoutId, MouseButton,
MouseDownEvent, PaintQuad, Pixels, Point, ShapedLine, SharedString, Style, TextRun,
UTF16Selection, Window, actions, div, fill, hsla, point, prelude::*, px, relative, size, white,
};
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
actions!(
text_input,
[
Backspace,
Delete,
Left,
Right,
Home,
End,
Paste,
ShowCharacterPalette
]
);
pub fn bind_text_input_keys(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("backspace", Backspace, None),
KeyBinding::new("delete", Delete, None),
KeyBinding::new("left", Left, None),
KeyBinding::new("right", Right, None),
KeyBinding::new("home", Home, None),
KeyBinding::new("end", End, None),
KeyBinding::new("cmd-v", Paste, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
]);
}
pub struct TextInput {
focus_handle: FocusHandle,
content: SharedString,
placeholder: SharedString,
cursor: usize,
last_layout: Option<ShapedLine>,
last_bounds: Option<Bounds<Pixels>>,
}
impl TextInput {
pub fn new(cx: &mut App, placeholder: impl Into<SharedString>) -> Entity<Self> {
let placeholder = placeholder.into();
cx.new(|cx| Self {
focus_handle: cx.focus_handle(),
content: "".into(),
placeholder,
cursor: 0,
last_layout: None,
last_bounds: None,
})
}
pub fn value(&self) -> String {
self.content.to_string()
}
pub fn clear(&mut self) {
self.content = "".into();
self.cursor = 0;
self.last_layout = None;
self.last_bounds = None;
}
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
self.cursor = offset.min(self.content.len());
cx.notify();
}
fn previous_boundary(&self) -> usize {
self.content
.grapheme_indices(true)
.rev()
.find_map(|(index, _)| (index < self.cursor).then_some(index))
.unwrap_or(0)
}
fn next_boundary(&self) -> usize {
self.content
.grapheme_indices(true)
.find_map(|(index, _)| (index > self.cursor).then_some(index))
.unwrap_or(self.content.len())
}
fn offset_from_utf16(&self, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in self.content.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
fn offset_to_utf16(&self, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in self.content.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
else {
return 0;
};
if position.x <= bounds.left() {
return 0;
}
if position.x >= bounds.right() {
return self.content.len();
}
line.closest_index_for_x(position.x - bounds.left())
}
fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(self.previous_boundary(), cx);
}
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(self.next_boundary(), cx);
}
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(0, cx);
}
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(self.content.len(), cx);
}
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
let previous = self.previous_boundary();
if previous < self.cursor {
self.replace_text_in_range(
Some(self.offset_to_utf16(previous)..self.offset_to_utf16(self.cursor)),
"",
window,
cx,
);
}
}
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
let next = self.next_boundary();
if self.cursor < next {
self.replace_text_in_range(
Some(self.offset_to_utf16(self.cursor)..self.offset_to_utf16(next)),
"",
window,
cx,
);
}
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
self.replace_text_in_range(None, &text.replace('\n', " "), window, cx);
}
}
fn on_mouse_down(
&mut self,
event: &MouseDownEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.move_to(self.index_for_mouse_position(event.position), cx);
}
fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
window: &mut Window,
_: &mut Context<Self>,
) {
window.show_character_palette();
}
}
impl EntityInputHandler for TextInput {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
actual_range: &mut Option<Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<String> {
let range =
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end);
actual_range.replace(self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end));
Some(self.content[range].to_string())
}
fn selected_text_range(
&mut self,
_ignore_disabled_input: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<UTF16Selection> {
let cursor = self.offset_to_utf16(self.cursor);
Some(UTF16Selection {
range: cursor..cursor,
reversed: false,
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
None
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let range = range_utf16
.map(|range| self.offset_from_utf16(range.start)..self.offset_from_utf16(range.end))
.unwrap_or(self.cursor..self.cursor);
self.content = format!(
"{}{}{}",
&self.content[..range.start],
new_text,
&self.content[range.end..]
)
.into();
self.cursor = range.start + new_text.len();
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
_new_selected_range_utf16: Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.replace_text_in_range(range_utf16, new_text, window, cx);
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let layout = self.last_layout.as_ref()?;
let start = self.offset_from_utf16(range_utf16.start);
let end = self.offset_from_utf16(range_utf16.end);
Some(Bounds::from_corners(
point(bounds.left() + layout.x_for_index(start), bounds.top()),
point(bounds.left() + layout.x_for_index(end), bounds.bottom()),
))
}
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let layout = self.last_layout.as_ref()?;
let bounds = self.last_bounds.as_ref()?;
let index = layout.index_for_x(point.x - bounds.left())?;
Some(self.offset_to_utf16(index))
}
}
struct TextElement {
input: Entity<TextInput>,
}
struct PrepaintState {
line: ShapedLine,
cursor: PaintQuad,
}
impl IntoElement for TextElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = relative(1.0).into();
style.size.height = window.line_height().into();
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let input = self.input.read(cx);
let style = window.text_style();
let text = if input.content.is_empty() {
input.placeholder.clone()
} else {
input.content.clone()
};
let color = if input.content.is_empty() {
hsla(0.0, 0.0, 0.0, 0.35)
} else {
style.color
};
let run = TextRun {
len: text.len(),
font: style.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let font_size = style.font_size.to_pixels(window.rem_size());
let line = window
.text_system()
.shape_line(text, font_size, &[run], None);
let cursor_x = bounds.left() + line.x_for_index(input.cursor);
let cursor = fill(
Bounds::new(
point(cursor_x, bounds.top()),
size(px(2.0), bounds.bottom() - bounds.top()),
),
theme::primary_action(),
);
PrepaintState { line, cursor }
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let focus_handle = self.input.read(cx).focus_handle.clone();
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.input.clone()),
cx,
);
prepaint
.line
.paint(bounds.origin, window.line_height(), window, cx)
.ok();
if focus_handle.is_focused(window) {
window.paint_quad(prepaint.cursor.clone());
}
let line = prepaint.line.clone();
self.input.update(cx, |input, _| {
input.last_layout = Some(line);
input.last_bounds = Some(bounds);
});
}
}
impl Render for TextInput {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("TextInput")
.track_focus(&self.focus_handle(cx))
.cursor(CursorStyle::IBeam)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
.on_action(cx.listener(Self::left))
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::show_character_palette))
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.bg(theme::panel())
.border_1()
.border_color(theme::panel_border())
.rounded_md()
.px_3()
.py_2()
.line_height(px(24.0))
.text_size(px(18.0))
.child(
div()
.h(px(24.0))
.w_full()
.bg(white())
.child(TextElement { input: cx.entity() }),
)
}
}
impl Focusable for TextInput {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
@@ -1,10 +0,0 @@
[package]
name = "domain"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
@@ -1,139 +0,0 @@
#[derive(Clone, Copy, Debug)]
pub struct ConnectorSeed {
pub id: &'static str,
pub label: &'static str,
pub description: &'static str,
}
pub const CONNECTOR_SEEDS: &[ConnectorSeed] = &[
ConnectorSeed {
id: "claude-pro",
label: "Claude Pro",
description: "Anthropic OAuth session for Claude Pro.",
},
ConnectorSeed {
id: "openai-codex",
label: "OpenAI Codex",
description: "OpenAI OAuth session for Codex desktop flows.",
},
ConnectorSeed {
id: "github-copilot",
label: "GitHub Copilot",
description: "GitHub Copilot OAuth session.",
},
ConnectorSeed {
id: "gemini-cli",
label: "Gemini CLI",
description: "Google Gemini CLI OAuth session.",
},
ConnectorSeed {
id: "antigravity",
label: "Antigravity",
description: "Google Antigravity OAuth session.",
},
ConnectorSeed {
id: "anthropic",
label: "Anthropic",
description: "Anthropic API key connector.",
},
ConnectorSeed {
id: "openai",
label: "OpenAI",
description: "OpenAI API key connector.",
},
ConnectorSeed {
id: "google",
label: "Google",
description: "Google AI Studio API key connector.",
},
ConnectorSeed {
id: "mistral",
label: "Mistral",
description: "Mistral API key connector.",
},
ConnectorSeed {
id: "groq",
label: "Groq",
description: "Groq API key connector.",
},
ConnectorSeed {
id: "cerebras",
label: "Cerebras",
description: "Cerebras API key connector.",
},
ConnectorSeed {
id: "xai",
label: "xAI",
description: "xAI API key connector.",
},
ConnectorSeed {
id: "openrouter",
label: "OpenRouter",
description: "OpenRouter API key connector.",
},
ConnectorSeed {
id: "vercel-ai-gateway",
label: "Vercel AI Gateway",
description: "Vercel AI Gateway API key connector.",
},
ConnectorSeed {
id: "zai",
label: "Z AI",
description: "Z AI API key connector.",
},
ConnectorSeed {
id: "opencode",
label: "OpenCode",
description: "OpenCode API key connector.",
},
ConnectorSeed {
id: "opencode-go",
label: "OpenCode Go",
description: "OpenCode Go API key connector.",
},
ConnectorSeed {
id: "huggingface",
label: "Hugging Face",
description: "Hugging Face API key connector.",
},
ConnectorSeed {
id: "kimi-coding",
label: "Kimi Coding",
description: "Kimi Coding API key connector.",
},
ConnectorSeed {
id: "minimax",
label: "MiniMax",
description: "MiniMax global API connector.",
},
ConnectorSeed {
id: "minimax-cn",
label: "MiniMax CN",
description: "MiniMax China API connector.",
},
ConnectorSeed {
id: "fireworks",
label: "Fireworks",
description: "Fireworks API key connector.",
},
ConnectorSeed {
id: "fireworks-firepass",
label: "Fireworks Firepass",
description: "Fireworks Firepass API key connector.",
},
ConnectorSeed {
id: "amazon-bedrock",
label: "Amazon Bedrock",
description: "AWS Bedrock cloud connector.",
},
ConnectorSeed {
id: "azure-openai",
label: "Azure OpenAI",
description: "Azure OpenAI cloud connector.",
},
ConnectorSeed {
id: "google-vertex",
label: "Google Vertex",
description: "Google Vertex AI cloud connector.",
},
];
@@ -1,130 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SectionId {
#[default]
Overview,
Runs,
Inbox,
Messages,
Approvals,
Documents,
Workspaces,
Settings,
}
impl SectionId {
pub fn label(self) -> &'static str {
match self {
Self::Overview => "Dashboard",
Self::Runs => "Runs",
Self::Inbox => "Inbox",
Self::Messages => "Messages",
Self::Approvals => "Approvals",
Self::Documents => "Documents",
Self::Workspaces => "Workspaces",
Self::Settings => "Settings",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CompanyStatus {
#[default]
Active,
Paused,
Archived,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionMode {
#[default]
AiCeo,
AiCofounder,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceState {
Ready,
Busy,
Blocked,
Missing,
Error,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentStatus {
Idle,
Active,
Running,
Paused,
Error,
PendingApproval,
Terminated,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
Backlog,
Todo,
InProgress,
InReview,
Done,
Blocked,
Cancelled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalState {
Pending,
Approved,
Rejected,
RevisionRequested,
}
impl ApprovalState {
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Approved => "approved",
Self::Rejected => "rejected",
Self::RevisionRequested => "revision_requested",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageChannel {
Direct,
Company,
Department,
Project,
Incident,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DocumentStatus {
Draft,
InReview,
Approved,
Archived,
Superseded,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ThreadWaitingFor {
HumanApproval,
HumanReply,
ToolResult,
ExternalEvent,
}
@@ -1,9 +0,0 @@
pub mod catalog;
pub mod enums;
pub mod models;
pub mod snapshot;
pub use catalog::{CONNECTOR_SEEDS, ConnectorSeed};
pub use enums::*;
pub use models::*;
pub use snapshot::ProfileSnapshot;
@@ -1,236 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Company {
pub id: String,
pub name: String,
pub description: String,
pub status: String,
pub auto_approve_hires: bool,
pub review_deliverables: bool,
pub execution_mode: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Connector {
pub id: String,
pub label: String,
pub description: String,
pub status: String,
pub version: Option<String>,
pub auth_state: String,
pub last_checked_at: Option<String>,
pub last_error: Option<String>,
pub model: Option<String>,
pub env_binding_text: String,
pub notes: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Workspace {
pub id: String,
pub company_id: String,
pub name: String,
pub local_path: String,
pub repo_url: String,
pub repo_ref: String,
pub project_id: Option<String>,
pub state: String,
pub is_primary: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Agent {
pub id: String,
pub company_id: String,
pub name: String,
pub role: String,
pub title: String,
pub department: Option<String>,
pub status: String,
pub reports_to: Option<String>,
pub connector_id: String,
pub workspace_id: Option<String>,
pub model: Option<String>,
pub capabilities: String,
pub budget_monthly_usd: f64,
pub spent_monthly_usd: f64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Goal {
pub id: String,
pub company_id: String,
pub title: String,
pub description: String,
pub parent_id: Option<String>,
pub owner_agent_id: Option<String>,
pub status: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Project {
pub id: String,
pub company_id: String,
pub goal_id: Option<String>,
pub name: String,
pub description: String,
pub lead_agent_id: Option<String>,
pub status: String,
pub target_date: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub company_id: String,
pub project_id: Option<String>,
pub goal_id: Option<String>,
pub parent_id: Option<String>,
pub title: String,
pub description: String,
pub assignee_agent_id: Option<String>,
pub workspace_id: Option<String>,
pub status: String,
pub priority: String,
pub active_run_id: Option<String>,
pub cost_usd: f64,
pub task_type: Option<String>,
pub requires_user_review: bool,
pub started_at: Option<String>,
pub completed_at: Option<String>,
pub cancelled_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Approval {
pub id: String,
pub company_id: String,
pub related_task_id: Option<String>,
pub related_agent_id: Option<String>,
pub requested_by_agent_id: Option<String>,
pub approval_type: String,
pub payload_summary: String,
pub impact_summary: String,
pub payload_json: Option<String>,
pub decision_note: String,
pub state: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Run {
pub id: String,
pub company_id: String,
pub task_id: Option<String>,
pub agent_id: String,
pub workspace_id: Option<String>,
pub connector_id: String,
pub status: String,
pub summary: String,
pub error_message: Option<String>,
pub model: Option<String>,
pub cost_usd: Option<f64>,
pub started_at: Option<String>,
pub finished_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Activity {
pub id: String,
pub company_id: String,
pub actor: String,
pub action: String,
pub entity_type: String,
pub entity_id: String,
pub detail: String,
pub created_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct AgentMessage {
pub id: String,
pub company_id: String,
pub from_agent_id: String,
pub to_agent_id: Option<String>,
pub sender_kind: String,
pub channel: String,
pub channel_target_id: Option<String>,
pub subject: String,
pub body: String,
pub priority: String,
pub read_at: Option<String>,
pub parent_message_id: Option<String>,
pub attachments_json: String,
pub created_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Document {
pub id: String,
pub company_id: String,
pub document_type: String,
pub title: String,
pub content: String,
pub author_agent_id: Option<String>,
pub reviewer_agent_id: Option<String>,
pub project_id: Option<String>,
pub goal_id: Option<String>,
pub parent_doc_id: Option<String>,
pub version: i64,
pub status: String,
pub tags_json: String,
pub latest_revision_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RunEventRecord {
pub id: String,
pub company_id: String,
pub run_id: String,
pub event_kind: String,
pub title: String,
pub detail: String,
pub created_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ArtifactRecord {
pub id: String,
pub company_id: String,
pub run_id: String,
pub provider: String,
pub external_id: String,
pub title: String,
pub metadata_json: String,
pub created_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ThreadWaitState {
pub id: String,
pub thread_id: String,
pub company_id: String,
pub waiting_for: String,
pub related_approval_id: Option<String>,
pub reason: String,
pub created_at: String,
pub resolved_at: Option<String>,
}
@@ -1,73 +0,0 @@
use crate::{
Activity, Agent, AgentMessage, Approval, ArtifactRecord, Company, Connector, Document, Goal,
Project, Run, RunEventRecord, Task, Workspace,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ProfileSnapshot {
pub current_company_id: Option<String>,
pub profile_path: String,
pub theme: String,
pub locale: String,
pub companies: Vec<Company>,
pub connectors: Vec<Connector>,
pub workspaces: Vec<Workspace>,
pub agents: Vec<Agent>,
pub goals: Vec<Goal>,
pub projects: Vec<Project>,
pub tasks: Vec<Task>,
pub approvals: Vec<Approval>,
pub runs: Vec<Run>,
pub run_events: Vec<RunEventRecord>,
pub artifacts: Vec<ArtifactRecord>,
pub activity: Vec<Activity>,
pub agent_messages: Vec<AgentMessage>,
pub documents: Vec<Document>,
}
impl ProfileSnapshot {
pub fn current_company(&self) -> Option<&Company> {
self.current_company_id
.as_ref()
.and_then(|company_id| {
self.companies
.iter()
.find(|company| &company.id == company_id)
})
.or_else(|| self.companies.first())
}
pub fn unread_messages(&self) -> usize {
self.agent_messages
.iter()
.filter(|message| message.read_at.is_none())
.count()
}
pub fn pending_approvals(&self) -> usize {
self.approvals
.iter()
.filter(|approval| approval.state == "pending")
.count()
}
pub fn active_tasks(&self) -> usize {
self.tasks
.iter()
.filter(|task| matches!(task.status.as_str(), "todo" | "in_progress" | "in_review"))
.count()
}
pub fn active_runs(&self) -> usize {
self.runs
.iter()
.filter(|run| {
matches!(
run.status.as_str(),
"queued" | "running" | "waiting_approval" | "paused"
)
})
.count()
}
}
@@ -1,12 +0,0 @@
[package]
name = "pi_bridge"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
@@ -1,132 +0,0 @@
use crate::{ClientRequest, RuntimeEvent};
use anyhow::{Context, Result};
use std::{
io::{BufRead, BufReader, Write},
path::PathBuf,
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
};
pub struct PiBridgeWriter {
stdin: ChildStdin,
}
pub struct PiBridgeEventStream {
child: Child,
stdout: BufReader<ChildStdout>,
}
pub struct PiBridgeClient {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
}
impl PiBridgeClient {
pub fn spawn(command: PathBuf, args: &[String], working_dir: Option<PathBuf>) -> Result<Self> {
let (child, stdin, stdout) = spawn_process(command, args, working_dir)?;
Ok(Self {
child,
stdin,
stdout,
})
}
pub fn spawn_split(
command: PathBuf,
args: &[String],
working_dir: Option<PathBuf>,
) -> Result<(PiBridgeWriter, PiBridgeEventStream)> {
let (child, stdin, stdout) = spawn_process(command, args, working_dir)?;
Ok((
PiBridgeWriter { stdin },
PiBridgeEventStream { child, stdout },
))
}
pub fn send(&mut self, message: &ClientRequest) -> Result<()> {
write_request(&mut self.stdin, message)
}
pub fn read_event(&mut self) -> Result<RuntimeEvent> {
read_event(&mut self.stdout)
}
pub fn wait(&mut self) -> Result<()> {
self.child
.wait()
.context("failed while waiting for Pi Mono sidecar")?;
Ok(())
}
}
impl PiBridgeWriter {
pub fn send(&mut self, message: &ClientRequest) -> Result<()> {
write_request(&mut self.stdin, message)
}
}
impl PiBridgeEventStream {
pub fn read_event(&mut self) -> Result<RuntimeEvent> {
read_event(&mut self.stdout)
}
pub fn wait(&mut self) -> Result<()> {
self.child
.wait()
.context("failed while waiting for Pi Mono sidecar")?;
Ok(())
}
}
fn spawn_process(
command: PathBuf,
args: &[String],
working_dir: Option<PathBuf>,
) -> Result<(Child, ChildStdin, BufReader<ChildStdout>)> {
let mut process = Command::new(&command);
process
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
if let Some(dir) = working_dir {
process.current_dir(dir);
}
let mut child = process
.spawn()
.with_context(|| format!("failed to spawn Pi Mono sidecar at {}", command.display()))?;
let stdin = child
.stdin
.take()
.context("Pi Mono sidecar stdin is unavailable")?;
let stdout = child
.stdout
.take()
.context("Pi Mono sidecar stdout is unavailable")?;
Ok((child, stdin, BufReader::new(stdout)))
}
fn write_request(stdin: &mut ChildStdin, message: &ClientRequest) -> Result<()> {
let payload = serde_json::to_vec(message).context("failed to serialize Pi bridge request")?;
stdin
.write_all(&payload)
.context("failed to write Pi bridge request")?;
stdin
.write_all(b"\n")
.context("failed to terminate Pi bridge request")?;
stdin.flush().context("failed to flush Pi bridge request")?;
Ok(())
}
fn read_event(stdout: &mut BufReader<ChildStdout>) -> Result<RuntimeEvent> {
let mut line = String::new();
let bytes = stdout
.read_line(&mut line)
.context("failed to read Pi bridge event")?;
if bytes == 0 {
return Err(anyhow::anyhow!("Pi Mono sidecar closed stdout"));
}
serde_json::from_str(line.trim()).context("failed to decode Pi bridge event")
}
@@ -1,5 +0,0 @@
pub mod client;
pub mod protocol;
pub use client::{PiBridgeClient, PiBridgeEventStream, PiBridgeWriter};
pub use protocol::*;
@@ -1,98 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RuntimeHandshake {
pub protocol_version: u32,
pub app_name: String,
pub app_version: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RunBudget {
pub thinking_level: Option<String>,
pub token_budget: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunRequest {
pub run_id: String,
pub session_id: Option<String>,
pub company_id: String,
pub task_id: Option<String>,
pub agent_id: String,
pub workspace_path: Option<String>,
pub connector_id: String,
pub model: Option<String>,
pub prompt: String,
pub context_json: serde_json::Value,
pub budget: RunBudget,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolApprovalDecision {
pub run_id: String,
pub tool_call_id: String,
pub approved: bool,
pub note: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SteerRequest {
pub run_id: String,
pub body: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FollowUpRequest {
pub run_id: String,
pub body: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ClientRequest {
Handshake(RuntimeHandshake),
StartRun(Box<RunRequest>),
ToolApproval(ToolApprovalDecision),
Steer(SteerRequest),
FollowUp(FollowUpRequest),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEvent {
HandshakeAck {
protocol_version: u32,
runtime_version: String,
},
Message {
run_id: String,
role: String,
body: String,
},
ToolCall {
run_id: String,
tool_call_id: String,
tool_name: String,
input_json: serde_json::Value,
},
ToolResult {
run_id: String,
tool_call_id: String,
output_json: serde_json::Value,
},
Artifact {
run_id: String,
provider: String,
external_id: String,
title: String,
metadata_json: serde_json::Value,
},
RunFinished {
run_id: String,
status: String,
summary: String,
session_id: Option<String>,
cost_usd: Option<f64>,
},
}
@@ -1,13 +0,0 @@
[package]
name = "runtime"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
anyhow.workspace = true
pi_bridge.workspace = true
[dev-dependencies]
serde_json.workspace = true
@@ -1,3 +0,0 @@
pub mod service;
pub use service::{RuntimeConfig, RuntimeService};
@@ -1,51 +0,0 @@
use anyhow::Result;
use pi_bridge::{
ClientRequest, FollowUpRequest, PiBridgeClient, RunRequest, RuntimeEvent, RuntimeHandshake,
SteerRequest, ToolApprovalDecision,
};
use std::path::PathBuf;
#[derive(Clone, Debug, Default)]
pub struct RuntimeConfig {
pub command: PathBuf,
pub args: Vec<String>,
pub working_dir: Option<PathBuf>,
}
pub struct RuntimeService {
client: PiBridgeClient,
}
impl RuntimeService {
pub fn connect(config: RuntimeConfig) -> Result<Self> {
Ok(Self {
client: PiBridgeClient::spawn(config.command, &config.args, config.working_dir)?,
})
}
pub fn handshake(&mut self, message: RuntimeHandshake) -> Result<RuntimeEvent> {
self.client.send(&ClientRequest::Handshake(message))?;
self.client.read_event()
}
pub fn start_run(&mut self, request: RunRequest) -> Result<()> {
self.client
.send(&ClientRequest::StartRun(Box::new(request)))
}
pub fn decide_tool_call(&mut self, decision: ToolApprovalDecision) -> Result<()> {
self.client.send(&ClientRequest::ToolApproval(decision))
}
pub fn steer(&mut self, request: SteerRequest) -> Result<()> {
self.client.send(&ClientRequest::Steer(request))
}
pub fn follow_up(&mut self, request: FollowUpRequest) -> Result<()> {
self.client.send(&ClientRequest::FollowUp(request))
}
pub fn next_event(&mut self) -> Result<RuntimeEvent> {
self.client.read_event()
}
}
@@ -1,170 +0,0 @@
use pi_bridge::{
FollowUpRequest, RunBudget, RunRequest, RuntimeEvent, RuntimeHandshake, SteerRequest,
ToolApprovalDecision,
};
use runtime::{RuntimeConfig, RuntimeService};
use std::path::PathBuf;
fn fake_sidecar_script() -> &'static str {
r#"
while IFS= read -r line; do
case "$line" in
*'"kind":"handshake"'*)
printf '%s\n' '{"kind":"handshake_ack","protocol_version":1,"runtime_version":"pi-mono-test"}'
;;
*'"kind":"start_run"'*)
printf '%s\n' '{"kind":"message","run_id":"run-1","role":"assistant","body":"Run queued"}'
printf '%s\n' '{"kind":"tool_call","run_id":"run-1","tool_call_id":"tool-1","tool_name":"write_file","input_json":{"path":"README.md"}}'
;;
*'"kind":"tool_approval"'*)
printf '%s\n' '{"kind":"tool_result","run_id":"run-1","tool_call_id":"tool-1","output_json":{"status":"approved"}}'
printf '%s\n' '{"kind":"run_finished","run_id":"run-1","status":"completed","summary":"Run complete","session_id":"session-1","cost_usd":1.25}'
;;
*'"kind":"steer"'*)
printf '%s\n' '{"kind":"message","run_id":"run-1","role":"system","body":"Steer accepted"}'
;;
*'"kind":"follow_up"'*)
printf '%s\n' '{"kind":"message","run_id":"run-1","role":"system","body":"Follow-up accepted"}'
;;
esac
done
"#
}
#[test]
fn runtime_service_round_trips_sidecar_contract() {
let mut service = RuntimeService::connect(RuntimeConfig {
command: PathBuf::from("/bin/sh"),
args: vec!["-c".to_string(), fake_sidecar_script().to_string()],
working_dir: None,
})
.expect("connect runtime");
let handshake = service
.handshake(RuntimeHandshake {
protocol_version: 1,
app_name: "AgentCompany-Rust".to_string(),
app_version: "0.1.0".to_string(),
})
.expect("handshake");
match handshake {
RuntimeEvent::HandshakeAck {
protocol_version,
runtime_version,
} => {
assert_eq!(protocol_version, 1);
assert_eq!(runtime_version, "pi-mono-test");
}
other => panic!("unexpected handshake event: {other:?}"),
}
service
.start_run(RunRequest {
run_id: "run-1".to_string(),
session_id: Some("session-1".to_string()),
company_id: "company-1".to_string(),
task_id: Some("task-1".to_string()),
agent_id: "agent-1".to_string(),
workspace_path: Some("/tmp/agentcompany".to_string()),
connector_id: "anthropic".to_string(),
model: Some("claude-sonnet".to_string()),
prompt: "Draft the weekly status update".to_string(),
context_json: serde_json::json!({ "thread_id": "thread-1" }),
budget: RunBudget {
thinking_level: Some("high".to_string()),
token_budget: Some(4096),
},
})
.expect("start run");
let first_event = service.next_event().expect("first event");
match first_event {
RuntimeEvent::Message { run_id, role, body } => {
assert_eq!(run_id, "run-1");
assert_eq!(role, "assistant");
assert_eq!(body, "Run queued");
}
other => panic!("unexpected first event: {other:?}"),
}
let second_event = service.next_event().expect("second event");
match second_event {
RuntimeEvent::ToolCall {
run_id,
tool_call_id,
tool_name,
input_json,
} => {
assert_eq!(run_id, "run-1");
assert_eq!(tool_call_id, "tool-1");
assert_eq!(tool_name, "write_file");
assert_eq!(input_json["path"], "README.md");
}
other => panic!("unexpected second event: {other:?}"),
}
service
.decide_tool_call(ToolApprovalDecision {
run_id: "run-1".to_string(),
tool_call_id: "tool-1".to_string(),
approved: true,
note: "Approved from test.".to_string(),
})
.expect("tool approval");
let third_event = service.next_event().expect("third event");
match third_event {
RuntimeEvent::ToolResult {
run_id,
tool_call_id,
output_json,
} => {
assert_eq!(run_id, "run-1");
assert_eq!(tool_call_id, "tool-1");
assert_eq!(output_json["status"], "approved");
}
other => panic!("unexpected third event: {other:?}"),
}
let fourth_event = service.next_event().expect("fourth event");
match fourth_event {
RuntimeEvent::RunFinished {
run_id,
status,
summary,
session_id,
cost_usd,
} => {
assert_eq!(run_id, "run-1");
assert_eq!(status, "completed");
assert_eq!(summary, "Run complete");
assert_eq!(session_id.as_deref(), Some("session-1"));
assert_eq!(cost_usd, Some(1.25));
}
other => panic!("unexpected fourth event: {other:?}"),
}
service
.steer(SteerRequest {
run_id: "run-1".to_string(),
body: "Tighten the summary.".to_string(),
})
.expect("steer");
let steer_event = service.next_event().expect("steer event");
match steer_event {
RuntimeEvent::Message { body, .. } => assert_eq!(body, "Steer accepted"),
other => panic!("unexpected steer event: {other:?}"),
}
service
.follow_up(FollowUpRequest {
run_id: "run-1".to_string(),
body: "Add customer risk notes.".to_string(),
})
.expect("follow up");
let follow_up_event = service.next_event().expect("follow up event");
match follow_up_event {
RuntimeEvent::Message { body, .. } => assert_eq!(body, "Follow-up accepted"),
other => panic!("unexpected follow-up event: {other:?}"),
}
}
@@ -1,18 +0,0 @@
[package]
name = "storage"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
anyhow.workspace = true
domain.workspace = true
pi_bridge.workspace = true
rusqlite.workspace = true
serde_json.workspace = true
support.workspace = true
uuid.workspace = true
[dev-dependencies]
tempfile.workspace = true
@@ -1,6 +0,0 @@
mod schema;
mod store;
pub use store::{
NewCompanyInput, OnboardingBootstrapInput, OnboardingBootstrapSummary, ProfileStore,
};
@@ -1,368 +0,0 @@
use anyhow::Result;
use domain::CONNECTOR_SEEDS;
use rusqlite::{Connection, params};
use support::time::now_iso;
pub fn initialize(conn: &Connection) -> Result<()> {
conn.execute_batch(
r#"
create table if not exists settings (
key text primary key,
value text not null
);
create table if not exists companies (
id text primary key,
name text not null,
description text not null default '',
status text not null default 'active',
auto_approve_hires integer not null default 0,
review_deliverables integer not null default 0,
execution_mode text not null default 'ai_ceo',
created_at text not null,
updated_at text not null
);
create table if not exists connectors (
id text primary key,
label text not null,
description text not null,
status text not null default 'not_installed',
command text not null default '',
version text null,
auth_state text not null default 'unknown',
last_checked_at text null,
last_error text null,
model text null,
env_binding_text text not null default '',
notes text not null default '',
updated_at text not null
);
create table if not exists workspaces (
id text primary key,
company_id text not null,
name text not null,
local_path text not null,
repo_url text not null default '',
repo_ref text not null default '',
project_id text null,
state text not null default 'ready',
is_primary integer not null default 0,
lock_holder_run_id text null,
lock_acquired_at text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists agents (
id text primary key,
company_id text not null,
name text not null,
role text not null default '',
title text not null default '',
department text null,
status text not null default 'idle',
reports_to text null,
connector_id text not null default 'anthropic',
workspace_id text null,
model text null,
capabilities text not null default '[]',
budget_monthly_usd real not null default 0,
spent_monthly_usd real not null default 0,
metadata_json text not null default '{}',
heartbeat_enabled integer not null default 0,
heartbeat_interval_sec integer not null default 0,
last_heartbeat_at text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists goals (
id text primary key,
company_id text not null,
title text not null,
description text not null default '',
parent_id text null,
owner_agent_id text null,
status text not null default 'planned',
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists projects (
id text primary key,
company_id text not null,
goal_id text null,
name text not null,
description text not null default '',
lead_agent_id text null,
status text not null default 'planned',
target_date text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists tasks (
id text primary key,
company_id text not null,
project_id text null,
goal_id text null,
parent_id text null,
title text not null,
description text not null default '',
assignee_agent_id text null,
workspace_id text null,
status text not null default 'todo',
priority text not null default 'medium',
active_run_id text null,
cost_usd real not null default 0,
task_type text null,
requires_user_review integer not null default 0,
started_at text null,
completed_at text null,
cancelled_at text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists approvals (
id text primary key,
company_id text not null,
related_task_id text null,
requested_by_agent_id text null,
related_agent_id text null,
type text not null,
payload_summary text not null default '',
impact_summary text not null default '',
payload_json text null,
decision_note text not null default '',
state text not null default 'pending',
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists runs (
id text primary key,
company_id text not null,
task_id text null,
agent_id text not null,
workspace_id text null,
connector_id text not null,
status text not null default 'queued',
summary text not null default '',
error_message text null,
exit_code integer null,
signal text null,
model text null,
cost_usd real null,
unattributed_cost integer not null default 0,
started_at text null,
finished_at text null,
log_path text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists activity (
id text primary key,
company_id text not null,
actor text not null,
action text not null,
entity_type text not null,
entity_id text not null,
detail text not null,
created_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists run_events (
id text primary key,
company_id text not null,
run_id text not null,
event_kind text not null,
title text not null,
detail text not null default '',
created_at text not null,
foreign key(company_id) references companies(id) on delete cascade,
foreign key(run_id) references runs(id) on delete cascade
);
create table if not exists artifacts (
id text primary key,
company_id text not null,
run_id text not null,
provider text not null,
external_id text not null,
title text not null,
metadata_json text not null default '{}',
created_at text not null,
foreign key(company_id) references companies(id) on delete cascade,
foreign key(run_id) references runs(id) on delete cascade
);
create table if not exists agent_messages (
id text primary key,
company_id text not null,
from_agent_id text not null,
to_agent_id text null,
sender_kind text not null default 'human',
channel text not null default 'company',
channel_target_id text null,
subject text not null,
body text not null,
priority text not null default 'normal',
read_at text null,
parent_message_id text null,
attachments_json text not null default '[]',
created_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists agent_message_reads (
message_id text not null,
agent_id text not null,
read_at text not null,
primary key(message_id, agent_id)
);
create table if not exists documents (
id text primary key,
company_id text not null,
type text not null,
title text not null,
content text not null default '',
author_agent_id text null,
reviewer_agent_id text null,
project_id text null,
goal_id text null,
parent_doc_id text null,
version integer not null default 1,
status text not null default 'draft',
tags_json text not null default '[]',
latest_revision_id text null,
source_run_id text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists document_revisions (
id text primary key,
company_id text not null,
document_id text not null,
revision_number integer not null,
body text not null,
change_summary text null,
created_by_agent_id text null,
created_by_user_id text null,
created_at text not null,
foreign key(company_id) references companies(id) on delete cascade,
foreign key(document_id) references documents(id) on delete cascade
);
create table if not exists conversation_threads (
id text primary key,
company_id text not null,
title text not null,
type text not null,
related_entity_type text null,
related_entity_id text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists conversation_events (
id text primary key,
thread_id text not null,
actor_id text not null,
actor_type text not null,
event_type text not null,
body text not null,
metadata_json text not null default '{}',
created_at text not null,
foreign key(thread_id) references conversation_threads(id) on delete cascade
);
create table if not exists conversation_participants (
thread_id text not null,
participant_id text not null,
participant_type text not null,
role text not null default 'member',
joined_at text not null,
primary key(thread_id, participant_id)
);
create table if not exists thread_wait_states (
id text primary key,
thread_id text not null,
company_id text not null,
waiting_for text not null,
related_approval_id text null,
reason text not null,
created_at text not null,
resolved_at text null,
foreign key(thread_id) references conversation_threads(id) on delete cascade,
foreign key(company_id) references companies(id) on delete cascade
);
create table if not exists work_products (
id text primary key,
company_id text not null,
project_id text null,
task_id text null,
type text not null,
provider text not null,
external_id text null,
title text not null,
url text null,
status text not null default 'active',
review_state text not null default 'none',
is_primary integer not null default 0,
summary text null,
metadata_json text null,
created_by_run_id text null,
created_at text not null,
updated_at text not null,
foreign key(company_id) references companies(id) on delete cascade
);
"#,
)?;
seed_connectors(conn)?;
Ok(())
}
fn seed_connectors(conn: &Connection) -> Result<()> {
let updated_at = now_iso()?;
let mut statement = conn.prepare(
r#"
insert into connectors (
id, label, description, status, command, version, auth_state,
last_checked_at, last_error, model, env_binding_text, notes, updated_at
)
values (?1, ?2, ?3, 'not_installed', '', null, 'unknown', null, null, null, '', '', ?4)
on conflict(id) do update set
label = excluded.label,
description = excluded.description,
updated_at = excluded.updated_at
"#,
)?;
for connector in CONNECTOR_SEEDS {
statement.execute(params![
connector.id,
connector.label,
connector.description,
updated_at
])?;
}
Ok(())
}
File diff suppressed because it is too large. Load diff
@@ -1,69 +0,0 @@
use storage::{NewCompanyInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn inbox_attention_derivation_round_trips_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 Inbox".to_string(),
description: "Verifies native inbox derivation".to_string(),
workspace_path: "/tmp/orbit-inbox".to_string(),
execution_mode: "ai_ceo".to_string(),
})
.expect("create company");
let ts = support::time::now_iso().expect("timestamp");
let approval_id = uuid::Uuid::new_v4().to_string();
let run_id = uuid::Uuid::new_v4().to_string();
let conn = rusqlite::Connection::open(store.db_path()).expect("open raw db");
conn.execute(
"insert into approvals (id, company_id, related_task_id, requested_by_agent_id, related_agent_id, type, payload_summary, impact_summary, payload_json, decision_note, state, created_at, updated_at) values (?1, ?2, null, null, null, 'runtime_tool_call', 'Review shell command', 'Blocks workspace write', null, '', 'pending', ?3, ?3)",
rusqlite::params![approval_id, company_id, ts],
)
.expect("insert approval");
conn.execute(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, error_message, model, cost_usd, started_at, finished_at, created_at, updated_at) values (?1, ?2, null, '__board__', null, 'anthropic', 'failed', 'Workspace write failed', 'permission denied', 'claude-sonnet', 0.5, ?3, ?3, ?3, ?3)",
rusqlite::params![run_id, company_id, ts],
)
.expect("insert failed run");
let message_id = store
.send_board_message(
&company_id,
"Board update: human review is required before retry.",
)
.expect("send board message");
let snapshot = store.snapshot().expect("snapshot");
assert_eq!(snapshot.pending_approvals(), 1);
assert_eq!(snapshot.unread_messages(), 1);
assert_eq!(snapshot.active_runs(), 0);
assert!(
snapshot
.runs
.iter()
.any(|run| run.id == run_id && run.status == "failed")
);
assert!(
snapshot
.agent_messages
.iter()
.any(|message| message.id == message_id && message.read_at.is_none())
);
store
.mark_message_read(&company_id, &message_id)
.expect("mark message read");
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("reopened snapshot");
assert_eq!(reopened_snapshot.pending_approvals(), 1);
assert_eq!(reopened_snapshot.unread_messages(), 0);
assert!(
reopened_snapshot.runs.iter().any(
|run| run.id == run_id && run.error_message.as_deref() == Some("permission denied")
)
);
}
@@ -1,62 +0,0 @@
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)
);
}
@@ -1,82 +0,0 @@
use storage::{NewCompanyInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn native_surface_snapshot_round_trips_reviews_runs_and_documents() {
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 Ops".to_string(),
description: "Local service operator".to_string(),
workspace_path: "/tmp/orbit-ops".to_string(),
execution_mode: "ai_ceo".to_string(),
})
.expect("create company");
let ts = support::time::now_iso().expect("timestamp");
let task_id = uuid::Uuid::new_v4().to_string();
let approval_id = uuid::Uuid::new_v4().to_string();
let run_id = uuid::Uuid::new_v4().to_string();
let conn = rusqlite::Connection::open(store.db_path()).expect("open raw db");
conn.execute(
"insert into tasks (id, company_id, project_id, goal_id, parent_id, title, description, assignee_agent_id, workspace_id, status, priority, active_run_id, cost_usd, task_type, requires_user_review, started_at, completed_at, cancelled_at, created_at, updated_at) values (?1, ?2, null, null, null, 'Operationalize launch plan', 'Awaiting review from board', null, null, 'in_review', 'high', ?3, 0, 'review', 1, ?4, null, null, ?4, ?4)",
rusqlite::params![task_id, company_id, run_id, ts],
)
.expect("insert review task");
conn.execute(
"insert into approvals (id, company_id, related_task_id, requested_by_agent_id, related_agent_id, type, payload_summary, impact_summary, payload_json, decision_note, state, created_at, updated_at) values (?1, ?2, ?3, null, null, 'plan_review', 'Review launch plan', 'Board decision blocks execution', null, '', 'pending', ?4, ?4)",
rusqlite::params![approval_id, company_id, task_id, ts],
)
.expect("insert pending approval");
conn.execute(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, error_message, model, cost_usd, started_at, finished_at, created_at, updated_at) values (?1, ?2, ?3, '__board__', null, 'anthropic', 'running', 'Executing launch checklist', null, 'claude-sonnet', 1.5, ?4, null, ?4, ?4)",
rusqlite::params![run_id, company_id, task_id, ts],
)
.expect("insert running run");
let document_id = store
.create_document(
&company_id,
"Launch brief",
"Board-ready operating brief for the native shell.",
)
.expect("create document");
let snapshot = store.snapshot().expect("snapshot");
assert_eq!(
snapshot.current_company_id.as_deref(),
Some(company_id.as_str())
);
assert_eq!(snapshot.pending_approvals(), 1);
assert_eq!(snapshot.active_runs(), 1);
assert_eq!(snapshot.documents.len(), 1);
assert_eq!(snapshot.documents[0].id, document_id);
assert!(snapshot.tasks.iter().any(|task| task.id == task_id));
assert!(
snapshot
.approvals
.iter()
.any(|approval| approval.id == approval_id)
);
assert!(snapshot.runs.iter().any(|run| run.id == run_id));
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("reopened snapshot");
assert_eq!(reopened_snapshot.pending_approvals(), 1);
assert_eq!(reopened_snapshot.active_runs(), 1);
assert!(
reopened_snapshot
.tasks
.iter()
.any(|task| task.id == task_id)
);
assert!(
reopened_snapshot
.documents
.iter()
.any(|document| document.id == document_id)
);
}
@@ -1,81 +0,0 @@
use storage::{OnboardingBootstrapInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn onboarding_bootstrap_creates_company_goal_agent_project_task_and_run() {
let sandbox = tempdir().expect("temp dir");
let mut store = ProfileStore::open(sandbox.path()).expect("open store");
let summary = store
.bootstrap_onboarding(OnboardingBootstrapInput {
company_name: "Orbit Ops".to_string(),
company_description: "Local-first operations company".to_string(),
workspace_path: "/tmp/orbit-ops".to_string(),
execution_mode: "ai_ceo".to_string(),
goal_title: "Ship the native shell".to_string(),
goal_description:
"Rebuild the product in Rust and GPUI without losing product grammar.".to_string(),
connector_id: "anthropic".to_string(),
connector_model: Some("claude-sonnet".to_string()),
})
.expect("bootstrap onboarding");
let snapshot = store.snapshot().expect("snapshot");
assert_eq!(
snapshot.current_company_id.as_deref(),
Some(summary.company_id.as_str())
);
assert!(
snapshot
.companies
.iter()
.any(|company| company.id == summary.company_id && company.name == "Orbit Ops")
);
assert!(
snapshot
.workspaces
.iter()
.any(|workspace| Some(workspace.id.as_str()) == summary.workspace_id.as_deref())
);
assert!(
snapshot
.goals
.iter()
.any(|goal| goal.title == "Ship the native shell" && goal.status == "active")
);
assert!(snapshot.agents.iter().any(|agent| {
agent.id == summary.lead_agent_id
&& agent.name == "CEO"
&& agent.connector_id == "anthropic"
&& agent.model.as_deref() == Some("claude-sonnet")
}));
assert!(
snapshot
.projects
.iter()
.any(|project| project.id == summary.project_id && project.status == "in_progress")
);
assert!(snapshot.tasks.iter().any(|task| {
task.id == summary.task_id
&& task.active_run_id.as_deref() == Some(summary.run_id.as_str())
&& task.assignee_agent_id.as_deref() == Some(summary.lead_agent_id.as_str())
}));
assert!(snapshot.runs.iter().any(|run| {
run.id == summary.run_id
&& run.agent_id == summary.lead_agent_id
&& run.status == "queued"
&& run.task_id.as_deref() == Some(summary.task_id.as_str())
}));
assert!(snapshot.activity.iter().any(|activity| {
activity.entity_id == summary.company_id && activity.action == "company.bootstrapped"
}));
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("reopened snapshot");
assert!(
reopened_snapshot
.runs
.iter()
.any(|run| run.id == summary.run_id && run.status == "queued")
);
}
@@ -1,81 +0,0 @@
use domain::ApprovalState;
use storage::{NewCompanyInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn profile_store_bootstraps_and_round_trips_core_flows() {
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 Lab".to_string(),
description: "Semantic communication thesis program".to_string(),
workspace_path: "/tmp/orbit-lab".to_string(),
execution_mode: "ai_ceo".to_string(),
})
.expect("create company");
let snapshot = store.snapshot().expect("snapshot");
assert_eq!(snapshot.companies.len(), 1);
assert_eq!(snapshot.workspaces.len(), 1);
assert_eq!(
snapshot.current_company_id.as_deref(),
Some(company_id.as_str())
);
assert_eq!(snapshot.connectors.len(), 26);
let approval_id = uuid::Uuid::new_v4().to_string();
let ts = support::time::now_iso().expect("timestamp");
let conn = rusqlite::Connection::open(store.db_path()).expect("open raw db");
conn.execute(
"insert into approvals (id, company_id, related_task_id, requested_by_agent_id, related_agent_id, type, payload_summary, impact_summary, payload_json, decision_note, state, created_at, updated_at) values (?1, ?2, null, null, null, 'strategy_review', 'Review launch plan', 'Affects milestone timing', null, '', 'pending', ?3, ?3)",
rusqlite::params![approval_id, company_id, ts],
)
.expect("insert approval");
store
.decide_approval(
&company_id,
&approval_id,
ApprovalState::Approved,
"Ship the native shell.",
)
.expect("decide approval");
let message_id = store
.send_board_message(&company_id, "Board update: thesis plan is in review.")
.expect("send message");
let document_id = store
.create_document(
&company_id,
"Weekly status",
"Native Rust shell connected to the real profile.",
)
.expect("create document");
let after = store.snapshot().expect("snapshot after writes");
assert_eq!(after.pending_approvals(), 0);
assert_eq!(after.agent_messages.len(), 1);
assert_eq!(after.unread_messages(), 1);
assert_eq!(after.documents.len(), 1);
assert_eq!(after.documents[0].id, document_id);
assert!(!after.activity.is_empty());
store
.mark_message_read(&company_id, &message_id)
.expect("mark message read");
let after_read = store.snapshot().expect("snapshot after read update");
assert_eq!(after_read.unread_messages(), 0);
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("snapshot after reopen");
assert_eq!(
reopened_snapshot.current_company_id.as_deref(),
Some(company_id.as_str())
);
assert_eq!(reopened_snapshot.pending_approvals(), 0);
assert_eq!(reopened_snapshot.unread_messages(), 0);
assert_eq!(reopened_snapshot.documents.len(), 1);
assert_eq!(reopened_snapshot.documents[0].id, document_id);
}
@@ -1,126 +0,0 @@
use pi_bridge::RuntimeEvent;
use storage::{NewCompanyInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn runtime_contract_entities_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 Runtime".to_string(),
description: "Runtime contract verification".to_string(),
workspace_path: "/tmp/orbit-runtime".to_string(),
execution_mode: "ai_ceo".to_string(),
})
.expect("create company");
let ts = support::time::now_iso().expect("timestamp");
let run_id = uuid::Uuid::new_v4().to_string();
let conn = rusqlite::Connection::open(store.db_path()).expect("open raw db");
conn.execute(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, error_message, model, cost_usd, started_at, finished_at, created_at, updated_at) values (?1, ?2, null, '__board__', null, 'anthropic', 'running', 'Runtime active', null, 'claude-sonnet', 0.25, ?3, null, ?3, ?3)",
rusqlite::params![run_id, company_id, ts],
)
.expect("insert run");
let event_id = store
.append_run_event(
&company_id,
&run_id,
"tool_call",
"write_file",
"Requested README update",
)
.expect("append run event");
let artifact_id = store
.register_artifact(
&company_id,
&run_id,
"filesystem",
"/tmp/orbit-runtime/README.md",
"README update",
"{\"path\":\"/tmp/orbit-runtime/README.md\"}",
)
.expect("register artifact");
store
.ingest_runtime_event(RuntimeEvent::Message {
run_id: run_id.clone(),
role: "assistant".to_string(),
body: "Draft ready".to_string(),
})
.expect("ingest message event");
store
.ingest_runtime_event(RuntimeEvent::Artifact {
run_id: run_id.clone(),
provider: "filesystem".to_string(),
external_id: "/tmp/orbit-runtime/output.txt".to_string(),
title: "output.txt".to_string(),
metadata_json: serde_json::json!({"path": "/tmp/orbit-runtime/output.txt"}),
})
.expect("ingest artifact event");
store
.ingest_runtime_event(RuntimeEvent::RunFinished {
run_id: run_id.clone(),
status: "completed".to_string(),
summary: "Run complete".to_string(),
session_id: Some("session-1".to_string()),
cost_usd: Some(1.25),
})
.expect("ingest run finished event");
let snapshot = store.snapshot().expect("snapshot");
assert!(snapshot.run_events.iter().any(|event| {
event.id == event_id
&& event.run_id == run_id
&& event.event_kind == "tool_call"
&& event.title == "write_file"
}));
assert!(snapshot.artifacts.iter().any(|artifact| {
artifact.id == artifact_id
&& artifact.run_id == run_id
&& artifact.provider == "filesystem"
&& artifact.title == "README update"
}));
assert!(snapshot.run_events.iter().any(|event| {
event.run_id == run_id && event.event_kind == "message" && event.detail == "Draft ready"
}));
assert!(
snapshot
.artifacts
.iter()
.any(|artifact| { artifact.run_id == run_id && artifact.title == "output.txt" })
);
let run = snapshot
.runs
.iter()
.find(|run| run.id == run_id)
.expect("run present");
assert_eq!(run.status, "completed");
assert_eq!(run.summary, "Run complete");
assert_eq!(run.cost_usd, Some(1.25));
assert!(run.finished_at.is_some());
let reopened = ProfileStore::open(sandbox.path()).expect("reopen store");
let reopened_snapshot = reopened.snapshot().expect("reopened snapshot");
assert!(
reopened_snapshot
.run_events
.iter()
.any(|event| event.id == event_id)
);
assert!(
reopened_snapshot
.artifacts
.iter()
.any(|artifact| artifact.id == artifact_id)
);
assert!(
reopened_snapshot
.runs
.iter()
.any(|run| run.id == run_id && run.status == "completed")
);
}
@@ -1,121 +0,0 @@
use pi_bridge::RuntimeEvent;
use storage::{NewCompanyInput, ProfileStore};
use tempfile::tempdir;
#[test]
fn live_runtime_events_drive_approvals_messages_and_run_state() {
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 Live Runtime".to_string(),
description: "Verifies live runtime wiring".to_string(),
workspace_path: "/tmp/orbit-live-runtime".to_string(),
execution_mode: "ai_ceo".to_string(),
})
.expect("create company");
let ts = support::time::now_iso().expect("timestamp");
let task_id = uuid::Uuid::new_v4().to_string();
let run_id = uuid::Uuid::new_v4().to_string();
let agent_id = uuid::Uuid::new_v4().to_string();
let 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, 'Primary Workspace', '/tmp/orbit-live-runtime', '', '', null, 'ready', 1, ?3, ?3)",
rusqlite::params![workspace_id, company_id, ts],
)
.expect("insert workspace");
conn.execute(
"insert into agents (id, company_id, name, role, title, department, status, reports_to, connector_id, workspace_id, model, capabilities, budget_monthly_usd, spent_monthly_usd, created_at, updated_at) values (?1, ?2, 'Alex Runtime', 'engineer', 'Runtime Engineer', null, 'idle', null, 'anthropic', ?3, 'claude-sonnet', '[]', 0, 0, ?4, ?4)",
rusqlite::params![agent_id, company_id, workspace_id, ts],
)
.expect("insert agent");
conn.execute(
"insert into tasks (id, company_id, project_id, goal_id, parent_id, title, description, assignee_agent_id, workspace_id, status, priority, active_run_id, cost_usd, task_type, requires_user_review, started_at, completed_at, cancelled_at, created_at, updated_at) values (?1, ?2, null, null, null, 'Ship runtime rewrite', 'Keep the native app in sync with Pi Mono.', ?3, ?4, 'todo', 'high', null, 0, 'execution', 1, null, null, null, ?5, ?5)",
rusqlite::params![task_id, company_id, agent_id, workspace_id, ts],
)
.expect("insert task");
conn.execute(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, error_message, model, cost_usd, started_at, finished_at, created_at, updated_at) values (?1, ?2, ?3, ?4, ?5, 'anthropic', 'queued', 'Queued from native UI', null, 'claude-sonnet', null, null, null, ?6, ?6)",
rusqlite::params![run_id, company_id, task_id, agent_id, workspace_id, ts],
)
.expect("insert run");
store.mark_run_started(&run_id).expect("mark run started");
store
.ingest_runtime_event(RuntimeEvent::ToolCall {
run_id: run_id.clone(),
tool_call_id: "tool-1".to_string(),
tool_name: "write_file".to_string(),
input_json: serde_json::json!({"path": "README.md"}),
})
.expect("ingest tool call");
store
.ingest_runtime_event(RuntimeEvent::Message {
run_id: run_id.clone(),
role: "assistant".to_string(),
body: "Draft ready for review".to_string(),
})
.expect("ingest message");
store
.ingest_runtime_event(RuntimeEvent::RunFinished {
run_id: run_id.clone(),
status: "completed".to_string(),
summary: "Native runtime finished cleanly".to_string(),
session_id: Some(run_id.clone()),
cost_usd: Some(2.5),
})
.expect("ingest finished event");
let snapshot = store.snapshot().expect("snapshot");
let task = snapshot
.tasks
.iter()
.find(|task| task.id == task_id)
.expect("task present");
assert_eq!(task.active_run_id, None);
let approval = snapshot
.approvals
.iter()
.find(|approval| approval.approval_type == "runtime_tool_call")
.expect("runtime approval present");
assert_eq!(approval.related_task_id.as_deref(), Some(task_id.as_str()));
assert_eq!(
approval.related_agent_id.as_deref(),
Some(agent_id.as_str())
);
assert_eq!(approval.payload_summary, "write_file");
assert!(
approval
.payload_json
.as_deref()
.expect("payload json")
.contains("tool-1")
);
let runtime_message = snapshot
.agent_messages
.iter()
.find(|message| {
message.channel == "run"
&& message.channel_target_id.as_deref() == Some(run_id.as_str())
})
.expect("runtime message projected");
assert_eq!(runtime_message.from_agent_id, agent_id);
assert_eq!(runtime_message.body, "Draft ready for review");
let run = snapshot
.runs
.iter()
.find(|run| run.id == run_id)
.expect("run present");
assert_eq!(run.status, "completed");
assert_eq!(run.summary, "Native runtime finished cleanly");
assert_eq!(run.cost_usd, Some(2.5));
assert!(run.started_at.is_some());
assert!(run.finished_at.is_some());
}
@@ -1,11 +0,0 @@
[package]
name = "support"
edition.workspace = true
license.workspace = true
publish.workspace = true
version.workspace = true
[dependencies]
anyhow.workspace = true
dirs.workspace = true
time.workspace = true
@@ -1,2 +0,0 @@
pub mod paths;
pub mod time;
@@ -1,79 +0,0 @@
use anyhow::{Result, anyhow};
use std::{
env, fs,
path::{Path, PathBuf},
};
pub const APP_NAME: &str = "AgentCompanyNative";
pub const PROFILE_DB_FILE_NAME: &str = "agentcompany-native-v1.sqlite3";
pub const PROFILE_ENV: &str = "AGENTCOMPANY_NATIVE_PROFILE_DIR";
pub const RUNTIME_COMMAND_ENV: &str = "AGENTCOMPANY_NATIVE_PI_COMMAND";
pub const RUNTIME_ARGS_ENV: &str = "AGENTCOMPANY_NATIVE_PI_ARGS_JSON";
pub const RUNTIME_WORKDIR_ENV: &str = "AGENTCOMPANY_NATIVE_PI_WORKDIR";
pub fn default_profile_dir() -> Result<PathBuf> {
dirs::data_dir()
.map(|path| path.join(APP_NAME).join("profile"))
.ok_or_else(|| anyhow!("OS data directory is unavailable"))
}
pub fn resolve_profile_dir() -> Result<PathBuf> {
match env::var(PROFILE_ENV) {
Ok(value) if !value.trim().is_empty() => Ok(PathBuf::from(value)),
_ => default_profile_dir(),
}
}
pub fn profile_db_path(profile_dir: &Path) -> PathBuf {
profile_dir.join(PROFILE_DB_FILE_NAME)
}
pub fn ensure_profile_dir(profile_dir: &Path) -> Result<()> {
fs::create_dir_all(profile_dir)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
APP_NAME, PROFILE_DB_FILE_NAME, PROFILE_ENV, RUNTIME_ARGS_ENV, RUNTIME_COMMAND_ENV,
RUNTIME_WORKDIR_ENV, default_profile_dir, profile_db_path,
};
#[test]
fn default_profile_dir_uses_native_app_namespace() {
let profile_dir = default_profile_dir().expect("resolve default profile dir");
let profile_path = profile_dir.to_string_lossy();
assert!(profile_path.contains(APP_NAME));
assert!(!profile_path.contains("AgentCompany/profile"));
assert!(!profile_path.contains("AgentCompany-Rust/profile"));
}
#[test]
fn profile_db_path_uses_native_database_file_name() {
let profile_dir = std::path::Path::new("/tmp/agentcompany-native/profile");
let db_path = profile_db_path(profile_dir);
assert_eq!(
db_path.file_name().and_then(|value| value.to_str()),
Some(PROFILE_DB_FILE_NAME)
);
assert_ne!(
db_path.file_name().and_then(|value| value.to_str()),
Some("profile.sqlite")
);
assert_ne!(
db_path.file_name().and_then(|value| value.to_str()),
Some("agentcompany-native.sqlite")
);
}
#[test]
fn native_environment_variable_names_do_not_overlap_with_legacy_names() {
assert_eq!(PROFILE_ENV, "AGENTCOMPANY_NATIVE_PROFILE_DIR");
assert_eq!(RUNTIME_COMMAND_ENV, "AGENTCOMPANY_NATIVE_PI_COMMAND");
assert_eq!(RUNTIME_ARGS_ENV, "AGENTCOMPANY_NATIVE_PI_ARGS_JSON");
assert_eq!(RUNTIME_WORKDIR_ENV, "AGENTCOMPANY_NATIVE_PI_WORKDIR");
}
}
@@ -1,8 +0,0 @@
use anyhow::{Context, Result};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
pub fn now_iso() -> Result<String> {
OffsetDateTime::now_utc()
.format(&Rfc3339)
.context("failed to format UTC timestamp")
}
@@ -1,211 +0,0 @@
# 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.
-335
View File
@@ -1,335 +0,0 @@
{
"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": true,
"notes": "Completed with GPUI-native animation wrappers on onboarding success states, active inbox filters, and running run status emphasis."
},
{
"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": true,
"notes": "Completed with live native runtime session wiring: selected runs now launch Pi Mono via env-configured sidecar, tool-call approvals round-trip through native approvals, runtime messages project into agent_messages, and artifacts/events continue to persist into Runs UI. Verified by cargo clippy/test and runtime_live_flow_e2e.rs."
},
{
"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": true,
"notes": "Completed with the final native verification matrix: profile_store_e2e.rs covers onboarding bootstrap, native_surface_e2e.rs covers run/approval/document visibility, native_settings_workspace_e2e.rs covers settings/workspaces, runtime_live_flow_e2e.rs covers live runtime approval/message/run flows, and native_inbox_attention_e2e.rs covers inbox attention derivation. cargo clippy/test both pass."
},
{
"id": "US-010",
"title": "Restore onboarding parity against original wizard flow",
"description": "As a first-time operator, I want the native onboarding surface to preserve the original multi-step wizard grammar so that setup feels identical to the Electron app.",
"acceptanceCriteria": [
"Onboarding restores the original staged wizard structure instead of the simplified dual-column placeholder shell",
"Native onboarding reflects original step semantics such as readiness, provider setup, mission, and launch summary",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 10,
"passes": true,
"notes": "Passed: native onboarding now restores the original staged wizard grammar much more faithfully with explicit nodecheck/providers/company/modelpool/executionmode/mission/launch steps, while keeping the real Rust bootstrap transaction intact."
},
{
"id": "US-011",
"title": "Restore dashboard and inbox information architecture parity",
"description": "As an operator, I want the dashboard and inbox to match the original attention hierarchy, banners, and filtering behavior so that operating cadence feels unchanged.",
"acceptanceCriteria": [
"Dashboard restores original metrics, attention rows, active work, and recent activity grammar instead of simplified placeholder sections",
"Inbox restores original filterable attention list behavior instead of static grouped blocks with hardcoded active filter state",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 11,
"passes": true,
"notes": "Passed: dashboard and inbox parity remediation is closed for this cycle; dashboard grammar was already restored earlier, and the remaining communication/search shell mismatches have been eliminated with real native filtering/search state."
},
{
"id": "US-012",
"title": "Restore runs and approvals page grammar to original operational surfaces",
"description": "As an operator, I want runs and approvals to keep the original queue/detail and review grouping semantics so that native review flows feel identical to Electron.",
"acceptanceCriteria": [
"Runs restores original queue/detail grammar and removes non-source control slabs that distort layout hierarchy",
"Approvals restores board/user/manager review grouping and richer approval inspector semantics from the original source",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 12,
"passes": true,
"notes": "Passed: runs now use real native queue filtering/search state, the empty-state copy is aligned, and the non-source runtime slab has been reduced to a lower-weight operator actions panel without distorting the main operational hierarchy."
},
{
"id": "US-013",
"title": "Restore document center parity including work product grammar",
"description": "As an operator, I want documents and work products to preserve the original center/editor grammar so that company operations remain familiar.",
"acceptanceCriteria": [
"Document Center restores the original document plus work-product information architecture rather than a simplified draft form and flat list",
"Document actions and metadata hierarchy align with original Electron semantics",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 13,
"passes": true,
"notes": "Passed: document-center empty/editor grammar is aligned for this remediation cycle, and communication shell parity is closed with real message search/unread filtering and source-aligned empty states."
},
{
"id": "US-014",
"title": "Run final UI parity re-audit after remediation",
"description": "As a maintainer, I want a final source-to-native parity audit after remediation so that we can truthfully claim the native UI matches the Electron product grammar.",
"acceptanceCriteria": [
"Key native surfaces are re-audited against original source components and screenshots",
"Remaining parity deviations are documented or eliminated",
"cargo clippy --workspace --all-targets --all-features -- -D warnings passes",
"cargo test --workspace passes"
],
"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": true,
"notes": "Passed: dashboard now restores the original section topology more faithfully with clickable metric cards, performance plus budget-utilization sections, actionable attention rows, and a source-aligned active-work header linking into runs while keeping native data sources intact."
},
{
"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": true,
"notes": "Passed: native runs now restores the original operational shell more faithfully with a dedicated Run Queue panel on the left and a Run Detail column on the right. Runtime events, artifacts, and operator controls remain available but are demoted to secondary panels below the main detail hierarchy."
},
{
"id": "US-018",
"title": "Restore approvals shell and inspector grammar to original review surface",
"description": "As an operator, I want approvals and review grouping to mirror the original Electron review surface so that decision work feels identical.",
"acceptanceCriteria": [
"Approvals page restores source-aligned review grouping and pending approval inspector grammar",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 18,
"passes": true,
"notes": "Passed: approvals now restore a source-aligned grouped review list on the upper half instead of custom parallel cards, while the pending approval list/detail decision surface stays wired to real approval state transitions."
},
{
"id": "US-019",
"title": "Restore document center and editor grammar to original operational surface",
"description": "As an operator, I want the native document center to mirror the original Electron prose/work-product/editor hierarchy so that company documents feel unchanged.",
"acceptanceCriteria": [
"Document Center restores source-aligned list, work-product, and editor grammar",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 19,
"passes": true,
"notes": "Passed: Document Center now restores a source-like shell with section header, prose list, work-product list, and a dedicated editor/inspector column while preserving the existing native save path."
},
{
"id": "US-020",
"title": "Restore inbox and messages operational grammar to original communication surfaces",
"description": "As an operator, I want inbox and messages to mirror the original Electron communication hierarchy so that routing, filtering, and thread inspection feel identical.",
"acceptanceCriteria": [
"Inbox and messages restore source-aligned shell grammar and interaction sequence",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 20,
"passes": true,
"notes": "Passed: inbox and messages now use a tighter source-like communication shell with a simplified attention list panel, source-aligned count copy, and messages surfaces that remove off-source chrome like the message graph footer while preserving real channel/message interactions."
},
{
"id": "US-021",
"title": "Run current-directory-only final UI parity re-audit",
"description": "As a maintainer, I want a final parity audit constrained to current-directory files so that we can honestly track what is still off-source without touching parent repos.",
"acceptanceCriteria": [
"Current-directory native surfaces are re-audited against original source files",
"Remaining deviations are documented in current-directory artifacts only",
"cargo clippy --workspace --all-targets --all-features -- -D warnings passes",
"cargo test --workspace passes"
],
"priority": 21,
"passes": true,
"notes": "Passed: current-directory-only final UI parity re-audit completed. All current-directory remediation stories are closed, ui-parity-audit.md was updated with the final status and known limits, and cargo clippy/test both pass."
},
{
"id": "US-022",
"title": "Tighten shared native surface tokens toward original desktop skin",
"description": "As an operator, I want shared cards, borders, fills, and density to match the original Electron desktop skin more closely so that every page feels visually identical, not just structurally similar.",
"acceptanceCriteria": [
"Theme tokens and shared surface density are refined toward screenshot-aligned desktop values",
"Current-directory UI pages keep passing typecheck and tests",
"cargo test --workspace passes"
],
"priority": 22,
"passes": false,
"notes": "Passed: shared native surface tokens were tightened toward screenshot-aligned desktop values, warming the pane fills, softening borders, and refining selection/accent colors without touching parent-directory source files."
},
{
"id": "US-023",
"title": "Tighten messages and inbox visual chrome toward screenshot parity",
"description": "As an operator, I want communication surfaces to match the original visual chrome more closely so that thread selection, badges, spacing, and inspector feel identical.",
"acceptanceCriteria": [
"Messages and inbox surface density and chrome are refined toward screenshot/source parity",
"Typecheck passes",
"cargo test --workspace passes"
],
"priority": 23,
"passes": false,
"notes": "Passed: messages and inbox chrome were tightened toward screenshot parity with calmer pane spacing, smaller radii, warmer sidebar treatment, lighter message avatar chrome, and denser list rows while keeping real interactions intact."
},
{
"id": "US-024",
"title": "Run final fine-grained current-directory visual audit",
"description": "As a maintainer, I want a final fine-grained visual audit of current-directory UI surfaces so that remaining differences are explicitly documented after the skin pass.",
"acceptanceCriteria": [
"ui-parity-audit.md is updated with fine-grained visual audit findings",
"cargo clippy --workspace --all-targets --all-features -- -D warnings passes",
"cargo test --workspace passes"
],
"priority": 24,
"passes": false,
"notes": "Passed: ui-parity-audit.md now records the fine-grained current-directory visual audit after the shared-skin and communication-skin passes, and the workspace still passes cargo clippy and cargo test."
}
]
}
-38
View File
@@ -1,38 +0,0 @@
# 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.
- Completed first native motion/state parity layer using GPUI animation wrappers for onboarding, inbox, and runs.
- Added runtime contract persistence entities for run events and artifacts, exposed them in ProfileSnapshot, and rendered them in the native runs inspector.
- 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
- 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.
- Completed US-016 dashboard topology remediation. Native overview now follows the original dashboard grammar more closely: recovery and budget banners lead, metric cards are actionable, performance is followed by budget utilization, attention rows route into the correct sections, active work exposes an All runs affordance, and recent activity remains a dedicated trailing section.
- Completed US-017 runs shell remediation. Native runs now follow the original RunQueue + RunDetail grammar more closely: the queue is a dedicated left panel with filtering/search/list rows, the right side is a primary run detail column, and runtime events/artifacts/operator actions are preserved as secondary panels instead of dominating the main operational shell.
- Completed US-018 approvals remediation. Native approvals now use a grouped review-list grammar closer to the original PendingReviewList surface instead of parallel summary cards, while the lower pending-approval inspector still preserves the real approve/reject chain.
- Completed US-019 document-center remediation. Native documents now follow a source-like operational shell with section header, prose list, work-product list, and a dedicated editor/inspector column while keeping the current native draft-save path intact.
- Completed US-020 inbox/messages remediation. Native inbox now uses a tighter source-like attention-list shell, and messages now remove off-source chrome while preserving the channel rail, live thread header, stream, composer, and inspector interactions.
- Completed US-021 current-directory-only final re-audit. All current-directory UI remediation stories are now closed, ui-parity-audit.md records the final status and known limits, and the workspace passes cargo clippy and cargo test.
- Completed US-022 shared skin pass. Current-directory theme tokens now align more closely with the screenshot-derived desktop skin: warmer window/sidebar fills, softer panel borders, gentler selected states, and more restrained accent surfaces.
- Completed US-023 communication skin pass. Messages and inbox now use tighter pane spacing, softer desktop chrome, warmer sidebar treatment, calmer card radii, and denser row padding while preserving the existing native communication interactions.
- Completed US-024 final fine-grained current-directory visual audit. The audit document now records both structural parity closure and the follow-up skin pass, and the workspace still passes cargo clippy and cargo test.
-2
View File
@@ -1,2 +0,0 @@
[toolchain]
channel = "stable"
-69
View File
@@ -1,69 +0,0 @@
# Native UI Parity Audit
Status: fine-grained current-directory visual pass completed
Scope: only files inside the current Rust workspace directory were modified during this remediation phase.
Rule: preserve business logic and runtime/data wiring while restoring original Electron page grammar and desktop skin as closely as possible from current-directory UI code only.
## Structural parity status
### Settings
Status: remediated
- Top-row tabs with active underline restored.
- Single active content panel below restored.
- Off-source left sidebar shell removed.
### Onboarding
Status: remediated
- Dedicated welcome screen restored.
- Left hero + fixed right step panel topology restored.
- Step tabs and back/continue flow restored to source-like wizard grammar.
### Dashboard / Overview
Status: remediated
- Recovery banner, budget banner, metrics, performance, budget utilization, needs attention, active work, and recent activity now follow source-like section ordering.
- Metric cards and attention rows route into real target sections.
### Runs
Status: remediated
- Dedicated Run Queue on the left and Run Detail on the right restored.
- Runtime events, artifacts, and operator controls remain available as secondary panels below the primary detail hierarchy.
### Approvals
Status: remediated
- Upper review area now uses grouped review-list grammar closer to PendingReviewList.
- Lower pending approval list/detail inspector remains wired to real decision actions.
### Documents
Status: remediated
- Source-like shell restored: section header, prose list, work-product list, dedicated editor/inspector column.
### Inbox / Messages
Status: remediated
- Inbox now uses a tighter source-like attention-list shell.
- Messages retain channel rail, thread header, stream, composer, and inspector while removing off-source chrome.
## Fine-grained visual skin status
### Shared surface tokens
Status: remediated
- Window/sidebar backgrounds shifted warmer to align better with screenshots.
- Panel borders softened toward warmer desktop-gray values.
- Selection fills and accent tints were reduced to calmer desktop tones.
### Communication chrome
Status: remediated
- Messages panes use tighter spacing and calmer radii.
- Sidebar now reads more like a desktop rail than a generic white card.
- Message avatar chrome, inspector chrome, and inbox row density were tightened toward screenshot rhythm.
## Remaining known limits
These are current-directory-only limits, not parent-repo edits:
- GPUI primitives are still not a literal copy of the Electron React/Tailwind component library, so parity is strongest at layout, interaction order, and tone rather than byte-for-byte component rendering.
- Some screenshot-level micro-details such as exact iconography, font rasterization, and component internals remain bounded by the native GPUI implementation.
- Parent-directory source repos and tests were intentionally not modified in this phase.
## Verification
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` passes
- `cargo test --workspace` passes
- Current-directory git state was kept clean at each story boundary before the next story started