From 73226701c022acdac3dc7b20a39717705da6614b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:23:19 -0300 Subject: [PATCH] agent_ui: Move fully complete plan to the thread view (#52462) When a plan generate by the plan tool fully completes, there's no need for that to be in the activity bar anymore. It's complete and in the next turn, the agent may come up with another plan and the cycle restarts. So, this PR moves a fully complete plan to the thread view, so that it stays as part of a given turn: image The way this PR does this is by adding a new entry to `AgentThreadEntry` and snapshotting the completed plan so we can display it properly in the thread. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 37 +++++++- crates/agent/src/agent.rs | 3 + .../src/conversation_view/thread_view.rs | 84 ++++++++++++++++++- crates/agent_ui/src/entry_view_state.rs | 18 ++-- 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index df59c67bb4576e34f76539df34147fb4606bb9f3..f33732f1e0f3623df5ce6833356f3547c5781adb 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -160,6 +160,7 @@ pub enum AgentThreadEntry { UserMessage(UserMessage), AssistantMessage(AssistantMessage), ToolCall(ToolCall), + CompletedPlan(Vec), } 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) { + 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.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; diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index f36d8c0497430c27c7cafd99445c8baad18406f5..b7aa9d1e311016f572928993e049798c2b5e3bb2 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -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) }) } diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 0c2ecf4bbefdbc2eb0431c0d7c094dc9f5b2155b..4ebe196e7ca7de9c6341925676423bdc4a8d8d38 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -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, + ) -> 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>, @@ -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(_) => {} } } diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index ef5e8a9812e8266566f027365e4b270177aab71c..dfa76e3716f0b938e8ff53e0799c12dd1a657a88 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -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::() { @@ -320,6 +327,7 @@ pub enum Entry { UserMessage(Entity), 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> { 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 { 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, } } }