fix(tui): add vertical scrolling to Editor when content exceeds terminal height

The Editor component now accepts TUI as the first constructor parameter,
enabling it to query terminal dimensions. When content exceeds available
height, the editor scrolls vertically keeping the cursor visible.

Features:
- Max editor height is 30% of terminal rows (minimum 5 lines)
- Page Up/Down keys scroll by page size
- Scroll indicators show lines above/below: ─── ↑ 5 more ───

Breaking change: Editor constructor signature changed from
  new Editor(theme)
to
  new Editor(tui, theme)

fixes #732
This commit is contained in:
Mario Zechner 2026-01-16 03:50:55 +01:00
parent d30f6460fa
commit 356a482527
17 changed files with 210 additions and 88 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Breaking Changes
- Extensions using `Editor` directly must now pass `TUI` as the first constructor argument: `new Editor(tui, theme)`. The `tui` parameter is available in extension factory functions. ([#732](https://github.com/badlogic/pi-mono/issues/732))
### Added
- New `input` event in extension system for intercepting, transforming, or handling user input before the agent processes it. Supports three result types: `continue` (pass through), `transform` (modify text/images), `handled` (respond without LLM). Handlers chain transforms and short-circuit on handled. ([#761](https://github.com/badlogic/pi-mono/pull/761) by [@nicobailon](https://github.com/nicobailon))
@ -13,6 +17,7 @@
### Fixed
- Editor no longer corrupts terminal display when loading large prompts via `setEditorText`. Content now scrolls vertically with indicators showing lines above/below the viewport. ([#732](https://github.com/badlogic/pi-mono/issues/732))
- Piped stdin now works correctly: `echo foo | pi` is equivalent to `pi -p foo`. When stdin is piped, print mode is automatically enabled since interactive mode requires a TTY ([#708](https://github.com/badlogic/pi-mono/issues/708))
- Session tree now preserves branch connectors and indentation when filters hide intermediate entries so descendants attach to the nearest visible ancestor and sibling branches align. Fixed in both TUI and HTML export ([#739](https://github.com/badlogic/pi-mono/pull/739) by [@w-winter](https://github.com/w-winter))
- Added `upstream connect`, `connection refused`, and `reset before headers` patterns to auto-retry error detection ([#733](https://github.com/badlogic/pi-mono/issues/733))

View file

@ -0,0 +1,35 @@
/**
* Load file into editor - for testing editor scrolling
*
* Usage: pi --extension ./examples/extensions/load-file.ts
*
* Commands:
* /load [path] - Load file into editor (defaults to README.md)
*/
import * as fs from "node:fs";
import * as path from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.registerCommand("load", {
description: "Load file into editor (defaults to README.md)",
handler: async (args, ctx) => {
const filePath = args.trim() || "README.md";
const fullPath = path.resolve(filePath);
if (!fs.existsSync(fullPath)) {
ctx.ui.notify(`File not found: ${fullPath}`, "error");
return;
}
try {
const content = fs.readFileSync(fullPath, "utf-8");
ctx.ui.setEditorText(content);
ctx.ui.notify(`Loaded ${filePath} (${content.split("\n").length} lines)`);
} catch (err) {
ctx.ui.notify(`Failed to read file: ${err}`, "error");
}
},
});
}

View file

@ -80,6 +80,6 @@ class ModalEditor extends CustomEditor {
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((_tui, theme, kb) => new ModalEditor(theme, kb));
ctx.ui.setEditorComponent((tui, theme, kb) => new ModalEditor(tui, theme, kb));
});
}

View file

@ -90,7 +90,7 @@ export default function question(pi: ExtensionAPI) {
noMatch: (t) => theme.fg("warning", t),
},
};
const editor = new Editor(editorTheme);
const editor = new Editor(tui, editorTheme);
editor.onSubmit = (value) => {
const trimmed = value.trim();

View file

@ -119,7 +119,7 @@ export default function questionnaire(pi: ExtensionAPI) {
noMatch: (t) => theme.fg("warning", t),
},
};
const editor = new Editor(editorTheme);
const editor = new Editor(tui, editorTheme);
// Helpers
function refresh() {

View file

@ -4,8 +4,7 @@
* Usage: pi --extension ./examples/extensions/rainbow-editor.ts
*/
import { CustomEditor, type ExtensionAPI, type KeybindingsManager } from "@mariozechner/pi-coding-agent";
import type { EditorTheme, TUI } from "@mariozechner/pi-tui";
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
// Base colors (coral → yellow → green → teal → blue → purple → pink)
const COLORS: [number, number, number][] = [
@ -44,14 +43,8 @@ function colorize(text: string, shinePos: number): string {
class RainbowEditor extends CustomEditor {
private animationTimer?: ReturnType<typeof setInterval>;
private tui: TUI;
private frame = 0;
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
super(theme, keybindings);
this.tui = tui;
}
private hasUltrathink(): boolean {
return /ultrathink/i.test(this.getText());
}

View file

@ -1,4 +1,4 @@
import { Editor, type EditorTheme } from "@mariozechner/pi-tui";
import { Editor, type EditorTheme, type TUI } from "@mariozechner/pi-tui";
import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js";
/**
@ -15,8 +15,8 @@ export class CustomEditor extends Editor {
/** Handler for extension-registered shortcuts. Returns true if handled. */
public onExtensionShortcut?: (data: string) => boolean;
constructor(theme: EditorTheme, keybindings: KeybindingsManager) {
super(theme);
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
super(tui, theme);
this.keybindings = keybindings;
}

View file

@ -44,7 +44,7 @@ export class ExtensionEditorComponent extends Container {
this.addChild(new Spacer(1));
// Create editor
this.editor = new Editor(getEditorTheme());
this.editor = new Editor(tui, getEditorTheme());
if (prefill) {
this.editor.setText(prefill);
}

View file

@ -240,7 +240,7 @@ export class InteractiveMode {
this.statusContainer = new Container();
this.widgetContainer = new Container();
this.keybindings = KeybindingsManager.create();
this.defaultEditor = new CustomEditor(getEditorTheme(), this.keybindings);
this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings);
this.editor = this.defaultEditor;
this.editorContainer = new Container();
this.editorContainer.addChild(this.editor as Component);