agent_ui: Show permission popover when inline prompt is above the viewport (#58081)

Follow up to https://github.com/zed-industries/zed/pull/57632, uses
changes from https://github.com/zed-industries/zed/pull/58061

Previously the floating permission popover only appeared when the inline
permission prompt was scrolled below the viewport. It now also appears
when the prompt is scrolled above the viewport, with the scroll button
pointing in the right direction.

Release Notes:

- Fixed the agent permission popover not appearing when the inline
prompt was scrolled above the viewport.
This commit is contained in:
Smit Barmase 2026-05-29 20:41:25 +05:30 committed by GitHub
parent c029cc4354
commit 906bff792c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 81 additions and 15 deletions

View file

@ -8113,9 +8113,17 @@ pub(crate) mod tests {
async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) { async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let (_view, thread_view, _entry_ix, cx) = let (_view, thread_view, entry_ix, cx) =
setup_pending_permission_thread("perm-no-bounds", cx).await; setup_pending_permission_thread("perm-no-bounds", cx).await;
// Pin the scroll top to the entry so it isn't treated as above the
// viewport, forcing the unmeasured-bounds path we want to exercise.
thread_view.read_with(cx, |view, _cx| {
view.list_state.scroll_to(ListOffset {
item_ix: entry_ix,
offset_in_item: px(0.0),
});
});
thread_view.update_in(cx, |view, window, cx| { thread_view.update_in(cx, |view, window, cx| {
assert!( assert!(
view.render_main_agent_awaiting_permission(window, cx) view.render_main_agent_awaiting_permission(window, cx)
@ -8176,8 +8184,8 @@ pub(crate) mod tests {
let (_view, thread_view, entry_ix, cx) = let (_view, thread_view, entry_ix, cx) =
setup_pending_permission_thread("perm-scroll", cx).await; setup_pending_permission_thread("perm-scroll", cx).await;
// Start off-screen below the viewport — row visible because the item // Start off-screen below the viewport. The row is visible because the
// has bounds that do not intersect the viewport. // item has bounds that do not intersect the viewport.
draw_thread_list_at( draw_thread_list_at(
&thread_view, &thread_view,
ListOffset { ListOffset {
@ -8221,6 +8229,69 @@ pub(crate) mod tests {
}); });
} }
#[gpui::test]
async fn test_permission_row_shown_when_inline_prompt_is_above_viewport(
cx: &mut TestAppContext,
) {
init_test(cx);
let (_view, thread_view, entry_ix, cx) =
setup_pending_permission_thread("perm-above", cx).await;
let thread = thread_view.read_with(cx, |view, _cx| view.thread.clone());
thread.update(cx, |thread, cx| {
let result = thread.handle_session_update(
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"More content".into(),
)),
cx,
);
assert!(
result.is_ok(),
"following assistant message should be accepted"
);
});
draw_thread_list_at(
&thread_view,
ListOffset {
item_ix: entry_ix + 1,
offset_in_item: px(0.0),
},
cx,
);
thread_view.read_with(cx, |view, _cx| {
assert!(
entry_ix < view.list_state.logical_scroll_top().item_ix,
"The tool call entry should be above the logical scroll top"
);
});
thread_view.update_in(cx, |view, window, cx| {
assert!(
view.render_main_agent_awaiting_permission(window, cx)
.is_some(),
"Floating row should be visible when the inline prompt is above the viewport"
);
});
// Scrolling up to the entry brings it back into view.
draw_thread_list_at(
&thread_view,
ListOffset {
item_ix: entry_ix,
offset_in_item: px(0.0),
},
cx,
);
thread_view.update_in(cx, |view, window, cx| {
assert!(
view.render_main_agent_awaiting_permission(window, cx)
.is_none(),
"Floating row should disappear after scrolling brings the inline prompt into view"
);
});
}
#[gpui::test] #[gpui::test]
async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) { async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);

View file

@ -3047,15 +3047,6 @@ impl ThreadView {
) )
} }
/// Returns true when the entry has been measured and sits entirely below
/// the current viewport.
fn entry_is_below_viewport(&self, entry_ix: usize) -> bool {
let viewport_bounds = self.list_state.viewport_bounds();
self.list_state
.bounds_for_item(entry_ix)
.is_some_and(|entry_bounds| entry_bounds.top() >= viewport_bounds.bottom())
}
pub(crate) fn render_main_agent_awaiting_permission( pub(crate) fn render_main_agent_awaiting_permission(
&self, &self,
window: &Window, window: &Window,
@ -3073,9 +3064,13 @@ impl ThreadView {
let thread = self.thread.read(cx); let thread = self.thread.read(cx);
let (entry_ix, tool_call) = thread.tool_call(&tool_call_id)?; let (entry_ix, tool_call) = thread.tool_call(&tool_call_id)?;
if !self.entry_is_below_viewport(entry_ix) { let scroll_icon = if self.list_state.item_is_above_viewport(entry_ix)? {
IconName::ArrowUp
} else if self.list_state.item_is_below_viewport(entry_ix)? {
IconName::ArrowDown
} else {
return None; return None;
} };
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
@ -3118,7 +3113,7 @@ impl ThreadView {
Button::new("main-agent-permission-scroll-to", "Scroll") Button::new("main-agent-permission-scroll-to", "Scroll")
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
.end_icon( .end_icon(
Icon::new(IconName::ArrowDown) Icon::new(scroll_icon)
.size(IconSize::XSmall) .size(IconSize::XSmall)
.color(Color::Default), .color(Color::Default),
) )