From 1279952de4a22f021bff7ee714e3aa18010a5378 Mon Sep 17 00:00:00 2001 From: Gators King <107609123+midastruth@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:06:55 +0800 Subject: [PATCH] feat(tui): add unbound single-line transcript scrolling actions (#7903) Add tui.altScreen.lineUp and tui.altScreen.lineDown keybinding actions for one-line viewport scrolling in fullscreen TUI. Both are unbound by default and can be configured via user keybindings. closes #7830 Co-authored-by: midas Co-authored-by: Mario Zechner --- 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 | 29 +++++++++++++++++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index a7f493d9c..c693346a1 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -97,7 +97,7 @@ 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. 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. +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 half-page steps, or bind `tui.altScreen.lineUp` and `tui.altScreen.lineDown` for single-line steps. Setting `"tui.altScreen.pageUp": []` disables that transcript shortcut entirely. User bindings replace the defaults for that action. | Keybinding id | Default | Description | |--------|---------|-------------| @@ -105,6 +105,8 @@ This routing remains configurable through the ordinary action bindings. For exam | `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.lineUp` | *(none)* | Scroll the transcript up by one line | +| `tui.altScreen.lineDown` | *(none)* | Scroll the transcript down by one line | | `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.search` | `ctrl+shift+f` | Search the rendered transcript | diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ec81ca022..c91740670 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added unbound single-line transcript scrolling actions, `tui.altScreen.lineUp` and `tui.altScreen.lineDown`, for fullscreen TUI keybindings ([#7830](https://github.com/earendil-works/pi/issues/7830)). - Added incremental primary-scroll-view search to the fullscreen TUI with configurable match styles, `Ctrl+Shift+F`, and next/previous navigation with `Enter`/`Ctrl+G` and `Shift+Enter`/`Ctrl+Shift+G`. ### Changed diff --git a/packages/tui/src/keybindings.ts b/packages/tui/src/keybindings.ts index 421657bfa..c1afc5df7 100644 --- a/packages/tui/src/keybindings.ts +++ b/packages/tui/src/keybindings.ts @@ -46,6 +46,8 @@ export interface Keybindings { "tui.altScreen.pageDown": true; "tui.altScreen.halfPageUp": true; "tui.altScreen.halfPageDown": true; + "tui.altScreen.lineUp": true; + "tui.altScreen.lineDown": true; "tui.altScreen.previousPrompt": true; "tui.altScreen.nextPrompt": true; "tui.altScreen.search": true; @@ -171,6 +173,14 @@ export const TUI_KEYBINDINGS = { defaultKeys: [], description: "Scroll viewport down half a page", }, + "tui.altScreen.lineUp": { + defaultKeys: [], + description: "Scroll viewport up one line", + }, + "tui.altScreen.lineDown": { + defaultKeys: [], + description: "Scroll viewport down one line", + }, "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 c3e9d6a27..8b7e8b870 100644 --- a/packages/tui/src/tui-alt-screen.ts +++ b/packages/tui/src/tui-alt-screen.ts @@ -601,6 +601,14 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { if (!isRelease) this.scrollBy(Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2))); return { consume: true }; } + if (keybindings.matches(data, "tui.altScreen.lineUp")) { + if (!isRelease) this.scrollBy(-1); + return { consume: true }; + } + if (keybindings.matches(data, "tui.altScreen.lineDown")) { + if (!isRelease) this.scrollBy(1); + 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 29e4bf6a1..9a4ecb881 100644 --- a/packages/tui/test/keybindings.test.ts +++ b/packages/tui/test/keybindings.test.ts @@ -34,6 +34,8 @@ describe("KeybindingsManager", () => { 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.lineUp"), []); + assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.lineDown"), []); 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.search"), ["ctrl+shift+f"]); diff --git a/packages/tui/test/tui-alt-screen.test.ts b/packages/tui/test/tui-alt-screen.test.ts index 6612cd006..7df229dbf 100644 --- a/packages/tui/test/tui-alt-screen.test.ts +++ b/packages/tui/test/tui-alt-screen.test.ts @@ -508,6 +508,35 @@ describe("TuiAltScreen", () => { } }); + it("scrolls the transcript by one line 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.lineUp": "ctrl+y", + "tui.altScreen.lineDown": "ctrl+e", + }), + ); + 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("\x19"); + await terminal.waitForRender(); + assert.strictEqual(tui.viewportTop, 19); + + terminal.sendInput("\x05"); + 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);