Skip to content

Add in-app scrollback with history mode#152

Merged
shellicar merged 5 commits intomainfrom
feature/150-in-app-scrollback
Mar 29, 2026
Merged

Add in-app scrollback with history mode#152
shellicar merged 5 commits intomainfrom
feature/150-in-app-scrollback

Conversation

@shellicar
Copy link
Copy Markdown
Owner

Summary

  • History region visible above the zone during a session
  • Page Up/Down and Shift+Up/Down scroll through previous output
  • History mode pins the viewport; Esc or scrolling to bottom returns to live
  • Scroll position indicator in the status bar when in history mode

Related Issues

Closes #150

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.
@shellicar shellicar added this to the 1.0 milestone Mar 29, 2026
@shellicar shellicar added the enhancement New feature or request label Mar 29, 2026
@shellicar shellicar self-assigned this Mar 29, 2026
@shellicar shellicar enabled auto-merge (squash) March 29, 2026 17:44
Copy link
Copy Markdown
Collaborator

@bananabot9000 bananabot9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

  1. displayBuffer grows unboundedwriteHistory() pushes to both historyBuffer (string[]) and displayBuffer (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.

  2. Status line [↑ N/M] — Shows in history mode. Clean indicator. Does it disappear immediately on returnToLive()? Looks like yes — isLive means renderZone skips the indicator. Good.

  3. Shift modifier detectioninput.ts checks \x1b[1;2 prefix 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.

  4. render() signature changerender(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 🍌

@shellicar shellicar merged commit 199bca1 into main Mar 29, 2026
4 checks passed
@shellicar shellicar deleted the feature/150-in-app-scrollback branch March 29, 2026 17:54
Copy link
Copy Markdown
Collaborator

@bananabot9000 bananabot9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

  1. displayBuffer grows unbounded -- writeHistory() pushes to both historyBuffer (string[]) and displayBuffer (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.

  2. Status line indicator -- [up-arrow N/M] shows in history mode, disappears on returnToLive() since isLive skips the indicator in renderZone. Clean.

  3. Shift modifier detection -- input.ts checks \x1b[1;2 prefix 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.

  4. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In-app scrollback with history/live mode

2 participants