sidebar: Surface subagent permission requests (#53428)

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- (preview only) sidebar: Fixed issue where tool confirmation indicator
would not show up when subagent asks for permissions
This commit is contained in:
Bennet Bo Fenner authored and GitHub committed 2026-04-08 19:39:06 +01:00
1 parent 77cfe1be5d
commit 844dcfda0f
4 files changed
+127 -9

No files matched your search

+8
View File
@@ -1893,6 +1893,14 @@ impl AgentPanel {
}
}
pub fn conversation_views(&self) -> Vec<Entity<ConversationView>> {
self.active_conversation_view()
.into_iter()
.cloned()
.chain(self.background_threads.values().cloned())
.collect()
}
pub fn active_thread_view(&self, cx: &App) -> Option<Entity<ThreadView>> {
let server_view = self.active_conversation_view()?;
server_view.read(cx).active_thread().cloned()
+14
View File
@@ -354,6 +354,20 @@ impl ConversationView {
.pending_tool_call(id, cx)
}
pub fn root_thread_has_pending_tool_call(&self, cx: &App) -> bool {
let Some(root_thread) = self.root_thread(cx) else {
return false;
};
let root_id = root_thread.read(cx).id.clone();
self.as_connected().is_some_and(|connected| {
connected
.conversation
.read(cx)
.pending_tool_call(&root_id, cx)
.is_some()
})
}
pub fn root_thread(&self, cx: &App) -> Option<Entity<ThreadView>> {
match &self.server_state {
ServerState::Connected(connected) => {
+21 -8
View File
@@ -4107,11 +4107,14 @@ fn all_thread_infos_for_workspace(
return None.into_iter().flatten();
};
let agent_panel = agent_panel.read(cx);
let threads = agent_panel
.parent_threads(cx)
.conversation_views()
.into_iter()
.map(|thread_view| {
.filter_map(|conversation_view| {
let has_pending_tool_call = conversation_view
.read(cx)
.root_thread_has_pending_tool_call(cx);
let thread_view = conversation_view.read(cx).root_thread(cx)?;
let thread_view_ref = thread_view.read(cx);
let thread = thread_view_ref.thread.read(cx);
@@ -4125,7 +4128,7 @@ fn all_thread_infos_for_workspace(
let session_id = thread.session_id().clone();
let is_background = agent_panel.is_background_thread(&session_id);
let status = if thread.is_waiting_for_confirmation() {
let status = if has_pending_tool_call {
AgentThreadStatus::WaitingForConfirmation
} else if thread.had_error() {
AgentThreadStatus::Error
@@ -4138,7 +4141,7 @@ fn all_thread_infos_for_workspace(
let diff_stats = thread.action_log().read(cx).diff_stats(cx);
ActiveThreadInfo {
Some(ActiveThreadInfo {
session_id,
title,
status,
@@ -4147,7 +4150,7 @@ fn all_thread_infos_for_workspace(
is_background,
is_title_generating,
diff_stats,
}
})
});
Some(threads).into_iter().flatten()
@@ -4310,7 +4313,14 @@ fn dump_single_workspace(workspace: &Workspace, output: &mut String, cx: &gpui::
let entry_count = thread.entries().len();
write!(output, "Active thread: {title} (session: {session_id})").ok();
write!(output, " [{status}, {entry_count} entries").ok();
if thread.is_waiting_for_confirmation() {
if panel
.active_conversation_view()
.is_some_and(|conversation_view| {
conversation_view
.read(cx)
.root_thread_has_pending_tool_call(cx)
})
{
write!(output, ", awaiting confirmation").ok();
}
writeln!(output, "]").ok();
@@ -4337,7 +4347,10 @@ fn dump_single_workspace(workspace: &Workspace, output: &mut String, cx: &gpui::
let entry_count = thread.entries().len();
write!(output, " - {title} (session: {session_id})").ok();
write!(output, " [{status}, {entry_count} entries").ok();
if thread.is_waiting_for_confirmation() {
if conversation_view
.read(cx)
.root_thread_has_pending_tool_call(cx)
{
write!(output, ", awaiting confirmation").ok();
}
writeln!(output, "]").ok();
+84 -1
View File
@@ -1,5 +1,5 @@
use super::*;
use acp_thread::StubAgentConnection;
use acp_thread::{AcpThread, PermissionOptions, StubAgentConnection};
use agent::ThreadStore;
use agent_ui::{
test_support::{active_session_id, open_thread_with_connection, send_message},
@@ -189,6 +189,35 @@ fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
cx.run_until_parked();
}
fn request_test_tool_authorization(
thread: &Entity<AcpThread>,
tool_call_id: &str,
option_id: &str,
cx: &mut gpui::VisualTestContext,
) {
let tool_call_id = acp::ToolCallId::new(tool_call_id);
let label = format!("Tool {tool_call_id}");
let option_id = acp::PermissionOptionId::new(option_id);
let _authorization_task = cx.update(|_, cx| {
thread.update(cx, |thread, cx| {
thread
.request_tool_call_authorization(
acp::ToolCall::new(tool_call_id, label)
.kind(acp::ToolKind::Edit)
.into(),
PermissionOptions::Flat(vec![acp::PermissionOption::new(
option_id,
"Allow",
acp::PermissionOptionKind::AllowOnce,
)]),
cx,
)
.unwrap()
})
});
cx.run_until_parked();
}
fn format_linked_worktree_chips(worktrees: &[WorktreeInfo]) -> String {
let mut seen = Vec::new();
let mut chips = Vec::new();
@@ -1284,6 +1313,60 @@ async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_subagent_permission_request_marks_parent_sidebar_thread_waiting(
cx: &mut TestAppContext,
) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let connection = StubAgentConnection::new().with_supports_load_session(true);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Done".into()),
)]);
open_thread_with_connection(&panel, connection, cx);
send_message(&panel, cx);
let parent_session_id = active_session_id(&panel, cx);
save_test_thread_metadata(&parent_session_id, &project, cx).await;
let subagent_session_id = acp::SessionId::new("subagent-session");
cx.update(|_, cx| {
let parent_thread = panel.read(cx).active_agent_thread(cx).unwrap();
parent_thread.update(cx, |thread: &mut AcpThread, cx| {
thread.subagent_spawned(subagent_session_id.clone(), cx);
});
});
cx.run_until_parked();
let subagent_thread = panel.read_with(cx, |panel, cx| {
panel
.active_conversation_view()
.and_then(|conversation| conversation.read(cx).thread_view(&subagent_session_id))
.map(|thread_view| thread_view.read(cx).thread.clone())
.expect("Expected subagent thread to be loaded into the conversation")
});
request_test_tool_authorization(&subagent_thread, "subagent-tool-call", "allow-subagent", cx);
let parent_status = sidebar.read_with(cx, |sidebar, _cx| {
sidebar
.contents
.entries
.iter()
.find_map(|entry| match entry {
ListEntry::Thread(thread) if thread.metadata.session_id == parent_session_id => {
Some(thread.status)
}
_ => None,
})
.expect("Expected parent thread entry in sidebar")
});
assert_eq!(parent_status, AgentThreadStatus::WaitingForConfirmation);
}
#[gpui::test]
async fn test_background_thread_completion_triggers_notification(cx: &mut TestAppContext) {
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;