Detailed changes
@@ -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,
})
@@ -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")
@@ -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(),
@@ -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 {
@@ -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(())
}
@@ -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<_>>();
@@ -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()
@@ -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,
}
}