diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs index 125bdbc8..c3d2e4f0 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs @@ -53,6 +53,56 @@ impl AppRoot { .iter() .filter_map(|run| run.cost_usd) .sum::(); + 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::>(); + 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::>(); + 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::>(); 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) -> 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, ) -> 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, ) -> 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) -> 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], diff --git a/AgentCompany-Rust/prd.json b/AgentCompany-Rust/prd.json index 8e6da5c6..8c43f228 100644 --- a/AgentCompany-Rust/prd.json +++ b/AgentCompany-Rust/prd.json @@ -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", diff --git a/AgentCompany-Rust/progress.txt b/AgentCompany-Rust/progress.txt index 41153470..984c4a68 100644 --- a/AgentCompany-Rust/progress.txt +++ b/AgentCompany-Rust/progress.txt @@ -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.