Skip to content

fix(tui): cap inline image height and preserve multiplexer scrollback#587

Merged
can1357 merged 4 commits intocan1357:mainfrom
smileynet:fix/puppeteer-excessive-scrolling
Apr 1, 2026
Merged

fix(tui): cap inline image height and preserve multiplexer scrollback#587
can1357 merged 4 commits intocan1357:mainfrom
smileynet:fix/puppeteer-excessive-scrolling

Conversation

@smileynet
Copy link
Copy Markdown
Contributor

What

Cap inline image height to prevent excessive scrolling, and preserve scrollback history in terminal multiplexers (tmux, screen, zellij).

Image height cap

Inline images rendered at unbounded height. A typical 1280x800 puppeteer screenshot consumed ~32 terminal rows, causing a massive scroll burst via \r\n.repeat(scroll) in doRender().

The ImageOptions.maxHeightCells parameter existed but was never passed by either call site:

Both only passed maxWidthCells via settings.get("tui.maxInlineImageColumns"). Height was an emergent property of (width cap x aspect ratio), never validated against viewport.

Changes:

  • New setting tui.maxInlineImageRows (default: 20, set 0 for viewport-only limit)
  • Shared resolveImageOptions() computes effective cap as min(setting, floor(viewport * 0.6))
  • Both Image construction sites now use resolveImageOptions() instead of reading width-only settings
  • Negative values clamped to 0; viewport-unknown fallback honors explicit setting
  • calculateImageFit() already handles maxHeightCells correctly -- no TUI package changes needed

Effective behavior by terminal size:

Terminal Raw 20-row cap Viewport 60% Effective Viewport remaining
80x24 83% 14 rows 14 10 rows
120x40 50% 24 rows 20 20 rows
200x50 40% 30 rows 20 30 rows

Multiplexer scrollback preservation

fullRender(true) emits \x1b[3J (CSI 3J) which clears the entire scrollback buffer. This fired on every terminal height change (line 1060). In multiplexers, height changes happen on pane zoom, split, and resize -- destroying scrollback that users actively navigate.

There was already a Termux exception for the same class of problem (soft keyboard height toggle).

Changes:

  • Cached isMultiplexer constant detecting tmux (TMUX), GNU screen (STY), and zellij (ZELLIJ)
  • Skip CSI 3J in multiplexers -- screen clear (\x1b[2J\x1b[H) is sufficient for re-rendering
  • Skip fullRender(true) on height change in multiplexers -- same pattern as existing Termux exception

Why

Puppeteer screenshots caused 30+ lines of scroll per image. In tmux, this was compounded by scrollback being wiped on every pane resize. The combination made puppeteer unusable in tmux and disruptive everywhere else.

Root cause analysis:

  1. maxHeightCells existed in the API but was never wired to callers (design oversight -- width was capped but height was not)
  2. Multiplexer pane operations trigger height changes that destroyed scrollback via CSI 3J

Testing

  • bun check:ts passes (biome + tsgo)
  • Verified in tmux: images are height-capped, scrollback preserved on pane zoom/resize
  • Verified edge cases: setting 0 (viewport-only limit), small terminals (80x24 caps at 14 rows), negative values (clamped)

  • bun check passes
  • Tested locally
  • CHANGELOG updated (if user-facing)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81e4034cfb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@smileynet smileynet force-pushed the fix/puppeteer-excessive-scrolling branch from 81e4034 to ce08ddf Compare March 31, 2026 16:27
Inline images (screenshots from puppeteer, assistant images) rendered at
unbounded height — a 1280x800 screenshot consumed ~32 terminal rows,
causing excessive scrolling. In multiplexers (tmux, screen, zellij),
this was compounded by fullRender clearing scrollback on every resize.

Image height cap:
- New setting tui.maxInlineImageRows (default 20)
- Effective cap: min(setting, floor(viewport * 0.6))
- Shared resolveImageOptions() replaces per-site settings reads
- Existing calculateImageFit() already handles maxHeightCells

Multiplexer scrollback preservation:
- Skip CSI 3J (clear scrollback) in tmux/screen/zellij
- Skip fullRender(true) on height change in multiplexers
- Cached isMultiplexer constant (TMUX, STY, ZELLIJ env vars)
@smileynet smileynet force-pushed the fix/puppeteer-excessive-scrolling branch from ce08ddf to 2f21637 Compare March 31, 2026 16:34
@smileynet
Copy link
Copy Markdown
Contributor Author

Verification Results

Tested the fix against 4 screenshot types x 4 terminal sizes. All 16 scenarios pass height cap constraints, plus scrollback preservation confirmed in tmux.

Setting: tui.maxInlineImageRows = 20 (default)
Multiplexer: YES (tmux)

--- 80x24 (small) (effective cap: 14 rows) ---
  1280x800 (standard)      32 ->  14 rows  (-56%)  PASS
  1920x1080 (full HD)      29 ->  14 rows  (-52%)  PASS
  800x1280 (portrait)      80 ->  14 rows  (-83%)  PASS
  390x844 (mobile)        109 ->  14 rows  (-87%)  PASS

--- 120x40 (medium) (effective cap: 20 rows) ---
  1280x800 (standard)      32 ->  20 rows  (-38%)  PASS
  1920x1080 (full HD)      29 ->  20 rows  (-31%)  PASS
  800x1280 (portrait)      80 ->  20 rows  (-75%)  PASS
  390x844 (mobile)        109 ->  20 rows  (-82%)  PASS

--- 200x50 (large) (effective cap: 20 rows) ---
  1280x800 (standard)      32 ->  20 rows  (-38%)  PASS
  1920x1080 (full HD)      29 ->  20 rows  (-31%)  PASS
  800x1280 (portrait)      80 ->  20 rows  (-75%)  PASS
  390x844 (mobile)        109 ->  20 rows  (-82%)  PASS

--- 80x15 (tiny pane) (effective cap: 9 rows) ---
  1280x800 (standard)      32 ->   9 rows  (-72%)  PASS
  1920x1080 (full HD)      29 ->   9 rows  (-69%)  PASS
  800x1280 (portrait)      80 ->   9 rows  (-89%)  PASS
  390x844 (mobile)        109 ->   9 rows  (-92%)  PASS

CSI 3J present in clear sequence: false (in tmux)  PASS

Worst case was a mobile viewport screenshot (390x844) on a 24-row terminal: 109 rows down to 14 (92% reduction). Portrait screenshots were the next worst at 80 -> 14.

Edge cases verified

Case Behavior
tui.maxInlineImageRows = 0 Viewport-only limit (60% of terminal height)
process.stdout.rows undefined (non-TTY) Falls back to explicit setting only, no silent clamping
process.stdout.rows = 0 (transitional) Falls back to explicit setting only
Negative setting value Clamped to 0, treated as viewport-only
Multiplexer detection Covers tmux (TMUX), GNU screen (STY), zellij (ZELLIJ)
CSI 3J in multiplexer Skipped -- screen clear (2J) only, scrollback preserved
Height change in multiplexer Skips fullRender(true), same pattern as existing Termux exception

@can1357 can1357 merged commit 7933a05 into can1357:main Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants