901 lines
31 KiB
Rust
901 lines
31 KiB
Rust
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())
|
|
}
|