Restore dashboard topology grammar
This commit is contained in:
1 parent
fe808d13a2
commit
78e406c20a
3 files changed
+214
-49
No files matched your search
@@ -53,6 +53,56 @@ impl AppRoot {
|
||||
.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()
|
||||
@@ -62,7 +112,7 @@ impl AppRoot {
|
||||
container.child(recovery_banner(interrupted_runs.as_slice()))
|
||||
})
|
||||
.when(!budget_alerts.is_empty(), |container| {
|
||||
container.child(budget_alert_banner(budget_alerts.as_slice()))
|
||||
container.child(budget_alert_banner(budget_alerts.as_slice(), cx))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
@@ -82,6 +132,8 @@ impl AppRoot {
|
||||
),
|
||||
theme::accent_blue_soft(),
|
||||
theme::accent_blue(),
|
||||
SectionId::Inbox,
|
||||
cx,
|
||||
))
|
||||
.child(metric_card(
|
||||
"APPROVALS",
|
||||
@@ -93,6 +145,8 @@ impl AppRoot {
|
||||
},
|
||||
theme::accent_orange_soft(),
|
||||
theme::accent_orange(),
|
||||
SectionId::Approvals,
|
||||
cx,
|
||||
))
|
||||
.child(metric_card(
|
||||
"AGENTS",
|
||||
@@ -107,56 +161,20 @@ impl AppRoot {
|
||||
),
|
||||
theme::accent_teal_soft(),
|
||||
theme::accent_teal(),
|
||||
SectionId::Messages,
|
||||
cx,
|
||||
))
|
||||
.child(metric_card(
|
||||
"TOTAL SPEND",
|
||||
format!("${total_spend:.2}"),
|
||||
"Local profile budget trace".to_string(),
|
||||
"Open settings and costs".to_string(),
|
||||
theme::panel_muted(),
|
||||
theme::text_primary(),
|
||||
SectionId::Settings,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.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(eyebrow_label("PERFORMANCE"))
|
||||
.child(
|
||||
div()
|
||||
.grid()
|
||||
@@ -167,9 +185,25 @@ impl AppRoot {
|
||||
.child(cost_trend_card(&self.snapshot))
|
||||
.child(agent_workload_card(&self.snapshot)),
|
||||
)
|
||||
.child(section_label("ACTIVE WORK"))
|
||||
.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(section_label("RECENT ACTIVITY"))
|
||||
.child(eyebrow_label("RECENT ACTIVITY"))
|
||||
.child(activity_feed_card(&self.snapshot))
|
||||
}
|
||||
}
|
||||
@@ -208,15 +242,22 @@ fn recovery_banner(runs: &[&domain::Run]) -> impl IntoElement {
|
||||
}))
|
||||
}
|
||||
|
||||
fn budget_alert_banner(agents: &[&domain::Agent]) -> impl IntoElement {
|
||||
fn budget_alert_banner(agents: &[&domain::Agent], cx: &mut Context<AppRoot>) -> impl IntoElement {
|
||||
div().gap_2().children(agents.iter().map(|agent| {
|
||||
div()
|
||||
.id(SharedString::from(format!("budget-alert-{}", agent.id)))
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::danger())
|
||||
.bg(theme::accent_orange_soft())
|
||||
.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()
|
||||
.text_sm()
|
||||
@@ -235,13 +276,22 @@ fn metric_card(
|
||||
subtitle: String,
|
||||
icon_bg: gpui::Hsla,
|
||||
tone: gpui::Hsla,
|
||||
target_section: SectionId,
|
||||
cx: &mut Context<AppRoot>,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(SharedString::from(format!("overview-metric-{label}")))
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.p_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()
|
||||
@@ -264,23 +314,60 @@ fn metric_card(
|
||||
)
|
||||
}
|
||||
|
||||
fn section_label(label: &'static str) -> impl IntoElement {
|
||||
fn eyebrow_label(label: &'static str) -> impl IntoElement {
|
||||
div().text_xs().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_xs()
|
||||
.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_lg()
|
||||
.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()
|
||||
@@ -498,6 +585,82 @@ fn mini_chart_card(
|
||||
}))
|
||||
}
|
||||
|
||||
fn budget_utilization_card(snapshot: &domain::ProfileSnapshot) -> impl IntoElement {
|
||||
div()
|
||||
.rounded_lg()
|
||||
.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_sm()
|
||||
.text_color(theme::text_primary())
|
||||
.child(agent.name.clone()),
|
||||
)
|
||||
.child(div().text_xs().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_xs()
|
||||
.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],
|
||||
|
||||
@@ -223,8 +223,8 @@
|
||||
"cargo test --workspace passes"
|
||||
],
|
||||
"priority": 16,
|
||||
"passes": false,
|
||||
"notes": "Reopened after strict source audit: current overview is closer than before but still custom in grammar and component topology."
|
||||
"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",
|
||||
|
||||
@@ -18,3 +18,5 @@ Started: 2026-04-19
|
||||
- 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.
|
||||
Reference in new issue
Block a user