From a3e93ec85ea90ce94a305ab6c5ca96fba74baf24 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 6 Aug 2026 22:30:13 +0200 Subject: [PATCH] feat(tui): add half-page transcript scrolling closes #7735 --- packages/coding-agent/docs/keybindings.md | 4 ++- packages/tui/CHANGELOG.md | 1 + packages/tui/src/keybindings.ts | 10 ++++++++ packages/tui/src/tui-alt-screen.ts | 8 ++++++ packages/tui/test/keybindings.test.ts | 2 ++ packages/tui/test/tui-alt-screen.test.ts | 30 +++++++++++++++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 5f5acc6a7..6c31b211d 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -95,12 +95,14 @@ Fullscreen transcript bindings take precedence over editor bindings. The default | `pageUp`, `pageDown` | Editor | Transcript | | `ctrl+pageUp`, `ctrl+pageDown` | Editor | Editor | -This routing remains configurable through the ordinary action bindings. For example, `"tui.altScreen.pageUp": "ctrl+pageUp"` makes `pageUp` control the editor and `ctrl+pageUp` control the transcript in fullscreen mode. Setting `"tui.altScreen.pageUp": []` disables that transcript shortcut entirely. User bindings replace the defaults for that action. +This routing remains configurable through the ordinary action bindings. For example, `"tui.altScreen.pageUp": "ctrl+pageUp"` makes `pageUp` control the editor and `ctrl+pageUp` control the transcript in fullscreen mode. Bind `tui.altScreen.halfPageUp` and `tui.altScreen.halfPageDown` for smaller transcript steps while keeping the full-page bindings. Setting `"tui.altScreen.pageUp": []` disables that transcript shortcut entirely. User bindings replace the defaults for that action. | Keybinding id | Default | Description | |--------|---------|-------------| | `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page | | `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page | +| `tui.altScreen.halfPageUp` | *(none)* | Scroll the transcript up by half a page | +| `tui.altScreen.halfPageDown` | *(none)* | Scroll the transcript down by half a page | | `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Jump to the previous marked message | | `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Jump to the next marked message | | `tui.altScreen.top` | `home` | Scroll to the beginning of the transcript | diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index a1c785a95..058ffb444 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added unbound half-page transcript scrolling actions, `tui.altScreen.halfPageUp` and `tui.altScreen.halfPageDown`, for fullscreen TUI keybindings ([#7735](https://github.com/earendil-works/pi/issues/7735)). - Added double-click word selection, word-aware drag selection, and triple-click line selection in the fullscreen TUI ([#7725](https://github.com/earendil-works/pi/issues/7725)). ### Fixed diff --git a/packages/tui/src/keybindings.ts b/packages/tui/src/keybindings.ts index d371d4c2f..b1a086d67 100644 --- a/packages/tui/src/keybindings.ts +++ b/packages/tui/src/keybindings.ts @@ -44,6 +44,8 @@ export interface Keybindings { // Alternate-screen viewport navigation "tui.altScreen.pageUp": true; "tui.altScreen.pageDown": true; + "tui.altScreen.halfPageUp": true; + "tui.altScreen.halfPageDown": true; "tui.altScreen.previousPrompt": true; "tui.altScreen.nextPrompt": true; "tui.altScreen.top": true; @@ -157,6 +159,14 @@ export const TUI_KEYBINDINGS = { defaultKeys: "pageDown", description: "Scroll viewport down one page", }, + "tui.altScreen.halfPageUp": { + defaultKeys: [], + description: "Scroll viewport up half a page", + }, + "tui.altScreen.halfPageDown": { + defaultKeys: [], + description: "Scroll viewport down half a page", + }, "tui.altScreen.previousPrompt": { defaultKeys: "ctrl+shift+up", description: "Jump to previous semantic prompt", diff --git a/packages/tui/src/tui-alt-screen.ts b/packages/tui/src/tui-alt-screen.ts index 121f1a207..cff48a261 100644 --- a/packages/tui/src/tui-alt-screen.ts +++ b/packages/tui/src/tui-alt-screen.ts @@ -427,6 +427,14 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { } return { consume: true }; } + if (keybindings.matches(data, "tui.altScreen.halfPageUp")) { + if (!isRelease) this.scrollBy(-Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2))); + return { consume: true }; + } + if (keybindings.matches(data, "tui.altScreen.halfPageDown")) { + if (!isRelease) this.scrollBy(Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2))); + return { consume: true }; + } if (keybindings.matches(data, "tui.altScreen.previousPrompt")) { if (!isRelease) this.scrollToPrompt(-1); return { consume: true }; diff --git a/packages/tui/test/keybindings.test.ts b/packages/tui/test/keybindings.test.ts index 811a12179..c79a024aa 100644 --- a/packages/tui/test/keybindings.test.ts +++ b/packages/tui/test/keybindings.test.ts @@ -32,6 +32,8 @@ describe("KeybindingsManager", () => { assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.pageUp"), ["pageUp"]); assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.pageDown"), ["pageDown"]); + assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.halfPageUp"), []); + assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.halfPageDown"), []); assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.previousPrompt"), ["ctrl+shift+up"]); assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.nextPrompt"), ["ctrl+shift+down"]); assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.top"), ["home"]); diff --git a/packages/tui/test/tui-alt-screen.test.ts b/packages/tui/test/tui-alt-screen.test.ts index cee169284..750b58b9b 100644 --- a/packages/tui/test/tui-alt-screen.test.ts +++ b/packages/tui/test/tui-alt-screen.test.ts @@ -5,6 +5,7 @@ import { Image } from "../src/components/image.ts"; import { ScrollView } from "../src/components/scroll-view.ts"; import { Text } from "../src/components/text.ts"; import { VStack } from "../src/components/v-stack.ts"; +import { getKeybindings, KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "../src/keybindings.ts"; import { encodeKitty, hyperlink, @@ -352,6 +353,35 @@ describe("TuiAltScreen", () => { tui.stop(); }); + it("scrolls the transcript by half a page with custom bindings", async () => { + const originalKeybindings = getKeybindings(); + const terminal = new VirtualTerminal(20, 10); + const tui = new TuiAltScreen(terminal); + setKeybindings( + new KeybindingsManager(TUI_KEYBINDINGS, { + "tui.altScreen.halfPageUp": "ctrl+u", + "tui.altScreen.halfPageDown": "ctrl+d", + }), + ); + try { + tui.addChild(new Text(Array.from({ length: 30 }, (_, index) => `line ${index + 1}`).join("\n"), 0, 0)); + tui.start(); + await terminal.waitForRender(); + assert.strictEqual(tui.viewportTop, 20); + + terminal.sendInput("\x15"); + await terminal.waitForRender(); + assert.strictEqual(tui.viewportTop, 15); + + terminal.sendInput("\x04"); + await terminal.waitForRender(); + assert.strictEqual(tui.viewportTop, 20); + } finally { + tui.stop(); + setKeybindings(originalKeybindings); + } + }); + it("routes Ctrl-modified viewport navigation to the focused component", async () => { const terminal = new VirtualTerminal(20, 6); const tui = new TuiAltScreen(terminal);