diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index fea3236e1697e3af189da2e6a0f14d70a6f1c6f6..be681a846f7963950370095f50095160649d1fcd 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -102,6 +102,7 @@ impl UserMessage { pub struct AssistantMessage { pub chunks: Vec, pub indented: bool, + pub is_subagent_output: bool, } impl AssistantMessage { @@ -983,7 +984,7 @@ pub enum AcpThreadEvent { ToolAuthorizationReceived(acp::ToolCallId), Retry(RetryStatus), SubagentSpawned(acp::SessionId), - Stopped, + Stopped(acp::StopReason), Error, LoadError(LoadError), PromptCapabilitiesUpdated, @@ -1425,6 +1426,7 @@ impl AcpThread { && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks, indented: existing_indented, + is_subagent_output: _, }) = last_entry && *existing_indented == indented { @@ -1456,6 +1458,7 @@ impl AcpThread { AgentThreadEntry::AssistantMessage(AssistantMessage { chunks: vec![chunk], indented, + is_subagent_output: false, }), cx, ); @@ -2033,7 +2036,7 @@ impl AcpThread { } } - cx.emit(AcpThreadEvent::Stopped); + cx.emit(AcpThreadEvent::Stopped(r.stop_reason)); Ok(Some(r)) } Err(e) => { @@ -2549,6 +2552,16 @@ impl AcpThread { self.terminals.insert(terminal_id.clone(), entity.clone()); entity } + + pub fn mark_as_subagent_output(&mut self, cx: &mut Context) { + for entry in self.entries.iter_mut().rev() { + if let AgentThreadEntry::AssistantMessage(assistant_message) = entry { + assistant_message.is_subagent_output = true; + cx.notify(); + return; + } + } + } } fn markdown_for_raw_output( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b02af97881cff92714641b7f4e3fd10601e0685f..8fa68b0c510c086d7c6e224b24675e6f19344b82 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1403,7 +1403,7 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped => { + AcpThreadEvent::Stopped(_) => { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::Refusal => { diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/connection_view.rs index df06ed2bae7f77cfb366f3499097ab8c43bdf78c..f5efa8aa2834829630bd60dd3ef012a92a33cb17 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/crates/agent_ui/src/connection_view.rs @@ -182,7 +182,7 @@ impl Conversation { | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::Retry(_) | AcpThreadEvent::SubagentSpawned(_) - | AcpThreadEvent::Stopped + | AcpThreadEvent::Stopped(_) | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::PromptCapabilitiesUpdated @@ -1190,13 +1190,18 @@ impl ConnectionView { }); } } - AcpThreadEvent::Stopped => { + AcpThreadEvent::Stopped(stop_reason) => { if let Some(active) = self.thread_view(&thread_id) { active.update(cx, |active, _cx| { active.thread_retry_status.take(); }); } if is_subagent { + if *stop_reason == acp::StopReason::EndTurn { + thread.update(cx, |thread, cx| { + thread.mark_as_subagent_output(cx); + }); + } return; } diff --git a/crates/agent_ui/src/connection_view/thread_view.rs b/crates/agent_ui/src/connection_view/thread_view.rs index 9578a0752b45ea48477f4fab7935f670f84c25d5..777a54312e8d4c35a100c6c1f7e5ac446613c4b9 100644 --- a/crates/agent_ui/src/connection_view/thread_view.rs +++ b/crates/agent_ui/src/connection_view/thread_view.rs @@ -3711,6 +3711,7 @@ impl ThreadView { AgentThreadEntry::AssistantMessage(AssistantMessage { chunks, indented: _, + is_subagent_output: _, }) => { let mut is_blank = true; let is_last = entry_ix + 1 == total_entries; @@ -3783,6 +3784,42 @@ impl ThreadView { .into_any(), }; + let is_subagent_output = self.is_subagent() + && matches!(entry, AgentThreadEntry::AssistantMessage(msg) if msg.is_subagent_output); + + let primary = if is_subagent_output { + v_flex() + .w_full() + .child( + h_flex() + .id("subagent_output") + .px_5() + .py_1() + .gap_2() + .child(Divider::horizontal()) + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::ForwardArrowUp) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child( + Label::new("Subagent Output") + .size(LabelSize::Custom(self.tool_name_font_size())) + .color(Color::Muted), + ), + ) + .child(Divider::horizontal()) + .tooltip(Tooltip::text("Everything below this line was sent as output from this subagent to the main agent.")), + ) + .child(primary) + .into_any_element() + } else { + primary + }; + let primary = if is_indented { let line_top = if is_first_indented { rems_from_px(-12.0) @@ -6397,12 +6434,9 @@ impl ThreadView { let session_id = thread.read(cx).session_id().clone(); this.when(is_expanded, |this| { this.child(self.render_subagent_expanded_content( - active_session_id, - entry_ix, thread_view, is_running, tool_call, - focus_handle, window, cx, )) @@ -6442,12 +6476,9 @@ impl ThreadView { fn render_subagent_expanded_content( &self, - active_session_id: &acp::SessionId, - entry_ix: usize, thread_view: &Entity, is_running: bool, tool_call: &ToolCall, - focus_handle: &FocusHandle, window: &Window, cx: &Context, ) -> impl IntoElement { @@ -6456,103 +6487,139 @@ impl ThreadView { let subagent_view = thread_view.read(cx); let session_id = subagent_view.thread.read(cx).session_id().clone(); - let base_container = || { - div() - .id(format!("subagent-content-{}", session_id)) - .relative() - .w_full() - .h_56() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .overflow_hidden() - }; + let is_canceled_or_failed = matches!( + tool_call.status, + ToolCallStatus::Canceled | ToolCallStatus::Failed | ToolCallStatus::Rejected + ); let editor_bg = cx.theme().colors().editor_background; - let overlay = || { + let overlay = { div() .absolute() .inset_0() .size_full() .bg(linear_gradient( 180., - linear_color_stop(editor_bg, 0.), + linear_color_stop(editor_bg.opacity(0.5), 0.), linear_color_stop(editor_bg.opacity(0.), 0.1), )) .block_mouse_except_scroll() }; - let show_thread_entries = is_running || tool_call.content.is_empty(); - - if show_thread_entries { - let scroll_handle = self - .subagent_scroll_handles - .borrow_mut() - .entry(session_id.clone()) - .or_default() - .clone(); - if is_running { - scroll_handle.scroll_to_bottom(); - } + let entries = subagent_view.thread.read(cx).entries(); + let total_entries = entries.len(); + let start_ix = total_entries.saturating_sub(MAX_PREVIEW_ENTRIES); - let entries = subagent_view.thread.read(cx).entries(); - let total_entries = entries.len(); - let start_ix = total_entries.saturating_sub(MAX_PREVIEW_ENTRIES); + let scroll_handle = self + .subagent_scroll_handles + .borrow_mut() + .entry(session_id.clone()) + .or_default() + .clone(); + if is_running { + scroll_handle.scroll_to_bottom(); + } - let rendered_entries: Vec = entries[start_ix..] - .iter() - .enumerate() - .map(|(i, entry)| { - let actual_ix = start_ix + i; - subagent_view.render_entry(actual_ix, total_entries + 1, entry, window, cx) - }) - .collect(); + let rendered_entries: Vec = entries[start_ix..] + .iter() + .enumerate() + .map(|(i, entry)| { + let actual_ix = start_ix + i; + subagent_view.render_entry(actual_ix, total_entries + 1, entry, window, cx) + }) + .collect(); - base_container() - .child( - div() - .id(format!("subagent-entries-{}", session_id)) - .size_full() - .track_scroll(&scroll_handle) - .pb_1() - .children(rendered_entries), - ) - .child(overlay()) - .into_any_element() - } else { - base_container() - .child( - v_flex() - .id(format!("subagent-done-content-{}", session_id)) - .size_full() - .justify_end() - .children(tool_call.content.iter().enumerate().map( - |(content_ix, content)| { - div().p_2().child(self.render_tool_call_content( - active_session_id, - entry_ix, - content, - content_ix, - tool_call, - true, - false, - matches!( - tool_call.status, - ToolCallStatus::Failed - | ToolCallStatus::Rejected - | ToolCallStatus::Canceled - ), - focus_handle, - window, - cx, - )) - }, - )), - ) - .child(overlay()) - .into_any_element() + let error_message = + self.subagent_error_message(subagent_view, &tool_call.status, tool_call, cx); + + let parent_thread = self.thread.read(cx); + let mut started_subagent_count = 0usize; + let mut turn_has_our_call = false; + for entry in parent_thread.entries().iter() { + match entry { + AgentThreadEntry::UserMessage(_) => { + if turn_has_our_call { + break; + } + started_subagent_count = 0; + turn_has_our_call = false; + } + AgentThreadEntry::ToolCall(tc) + if tc.is_subagent() && !matches!(tc.status, ToolCallStatus::Pending) => + { + started_subagent_count += 1; + if tc.id == tool_call.id { + turn_has_our_call = true; + } + } + _ => {} + } } + + v_flex() + .relative() + .w_full() + .border_t_1() + .when(is_canceled_or_failed, |this| this.border_dashed()) + .border_color(self.tool_card_border_color(cx)) + .overflow_hidden() + .child( + div() + .id(format!("subagent-entries-{}", session_id)) + .flex_1() + .min_h_0() + .pb_1() + .overflow_hidden() + .track_scroll(&scroll_handle) + .children(rendered_entries), + ) + .when_some(error_message, |this, message| { + this.child( + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title(message), + ) + }) + .when(started_subagent_count > 1, |this| { + this.h_56().child(overlay) + }) + .into_any_element() } + fn subagent_error_message( + &self, + subagent_view: &ThreadView, + status: &ToolCallStatus, + tool_call: &ToolCall, + cx: &App, + ) -> Option { + if matches!(status, ToolCallStatus::Canceled | ToolCallStatus::Rejected) { + return None; + } + + subagent_view + .thread_error + .as_ref() + .and_then(|e| match e { + ThreadError::Refusal => Some("The agent refused to respond to this prompt.".into()), + ThreadError::Other { message, .. } => Some(message.clone()), + ThreadError::PaymentRequired | ThreadError::AuthenticationRequired(_) => None, + }) + .or_else(|| { + tool_call.content.iter().find_map(|content| { + if let ToolCallContent::ContentBlock(block) = content { + if let acp_thread::ContentBlock::Markdown { markdown } = block { + let source = markdown.read(cx).source().to_string(); + if !source.is_empty() { + return Some(SharedString::from(source)); + } + } + } + None + }) + }) + } fn render_rules_item(&self, cx: &Context) -> Option { let project_context = self .as_native_thread(cx)?