Add PageUp/PageDown scrolling in agent view (#52657)

Fixes #52656

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

Closes #52656 

Release Notes:

- Added keybindings for scrolling in agent view

---------

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
This commit is contained in:
Aleksei GusevandOleksiy Syvokon authored and GitHub committed 2026-04-02 18:26:18 +00:00
1 parent f29d786074
commit 4567feb749
9 files changed
+231 -18

No files matched your search

+24
View File
@@ -284,12 +284,36 @@
"context": "AcpThread",
"bindings": {
"ctrl--": "pane::GoBack",
"pageup": "agent::ScrollOutputPageUp",
"pagedown": "agent::ScrollOutputPageDown",
"home": "agent::ScrollOutputToTop",
"end": "agent::ScrollOutputToBottom",
"up": "agent::ScrollOutputLineUp",
"down": "agent::ScrollOutputLineDown",
"shift-pageup": "agent::ScrollOutputToPreviousMessage",
"shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-alt-pageup": "agent::ScrollOutputPageUp",
"ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
"ctrl-alt-home": "agent::ScrollOutputToTop",
"ctrl-alt-end": "agent::ScrollOutputToBottom",
"ctrl-alt-up": "agent::ScrollOutputLineUp",
"ctrl-alt-down": "agent::ScrollOutputLineDown",
"ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
"ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-pageup": "agent::ScrollOutputPageUp",
"ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
"ctrl-alt-home": "agent::ScrollOutputToTop",
"ctrl-alt-end": "agent::ScrollOutputToBottom",
"ctrl-alt-up": "agent::ScrollOutputLineUp",
"ctrl-alt-down": "agent::ScrollOutputLineDown",
"ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
"ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-d": "git::Diff",
"shift-alt-y": "agent::KeepAll",
+24
View File
@@ -327,12 +327,36 @@
"context": "AcpThread",
"bindings": {
"ctrl--": "pane::GoBack",
"pageup": "agent::ScrollOutputPageUp",
"pagedown": "agent::ScrollOutputPageDown",
"home": "agent::ScrollOutputToTop",
"end": "agent::ScrollOutputToBottom",
"up": "agent::ScrollOutputLineUp",
"down": "agent::ScrollOutputLineDown",
"shift-pageup": "agent::ScrollOutputToPreviousMessage",
"shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-pageup": "agent::ScrollOutputPageUp",
"ctrl-pagedown": "agent::ScrollOutputPageDown",
"ctrl-home": "agent::ScrollOutputToTop",
"ctrl-end": "agent::ScrollOutputToBottom",
"ctrl-alt-up": "agent::ScrollOutputLineUp",
"ctrl-alt-down": "agent::ScrollOutputLineDown",
"ctrl-alt-pageup": "agent::ScrollOutputToPreviousMessage",
"ctrl-alt-pagedown": "agent::ScrollOutputToNextMessage",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-pageup": "agent::ScrollOutputPageUp",
"ctrl-pagedown": "agent::ScrollOutputPageDown",
"ctrl-home": "agent::ScrollOutputToTop",
"ctrl-end": "agent::ScrollOutputToBottom",
"ctrl-alt-up": "agent::ScrollOutputLineUp",
"ctrl-alt-down": "agent::ScrollOutputLineDown",
"ctrl-alt-pageup": "agent::ScrollOutputToPreviousMessage",
"ctrl-alt-pagedown": "agent::ScrollOutputToNextMessage",
"shift-ctrl-r": "agent::OpenAgentDiff",
"shift-ctrl-d": "git::Diff",
"shift-alt-y": "agent::KeepAll",
+24
View File
@@ -285,12 +285,36 @@
"context": "AcpThread",
"bindings": {
"ctrl--": "pane::GoBack",
"pageup": "agent::ScrollOutputPageUp",
"pagedown": "agent::ScrollOutputPageDown",
"home": "agent::ScrollOutputToTop",
"end": "agent::ScrollOutputToBottom",
"up": "agent::ScrollOutputLineUp",
"down": "agent::ScrollOutputLineDown",
"shift-pageup": "agent::ScrollOutputToPreviousMessage",
"shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-alt-pageup": "agent::ScrollOutputPageUp",
"ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
"ctrl-alt-home": "agent::ScrollOutputToTop",
"ctrl-alt-end": "agent::ScrollOutputToBottom",
"ctrl-alt-up": "agent::ScrollOutputLineUp",
"ctrl-alt-down": "agent::ScrollOutputLineDown",
"ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
"ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-pageup": "agent::ScrollOutputPageUp",
"ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
"ctrl-alt-home": "agent::ScrollOutputToTop",
"ctrl-alt-end": "agent::ScrollOutputToBottom",
"ctrl-alt-up": "agent::ScrollOutputLineUp",
"ctrl-alt-down": "agent::ScrollOutputLineDown",
"ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
"ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-d": "git::Diff",
"shift-alt-y": "agent::KeepAll",
+16
View File
@@ -173,6 +173,22 @@ actions!(
ToggleThinkingEffortMenu,
/// Toggles fast mode for models that support it.
ToggleFastMode,
/// Scroll the output by one page up.
ScrollOutputPageUp,
/// Scroll the output by one page down.
ScrollOutputPageDown,
/// Scroll the output up by three lines.
ScrollOutputLineUp,
/// Scroll the output down by three lines.
ScrollOutputLineDown,
/// Scroll the output to the top.
ScrollOutputToTop,
/// Scroll the output to the bottom.
ScrollOutputToBottom,
/// Scroll the output to the previous user message.
ScrollOutputToPreviousMessage,
/// Scroll the output to the next user message.
ScrollOutputToNextMessage,
]
);
+5 -2
View File
@@ -85,8 +85,11 @@ use crate::{
AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector,
CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread,
OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce,
RemoveFirstQueuedMessage, SendImmediately, SendNextQueuedMessage, ToggleFastMode,
ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject,
RemoveFirstQueuedMessage, ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown,
ScrollOutputPageUp, ScrollOutputToBottom, ScrollOutputToNextMessage,
ScrollOutputToPreviousMessage, ScrollOutputToTop, SendImmediately, SendNextQueuedMessage,
ToggleFastMode, ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode,
UndoLastReject,
};
const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
@@ -552,17 +552,10 @@ impl ThreadView {
let scroll_top = list_state.logical_scroll_top();
let _ = thread_view.update(cx, |this, cx| {
if !is_following_tail {
let is_at_bottom = {
let current_offset =
list_state.scroll_px_offset_for_scrollbar().y.abs();
let max_offset = list_state.max_offset_for_scrollbar().y;
current_offset >= max_offset - px(1.0)
};
let is_generating =
matches!(this.thread.read(cx).status(), ThreadStatus::Generating);
if is_at_bottom && is_generating {
if list_state.is_at_bottom() && is_generating {
list_state.set_follow_tail(true);
}
}
@@ -4952,7 +4945,7 @@ impl ThreadView {
}
pub fn scroll_to_end(&mut self, cx: &mut Context<Self>) {
self.list_state.scroll_to_end();
self.list_state.set_follow_tail(true);
cx.notify();
}
@@ -4974,10 +4967,122 @@ impl ThreadView {
}
pub(crate) fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
self.list_state.set_follow_tail(false);
self.list_state.scroll_to(ListOffset::default());
cx.notify();
}
fn scroll_output_page_up(
&mut self,
_: &ScrollOutputPageUp,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let page_height = self.list_state.viewport_bounds().size.height;
self.list_state.set_follow_tail(false);
self.list_state.scroll_by(-page_height * 0.9);
cx.notify();
}
fn scroll_output_page_down(
&mut self,
_: &ScrollOutputPageDown,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let page_height = self.list_state.viewport_bounds().size.height;
self.list_state.set_follow_tail(false);
self.list_state.scroll_by(page_height * 0.9);
if self.list_state.is_at_bottom() {
self.list_state.set_follow_tail(true);
}
cx.notify();
}
fn scroll_output_line_up(
&mut self,
_: &ScrollOutputLineUp,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.list_state.set_follow_tail(false);
self.list_state.scroll_by(-window.line_height() * 3.);
cx.notify();
}
fn scroll_output_line_down(
&mut self,
_: &ScrollOutputLineDown,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.list_state.set_follow_tail(false);
self.list_state.scroll_by(window.line_height() * 3.);
if self.list_state.is_at_bottom() {
self.list_state.set_follow_tail(true);
}
cx.notify();
}
fn scroll_output_to_top(
&mut self,
_: &ScrollOutputToTop,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.scroll_to_top(cx);
}
fn scroll_output_to_bottom(
&mut self,
_: &ScrollOutputToBottom,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.scroll_to_end(cx);
}
fn scroll_output_to_previous_message(
&mut self,
_: &ScrollOutputToPreviousMessage,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let entries = self.thread.read(cx).entries();
let current_ix = self.list_state.logical_scroll_top().item_ix;
if let Some(target_ix) = (0..current_ix)
.rev()
.find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
{
self.list_state.set_follow_tail(false);
self.list_state.scroll_to(ListOffset {
item_ix: target_ix,
offset_in_item: px(0.),
});
cx.notify();
}
}
fn scroll_output_to_next_message(
&mut self,
_: &ScrollOutputToNextMessage,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let entries = self.thread.read(cx).entries();
let current_ix = self.list_state.logical_scroll_top().item_ix;
if let Some(target_ix) = (current_ix + 1..entries.len())
.find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
{
self.list_state.set_follow_tail(false);
self.list_state.scroll_to(ListOffset {
item_ix: target_ix,
offset_in_item: px(0.),
});
cx.notify();
}
}
pub fn open_thread_as_markdown(
&self,
workspace: Entity<Workspace>,
@@ -8541,6 +8646,14 @@ impl Render for ThreadView {
.on_action(cx.listener(Self::handle_toggle_command_pattern))
.on_action(cx.listener(Self::open_permission_dropdown))
.on_action(cx.listener(Self::open_add_context_menu))
.on_action(cx.listener(Self::scroll_output_page_up))
.on_action(cx.listener(Self::scroll_output_page_down))
.on_action(cx.listener(Self::scroll_output_line_up))
.on_action(cx.listener(Self::scroll_output_line_down))
.on_action(cx.listener(Self::scroll_output_to_top))
.on_action(cx.listener(Self::scroll_output_to_bottom))
.on_action(cx.listener(Self::scroll_output_to_previous_message))
.on_action(cx.listener(Self::scroll_output_to_next_message))
.on_action(cx.listener(|this, _: &ToggleFastMode, _window, cx| {
this.toggle_fast_mode(cx);
}))
+7
View File
@@ -427,6 +427,13 @@ impl ListState {
self.0.borrow().follow_tail
}
/// Returns whether the list is scrolled to the bottom (within 1px).
pub fn is_at_bottom(&self) -> bool {
let current_offset = self.scroll_px_offset_for_scrollbar().y.abs();
let max_offset = self.max_offset_for_scrollbar().y;
current_offset >= max_offset - px(1.0)
}
/// Scroll the list to the given offset
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut();
+6 -6
View File
@@ -109,12 +109,12 @@ impl VimTestContext {
}
cx.bind_keys(default_key_bindings);
if enabled {
let vim_key_bindings = settings::KeymapFile::load_asset(
"keymaps/vim.json",
Some(settings::KeybindSource::Vim),
cx,
)
.unwrap();
let mut vim_key_bindings =
settings::KeymapFile::load_asset_allow_partial_failure("keymaps/vim.json", cx)
.unwrap();
for key_binding in &mut vim_key_bindings {
key_binding.set_meta(settings::KeybindSource::Vim.meta());
}
cx.bind_keys(vim_key_bindings);
}
}
+3 -1
View File
@@ -67,7 +67,9 @@ Right-click on any agent response in the thread view to access a context menu wi
### Navigating the Thread {#navigating-the-thread}
In long conversations, use the scroll arrow buttons at the bottom of the panel to jump to your most recent prompt or to the very beginning of the thread.
In long conversations, use the scroll arrow buttons at the bottom of the panel to jump to your most recent prompt or to the very beginning of the thread. You can also scroll the thread using arrow keys, Page Up/Down, Home/End, and Shift+Page Up/Down to jump between messages, when the thread pane is focused.
When focus is in the message editor, you can also use {#kb agent::ScrollOutputPageUp}, {#kb agent::ScrollOutputPageDown}, {#kb agent::ScrollOutputToTop}, {#kb agent::ScrollOutputToBottom}, {#kb agent::ScrollOutputLineUp}, and {#kb agent::ScrollOutputLineDown} to navigate the thread, or {#kb agent::ScrollOutputToPreviousMessage} and {#kb agent::ScrollOutputToNextMessage} to jump between your prompts.
### Navigating History {#navigating-history}