Detailed changes
@@ -160,6 +160,7 @@ pub enum AgentThreadEntry {
UserMessage(UserMessage),
AssistantMessage(AssistantMessage),
ToolCall(ToolCall),
+ CompletedPlan(Vec<PlanEntry>),
}
impl AgentThreadEntry {
@@ -168,6 +169,7 @@ impl AgentThreadEntry {
Self::UserMessage(message) => message.indented,
Self::AssistantMessage(message) => message.indented,
Self::ToolCall(_) => false,
+ Self::CompletedPlan(_) => false,
}
}
@@ -176,6 +178,14 @@ impl AgentThreadEntry {
Self::UserMessage(message) => message.to_markdown(cx),
Self::AssistantMessage(message) => message.to_markdown(cx),
Self::ToolCall(tool_call) => tool_call.to_markdown(cx),
+ Self::CompletedPlan(entries) => {
+ let mut md = String::from("## Plan\n\n");
+ for entry in entries {
+ let source = entry.content.read(cx).source().to_string();
+ md.push_str(&format!("- [x] {}\n", source));
+ }
+ md
+ }
}
}
@@ -1298,7 +1308,9 @@ impl AcpThread {
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) => return true,
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
false
@@ -1320,7 +1332,9 @@ impl AcpThread {
) if call.diffs().next().is_some() => {
return true;
}
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
@@ -1337,7 +1351,9 @@ impl AcpThread {
}) => {
return true;
}
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
@@ -1348,7 +1364,9 @@ impl AcpThread {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(..) => return false,
- AgentThreadEntry::AssistantMessage(..) => continue,
+ AgentThreadEntry::AssistantMessage(..) | AgentThreadEntry::CompletedPlan(..) => {
+ continue;
+ }
AgentThreadEntry::ToolCall(..) => return true,
}
}
@@ -2065,6 +2083,13 @@ impl AcpThread {
cx.notify();
}
+ pub fn snapshot_completed_plan(&mut self, cx: &mut Context<Self>) {
+ if !self.plan.is_empty() && self.plan.stats().pending == 0 {
+ let completed_entries = std::mem::take(&mut self.plan.entries);
+ self.push_entry(AgentThreadEntry::CompletedPlan(completed_entries), cx);
+ }
+ }
+
fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
self.plan
.entries
@@ -2223,6 +2248,10 @@ impl AcpThread {
this.mark_pending_tools_as_canceled();
}
+ if !canceled {
+ this.snapshot_completed_plan(cx);
+ }
+
// Handle refusal - distinguish between user prompt and tool call refusals
if let acp::StopReason::Refusal = r.stop_reason {
this.had_error = true;
@@ -942,6 +942,9 @@ impl NativeAgent {
NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx)
})
.await?;
+ acp_thread.update(cx, |thread, cx| {
+ thread.snapshot_completed_plan(cx);
+ });
Ok(acp_thread)
})
}
@@ -1,7 +1,10 @@
-use crate::{DEFAULT_THREAD_TITLE, SelectPermissionGranularity};
+use crate::{
+ DEFAULT_THREAD_TITLE, SelectPermissionGranularity,
+ agent_configuration::configure_context_server_modal::default_markdown_style,
+};
use std::cell::RefCell;
-use acp_thread::ContentBlock;
+use acp_thread::{ContentBlock, PlanEntry};
use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody};
use editor::actions::OpenExcerpts;
@@ -2789,6 +2792,76 @@ impl ThreadView {
.into_any_element()
}
+ fn render_completed_plan(
+ &self,
+ entries: &[PlanEntry],
+ window: &Window,
+ cx: &Context<Self>,
+ ) -> AnyElement {
+ v_flex()
+ .px_5()
+ .py_1p5()
+ .w_full()
+ .child(
+ v_flex()
+ .w_full()
+ .rounded_md()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ h_flex()
+ .px_2()
+ .py_1()
+ .gap_1()
+ .bg(self.tool_card_header_bg(cx))
+ .border_b_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ Label::new("Completed Plan")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(format!(
+ "— {} {}",
+ entries.len(),
+ if entries.len() == 1 { "step" } else { "steps" }
+ ))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ v_flex().children(entries.iter().enumerate().map(|(index, entry)| {
+ h_flex()
+ .py_1()
+ .px_2()
+ .gap_1p5()
+ .when(index < entries.len() - 1, |this| {
+ this.border_b_1().border_color(cx.theme().colors().border)
+ })
+ .child(
+ Icon::new(IconName::TodoComplete)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .child(
+ div()
+ .max_w_full()
+ .overflow_x_hidden()
+ .text_xs()
+ .text_color(cx.theme().colors().text_muted)
+ .child(MarkdownElement::new(
+ entry.content.clone(),
+ default_markdown_style(window, cx),
+ )),
+ )
+ })),
+ ),
+ )
+ .into_any()
+ }
+
fn render_edits_summary(
&self,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@@ -4546,6 +4619,9 @@ impl ThreadView {
cx,
)
.into_any(),
+ AgentThreadEntry::CompletedPlan(entries) => {
+ self.render_completed_plan(entries, window, cx)
+ }
};
let is_subagent_output = self.is_subagent()
@@ -5411,7 +5487,9 @@ impl ThreadView {
return false;
}
}
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
@@ -235,6 +235,11 @@ impl EntryViewState {
};
entry.sync(message);
}
+ AgentThreadEntry::CompletedPlan(_) => {
+ if !matches!(self.entries.get(index), Some(Entry::CompletedPlan)) {
+ self.set_entry(index, Entry::CompletedPlan);
+ }
+ }
};
}
@@ -253,7 +258,9 @@ impl EntryViewState {
pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
- Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
+ Entry::UserMessage { .. }
+ | Entry::AssistantMessage { .. }
+ | Entry::CompletedPlan => {}
Entry::ToolCall(ToolCallEntry { content }) => {
for view in content.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
@@ -320,6 +327,7 @@ pub enum Entry {
UserMessage(Entity<MessageEditor>),
AssistantMessage(AssistantMessageEntry),
ToolCall(ToolCallEntry),
+ CompletedPlan,
}
impl Entry {
@@ -327,14 +335,14 @@ impl Entry {
match self {
Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
Self::AssistantMessage(message) => Some(message.focus_handle.clone()),
- Self::ToolCall(_) => None,
+ Self::ToolCall(_) | Self::CompletedPlan => None,
}
}
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self {
Self::UserMessage(editor) => Some(editor),
- Self::AssistantMessage(_) | Self::ToolCall(_) => None,
+ Self::AssistantMessage(_) | Self::ToolCall(_) | Self::CompletedPlan => None,
}
}
@@ -361,7 +369,7 @@ impl Entry {
) -> Option<ScrollHandle> {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
- Self::UserMessage(_) | Self::ToolCall(_) => None,
+ Self::UserMessage(_) | Self::ToolCall(_) | Self::CompletedPlan => None,
}
}
@@ -376,7 +384,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(),
- Self::UserMessage(_) | Self::AssistantMessage(_) => false,
+ Self::UserMessage(_) | Self::AssistantMessage(_) | Self::CompletedPlan => false,
}
}
}