Restore native onboarding and dashboard grammar

This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 15:50:28 -04:00
1 parent cb0ac485da
commit 320706fecf
4 files changed
+551 -183

No files matched your search

@@ -406,7 +406,7 @@ impl Render for AppRoot {
.overflow_scroll()
.p_8()
.child(match self.state.section {
SectionId::Overview => self.render_overview().into_any_element(),
SectionId::Overview => self.render_overview(cx).into_any_element(),
SectionId::Runs => self.render_runs(cx).into_any_element(),
SectionId::Inbox => self.render_inbox().into_any_element(),
SectionId::Messages => self.render_messages(cx).into_any_element(),
@@ -1,240 +1,608 @@
use super::*;
impl AppRoot {
pub(super) fn render_overview(&self) -> impl IntoElement + '_ {
let company = self.snapshot.current_company();
let company_name = company
.map(|item| item.name.clone())
.unwrap_or_else(|| "No company".to_string());
let queued_tasks = self
pub(super) fn render_overview(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let pending_approvals = self
.snapshot
.tasks
.approvals
.iter()
.filter(|task| task.status == "todo")
.count();
let running_agents = self
.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.status == "running")
.count();
.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>();
div()
.flex()
.flex_col()
.gap_8()
.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()))
})
.child(
div()
.gap_2()
.child(
div()
.text_3xl()
.text_color(theme::text_primary())
.child(company_name),
)
.child(
div()
.flex()
.gap_3()
.items_center()
.text_sm()
.text_color(theme::text_muted())
.child(metadata_dot(theme::accent_teal()))
.child(format!("{} agents active", self.snapshot.agents.len()))
.child(div().child("•"))
.child(metadata_dot(theme::badge_dark()))
.child(format!(
"{} tasks in progress",
self.snapshot.active_tasks()
))
.child(div().child("•"))
.child(format!(
"{} completed",
completed_task_count(&self.snapshot)
)),
),
)
.child(
div()
.flex()
.gap_4()
.child(metric_panel(
.grid()
.grid_cols(4)
.gap_3()
.child(metric_card(
"ACTIVE TASKS",
self.snapshot.active_tasks().to_string(),
format!("{} queued", queued_tasks),
format!(
"{} queued",
self.snapshot
.tasks
.iter()
.filter(|task| task.status == "todo")
.count()
),
theme::accent_blue_soft(),
theme::accent_blue(),
))
.child(metric_panel(
.child(metric_card(
"APPROVALS",
self.snapshot.pending_approvals().to_string(),
if self.snapshot.pending_approvals() == 0 {
pending_approvals.len().to_string(),
if pending_approvals.is_empty() {
"All clear".to_string()
} else {
"Needs board attention".to_string()
"Needs attention".to_string()
},
theme::panel_muted(),
theme::text_primary(),
theme::accent_orange_soft(),
theme::accent_orange(),
))
.child(metric_panel(
.child(metric_card(
"AGENTS",
self.snapshot.agents.len().to_string(),
format!("{} running", running_agents),
format!(
"{} running",
self.snapshot
.agents
.iter()
.filter(|agent| agent.status == "running")
.count()
),
theme::accent_teal_soft(),
theme::accent_teal(),
))
.child(metric_panel(
.child(metric_card(
"TOTAL SPEND",
total_spend_label(&self.snapshot),
format!("${total_spend:.2}"),
"Local profile budget trace".to_string(),
theme::panel_muted(),
theme::text_primary(),
)),
)
.child(section_header("ACTIVE WORK", Some("All runs →")))
.when(
!(pending_approvals.is_empty()
&& failed_runs.is_empty()
&& unread_messages.is_empty()),
|container| {
container.child(section_label("NEEDS ATTENTION")).child(
div()
.gap_2()
.children(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(),
)
}))
.children(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(),
)
}))
.children(unread_messages.iter().take(3).map(|message| {
attention_row(
message.subject.clone(),
message.body.clone(),
theme::accent_teal_soft(),
theme::accent_teal(),
)
})),
)
},
)
.child(section_label("PERFORMANCE"))
.child(
div()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(self.snapshot.runs.iter().take(6).map(|run| {
let task_title = self
.snapshot
.tasks
.iter()
.find(|task| Some(task.id.as_str()) == run.task_id.as_deref())
.map(|task| task.title.clone())
.unwrap_or_else(|| {
if run.summary.trim().is_empty() {
format!("Run {}", trim_id(&run.id))
} else {
run.summary.clone()
}
});
let agent_name = self
.snapshot
.agents
.iter()
.find(|agent| agent.id == run.agent_id)
.map(|agent| agent.name.clone())
.unwrap_or_else(|| "Unknown agent".to_string());
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(task_title),
)
.child(
div()
.text_sm()
.text_color(theme::text_muted())
.child(format!("{} · {}", agent_name, run.status)),
)
})),
)
.child(section_header("RECENT ACTIVITY", None))
.child(
div()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(self.snapshot.activity.iter().take(8).map(|item| {
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(format!(
"{} {}",
item.actor,
item.action.replace('_', " ")
)),
)
.child(
div()
.text_sm()
.text_color(theme::text_muted())
.child(item.detail.clone()),
)
})),
.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(section_label("ACTIVE WORK"))
.child(active_work_card(&self.snapshot, active_work.as_slice(), cx))
.child(section_label("RECENT ACTIVITY"))
.child(activity_feed_card(&self.snapshot))
}
}
fn completed_task_count(snapshot: &domain::ProfileSnapshot) -> usize {
snapshot
.tasks
.iter()
.filter(|task| task.status == "done")
.count()
fn recovery_banner(runs: &[&domain::Run]) -> impl IntoElement {
div()
.rounded_lg()
.border_1()
.border_color(theme::danger())
.bg(theme::danger_bg())
.p_4()
.gap_3()
.child(div().text_sm().text_color(theme::danger()).child(format!(
"{} interrupted task(s) detected from the previous session",
runs.len()
)))
.children(runs.iter().map(|run| {
div()
.rounded_md()
.bg(theme::panel())
.px_4()
.py_3()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(overview_run_title_from_run(run)),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.text_color(theme::text_muted())
.child(run.id.get(..8).unwrap_or(run.id.as_str()).to_string()),
)
}))
}
fn total_spend_label(snapshot: &domain::ProfileSnapshot) -> String {
let spend = snapshot
fn budget_alert_banner(agents: &[&domain::Agent]) -> impl IntoElement {
div().gap_2().children(agents.iter().map(|agent| {
div()
.rounded_lg()
.border_1()
.border_color(theme::danger())
.bg(theme::accent_orange_soft())
.px_4()
.py_3()
.child(
div()
.text_sm()
.text_color(theme::accent_orange())
.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,
) -> impl IntoElement {
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_4()
.child(
div()
.flex()
.gap_4()
.items_center()
.child(div().w(px(48.0)).h(px(48.0)).rounded_lg().bg(icon_bg))
.child(
div()
.flex_1()
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.child(div().text_3xl().text_color(tone).child(value))
.child(
div()
.mt(px(6.0))
.text_xs()
.text_color(theme::text_muted())
.child(subtitle),
),
),
)
}
fn section_label(label: &'static str) -> impl IntoElement {
div().text_xs().text_color(theme::text_muted()).child(label)
}
fn attention_row(
title: String,
subtitle: String,
tile_bg: gpui::Hsla,
accent: gpui::Hsla,
) -> impl IntoElement {
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.px_4()
.py_3()
.child(
div()
.flex()
.gap_3()
.items_center()
.child(div().w(px(36.0)).h(px(36.0)).rounded_lg().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_xs()
.text_color(theme::text_muted())
.line_clamp(2)
.child(subtitle),
),
)
.child(div().w(px(10.0)).h(px(10.0)).rounded_full().bg(accent)),
)
}
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_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("SUCCESS RATE"),
)
.child(
div()
.mt(px(10.0))
.text_3xl()
.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)
.sum::<f64>();
format!("${spend:.2}")
}
fn trim_id(value: &str) -> &str {
value.get(..8).unwrap_or(value)
}
fn metadata_dot(color: gpui::Hsla) -> impl IntoElement {
div().w(px(8.0)).h(px(8.0)).rounded_full().bg(color)
}
fn section_header(label: &'static str, action: Option<&'static str>) -> impl IntoElement {
.collect::<Vec<_>>();
let total = costs.iter().sum::<f64>();
let average = if costs.is_empty() {
0.0
} else {
total / costs.len() as f64
};
div()
.flex()
.items_center()
.justify_between()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("COST TREND"),
)
.child(
div()
.mt(px(10.0))
.text_3xl()
.text_color(theme::text_primary())
.child(format!("${total:.2}")),
)
.child(
div()
.mt(px(8.0))
.text_xs()
.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_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.p_5()
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.when_some(action, |container, action_label| {
.children(rows.into_iter().map(|(name, count)| {
div()
.mt(px(12.0))
.child(
div()
.flex()
.justify_between()
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(name),
)
.child(
div()
.text_xs()
.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 active_work_card<'a>(
snapshot: &'a domain::ProfileSnapshot,
runs: &[&'a domain::Run],
cx: &mut Context<AppRoot>,
) -> impl IntoElement + 'a {
div()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.when(runs.is_empty(), |container| {
container.child(
div()
.text_sm()
.text_color(theme::accent_blue())
.child(action_label),
.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_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(overview_run_title(snapshot, run)),
)
.child(
div()
.mt(px(4.0))
.text_sm()
.text_color(theme::text_muted())
.child(format!(
"{} · {}",
overview_run_agent(snapshot, run),
run.status
)),
)
}))
})
}
fn metric_panel(
label: &'static str,
value: String,
detail: String,
tile_bg: gpui::Hsla,
value_color: gpui::Hsla,
) -> impl IntoElement {
fn activity_feed_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
div()
.flex_1()
.p_5()
.gap_3()
.border_1()
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(div().w(px(56.0)).h(px(56.0)).rounded_lg().bg(tile_bg))
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.child(div().text_3xl().text_color(value_color).child(value))
.child(
.children(snapshot.activity.iter().take(10).map(|item| {
div()
.text_sm()
.text_color(theme::text_muted())
.child(detail),
)
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(format!("{} {}", item.actor, item.action.replace('_', " "))),
)
.child(
div()
.mt(px(4.0))
.text_xs()
.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 -1
View File
@@ -155,7 +155,7 @@
],
"priority": 11,
"passes": false,
"notes": "Audit found missing dashboard banners/charts/attention structure and inbox filter interaction parity."
"notes": "In progress: native dashboard now restores banner/attention/performance/active-work/activity grammar with real local data instead of the earlier simplified placeholder overview. Remaining gap is finer inbox parity and some dashboard micro-structure details versus the Electron source."
},
{
"id": "US-012",
+1 -1
View File
@@ -14,4 +14,4 @@ Started: 2026-04-19
- Added runtime_contract_snapshot_e2e.rs to verify runtime event and artifact persistence across reopen.
- Runtime contract integration advanced: RuntimeEvent ingestion now persists message/tool/artifact/run-finished events into storage and exposes them in the runs inspector.
- Wired a live native runtime session into the GPUI Runs surface. Selected runs now launch Pi Mono through env-configured stdio sidecar settings, runtime tool calls create approval rows, approval decisions send ToolApproval back into the live session, runtime messages project into agent_messages, and run lifecycle still persists into run_events/artifacts/runs.\n- Closed the final native verification matrix. The workspace now has explicit storage-backed e2e coverage for onboarding bootstrap, approvals/documents/runs visibility, settings/workspace mutations, live runtime contract flows, and inbox attention derivation.\n- Reopened Ralph parity work after a direct source audit. The current native app has real runtime/data flows, but several surfaces still diverge from Electron page grammar: onboarding is too collapsed, dashboard misses banner/attention/chart structure, inbox filters are not real, run detail is dominated by native control slabs not present in source, approvals flatten board/user/manager semantics, and documents still lack work-product/editor hierarchy. Added US-010 through US-014 to close these parity gaps honestly.
- Rebuilt native onboarding away from the old static split shell. It now has a real multi-stage company/mission/launch flow, screenshot-aligned narrative/summary composition, real connector readiness derived from snapshot data, and a storage bootstrap transaction that creates company, workspace, goal, CEO, kickoff project, kickoff task, and queued run. Remaining US-010 work is the finer original wizard-tab parity.\n
- Rebuilt 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