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

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Bennet Bo Fenner 2026-03-02 14:21:26 +01:00 committed by GitHub
parent 8cd192ec3a
commit ef60143e7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 286 additions and 173 deletions

View file

@ -2,14 +2,42 @@ mod connection;
mod diff;
mod mention;
mod 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};
use std::ops::Range;
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";
/// 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()
@ -18,51 +46,31 @@ pub fn tool_name_from_meta(meta: &Option<acp::Meta>) -> Option<SharedString> {
.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::*;
/// 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";
use action_log::{ActionLog, ActionLogTelemetry};
use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result, anyhow};
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use itertools::Itertools;
use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};
use markdown::Markdown;
use project::{AgentLocation, Project, git_store::GitStoreCheckpoint};
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Formatter, Write};
use std::ops::Range;
use std::process::ExitStatus;
use std::rc::Rc;
use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use text::Bias;
use ui::App;
use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle};
use uuid::Uuid;
#[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 {
@ -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,
})

View file

@ -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")

View file

@ -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(),

View file

@ -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 {

View file

@ -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()
.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(),
}
}
}
@ -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(())
}

View file

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

View file

@ -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()

View file

@ -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,
}
}