Display ACP plans (#34816)

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Agus Zubiaga 2025-07-21 11:11:37 -03:00 committed by GitHub
parent 35b4a918c9
commit 405244d422
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 379 additions and 34 deletions

View file

@ -1,3 +1,4 @@
use acp_thread::Plan;
use agent_servers::AgentServer;
use std::cell::RefCell;
use std::collections::BTreeMap;
@ -66,7 +67,8 @@ pub struct AcpThreadView {
expanded_tool_calls: HashSet<ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
editor_is_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
}
@ -186,7 +188,8 @@ impl AcpThreadView {
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
edits_expanded: false,
editor_is_expanded: false,
plan_expanded: false,
editor_expanded: false,
message_history,
}
}
@ -332,14 +335,14 @@ impl AcpThreadView {
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.set_editor_is_expanded(!self.editor_is_expanded, cx);
self.set_editor_is_expanded(!self.editor_expanded, cx);
cx.notify();
}
fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
self.editor_is_expanded = is_expanded;
self.editor_expanded = is_expanded;
self.message_editor.update(cx, |editor, _| {
if self.editor_is_expanded {
if self.editor_expanded {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
@ -1477,7 +1480,7 @@ impl AcpThreadView {
container.into_any()
}
fn render_edits_bar(
fn render_activity_bar(
&self,
thread_entity: &Entity<AcpThread>,
window: &mut Window,
@ -1486,8 +1489,9 @@ impl AcpThreadView {
let thread = thread_entity.read(cx);
let action_log = thread.action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let plan = thread.plan();
if changed_buffers.is_empty() {
if changed_buffers.is_empty() && plan.is_empty() {
return None;
}
@ -1496,7 +1500,6 @@ impl AcpThreadView {
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let pending_edits = thread.has_pending_edit_tool_calls();
let expanded = self.edits_expanded;
v_flex()
.mt_1()
@ -1512,27 +1515,165 @@ impl AcpThreadView {
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child(self.render_edits_bar_summary(
action_log,
&changed_buffers,
expanded,
pending_edits,
window,
cx,
))
.when(expanded, |parent| {
parent.child(self.render_edits_bar_files(
action_log,
&changed_buffers,
pending_edits,
cx,
))
.when(!plan.is_empty(), |this| {
this.child(self.render_plan_summary(plan, window, cx))
.when(self.plan_expanded, |parent| {
parent.child(self.render_plan_entries(plan, window, cx))
})
})
.when(!changed_buffers.is_empty(), |this| {
this.child(Divider::horizontal())
.child(self.render_edits_summary(
action_log,
&changed_buffers,
self.edits_expanded,
pending_edits,
window,
cx,
))
.when(self.edits_expanded, |parent| {
parent.child(self.render_edited_files(
action_log,
&changed_buffers,
pending_edits,
cx,
))
})
})
.into_any()
.into()
}
fn render_edits_bar_summary(
fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
let stats = plan.stats();
let title = if let Some(entry) = stats.in_progress_entry
&& !self.plan_expanded
{
h_flex()
.w_full()
.gap_1()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.justify_between()
.child(
h_flex()
.gap_1()
.child(
Label::new("Current:")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
)
.when(stats.pending > 0, |this| {
this.child(
Label::new(format!("{} left", stats.pending))
.size(LabelSize::Small)
.color(Color::Muted)
.mr_1(),
)
})
} else {
let status_label = if stats.pending == 0 {
"All Done".to_string()
} else if stats.completed == 0 {
format!("{}", plan.entries.len())
} else {
format!("{}/{}", stats.completed, plan.entries.len())
};
h_flex()
.w_full()
.gap_1()
.justify_between()
.child(
Label::new("Plan")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new(status_label)
.size(LabelSize::Small)
.color(Color::Muted)
.mr_1(),
)
};
h_flex()
.p_1()
.justify_between()
.when(self.plan_expanded, |this| {
this.border_b_1().border_color(cx.theme().colors().border)
})
.child(
h_flex()
.id("plan_summary")
.w_full()
.gap_1()
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
.child(title)
.on_click(cx.listener(|this, _, _, cx| {
this.plan_expanded = !this.plan_expanded;
cx.notify();
})),
)
}
fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
let element = h_flex()
.py_1()
.px_2()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.when(index < plan.entries.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.child(
h_flex()
.id(("plan_entry", index))
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element(),
acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"running",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
.color(Color::Success)
.into_any_element(),
})
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
);
Some(element)
}))
}
fn render_edits_summary(
&self,
action_log: &Entity<ActionLog>,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@ -1678,7 +1819,7 @@ impl AcpThreadView {
)
}
fn render_edits_bar_files(
fn render_edited_files(
&self,
action_log: &Entity<ActionLog>,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@ -1831,7 +1972,7 @@ impl AcpThreadView {
fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let focus_handle = self.message_editor.focus_handle(cx);
let editor_bg_color = cx.theme().colors().editor_background;
let (expand_icon, expand_tooltip) = if self.editor_is_expanded {
let (expand_icon, expand_tooltip) = if self.editor_expanded {
(IconName::Minimize, "Minimize Message Editor")
} else {
(IconName::Maximize, "Expand Message Editor")
@ -1844,7 +1985,7 @@ impl AcpThreadView {
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(editor_bg_color)
.when(self.editor_is_expanded, |this| {
.when(self.editor_expanded, |this| {
this.h(vh(0.8, window)).size_full().justify_between()
})
.child(
@ -2243,7 +2384,7 @@ impl Render for AcpThreadView {
.child(LoadingLabel::new("").size(LabelSize::Small))
.into(),
})
.children(self.render_edits_bar(&thread, window, cx))
.children(self.render_activity_bar(&thread, window, cx))
} else {
this.child(self.render_empty_state(cx))
}
@ -2409,3 +2550,27 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd
..Default::default()
}
}
fn plan_label_markdown_style(
status: &acp::PlanEntryStatus,
window: &Window,
cx: &App,
) -> MarkdownStyle {
let default_md_style = default_markdown_style(false, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
color: cx.theme().colors().text_muted,
strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
Some(gpui::StrikethroughStyle {
thickness: px(1.),
color: Some(cx.theme().colors().text_muted.opacity(0.8)),
})
} else {
None
},
..default_md_style.base_text_style
},
..default_md_style
}
}