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 <midas@traderdifferent.com>
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
Gators King 2026-08-11 17:06:55 +08:00 • committed by GitHub
parent 24047f5dfb
commit 1279952de4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 53 additions and 1 deletions

View file

@ -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 |

View file

@ -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

View file

@ -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",

View file

@ -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 };

View file

@ -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"]);

View file

@ -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);