diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index be681a846f7963950370095f50095160649d1fcd..d0e8860084acd0a4dba7daadb000ed1f80033cf2 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2,55 +2,23 @@ mod connection; mod diff; mod mention; mod terminal; - -/// Key used in ACP ToolCall meta to store the tool's programmatic name. -/// This is a workaround since ACP's ToolCall doesn't have a dedicated name field. -pub const TOOL_NAME_META_KEY: &str = "tool_name"; - -/// Key used in ACP ToolCall meta to store the session id when a subagent is spawned. -pub const SUBAGENT_SESSION_ID_META_KEY: &str = "subagent_session_id"; - -/// Helper to extract tool name from ACP meta -pub fn tool_name_from_meta(meta: &Option) -> Option { - meta.as_ref() - .and_then(|m| m.get(TOOL_NAME_META_KEY)) - .and_then(|v| v.as_str()) - .map(|s| SharedString::from(s.to_owned())) -} - -/// Helper to extract subagent session id from ACP meta -pub fn subagent_session_id_from_meta(meta: &Option) -> Option { - meta.as_ref() - .and_then(|m| m.get(SUBAGENT_SESSION_ID_META_KEY)) - .and_then(|v| v.as_str()) - .map(|s| acp::SessionId::from(s.to_string())) -} - -/// Helper to create meta with tool name -pub fn meta_with_tool_name(tool_name: &str) -> acp::Meta { - acp::Meta::from_iter([(TOOL_NAME_META_KEY.into(), tool_name.into())]) -} -use collections::HashSet; -pub use connection::*; -pub use diff::*; -use language::language_settings::FormatOnSave; -pub use mention::*; -use project::lsp_store::{FormatTrigger, LspFormatTarget}; -use serde::{Deserialize, Serialize}; -use serde_json::to_string_pretty; - -use task::{Shell, ShellBuilder}; -pub use terminal::*; - use action_log::{ActionLog, ActionLogTelemetry}; use agent_client_protocol::{self as acp}; use anyhow::{Context as _, Result, anyhow}; +use collections::HashSet; +pub use connection::*; +pub use diff::*; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; use itertools::Itertools; +use language::language_settings::FormatOnSave; use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; use markdown::Markdown; +pub use mention::*; +use project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{AgentLocation, Project, git_store::GitStoreCheckpoint}; +use serde::{Deserialize, Serialize}; +use serde_json::to_string_pretty; use std::collections::HashMap; use std::error::Error; use std::fmt::{Formatter, Write}; @@ -59,11 +27,51 @@ use std::process::ExitStatus; use std::rc::Rc; use std::time::{Duration, Instant}; use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; +use task::{Shell, ShellBuilder}; +pub use terminal::*; use text::Bias; use ui::App; use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle}; use uuid::Uuid; +/// Key used in ACP ToolCall meta to store the tool's programmatic name. +/// This is a workaround since ACP's ToolCall doesn't have a dedicated name field. +pub const TOOL_NAME_META_KEY: &str = "tool_name"; + +/// Helper to extract tool name from ACP meta +pub fn tool_name_from_meta(meta: &Option) -> Option { + meta.as_ref() + .and_then(|m| m.get(TOOL_NAME_META_KEY)) + .and_then(|v| v.as_str()) + .map(|s| SharedString::from(s.to_owned())) +} + +/// Helper to create meta with tool name +pub fn meta_with_tool_name(tool_name: &str) -> acp::Meta { + acp::Meta::from_iter([(TOOL_NAME_META_KEY.into(), tool_name.into())]) +} + +/// Key used in ACP ToolCall meta to store the session id and message indexes +pub const SUBAGENT_SESSION_INFO_META_KEY: &str = "subagent_session_info"; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SubagentSessionInfo { + /// The session id of the subagent sessiont that was spawned + pub session_id: acp::SessionId, + /// The index of the message of the start of the "turn" run by this tool call + pub message_start_index: usize, + /// The index of the output of the message that the subagent has returned + #[serde(skip_serializing_if = "Option::is_none")] + pub message_end_index: Option, +} + +/// Helper to extract subagent session id from ACP meta +pub fn subagent_session_info_from_meta(meta: &Option) -> Option { + meta.as_ref() + .and_then(|m| m.get(SUBAGENT_SESSION_INFO_META_KEY)) + .and_then(|v| serde_json::from_value(v.clone()).ok()) +} + #[derive(Debug)] pub struct UserMessage { pub id: Option, @@ -223,7 +231,7 @@ pub struct ToolCall { pub raw_input_markdown: Option>, pub raw_output: Option, pub tool_name: Option, - pub subagent_session_id: Option, + pub subagent_session_info: Option, } impl ToolCall { @@ -262,7 +270,7 @@ impl ToolCall { let tool_name = tool_name_from_meta(&tool_call.meta); - let subagent_session = subagent_session_id_from_meta(&tool_call.meta); + let subagent_session_info = subagent_session_info_from_meta(&tool_call.meta); let result = Self { id: tool_call.tool_call_id, @@ -277,7 +285,7 @@ impl ToolCall { raw_input_markdown, raw_output: tool_call.raw_output, tool_name, - subagent_session_id: subagent_session, + subagent_session_info, }; Ok(result) } @@ -310,8 +318,8 @@ impl ToolCall { self.status = status.into(); } - if let Some(subagent_session_id) = subagent_session_id_from_meta(&meta) { - self.subagent_session_id = Some(subagent_session_id); + if let Some(subagent_session_info) = subagent_session_info_from_meta(&meta) { + self.subagent_session_info = Some(subagent_session_info); } if let Some(title) = title { @@ -402,7 +410,7 @@ impl ToolCall { pub fn is_subagent(&self) -> bool { self.tool_name.as_ref().is_some_and(|s| s == "spawn_agent") - || self.subagent_session_id.is_some() + || self.subagent_session_info.is_some() } pub fn to_markdown(&self, cx: &App) -> String { @@ -1528,7 +1536,7 @@ impl AcpThread { raw_input_markdown: None, raw_output: None, tool_name: None, - subagent_session_id: None, + subagent_session_info: None, }; self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx); return Ok(()); @@ -1690,10 +1698,14 @@ impl AcpThread { pub fn tool_call_for_subagent(&self, session_id: &acp::SessionId) -> Option<&ToolCall> { self.entries.iter().find_map(|entry| match entry { - AgentThreadEntry::ToolCall(tool_call) - if tool_call.subagent_session_id.as_ref() == Some(session_id) => - { - Some(tool_call) + AgentThreadEntry::ToolCall(tool_call) => { + if let Some(subagent_session_info) = &tool_call.subagent_session_info + && &subagent_session_info.session_id == session_id + { + Some(tool_call) + } else { + None + } } _ => None, }) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index d468d529c5ac672600d5280a11a45f4f6ad1b2f9..f20c7a8f1d5c780ffd3214c8736e7c921f32d134 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1748,6 +1748,10 @@ impl SubagentHandle for NativeSubagentHandle { self.session_id.clone() } + fn num_entries(&self, cx: &App) -> usize { + self.subagent_thread.read(cx).num_messages() + } + fn send(&self, message: String, cx: &AsyncApp) -> Task> { let thread = self.subagent_thread.clone(); let acp_thread = self.acp_thread.clone(); @@ -1832,7 +1836,7 @@ impl SubagentHandle for NativeSubagentHandle { if content.is_empty() { None } else { - Some(content) + Some( content) } }) .context("No response from subagent") diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index f62edb091463860f5fcf105f2383c352660166a6..8d75aae7e2948ef9c0934a72da112b926f633941 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -159,7 +159,7 @@ impl crate::TerminalHandle for FakeTerminalHandle { struct FakeSubagentHandle { session_id: acp::SessionId, - wait_for_summary_task: Shared>, + send_task: Shared>, } impl SubagentHandle for FakeSubagentHandle { @@ -167,8 +167,12 @@ impl SubagentHandle for FakeSubagentHandle { self.session_id.clone() } + fn num_entries(&self, _cx: &App) -> usize { + unimplemented!() + } + fn send(&self, _message: String, cx: &AsyncApp) -> Task> { - let task = self.wait_for_summary_task.clone(); + let task = self.send_task.clone(); cx.background_spawn(async move { Ok(task.await) }) } } @@ -273,8 +277,17 @@ async fn test_echo(cx: &mut TestAppContext) { let events = events.collect().await; thread.update(cx, |thread, _cx| { - assert_eq!(thread.last_message().unwrap().role(), Role::Assistant); - assert_eq!(thread.last_message().unwrap().to_markdown(), "Hello\n") + assert_eq!( + thread.last_received_or_pending_message().unwrap().role(), + Role::Assistant + ); + assert_eq!( + thread + .last_received_or_pending_message() + .unwrap() + .to_markdown(), + "Hello\n" + ) }); assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); } @@ -426,9 +439,15 @@ async fn test_thinking(cx: &mut TestAppContext) { let events = events.collect().await; thread.update(cx, |thread, _cx| { - assert_eq!(thread.last_message().unwrap().role(), Role::Assistant); assert_eq!( - thread.last_message().unwrap().to_markdown(), + thread.last_received_or_pending_message().unwrap().role(), + Role::Assistant + ); + assert_eq!( + thread + .last_received_or_pending_message() + .unwrap() + .to_markdown(), indoc! {" Think Hello @@ -706,7 +725,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { thread.update(cx, |thread, _cx| { assert!( thread - .last_message() + .last_received_or_pending_message() .unwrap() .as_agent_message() .unwrap() @@ -743,7 +762,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { if let Ok(ThreadEvent::ToolCall(tool_call)) = event { thread.update(cx, |thread, _cx| { // Look for a tool use in the thread's last message - let message = thread.last_message().unwrap(); + let message = thread.last_received_or_pending_message().unwrap(); let agent_message = message.as_agent_message().unwrap(); let last_content = agent_message.content.last().unwrap(); if let AgentMessageContent::ToolUse(last_tool_use) = last_content { @@ -1213,7 +1232,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]); thread.update(cx, |thread, _cx| { - let last_message = thread.last_message().unwrap(); + let last_message = thread.last_received_or_pending_message().unwrap(); let agent_message = last_message.as_agent_message().unwrap(); let text = agent_message .content @@ -1919,7 +1938,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { .collect::>() .await; thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); + let message = thread.last_received_or_pending_message().unwrap(); let agent_message = message.as_agent_message().unwrap(); assert_eq!( agent_message.content, @@ -1988,7 +2007,7 @@ async fn test_terminal_tool_cancellation_captures_output(cx: &mut TestAppContext // Verify the tool result contains the terminal output, not just "Tool canceled by user" thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); + let message = thread.last_received_or_pending_message().unwrap(); let agent_message = message.as_agent_message().unwrap(); let tool_use = agent_message @@ -2144,7 +2163,7 @@ async fn verify_thread_recovery( let events = events.collect::>().await; thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); + let message = thread.last_received_or_pending_message().unwrap(); let agent_message = message.as_agent_message().unwrap(); assert_eq!( agent_message.content, @@ -2453,7 +2472,7 @@ async fn test_terminal_tool_stopped_via_terminal_card_button(cx: &mut TestAppCon // Verify the tool result indicates user stopped thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); + let message = thread.last_received_or_pending_message().unwrap(); let agent_message = message.as_agent_message().unwrap(); let tool_use = agent_message @@ -2548,7 +2567,7 @@ async fn test_terminal_tool_timeout_expires(cx: &mut TestAppContext) { // Verify the tool result indicates timeout, not user stopped thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); + let message = thread.last_received_or_pending_message().unwrap(); let agent_message = message.as_agent_message().unwrap(); let tool_use = agent_message @@ -3444,7 +3463,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { events.collect::>().await; thread.read_with(cx, |thread, _cx| { assert_eq!( - thread.last_message(), + thread.last_received_or_pending_message(), Some(Message::Agent(AgentMessage { content: vec![AgentMessageContent::Text("Done".into())], tool_results: IndexMap::default(), diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 63e180e7a9686991ba67e813c51b65bcc5a8bedf..64ab5795e7dadcb86f4df45e08157c851d4c7c25 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -605,7 +605,12 @@ pub trait TerminalHandle { } pub trait SubagentHandle { + /// The session ID of this subagent thread fn id(&self) -> acp::SessionId; + /// The current number of entries in the thread. + /// Useful for knowing where the next turn will begin + fn num_entries(&self, cx: &App) -> usize; + /// Runs a turn for a given message and returns both the response and the index of that output message. fn send(&self, message: String, cx: &AsyncApp) -> Task>; } @@ -1324,7 +1329,16 @@ impl Thread { cx.notify(); } - pub fn last_message(&self) -> Option { + pub fn last_message(&self) -> Option<&Message> { + self.messages.last() + } + + pub fn num_messages(&self) -> usize { + self.messages.len() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn last_received_or_pending_message(&self) -> Option { if let Some(message) = self.pending_message.clone() { Some(Message::Agent(message)) } else { diff --git a/crates/agent/src/tools/spawn_agent_tool.rs b/crates/agent/src/tools/spawn_agent_tool.rs index 7713da050996f6fb4c07d56f51a218dfb88d5db5..a54e19d6a6ebaa3422c43152ba91b03c12b16ce8 100644 --- a/crates/agent/src/tools/spawn_agent_tool.rs +++ b/crates/agent/src/tools/spawn_agent_tool.rs @@ -1,4 +1,4 @@ -use acp_thread::SUBAGENT_SESSION_ID_META_KEY; +use acp_thread::{SUBAGENT_SESSION_INFO_META_KEY, SubagentSessionInfo}; use agent_client_protocol as acp; use anyhow::Result; use gpui::{App, SharedString, Task}; @@ -24,6 +24,7 @@ use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream, ToolInput}; /// /// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories). #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] pub struct SpawnAgentToolInput { /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives") pub label: String, @@ -34,26 +35,46 @@ pub struct SpawnAgentToolInput { pub session_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] +#[serde(rename_all = "snake_case")] pub enum SpawnAgentToolOutput { Success { session_id: acp::SessionId, output: String, + session_info: SubagentSessionInfo, }, Error { #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] session_id: Option, error: String, + session_info: Option, }, } impl From for LanguageModelToolResultContent { fn from(output: SpawnAgentToolOutput) -> Self { - serde_json::to_string(&output) + match output { + SpawnAgentToolOutput::Success { + session_id, + output, + session_info: _, // Don't show this to the model + } => serde_json::to_string( + &serde_json::json!({ "session_id": session_id, "output": output }), + ) + .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}")) + .into(), + SpawnAgentToolOutput::Error { + session_id, + error, + session_info: _, // Don't show this to the model + } => serde_json::to_string( + &serde_json::json!({ "session_id": session_id, "error": error }), + ) .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}")) - .into() + .into(), + } } } @@ -106,9 +127,10 @@ impl AgentTool for SpawnAgentTool { .map_err(|e| SpawnAgentToolOutput::Error { session_id: None, error: format!("Failed to receive tool input: {e}"), + session_info: None, })?; - let (subagent, subagent_session_id) = cx.update(|cx| { + let (subagent, mut session_info) = cx.update(|cx| { let subagent = if let Some(session_id) = input.session_id { self.environment.resume_subagent(session_id, cx) } else { @@ -117,43 +139,48 @@ impl AgentTool for SpawnAgentTool { let subagent = subagent.map_err(|err| SpawnAgentToolOutput::Error { session_id: None, error: err.to_string(), + session_info: None, })?; - let subagent_session_id = subagent.id(); + let session_info = SubagentSessionInfo { + session_id: subagent.id(), + message_start_index: subagent.num_entries(cx), + message_end_index: None, + }; - event_stream.subagent_spawned(subagent_session_id.clone()); - let meta = acp::Meta::from_iter([( - SUBAGENT_SESSION_ID_META_KEY.into(), - subagent_session_id.to_string().into(), - )]); - event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta)); + event_stream.subagent_spawned(subagent.id()); + event_stream.update_fields_with_meta( + acp::ToolCallUpdateFields::new(), + Some(acp::Meta::from_iter([( + SUBAGENT_SESSION_INFO_META_KEY.into(), + serde_json::json!(&session_info), + )])), + ); - Ok((subagent, subagent_session_id)) + Ok((subagent, session_info)) })?; match subagent.send(input.message, cx).await { Ok(output) => { - event_stream.update_fields( + session_info.message_end_index = + cx.update(|cx| Some(subagent.num_entries(cx).saturating_sub(1))); + event_stream.update_fields_with_meta( acp::ToolCallUpdateFields::new().content(vec![output.clone().into()]), + Some(acp::Meta::from_iter([( + SUBAGENT_SESSION_INFO_META_KEY.into(), + serde_json::json!(&session_info), + )])), ); Ok(SpawnAgentToolOutput::Success { - session_id: subagent_session_id, + session_id: session_info.session_id.clone(), + session_info, output, }) } - Err(e) => { - let error = e.to_string(); - // workaround for now because the agent loop will always mark this as ToolCallStatus::Failed - let canceled = error == "User canceled"; - event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![ - acp::ToolCallContent::Content(acp::Content::new(error.clone()).meta( - acp::Meta::from_iter([("cancelled".into(), canceled.into())]), - )), - ])); - Err(SpawnAgentToolOutput::Error { - session_id: Some(subagent_session_id), - error, - }) - } + Err(e) => Err(SpawnAgentToolOutput::Error { + session_id: Some(session_info.session_id.clone()), + error: e.to_string(), + session_info: Some(session_info), + }), } }) } @@ -165,25 +192,29 @@ impl AgentTool for SpawnAgentTool { event_stream: ToolCallEventStream, _cx: &mut App, ) -> Result<()> { - let session_id = match &output { - SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id), - SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(), + let (content, session_info) = match output { + SpawnAgentToolOutput::Success { + output, + session_info, + .. + } => (output.into(), Some(session_info)), + SpawnAgentToolOutput::Error { + error, + session_info, + .. + } => (error.into(), session_info), }; - if let Some(session_id) = session_id { - event_stream.subagent_spawned(session_id.clone()); - let meta = acp::Meta::from_iter([( - SUBAGENT_SESSION_ID_META_KEY.into(), - session_id.to_string().into(), - )]); - event_stream.update_fields_with_meta(acp::ToolCallUpdateFields::new(), Some(meta)); - } - - let content = match &output { - SpawnAgentToolOutput::Success { output, .. } => output.into(), - SpawnAgentToolOutput::Error { error, .. } => error.into(), - }; - event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![content])); + let meta = session_info.map(|session_info| { + acp::Meta::from_iter([( + SUBAGENT_SESSION_INFO_META_KEY.into(), + serde_json::json!(&session_info), + )]) + }); + event_stream.update_fields_with_meta( + acp::ToolCallUpdateFields::new().content(vec![content]), + meta, + ); Ok(()) } diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/connection_view.rs index 9b3c3cd4270722ca309de3f18c0a61894029c3df..96b4b69eb24339003d2ce31d33ccf15437b906f3 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/crates/agent_ui/src/connection_view.rs @@ -872,7 +872,10 @@ impl ConnectionView { .entries() .iter() .filter_map(|entry| match entry { - AgentThreadEntry::ToolCall(call) => call.subagent_session_id.clone(), + AgentThreadEntry::ToolCall(call) => call + .subagent_session_info + .as_ref() + .map(|i| i.session_id.clone()), _ => None, }) .collect::>(); diff --git a/crates/agent_ui/src/connection_view/thread_view.rs b/crates/agent_ui/src/connection_view/thread_view.rs index 5b40a8a9ae76728eabbf6d45b926407a3911c3da..8a29d16b1acf165ba77093dced980a7f51fe2e37 100644 --- a/crates/agent_ui/src/connection_view/thread_view.rs +++ b/crates/agent_ui/src/connection_view/thread_view.rs @@ -3923,7 +3923,7 @@ impl ThreadView { let thread = self.thread.clone(); let comments_editor = self.thread_feedback.comments_editor.clone(); - let primary = if entry_ix == total_entries - 1 { + let primary = if entry_ix + 1 == total_entries { v_flex() .w_full() .child(primary) @@ -5002,15 +5002,20 @@ impl ThreadView { div().w_full().map(|this| { if tool_call.is_subagent() { - this.child(self.render_subagent_tool_call( - active_session_id, - entry_ix, - tool_call, - tool_call.subagent_session_id.clone(), - focus_handle, - window, - cx, - )) + this.child( + self.render_subagent_tool_call( + active_session_id, + entry_ix, + tool_call, + tool_call + .subagent_session_info + .as_ref() + .map(|i| i.session_id.clone()), + focus_handle, + window, + cx, + ), + ) } else if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -6667,6 +6672,34 @@ impl ThreadView { .into_any_element() } + /// This will return `true` if there were no other tool calls during the same turn as the given tool call (no concurrent tool calls). + fn should_show_subagent_fullscreen(&self, tool_call: &ToolCall, cx: &App) -> bool { + let parent_thread = self.thread.read(cx); + + let Some(tool_call_index) = parent_thread + .entries() + .iter() + .position(|e| matches!(e, AgentThreadEntry::ToolCall(tc) if tc.id == tool_call.id)) + else { + return false; + }; + + if let Some(AgentThreadEntry::ToolCall(_)) = + parent_thread.entries().get(tool_call_index + 1) + { + return false; + } + + if let Some(AgentThreadEntry::ToolCall(_)) = parent_thread + .entries() + .get(tool_call_index.saturating_sub(1)) + { + return false; + } + + true + } + fn render_subagent_expanded_content( &self, thread_view: &Entity, @@ -6677,29 +6710,7 @@ impl ThreadView { ) -> impl IntoElement { const MAX_PREVIEW_ENTRIES: usize = 8; - 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; - } - } - _ => {} - } - } + let should_show_subagent_fullscreen = self.should_show_subagent_fullscreen(tool_call, cx); let subagent_view = thread_view.read(cx); let session_id = subagent_view.thread.read(cx).session_id().clone(); @@ -6725,11 +6736,22 @@ impl ThreadView { let entries = subagent_view.thread.read(cx).entries(); let total_entries = entries.len(); - let start_ix = if started_subagent_count > 1 { - total_entries.saturating_sub(MAX_PREVIEW_ENTRIES) + let mut entry_range = if let Some(info) = tool_call.subagent_session_info.as_ref() { + info.message_start_index + ..info + .message_end_index + .map(|i| (i + 1).min(total_entries)) + .unwrap_or(total_entries) } else { - 0 + 0..total_entries + }; + if !should_show_subagent_fullscreen { + entry_range.start = entry_range + .end + .saturating_sub(MAX_PREVIEW_ENTRIES) + .max(entry_range.start); }; + let start_ix = entry_range.start; let scroll_handle = self .subagent_scroll_handles @@ -6741,12 +6763,14 @@ impl ThreadView { scroll_handle.scroll_to_bottom(); } - let rendered_entries: Vec = entries[start_ix..] + let rendered_entries: Vec = entries + .get(entry_range) + .unwrap_or_default() .iter() .enumerate() .map(|(i, entry)| { let actual_ix = start_ix + i; - subagent_view.render_entry(actual_ix, total_entries + 1, entry, window, cx) + subagent_view.render_entry(actual_ix, total_entries, entry, window, cx) }) .collect(); @@ -6764,7 +6788,7 @@ impl ThreadView { .track_scroll(&scroll_handle) .children(rendered_entries), ) - .when(started_subagent_count > 1, |this| { + .when(!should_show_subagent_fullscreen, |this| { this.h_56().child(overlay) }) .into_any_element() diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index b06d67f63b997e67ca891ab6238e0bd2ce94a304..071142f083bc94b5d057a366d124d11e7822d1fd 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/crates/agent_ui/src/entry_view_state.rs @@ -126,14 +126,19 @@ impl EntryViewState { let terminals = tool_call.terminals().cloned().collect::>(); let diffs = tool_call.diffs().cloned().collect::>(); - let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) { - views + let views = if let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) { + &mut tool_call.content } else { - self.set_entry(index, Entry::empty()); - let Some(Entry::Content(views)) = self.entries.get_mut(index) else { + self.set_entry( + index, + Entry::ToolCall(ToolCallEntry { + content: HashMap::default(), + }), + ); + let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) else { unreachable!() }; - views + &mut tool_call.content }; let is_tool_call_completed = @@ -250,8 +255,8 @@ impl EntryViewState { for entry in self.entries.iter() { match entry { Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} - Entry::Content(response_views) => { - for view in response_views.values() { + Entry::ToolCall(ToolCallEntry { content }) => { + for view in content.values() { if let Ok(diff_editor) = view.clone().downcast::() { diff_editor.update(cx, |diff_editor, cx| { diff_editor.set_text_style_refinement( @@ -305,25 +310,30 @@ impl AssistantMessageEntry { } } +#[derive(Debug)] +pub struct ToolCallEntry { + content: HashMap, +} + #[derive(Debug)] pub enum Entry { UserMessage(Entity), AssistantMessage(AssistantMessageEntry), - Content(HashMap), + ToolCall(ToolCallEntry), } impl Entry { pub fn focus_handle(&self, cx: &App) -> Option { match self { Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), - Self::AssistantMessage(_) | Self::Content(_) => None, + Self::AssistantMessage(_) | Self::ToolCall(_) => None, } } pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), - Self::AssistantMessage(_) | Self::Content(_) => None, + Self::AssistantMessage(_) | Self::ToolCall(_) => None, } } @@ -350,25 +360,21 @@ impl Entry { ) -> Option { match self { Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), - Self::UserMessage(_) | Self::Content(_) => None, + Self::UserMessage(_) | Self::ToolCall(_) => None, } } fn content_map(&self) -> Option<&HashMap> { match self { - Self::Content(map) => Some(map), + Self::ToolCall(ToolCallEntry { content }) => Some(content), _ => None, } } - fn empty() -> Self { - Self::Content(HashMap::default()) - } - #[cfg(test)] pub fn has_content(&self) -> bool { match self { - Self::Content(map) => !map.is_empty(), + Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(), Self::UserMessage(_) | Self::AssistantMessage(_) => false, } }