Add in-app scrollback with history mode#152
Conversation
Introduces HistoryViewport and a two-region rendering model so completed response output is visible above the zone while in the alt buffer. - Add HistoryViewport class (src/HistoryViewport.ts): live/history mode, resolve() auto-follows in live mode, scroll methods (pageUp/Down, lineUp/Down), returnToLive(), top-padding to keep content adjacent to zone - Export wrapLine from Layout.ts for use by Terminal.renderZone() - Terminal gains displayBuffer (persists across flushes), historyViewport, lastHistoryFrame; writeHistory() pushes to both historyBuffer and displayBuffer; renderZone() implements two-region data flow - Terminal exposes isHistoryMode getter and scroll delegation methods - Renderer.render() accepts historyRows + zoneFrame; cursor absolute position accounts for history row count - Update existing renderer/integration tests to pass [] as historyRows - Add HistoryViewport unit tests and two-region integration tests (208 total)
Users can now scroll through session history without leaving the app. Page Up/Down scrolls by screen; Shift+Up/Down scrolls line by line. Escape returns to live mode. The status bar shows [↑ N/M] when in history mode so the user knows where they are in the buffer. New KeyAction types (page_up, page_down, shift+up, shift+down) with translateKey cases. History scroll keys route early in handleKey so they work during all phases (idle, busy, prompting). Escape checks isHistoryMode before command mode exit and query abort.
bananabot9000
left a comment
There was a problem hiding this comment.
PR #152 Review — In-App Scrollback with History Mode ✅
Clean implementation of the two-region rendering model. The HistoryViewport abstraction is solid — live/history mode state machine, proper top-padding for short content, auto-follow in live mode. This is exactly what was needed after #151 removed terminal scrollback.
What I like
HistoryViewport design — The resolve() method returning { lines, position, total, isLive } is a clean interface. Consumers don't need to know about internal scroll state. The top-padding logic (filling with empty strings when content < viewport height) handles the edge case of "just started, only 2 history lines" gracefully.
Key routing placement — History scroll keys checked early in handleKey (after ctrl+c, before ctrl+/). Escape checks isHistoryMode before command mode exit. This means you can scroll history, press Escape to return to live, without accidentally toggling command mode. Good priority ordering.
Two-region renderZone() — The 5-step data flow (get zone frame → get history viewport → combine → render → flush to main) is readable and each step has a clear responsibility. displayBuffer vs historyBuffer separation (display for viewport, history for main buffer flush) is the right call.
Test coverage — 15+ HistoryViewport tests, 7 input tests, integration tests for two-region rendering. The displayBuffer tests verify auto-follow behaviour and zone height change adaptation. 218 tests passing.
wrapLine export from Layout.ts — Was module-private, now exported for Terminal to use when pushing to displayBuffer. Minimal change, correct scope expansion.
Observations (non-blocking)
-
displayBuffergrows unbounded —writeHistory()pushes to bothhistoryBuffer(string[]) anddisplayBuffer(string[]). Neither is ever trimmed. For long sessions this could get large. Not a problem now (history lines are small, sessions compact before memory matters), but worth a future cap if needed. -
Status line
[↑ N/M]— Shows in history mode. Clean indicator. Does it disappear immediately onreturnToLive()? Looks like yes —isLivemeansrenderZoneskips the indicator. Good. -
Shift modifier detection —
input.tschecks\x1b[1;2prefix for shift+arrow. This is the standard xterm encoding. Should work in all modern terminals. Edge case: some terminals send different sequences for shift+pgup/pgdn, but those are caught by the named key check first. -
render()signature change —render(historyRows: string[], zoneFrame: ViewportResult)in TerminalRenderer. All existing tests updated to pass[]as historyRows. Clean migration.
LGTM. The alternate buffer now has eyes. 👀
Co-reviewed-by: BananaBot9000 🍌
bananabot9000
left a comment
There was a problem hiding this comment.
PR #152 Review -- In-App Scrollback with History Mode
Clean implementation of the two-region rendering model. The HistoryViewport abstraction is solid -- live/history mode state machine, proper top-padding for short content, auto-follow in live mode. This is exactly what was needed after #151 removed terminal scrollback.
What I like
HistoryViewport design -- The resolve() method returning { lines, position, total, isLive } is a clean interface. Consumers don't need to know about internal scroll state. The top-padding logic (filling with empty strings when content < viewport height) handles the edge case of "just started, only 2 history lines" gracefully.
Key routing placement -- History scroll keys checked early in handleKey (after ctrl+c, before ctrl+/). Escape checks isHistoryMode before command mode exit. This means you can scroll history, press Escape to return to live, without accidentally toggling command mode. Good priority ordering.
Two-region renderZone() -- The 5-step data flow (get zone frame, get history viewport, combine, render, flush to main) is readable and each step has a clear responsibility. displayBuffer vs historyBuffer separation (display for viewport, history for main buffer flush) is the right call.
Test coverage -- 15+ HistoryViewport tests, 7 input tests, integration tests for two-region rendering. The displayBuffer tests verify auto-follow behaviour and zone height change adaptation. 218 tests passing.
wrapLine export from Layout.ts -- Was module-private, now exported for Terminal to use when pushing to displayBuffer. Minimal change, correct scope expansion.
Observations (non-blocking)
-
displayBuffer grows unbounded --
writeHistory()pushes to bothhistoryBuffer(string[]) anddisplayBuffer(string[]). Neither is ever trimmed. For long sessions this could get large. Not a problem now (history lines are small, sessions compact before memory matters), but worth a future cap if needed. -
Status line indicator --
[up-arrow N/M]shows in history mode, disappears onreturnToLive()sinceisLiveskips the indicator in renderZone. Clean. -
Shift modifier detection --
input.tschecks\x1b[1;2prefix for shift+arrow. Standard xterm encoding. Should work in all modern terminals. Edge case: some terminals send different sequences for shift+pgup/pgdn, but those are caught by the named key check first. -
render() signature change --
render(historyRows, zoneFrame)in TerminalRenderer. All existing tests updated to pass[]as historyRows. Clean migration.
LGTM. The alternate buffer now has eyes.
Co-reviewed-by: BananaBot9000
Summary
Related Issues
Closes #150