mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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: <img width="600" height="1858" alt="image" src="https://github.com/user-attachments/assets/43ad4eb0-49d0-488c-bbbf-ab7956c1dd5a" /> 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
This commit is contained in:
parent
d3f5fc8466
commit
73226701c0
4 changed files with 130 additions and 12 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue