From fef07d388ed8e206a7a86444a64a2fd696639a53 Mon Sep 17 00:00:00 2001 From: Aleksei Gusev Date: Sun, 29 Mar 2026 12:00:29 +0300 Subject: [PATCH] Add PageUp/PageDown scrolling in agent view Fixes #52656 --- assets/keymaps/default-linux.json | 4 ++ assets/keymaps/default-macos.json | 4 ++ assets/keymaps/default-windows.json | 4 ++ crates/agent_ui/src/agent_ui.rs | 4 ++ crates/agent_ui/src/conversation_view.rs | 5 ++- .../src/conversation_view/thread_view.rs | 38 +++++++++++++++---- crates/gpui/src/elements/list.rs | 7 ++++ 7 files changed, 56 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 617d7a6d0662264858ac3066d40481135dab9ae6..12e257c19d47ccaf84df7e0f379a9560ad5456c6 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -308,12 +308,16 @@ "context": "AcpThread", "bindings": { "ctrl--": "pane::GoBack", + "ctrl-pageup": "agent::ScrollOutputPageUp", + "ctrl-pagedown": "agent::ScrollOutputPageDown", }, }, { "context": "AcpThread > Editor", "use_key_equivalents": true, "bindings": { + "ctrl-pageup": "agent::ScrollOutputPageUp", + "ctrl-pagedown": "agent::ScrollOutputPageDown", "ctrl-shift-r": "agent::OpenAgentDiff", "ctrl-shift-d": "git::Diff", "shift-alt-y": "agent::KeepAll", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index d3dda49c9a52a8c9b52dfddc04ae573f2fa4cf28..eb4b0ba425a829b0813676ef057e1b32e92ed2de 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -354,12 +354,16 @@ "context": "AcpThread", "bindings": { "ctrl--": "pane::GoBack", + "ctrl-pageup": "agent::ScrollOutputPageUp", + "ctrl-pagedown": "agent::ScrollOutputPageDown", }, }, { "context": "AcpThread > Editor", "use_key_equivalents": true, "bindings": { + "ctrl-pageup": "agent::ScrollOutputPageUp", + "ctrl-pagedown": "agent::ScrollOutputPageDown", "shift-ctrl-r": "agent::OpenAgentDiff", "shift-ctrl-d": "git::Diff", "shift-alt-y": "agent::KeepAll", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index e665d26aaf0c90d6c2fa4ee66284687c843fcd62..08bbf418670c12e149be68b75f747cc0ca17d11e 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -310,12 +310,16 @@ "context": "AcpThread", "bindings": { "ctrl--": "pane::GoBack", + "ctrl-pageup": "agent::ScrollOutputPageUp", + "ctrl-pagedown": "agent::ScrollOutputPageDown", }, }, { "context": "AcpThread > Editor", "use_key_equivalents": true, "bindings": { + "ctrl-pageup": "agent::ScrollOutputPageUp", + "ctrl-pagedown": "agent::ScrollOutputPageDown", "ctrl-shift-r": "agent::OpenAgentDiff", "ctrl-shift-d": "git::Diff", "shift-alt-y": "agent::KeepAll", diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 2395a74c281f5b84ab1f328b175fe8385ce3fb12..9d8fff1236acad3937ecbe50bd3171cfa6e8bd20 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -179,6 +179,10 @@ 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, ] ); diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 3fabb528315f8f32c03d358c13d123e5bb299fd7..90ebd5e38d87a57d2c502c01d6bc5ab9ed99cfa8 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -84,8 +84,9 @@ 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, ScrollOutputPageDown, ScrollOutputPageUp, SendImmediately, + SendNextQueuedMessage, ToggleFastMode, ToggleProfileSelector, ToggleThinkingEffortMenu, + ToggleThinkingMode, UndoLastReject, }; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 7caeb687bebb32083c2647a2bc0d359b36e03b58..9cedc4b9b1e583d948a6fc4674cfa8db56cc22dd 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -549,17 +549,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); } } @@ -4977,6 +4970,33 @@ impl ThreadView { cx.notify(); } + fn scroll_output_page_up( + &mut self, + _: &ScrollOutputPageUp, + _window: &mut Window, + cx: &mut Context, + ) { + 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, + ) { + 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(); + } + pub fn open_thread_as_markdown( &self, workspace: Entity, @@ -8448,6 +8468,8 @@ 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(|this, _: &ToggleFastMode, _window, cx| { this.toggle_fast_mode(cx); })) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index ed441e3b40534690d02b31109e719c60dd5802e0..b4c8e7ca9015190fb8bb1698f79f1b025bfa4829 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -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();