From 6a2baed5ae609ab720de631bba6506563cd4ebdd 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 f5c91cf342..a3c87c8d66 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 2778a5b4a2..0c2ecf4bbe 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,