Restore native onboarding and dashboard grammar
This commit is contained in:
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())
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user