-
Notifications
You must be signed in to change notification settings - Fork 11.1k
fix(tui): scroll transcript with PageUp/PageDown while composer is focused #4991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,6 +166,11 @@ class DeerFlowTUI(App): | |
| # of these so they never steal keys from a modal overlay or the composer. | ||
| Binding("down", "nav_down", show=False, priority=True), | ||
| Binding("up", "nav_up", show=False, priority=True), | ||
| # PageUp/PageDown scroll the transcript while the composer stays focused; | ||
| # they route to the palette when it is open and are gated by check_action | ||
| # against modal overlays, same as up/down (issue #4974). | ||
| Binding("pageup", "scroll_page_up", show=False, priority=True), | ||
| Binding("pagedown", "scroll_page_down", show=False, priority=True), | ||
| Binding("tab", "palette_complete", show=False, priority=True), | ||
| Binding("escape", "escape", show=False, priority=True), | ||
| Binding("enter", "palette_accept", show=False, priority=True), | ||
|
|
@@ -260,16 +265,32 @@ def on_input_changed(self, event: Input.Changed) -> None: | |
| # ----- slash command palette ----------------------------------------- # | ||
|
|
||
| def check_action(self, action: str, parameters): # noqa: D401 - Textual hook | ||
| custom = {"nav_up", "nav_down", "palette_complete", "palette_accept", "escape"} | ||
| custom = { | ||
| "nav_up", | ||
| "nav_down", | ||
| "scroll_page_up", | ||
| "scroll_page_down", | ||
| "palette_complete", | ||
| "palette_accept", | ||
| "escape", | ||
| } | ||
| if action in custom: | ||
| # A modal overlay (e.g. the model/thread picker) is on top — never | ||
| # intercept its keys; let the overlay handle them natively. | ||
| if len(self.screen_stack) > 1: | ||
| return None | ||
| # nav (history), Tab and Esc are always consumed (Tab can't move focus | ||
| # off the composer; Esc closes the palette or interrupts a run). Enter | ||
| # falls through to the Input when the palette is closed so it submits. | ||
| if action in {"nav_up", "nav_down", "palette_complete", "escape"}: | ||
| # nav (history), transcript paging, Tab and Esc are always consumed | ||
| # (Tab can't move focus off the composer; Esc closes the palette or | ||
| # interrupts a run). Enter falls through to the Input when the | ||
| # palette is closed so it submits. | ||
| if action in { | ||
| "nav_up", | ||
| "nav_down", | ||
| "scroll_page_up", | ||
| "scroll_page_down", | ||
| "palette_complete", | ||
| "escape", | ||
| }: | ||
| return True | ||
| return True if self._palette_open else None | ||
| return True | ||
|
|
@@ -286,6 +307,18 @@ def action_nav_down(self) -> None: | |
| else: | ||
| self._history_move(self._history.down()) | ||
|
|
||
| def action_scroll_page_up(self) -> None: | ||
| if self._palette_open: | ||
| self.action_palette_up() | ||
| else: | ||
| self.query_one("#scroll", VerticalScroll).scroll_page_up(animate=False) | ||
|
|
||
| def action_scroll_page_down(self) -> None: | ||
| if self._palette_open: | ||
| self.action_palette_down() | ||
| else: | ||
| self.query_one("#scroll", VerticalScroll).scroll_page_down(animate=False) | ||
|
|
||
| def _history_move(self, value: str) -> None: | ||
| composer = self.query_one("#composer", Input) | ||
| composer.value = value | ||
|
|
@@ -702,8 +735,15 @@ def _refresh_header(self) -> None: | |
| ) | ||
|
|
||
| def _refresh_transcript(self) -> None: | ||
| scroll = self.query_one("#scroll", VerticalScroll) | ||
| # Only auto-follow when the user is already at the bottom. If they have | ||
| # scrolled up to read earlier content, a streamed update must not snap | ||
| # them back to the end (issue #4974); follow resumes on the next refresh | ||
| # once they scroll back down. | ||
| follow = scroll.scroll_y >= scroll.max_scroll_y - 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Stop following as soon as the user leaves the exact bottom. This tolerance treats |
||
| self.query_one("#transcript", Static).update(render_transcript(self.state)) | ||
| self.query_one("#scroll", VerticalScroll).scroll_end(animate=False) | ||
| if follow: | ||
| scroll.scroll_end(animate=False) | ||
|
|
||
| def _refresh_status(self) -> None: | ||
| spinner = SYMBOLS["spinner"][self._spinner_idx] if self._streaming else "" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Expose the new transcript keys in user-facing help. These bindings are declared with
show=False, while_HELP_KEYSand the key table inbackend/docs/TUI.mdstill omit PageUp/PageDown. Consequently the new feature has no in-app or documented discovery path, despite issue #4974 explicitly requiring the key help to identify transcript navigation. Please update both/helpand the TUI key documentation, with a small assertion so they cannot drift.