From d3f5fc8466444c21332bfd70ba709d92c1903c88 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Thu, 26 Mar 2026 18:23:03 -0300
Subject: [PATCH] agent_ui: Display an activity bar for subagents waiting for
permission (#52460)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes https://github.com/zed-industries/zed/issues/52346
Given the parallel nature of subagents calls, it's possible that there
is a subagent way out of view that's waiting for the user to give
permissions. Right now, it's kind of hard to know this and you may think
something wrong is happening given the thread generation isn't making
any progress.
This PR adds an "activity bar" to the thread view that displays
subagents on a "waiting for confirmation" status. We display the
subagent's summary label as well as allow clicking on it to quickly
scrolling to that subagent.
Release Notes:
- Agent: Improved the experience of interacting with subagents waiting
for confirmation.
---
crates/agent_ui/src/conversation_view.rs | 14 ++
.../src/conversation_view/thread_view.rs | 130 +++++++++++++++++-
2 files changed, 143 insertions(+), 1 deletion(-)
diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs
index f5c91cf342c69badf2915e21c17f819963416ec5..a3c87c8d66031f553bcd4cb8dc82c681a0b79c94 100644
--- a/crates/agent_ui/src/conversation_view.rs
+++ b/crates/agent_ui/src/conversation_view.rs
@@ -237,6 +237,20 @@ impl Conversation {
))
}
+ pub fn subagents_awaiting_permission(&self, cx: &App) -> Vec<(acp::SessionId, usize)> {
+ self.permission_requests
+ .iter()
+ .filter_map(|(session_id, tool_call_ids)| {
+ let thread = self.threads.get(session_id)?;
+ if thread.read(cx).parent_session_id().is_some() && !tool_call_ids.is_empty() {
+ Some((session_id.clone(), tool_call_ids.len()))
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+
pub fn authorize_pending_tool_call(
&mut self,
session_id: &acp::SessionId,
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index 2778a5b4a2583a0b232f86184f33c4446bc18ea5..0c2ecf4bbefdbc2eb0431c0d7c094dc9f5b2155b 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -2155,7 +2155,14 @@ impl ThreadView {
let plan = thread.plan();
let queue_is_empty = !self.has_queued_messages();
- if changed_buffers.is_empty() && plan.is_empty() && queue_is_empty {
+ let subagents_awaiting_permission = self.render_subagents_awaiting_permission(cx);
+ let has_subagents_awaiting = subagents_awaiting_permission.is_some();
+
+ if changed_buffers.is_empty()
+ && plan.is_empty()
+ && queue_is_empty
+ && !has_subagents_awaiting
+ {
return None;
}
@@ -2183,6 +2190,14 @@ impl ThreadView {
blur_radius: px(2.),
spread_radius: px(0.),
}])
+ .when_some(subagents_awaiting_permission, |this, element| {
+ this.child(element)
+ })
+ .when(
+ has_subagents_awaiting
+ && (!plan.is_empty() || !changed_buffers.is_empty() || !queue_is_empty),
+ |this| this.child(Divider::horizontal().color(DividerColor::Border)),
+ )
.when(!plan.is_empty(), |this| {
this.child(self.render_plan_summary(plan, window, cx))
.when(plan_expanded, |parent| {
@@ -2442,6 +2457,119 @@ impl ThreadView {
)
}
+ fn render_subagents_awaiting_permission(&self, cx: &Context) -> Option {
+ let awaiting = self.conversation.read(cx).subagents_awaiting_permission(cx);
+
+ if awaiting.is_empty() {
+ return None;
+ }
+
+ let thread = self.thread.read(cx);
+ let entries = thread.entries();
+ let mut subagent_items: Vec<(SharedString, usize)> = Vec::new();
+
+ for (session_id, _) in &awaiting {
+ for (entry_ix, entry) in entries.iter().enumerate() {
+ if let AgentThreadEntry::ToolCall(tool_call) = entry {
+ if let Some(info) = &tool_call.subagent_session_info {
+ if &info.session_id == session_id {
+ let subagent_summary: SharedString = {
+ let summary_text = tool_call.label.read(cx).source().to_string();
+ if !summary_text.is_empty() {
+ summary_text.into()
+ } else {
+ "Subagent".into()
+ }
+ };
+ subagent_items.push((subagent_summary, entry_ix));
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if subagent_items.is_empty() {
+ return None;
+ }
+
+ let item_count = subagent_items.len();
+
+ Some(
+ v_flex()
+ .child(
+ h_flex()
+ .py_1()
+ .px_2()
+ .w_full()
+ .gap_1()
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Label::new("Subagents Awaiting Permission:")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new(item_count.to_string()).size(LabelSize::Small)),
+ )
+ .child(
+ v_flex().children(subagent_items.into_iter().enumerate().map(
+ |(ix, (label, entry_ix))| {
+ let is_last = ix == item_count - 1;
+ let group = format!("group-{}", entry_ix);
+
+ h_flex()
+ .cursor_pointer()
+ .id(format!("subagent-permission-{}", entry_ix))
+ .group(&group)
+ .p_1()
+ .pl_2()
+ .min_w_0()
+ .w_full()
+ .gap_1()
+ .justify_between()
+ .bg(cx.theme().colors().editor_background)
+ .hover(|s| s.bg(cx.theme().colors().element_hover))
+ .when(!is_last, |this| {
+ this.border_b_1().border_color(cx.theme().colors().border)
+ })
+ .child(
+ h_flex()
+ .gap_1p5()
+ .child(
+ Icon::new(IconName::Circle)
+ .size(IconSize::XSmall)
+ .color(Color::Warning),
+ )
+ .child(
+ Label::new(label)
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .truncate(),
+ ),
+ )
+ .child(
+ div().visible_on_hover(&group).child(
+ Label::new("Scroll to Subagent")
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .truncate(),
+ ),
+ )
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.list_state.scroll_to(ListOffset {
+ item_ix: entry_ix,
+ offset_in_item: px(0.0),
+ });
+ cx.notify();
+ }))
+ },
+ )),
+ )
+ .into_any(),
+ )
+ }
+
fn render_message_queue_summary(
&self,
_window: &mut Window,