Decide who owns the scrollback
Two terminal renderers, one interface, and a test that counts full redraws.
- Problem
A terminal UI that half-owns scrolling, or redraws the whole screen, flickers, loses scrollback and breaks selection.
- Idea
Decide who owns the scrollback in each mode, redraw only what changed, and count full redraws in tests.
Terminal apps have to answer a question most UIs never face: who owns the scrolling? If the terminal owns it, you get native scrollback, search and selection, but the app controls very little. If the app owns it, it can do more, but it has to rebuild everything the terminal used to do for free. This repo ships both, behind one interface, and each respects its answer.
Try it: differential rendering
highlighted lines are the ones rewritten this frame
Typing rewrites one line. Changing line 3 rewrites from line 3 down. Resizing the terminal forces a full redraw, which is counted. On Termux, the keyboard changing the height doesn't trigger one, because it would replay the whole history.
Two renderers, one interface
Fullscreen (default)
The terminal's alternate screen: the app owns the viewport, so it rebuilds mouse and keyboard scrolling, draggable scrollbars, selection that copies to the clipboard, clickable links and jumps between prompts. On exit it prints the complete conversation back into normal scrollback.
Regular mode
The main terminal buffer: the terminal owns scrollback. The app doesn't capture the mouse, and fixed layout regions are deliberately unavailable, "because the terminal owns its scrollback."
Components render to a shared TUI interface; the concrete renderer is picked once, at construction. Neither mode half-owns the screen, which is where flicker, lost scrollback and broken selection come from.
The rendering rules
- Diff against the last frameRender all components to lines, composite overlays, then rewrite only from the first changed line to the end.
- Paint atomicallyEvery update is wrapped in synchronized output (
CSI ?2026h…?2026l), so the terminal shows the change in one frame instead of mid-write. - Full redraws only when correctness needs themA width change (wrapping changes everywhere), a height change, or a change above the visible viewport, which differential rendering can't reach.
- Except where a redraw would hurtOn Termux, the software keyboard changes the height every time it opens. A full redraw there replays the entire history, so that case is skipped.
Count the expensive thing
Full redraws are counted (fullRedraws), and a test fails if showing an edit tool's result causes one:
const redrawsBefore = tui.fullRedraws
// ...render the edit tool's diff...
expect(tui.fullRedraws).toBe(redrawsBefore)
A dev-only /tui command prints the counter for the current session. A screenshot test wouldn't notice a redraw that produces the same pixels; a counter does.
Every key is a setting
Shortcuts are named actions with defaults, like app.model.select on ctrl+l, and you can rebind or disable any of them in keybindings.json. The agent guide forbids hard-coded key checks for exactly that reason.
Steal this
Decide who owns each piece of UI and don't half-own it. Render differentially, paint atomically, and count your most expensive operation in tests. A counter catches regressions a screenshot never will.
Use it in your code
A prompt for your coding agent
Run it from the root of your repo. It checks whether the idea fits before it changes anything.
Find this UI's most expensive operation, count it, and add a test that keeps it rare.
The idea: decide clearly who owns each piece of UI behavior, and don't half-own it. Then count the expensive operation (full re-renders, full redraws, forced layouts, refetches) and assert on that count in tests. A counter catches regressions a screenshot never will.
1. Look at how this UI renders and updates, and identify the most expensive operation it does often, such as re-rendering a whole list, refetching a whole page or redrawing the whole screen.
2. Add a cheap counter for it, as a debug-only metric or test hook, with no behavior change.
3. Write a test for a common interaction asserting that the count doesn't go up when it shouldn't, for example editing one row doesn't re-render the whole list.
4. Point out anywhere the app half-owns something the platform already does (scrolling, focus, selection, history), and suggest whether to own it fully or leave it to the platform.
Keep the change small and explain the number you assert on.Read the source
Where this comes from
Pi (earendil-works/pi), the minimal, extensible agent harness. The files behind this lesson: