agent: Show full subagent output if no concurrent tool calls (#50478)

Bennet Bo Fenner and Ben Brandt created

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Change summary

crates/acp_thread/src/acp_thread.rs                | 114 ++++++++------
crates/agent/src/agent.rs                          |   6 
crates/agent/src/tests/mod.rs                      |  49 ++++-
crates/agent/src/thread.rs                         |  16 +
crates/agent/src/tools/spawn_agent_tool.rs         | 123 ++++++++++-----
crates/agent_ui/src/connection_view.rs             |   5 
crates/agent_ui/src/connection_view/thread_view.rs | 102 ++++++++-----
crates/agent_ui/src/entry_view_state.rs            |  40 ++--
8 files changed, 284 insertions(+), 171 deletions(-)

Detailed changes

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<acp::Meta>) -> Option<SharedString> {
-    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<acp::Meta>) -> Option<acp::SessionId> {
-    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<acp::Meta>) -> Option<SharedString> {
+    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<usize>,
+}
+
+/// Helper to extract subagent session id from ACP meta
+pub fn subagent_session_info_from_meta(meta: &Option<acp::Meta>) -> Option<SubagentSessionInfo> {
+    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<UserMessageId>,
@@ -223,7 +231,7 @@ pub struct ToolCall {
     pub raw_input_markdown: Option<Entity<Markdown>>,
     pub raw_output: Option<serde_json::Value>,
     pub tool_name: Option<SharedString>,
-    pub subagent_session_id: Option<acp::SessionId>,
+    pub subagent_session_info: Option<SubagentSessionInfo>,
 }
 
 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,
         })

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<Result<String>> {
         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")

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<Task<String>>,
+    send_task: Shared<Task<String>>,
 }
 
 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<Result<String>> {
-        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>Think</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::<Vec<_>>()
         .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::<Vec<_>>().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::<Vec<_>>().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(),

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<Result<String>>;
 }
 
@@ -1324,7 +1329,16 @@ impl Thread {
         cx.notify();
     }
 
-    pub fn last_message(&self) -> Option<Message> {
+    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<Message> {
         if let Some(message) = self.pending_message.clone() {
             Some(Message::Agent(message))
         } else {

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<acp::SessionId>,
 }
 
-#[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<acp::SessionId>,
         error: String,
+        session_info: Option<SubagentSessionInfo>,
     },
 }
 
 impl From<SpawnAgentToolOutput> 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(())
     }

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::<Vec<_>>();

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<ThreadView>,
@@ -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<AnyElement> = entries[start_ix..]
+        let rendered_entries: Vec<AnyElement> = 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()

crates/agent_ui/src/entry_view_state.rs 🔗

@@ -126,14 +126,19 @@ impl EntryViewState {
                 let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
                 let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
 
-                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::<Editor>() {
                             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<EntityId, AnyEntity>,
+}
+
 #[derive(Debug)]
 pub enum Entry {
     UserMessage(Entity<MessageEditor>),
     AssistantMessage(AssistantMessageEntry),
-    Content(HashMap<EntityId, AnyEntity>),
+    ToolCall(ToolCallEntry),
 }
 
 impl Entry {
     pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> {
         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<MessageEditor>> {
         match self {
             Self::UserMessage(editor) => Some(editor),
-            Self::AssistantMessage(_) | Self::Content(_) => None,
+            Self::AssistantMessage(_) | Self::ToolCall(_) => None,
         }
     }
 
@@ -350,25 +360,21 @@ impl Entry {
     ) -> Option<ScrollHandle> {
         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<EntityId, AnyEntity>> {
         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,
         }
     }