Skip to content

[Bug]: Ghostty scrollback is configured as bytes despite being named as rows #8701

Description

@zero-two-rafaeltab

Impact

T3 silently discards most of the terminal scrollback it appears to configure. The deterministic reproduction retains only 800 of the intended 10,000 rows at 80 columns—a loss of more than 90%—and the effective count can be even lower at wider terminal sizes.

This means output from long-running agent commands, builds, test suites, logs, and stack traces can disappear from the terminal before users have a chance to inspect or copy it. There is no truncation marker or warning, so the terminal simply starts at a much later line when the user scrolls to the top. The loss happens inside the renderer and can occur even while the raw output remains below T3's separate 512 KiB client-buffer limit.

The affected renderer is used by web and desktop, and Android mirrors the same incorrect value in its native terminal bridge.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web (also affects desktop because it uses the web terminal, and Android mirrors the same configuration in its native bridge)

Problem statement

T3 configures the embedded Ghostty terminal with a value named MAX_SCROLLBACK_ROWS = 10_000 on web and kMaxScrollbackRows = 10000 on Android. The pinned libghostty-vt C header also describes max_scrollback as a maximum number of lines.

The pinned Ghostty implementation actually interprets that field as a byte budget. T3 is therefore requesting 10,000 bytes while its naming and the public header indicate an intended limit of 10,000 rows.

Ghostty does not retain a literal 10 KB buffer because it rounds/clamps the request to its internal page and viewport minimum. That masks the unit mismatch, but the resulting history is still far below 10,000 rows and varies with terminal width.

Steps to reproduce

A focused failing test using T3's actual vendored Ghostty WASM is available here:

Run it with:

pnpm --filter @t3tools/web exec vp test run --project unit src/terminal/ghostty/runtimeAbi.test.ts

The test creates an 80×10 terminal, writes 12,000 numbered lines, passes T3's current 10_000 value through the pinned C ABI, and reads the retained scrollback from Ghostty.

Expected behavior

The renderer should retain approximately the configured 10,000 scrollback rows, subject only to Ghostty's documented page-granularity approximation.

Actual behavior

Only 800 scrollback rows remain in the reproduction:

AssertionError: expected 800 to be greater than or equal to 10000

Older output is silently pruned by the renderer even when another T3 terminal-history layer still retains it. The exact number varies with terminal width because Ghostty budgets parsed grid cells rather than UTF-8 transcript bytes.

Cause

The Ghostty revision pinned by T3 has contradictory API documentation:

  • Its public C header describes max_scrollback as lines.
  • Its implementation forwards the value to a byte-based screen/page budget.
  • T3 follows the public-header interpretation and names the value as rows.

Upstream tracked this exact documentation mismatch in ghostty-org/ghostty discussion #12587. The latest upstream API resolves the ambiguity with separate byte and line settings in the ABI-breaking ghostty-org/ghostty PR #13481.

Version or commit

main at 1f8ed54add4133ac39effceded8fc1fff12d8e03, with vendored libghostty-vt revision 9f62873bf195e4d8a762d768a1405a5f2f7b1697.

Environment

Renderer-level and browser-independent. Reproduced directly against the vendored web WASM; the equivalent 10000 value is also present in the Android native terminal bridge.

Suggested fix

Prefer upgrading the canonical libghostty-vt pin to a revision containing the new explicit scrollback API, then migrate both embedders:

  1. Configure the intended 10,000-row behavior through GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES.
  2. Configure GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES independently as a deliberate memory-safety budget. Do not inherit the low-level library's 10,000-byte default; when both limits are set, Ghostty applies whichever is reached first.
  3. Update the ABI regression test to use the new setters and prove that representative output retains approximately 10,000 rows.
  4. Keep the web/desktop and Android values aligned through the repository's canonical Ghostty integration rather than duplicating an ambiguous constant.

If upgrading Ghostty is too broad for the immediate fix, the smaller interim correction is to rename the current constants as bytes and use a deliberate byte budget. The Ghostty application version corresponding to T3's uncompressed pin used 10 MB, but this fallback would remain a byte budget and would not guarantee exactly 10,000 rows.

Testing decisions

  • Keep the regression at the existing vendored-WASM ABI seam so it tests the artifact T3 ships rather than a mock.
  • Add equivalent focused coverage for the Android C ABI configuration when migrating its native library.
  • Verify representative narrow and wide terminal dimensions because a parsed-cell byte budget retains different row counts at different widths.

Workaround

There is no user-facing workaround. A source build can increase the current value, but its units must be treated as bytes rather than rows.

Out of scope

Making T3's separate raw client transcript and server terminal history limits configurable. Those limits operate on different representations and should be handled independently from this renderer configuration bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions