T24b: Agent, progress
- AgentStepList: an agent's steps down a data_display::Timeline rail, marked by chat::step_mark, with detail and time - AgentPlan: items waiting, at work or done, with a count; done items go quiet and struck through - chat::step_mark reads motion::changes: a check or cross that arrives pops in, still on first paint - AgentStatus: a pill for planning, working, waiting, done or failed - AgentProgress: the step at work, done of total, the time so far and a bar split by step; progress past its total fails loud - SubAgentTree is lists::Tree with each agent's state as its note - gallery: a plan the button advances, steps, statuses and a tree
This commit is contained in:
8 files changed
+551
-41
No files matched your search
@@ -1,3 +1,4 @@
|
||||
mod progress;
|
||||
mod tools;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
@@ -9,7 +10,7 @@ pub const PAGE: Page = Page {
|
||||
number: 24,
|
||||
slug: "agent",
|
||||
title: "Agent",
|
||||
summary: "An assistant at work: the tools it calls and what it asks before it acts.",
|
||||
summary: "An assistant at work: the tools it calls, what it asks before it acts, and how far it has come.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
@@ -27,11 +28,16 @@ const SCRIPT: &[Step] = &[
|
||||
Step::DownAt("agent-call", 60.0, 18.0),
|
||||
Step::UpAt("agent-call", 60.0, 18.0),
|
||||
Step::Wait(200),
|
||||
Step::Click("agent-advance"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("plan"),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(tools::calls(window, cx))
|
||||
.child(tools::approval(window, cx))
|
||||
.child(progress::steps(window, cx))
|
||||
.child(progress::agents(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ely_gpui_component::{
|
||||
agent::{AgentPlan, AgentProgress, AgentState, AgentStatus, AgentStep, AgentStepList},
|
||||
buttons::{Button, ButtonVariant},
|
||||
chat::StepState,
|
||||
data_display::Tone,
|
||||
lists::{Tree, TreeNode},
|
||||
primitives::IconName,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, section, set},
|
||||
};
|
||||
|
||||
const PLAN: [&str; 5] = [
|
||||
"Read the theme's palette",
|
||||
"Measure contrast in both themes",
|
||||
"Lift the accents for dark",
|
||||
"Run the theme tests",
|
||||
"Write the release note",
|
||||
];
|
||||
|
||||
/// Where each item of the plan stands once `done` have finished; the next one works.
|
||||
fn plan(done: usize) -> Vec<(&'static str, StepState)> {
|
||||
PLAN.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, label)| {
|
||||
let state = match ix.cmp(&done) {
|
||||
std::cmp::Ordering::Less => StepState::Done,
|
||||
std::cmp::Ordering::Equal => StepState::Working,
|
||||
std::cmp::Ordering::Greater => StepState::Waiting,
|
||||
};
|
||||
(*label, state)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A step as the agent reports it.
|
||||
fn step(title: &str, state: StepState, detail: Option<&str>, took: Option<u64>) -> AgentStep {
|
||||
AgentStep {
|
||||
title: title.to_string().into(),
|
||||
state,
|
||||
detail: detail.map(|text| text.to_string().into()),
|
||||
took: took.map(Duration::from_millis),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn steps(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let done = keep("agent-plan-done", || 2_usize, window, cx);
|
||||
let now = *done.read(cx);
|
||||
let state = if now == PLAN.len() {
|
||||
AgentState::Done
|
||||
} else {
|
||||
AgentState::Working
|
||||
};
|
||||
let current = PLAN.get(now).copied().unwrap_or("All done");
|
||||
let steps = [
|
||||
step(
|
||||
"Read src/theme/palette.rs",
|
||||
StepState::Done,
|
||||
None,
|
||||
Some(340),
|
||||
),
|
||||
step(
|
||||
"Searched the web for oklch lift",
|
||||
StepState::Done,
|
||||
Some("Five sources, two on contrast in dark themes."),
|
||||
Some(2_100),
|
||||
),
|
||||
step(
|
||||
"Ran the theme tests",
|
||||
StepState::Failed,
|
||||
Some("2 failed: lift_keeps_hue, dark_lifts_further"),
|
||||
Some(12_400),
|
||||
),
|
||||
step("Fixing the lift for dark", StepState::Working, None, None),
|
||||
step("Run the tests again", StepState::Waiting, None, None),
|
||||
];
|
||||
section(
|
||||
"AgentStepList / TaskTimeline / AgentPlan / TodoList / AgentStatus / AgentProgress",
|
||||
"An agent's work as it goes: its steps down a rail, its plan ticking off as each item finishes, where it stands in a word, and how far it has come. The button finishes the next item.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(AgentStatus::new("agent-status", state))
|
||||
.child(AgentStatus::new("agent-status-waiting", AgentState::Waiting))
|
||||
.child(AgentStatus::new("agent-status-failed", AgentState::Failed)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.w(px(320.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(AgentPlan::new("agent-plan", "Lift the dark theme", plan(now)))
|
||||
.child(
|
||||
AgentProgress::new("agent-progress", current, now, PLAN.len())
|
||||
.elapsed(Duration::from_secs(95 + 40 * now as u64)),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(360.)).child(AgentStepList::new("agent-steps", steps))),
|
||||
)
|
||||
.child(probe(
|
||||
"agent-advance",
|
||||
Button::new("agent-advance", "Finish the next item")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.disabled(now == PLAN.len())
|
||||
.on_click(move |_, _, cx| {
|
||||
let next = (*done.read(cx) + 1).min(PLAN.len());
|
||||
set(&done, next, cx)
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn agents(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let agent = |key: &str, name: &str, state: StepState| {
|
||||
let (note, tone) = match state {
|
||||
StepState::Waiting => ("waiting", Tone::Neutral),
|
||||
StepState::Working => ("working", Tone::Neutral),
|
||||
StepState::Done => ("done", Tone::Neutral),
|
||||
StepState::Failed => ("failed", Tone::Danger),
|
||||
};
|
||||
TreeNode::new(key.to_string(), name.to_string())
|
||||
.icon(IconName::Bot)
|
||||
.note(note)
|
||||
.tone(tone)
|
||||
};
|
||||
section(
|
||||
"SubAgentTree",
|
||||
"Agents an agent sent out, and theirs in turn, each with where it stands: lists::Tree, a node per agent with its state as its note, a failed one tinted.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(420.)).h(px(220.)).child(
|
||||
Tree::new(
|
||||
"agent-tree",
|
||||
[agent("lead", "Lead: lift the dark theme", StepState::Working).children([
|
||||
agent("research", "Research: contrast in dark themes", StepState::Done),
|
||||
agent("code", "Code: change the lift", StepState::Working).children([
|
||||
agent("tests", "Tests: run the theme suite", StepState::Failed),
|
||||
agent("fix", "Fix: dark lifts further", StepState::Working),
|
||||
]),
|
||||
agent("notes", "Notes: write the release note", StepState::Waiting),
|
||||
])],
|
||||
)
|
||||
.open(["lead", "code"])
|
||||
.size_full(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
mod permission;
|
||||
mod progress;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests;
|
||||
mod tools;
|
||||
|
||||
pub use permission::{Permission, PermissionPrompt, ToolApprovalDialog};
|
||||
pub use progress::{AgentPlan, AgentProgress, AgentState, AgentStatus, AgentStep, AgentStepList};
|
||||
pub use tools::{ToolCallCard, ToolCallGroup};
|
||||
@@ -0,0 +1,317 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
chat::{StepState, step_mark},
|
||||
data_display::{Timeline, TimelineItem},
|
||||
motion::{ProgressBar, Spinner},
|
||||
primitives::{Icon, IconName, Severity},
|
||||
theme::{ActiveTheme, IconSize, TextSize},
|
||||
typography::{Ellipsis, format::took, tabular},
|
||||
};
|
||||
|
||||
/// One step of an agent's work: what it did, where it stands, a line below, how long it took.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AgentStep {
|
||||
pub title: SharedString,
|
||||
pub state: StepState,
|
||||
pub detail: Option<SharedString>,
|
||||
pub took: Option<Duration>,
|
||||
}
|
||||
|
||||
/// An agent's steps down a rail, each marked by where it stands.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AgentStepList {
|
||||
id: ElementId,
|
||||
steps: Vec<AgentStep>,
|
||||
}
|
||||
|
||||
impl AgentStepList {
|
||||
pub fn new(id: impl Into<ElementId>, steps: impl IntoIterator<Item = AgentStep>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
steps: steps.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AgentStepList {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let marks: Vec<AnyElement> = (self.steps.iter().enumerate())
|
||||
.map(|(ix, step)| {
|
||||
step_mark(
|
||||
(self.id.clone(), format!("step-{ix}")).into(),
|
||||
step.state,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let colors = cx.theme().colors.clone();
|
||||
self.steps
|
||||
.into_iter()
|
||||
.zip(marks)
|
||||
.fold(Timeline::new(), |timeline, (step, mark)| {
|
||||
let title = div()
|
||||
.text_color(match step.state {
|
||||
StepState::Waiting => colors.fg_subtle,
|
||||
_ => colors.fg,
|
||||
})
|
||||
.child(step.title);
|
||||
let item = TimelineItem::new(title)
|
||||
.marker(mark)
|
||||
.when_some(step.took, |item, spent| item.time(took(spent)));
|
||||
timeline.item(match step.detail {
|
||||
Some(detail) => item.child(detail),
|
||||
None => item,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An agent's plan: items waiting, at work or done, with how many are done; a done item goes quiet and struck through.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AgentPlan {
|
||||
id: ElementId,
|
||||
title: SharedString,
|
||||
items: Vec<(SharedString, StepState)>,
|
||||
}
|
||||
|
||||
impl AgentPlan {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
title: impl Into<SharedString>,
|
||||
items: impl IntoIterator<Item = (impl Into<SharedString>, StepState)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: title.into(),
|
||||
items: items
|
||||
.into_iter()
|
||||
.map(|(label, state)| (label.into(), state))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AgentPlan {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let marks: Vec<AnyElement> = (self.items.iter().enumerate())
|
||||
.map(|(ix, (_, state))| {
|
||||
step_mark(
|
||||
(self.id.clone(), format!("item-{ix}")).into(),
|
||||
*state,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let done = self
|
||||
.items
|
||||
.iter()
|
||||
.filter(|(_, state)| *state == StepState::Done)
|
||||
.count();
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.child(Ellipsis::new(self.title)),
|
||||
)
|
||||
.child(
|
||||
tabular(div().flex_none().text_color(colors.fg_subtle))
|
||||
.child(format!("{done} of {}", self.items.len())),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
self.items
|
||||
.into_iter()
|
||||
.zip(marks)
|
||||
.map(|((label, state), mark)| {
|
||||
div().flex().items_center().gap_2().child(mark).child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_color(match state {
|
||||
StepState::Waiting => colors.fg_muted,
|
||||
StepState::Working => colors.fg,
|
||||
StepState::Done => colors.fg_subtle,
|
||||
StepState::Failed => Severity::Danger.color(&colors),
|
||||
})
|
||||
.when(state == StepState::Done, |text| text.line_through())
|
||||
.child(label),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an agent stands.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AgentState {
|
||||
Planning,
|
||||
Working,
|
||||
Waiting,
|
||||
Done,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl AgentState {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Planning => "Planning",
|
||||
Self::Working => "Working",
|
||||
Self::Waiting => "Waiting for you",
|
||||
Self::Done => "Done",
|
||||
Self::Failed => "Failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pill that says where an agent stands, moving while it plans or works.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AgentStatus {
|
||||
id: ElementId,
|
||||
state: AgentState,
|
||||
}
|
||||
|
||||
impl AgentStatus {
|
||||
pub fn new(id: impl Into<ElementId>, state: AgentState) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AgentStatus {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let tone = match self.state {
|
||||
AgentState::Planning | AgentState::Working => Severity::Info,
|
||||
AgentState::Waiting => Severity::Warning,
|
||||
AgentState::Done => Severity::Success,
|
||||
AgentState::Failed => Severity::Danger,
|
||||
};
|
||||
let ink = tone.color(&colors);
|
||||
let mark = match self.state {
|
||||
AgentState::Planning | AgentState::Working => Spinner::new((self.id.clone(), "mark"))
|
||||
.size(IconSize::Xs)
|
||||
.color(ink)
|
||||
.into_any_element(),
|
||||
AgentState::Waiting => Icon::new(IconName::Hand)
|
||||
.size(IconSize::Xs)
|
||||
.color(ink)
|
||||
.into_any_element(),
|
||||
AgentState::Done => Icon::new(IconName::Check)
|
||||
.size(IconSize::Xs)
|
||||
.color(ink)
|
||||
.into_any_element(),
|
||||
AgentState::Failed => Icon::new(IconName::X)
|
||||
.size(IconSize::Xs)
|
||||
.color(ink)
|
||||
.into_any_element(),
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded_full()
|
||||
.bg(tone.subtle(&colors))
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(ink)
|
||||
.child(mark)
|
||||
.child(self.state.label())
|
||||
}
|
||||
}
|
||||
|
||||
/// How far an agent has come: the step at work, how many are done of how many, the time so far, and a bar split by step.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AgentProgress {
|
||||
id: ElementId,
|
||||
current: SharedString,
|
||||
done: usize,
|
||||
total: usize,
|
||||
elapsed: Option<Duration>,
|
||||
}
|
||||
|
||||
impl AgentProgress {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
current: impl Into<SharedString>,
|
||||
done: usize,
|
||||
total: usize,
|
||||
) -> Self {
|
||||
assert!(total > 0 && done <= total, "progress of {done} in {total}");
|
||||
Self {
|
||||
id: id.into(),
|
||||
current: current.into(),
|
||||
done,
|
||||
total,
|
||||
elapsed: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elapsed(mut self, elapsed: Duration) -> Self {
|
||||
self.elapsed = Some(elapsed);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AgentProgress {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_color(colors.fg)
|
||||
.child(Ellipsis::new(self.current)),
|
||||
)
|
||||
.child(
|
||||
tabular(div().flex_none().text_color(colors.fg_subtle))
|
||||
.child(format!("{} of {}", self.done, self.total)),
|
||||
)
|
||||
.children(self.elapsed.map(|spent| {
|
||||
tabular(div().flex_none().text_color(colors.fg_subtle)).child(took(spent))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
ProgressBar::new((self.id, "bar"), self.done as f32 / self.total as f32)
|
||||
.segments(self.total),
|
||||
)
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -3,7 +3,7 @@ use gpui::{
|
||||
TestAppContext, VisualTestContext, Window, div,
|
||||
};
|
||||
|
||||
use super::{Permission, PermissionPrompt, ToolApprovalDialog};
|
||||
use super::{AgentProgress, Permission, PermissionPrompt, ToolApprovalDialog};
|
||||
use crate::{forms, primitives::FocusNext, theme::Theme};
|
||||
|
||||
/// An approval dialog while open, and the answers it gave.
|
||||
@@ -123,3 +123,9 @@ fn the_prompt_walks_deny_always_then_once(cx: &mut TestAppContext) {
|
||||
[Permission::Once]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "progress of 6 in 5")]
|
||||
fn progress_past_its_total_fails_loud() {
|
||||
let _ = AgentProgress::new("progress", "Writing", 6, 5);
|
||||
}
|
||||
+2
-1
@@ -98,6 +98,7 @@ impl RenderOnce for ToolCallCard {
|
||||
let open = *state.read(cx);
|
||||
let opens = self.arguments.is_some() || self.result.is_some();
|
||||
let failed = self.status == StepState::Failed;
|
||||
let mark = step_mark((self.id.clone(), "mark").into(), self.status, window, cx);
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let label = |text: &'static str| {
|
||||
@@ -120,7 +121,7 @@ impl RenderOnce for ToolCallCard {
|
||||
opens.then_some(state),
|
||||
cx,
|
||||
)
|
||||
.child(step_mark((self.id.clone(), "mark").into(), self.status, cx))
|
||||
.child(mark)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
|
||||
+49
-33
@@ -1,15 +1,15 @@
|
||||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, HighlightStyle, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
StyledText, Window, div, prelude::*, transparent_black,
|
||||
Animation, AnimationExt, AnyElement, App, ElementId, FontWeight, HighlightStyle, Hsla,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledText, Window, div, prelude::*, transparent_black,
|
||||
};
|
||||
|
||||
use super::cite::Source;
|
||||
use crate::{
|
||||
forms::Run,
|
||||
motion::Spinner,
|
||||
motion::{self, Spinner},
|
||||
primitives::{FocusRing, Icon, IconName},
|
||||
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||
typography::{Ellipsis, tabular},
|
||||
@@ -24,19 +24,31 @@ pub enum StepState {
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// A step's mark in an icon's box: a dot waiting, a spinner working, a check done, a cross failed.
|
||||
pub(crate) fn step_mark(id: ElementId, state: StepState, cx: &App) -> AnyElement {
|
||||
/// A step's mark in an icon's box: a dot waiting, a spinner working, a check done, a cross failed. A check or cross that arrives pops in.
|
||||
pub(crate) fn step_mark(
|
||||
id: ElementId,
|
||||
state: StepState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let changes = motion::changes((id.clone(), "changes"), state, window, cx);
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let pop = |icon: IconName, ink: Hsla| {
|
||||
let icon = Icon::new(icon).size(IconSize::Sm).color(ink);
|
||||
if changes == 0 {
|
||||
return icon.into_any_element();
|
||||
}
|
||||
icon.with_animation(
|
||||
(id.clone(), format!("pop-{changes}")),
|
||||
Animation::new(motion::duration(motion::FAST, cx)).with_easing(motion::ease_out_cubic),
|
||||
move |icon, t| icon.scale(0.6 + 0.4 * t).color(ink.opacity(t)),
|
||||
)
|
||||
.into_any_element()
|
||||
};
|
||||
let mark = match state {
|
||||
StepState::Done => Icon::new(IconName::Check)
|
||||
.size(IconSize::Sm)
|
||||
.color(colors.success)
|
||||
.into_any_element(),
|
||||
StepState::Failed => Icon::new(IconName::X)
|
||||
.size(IconSize::Sm)
|
||||
.color(colors.danger)
|
||||
.into_any_element(),
|
||||
StepState::Done => pop(IconName::Check, colors.success),
|
||||
StepState::Failed => pop(IconName::X, colors.danger),
|
||||
StepState::Working => Spinner::new(id).size(IconSize::Sm).into_any_element(),
|
||||
StepState::Waiting => div()
|
||||
.size(theme.status_dot())
|
||||
@@ -77,7 +89,17 @@ impl SearchProgress {
|
||||
}
|
||||
|
||||
impl RenderOnce for SearchProgress {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let marks: Vec<AnyElement> = (self.steps.iter().enumerate())
|
||||
.map(|(ix, (_, state))| {
|
||||
step_mark(
|
||||
(self.id.clone(), format!("step-{ix}")).into(),
|
||||
*state,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
div()
|
||||
@@ -88,24 +110,18 @@ impl RenderOnce for SearchProgress {
|
||||
.children(
|
||||
self.steps
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (label, state))| {
|
||||
let id = (self.id.clone(), format!("step-{ix}")).into();
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(step_mark(id, state, cx))
|
||||
.child(
|
||||
div()
|
||||
.text_color(match state {
|
||||
StepState::Waiting => colors.fg_subtle,
|
||||
StepState::Working => colors.fg,
|
||||
StepState::Done => colors.fg_muted,
|
||||
StepState::Failed => colors.danger,
|
||||
})
|
||||
.child(label),
|
||||
)
|
||||
.zip(marks)
|
||||
.map(|((label, state), mark)| {
|
||||
div().flex().items_center().gap_2().child(mark).child(
|
||||
div()
|
||||
.text_color(match state {
|
||||
StepState::Waiting => colors.fg_subtle,
|
||||
StepState::Working => colors.fg,
|
||||
StepState::Done => colors.fg_muted,
|
||||
StepState::Failed => colors.danger,
|
||||
})
|
||||
.child(label),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
+5
-5
@@ -171,11 +171,11 @@
|
||||
|
||||
## T24b Agent · Progress
|
||||
|
||||
- [ ] AgentStepList / TaskTimeline
|
||||
- [ ] AgentPlan / TodoList
|
||||
- [ ] AgentStatus
|
||||
- [ ] AgentProgress
|
||||
- [ ] SubAgentTree
|
||||
- [x] AgentStepList / TaskTimeline → `agent::AgentStepList`, over `data_display::Timeline`
|
||||
- [x] AgentPlan / TodoList → `agent::AgentPlan`, marks that pop in as items finish
|
||||
- [x] AgentStatus
|
||||
- [x] AgentProgress
|
||||
- [x] SubAgentTree → `lists::Tree`, a node per agent with its state as note and tone
|
||||
|
||||
## T24c Agent · Changes
|
||||
|
||||
|
||||
Reference in new issue
Block a user