From cafcf906fdf90786e737fb9b2a3cd7b4c19020a3 Mon Sep 17 00:00:00 2001 From: Sam Biggins Date: Tue, 31 Mar 2026 09:18:24 -0700 Subject: [PATCH 1/2] fix(tui): cap inline image height and preserve multiplexer scrollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/config/settings-schema.ts | 6 ++++++ .../src/modes/components/assistant-message.ts | 3 ++- .../src/modes/components/tool-execution.ts | 5 ++--- .../coding-agent/src/tools/render-utils.ts | 20 +++++++++++++++++++ packages/tui/src/tui.ts | 8 ++++++-- 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index f38937301..67e63b37e 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -372,6 +372,12 @@ export const SETTINGS_SCHEMA = { "Maximum width in terminal columns for inline images (default 100). Set to 0 for unlimited (bounded only by terminal width).", }, + "tui.maxInlineImageRows": { + type: "number", + default: 20, + description: + "Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).", + }, // Display rendering "display.tabWidth": { type: "number", diff --git a/packages/coding-agent/src/modes/components/assistant-message.ts b/packages/coding-agent/src/modes/components/assistant-message.ts index 92157abf7..7bdf64ff9 100644 --- a/packages/coding-agent/src/modes/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/components/assistant-message.ts @@ -4,6 +4,7 @@ import { formatNumber, logger } from "@oh-my-pi/pi-utils"; import { settings } from "../../config/settings"; import { hasPendingMermaid, prerenderMermaid } from "../../modes/theme/mermaid-cache"; import { getMarkdownTheme, theme } from "../../modes/theme/theme"; +import { resolveImageOptions } from "../../tools/render-utils"; /** * Component that renders a complete assistant message @@ -76,7 +77,7 @@ export class AssistantMessageComponent extends Container { image.data, image.mimeType, { fallbackColor: (text: string) => theme.fg("toolOutput", text) }, - { maxWidthCells: settings.get("tui.maxInlineImageColumns") }, + resolveImageOptions(), ), ); continue; diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 4e44cd4af..842008d03 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -14,7 +14,6 @@ import { type TUI, } from "@oh-my-pi/pi-tui"; import { getProjectDir, logger } from "@oh-my-pi/pi-utils"; -import { settings } from "../../config/settings"; import type { Theme } from "../../modes/theme/theme"; import { theme } from "../../modes/theme/theme"; import { computeEditDiff, computeHashlineDiff, computePatchDiff, type DiffError, type DiffResult } from "../../patch"; @@ -31,7 +30,7 @@ import { stripInternalArgs, } from "../../tools/json-tree"; import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python"; -import { formatExpandHint, replaceTabs, truncateToWidth } from "../../tools/render-utils"; +import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils"; import { toolRenderers } from "../../tools/renderers"; import { renderStatusLine } from "../../tui"; import { convertToPng } from "../../utils/image-convert"; @@ -531,7 +530,7 @@ export class ToolExecutionComponent extends Container { imageData, imageMimeType, { fallbackColor: (s: string) => theme.fg("toolOutput", s) }, - { maxWidthCells: settings.get("tui.maxInlineImageColumns") }, + resolveImageOptions(), ); this.#imageComponents.push(imageComponent); this.addChild(imageComponent); diff --git a/packages/coding-agent/src/tools/render-utils.ts b/packages/coding-agent/src/tools/render-utils.ts index a08e4fe08..39167a7e1 100644 --- a/packages/coding-agent/src/tools/render-utils.ts +++ b/packages/coding-agent/src/tools/render-utils.ts @@ -7,6 +7,7 @@ import * as os from "node:os"; import { type Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui"; import { getIndentation, pluralize } from "@oh-my-pi/pi-utils"; +import { settings } from "../config/settings"; import type { Theme } from "../modes/theme/theme"; import { formatDimensionNote, type ResizedImage } from "../utils/image-resize"; @@ -19,6 +20,25 @@ export function replaceTabs(text: string, file?: string): string { // Standardized Display Constants // ============================================================================= +/** Resolve inline image dimension caps from settings and viewport. */ +export function resolveImageOptions(): { maxWidthCells: number; maxHeightCells?: number } { + const maxWidthCells = settings.get("tui.maxInlineImageColumns"); + const rowSetting = Math.max(0, settings.get("tui.maxInlineImageRows")); + const viewportRows = process.stdout.rows; + const viewportFraction = viewportRows ? Math.floor(viewportRows * 0.6) : 0; + let maxHeightCells: number | undefined; + if (rowSetting === 0) { + // No explicit cap — use viewport fraction as safety bound + maxHeightCells = viewportFraction || undefined; + } else if (viewportFraction > 0) { + maxHeightCells = Math.min(rowSetting, viewportFraction); + } else { + // Viewport size unknown (transitional state) — honor explicit setting + maxHeightCells = rowSetting; + } + return { maxWidthCells, maxHeightCells }; +} + /** Preview limits for collapsed/expanded views */ export const PREVIEW_LIMITS = { /** Lines shown in collapsed view */ diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 7f3d39b8d..794c5c06a 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -112,6 +112,9 @@ function isTermuxSession(): boolean { return Boolean(process.env.TERMUX_VERSION); } +/** Detect terminal multiplexers where scrollback clearing and height-change redraws are hostile. */ +const isMultiplexer = Boolean(Bun.env.TMUX || Bun.env.STY || Bun.env.ZELLIJ); + /** * Options for overlay positioning and sizing. * Values can be absolute numbers or percentage strings (e.g., "50%"). @@ -1007,7 +1010,8 @@ export class TUI extends Container { const fullRender = (clear: boolean): void => { this.#fullRedrawCount += 1; let buffer = "\x1b[?2026h"; // Begin synchronized output - if (clear) buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback + // Skip clearing scrollback (3J) in multiplexers — users actively navigate scrollback history + if (clear) buffer += isMultiplexer ? "\x1b[2J\x1b[H" : "\x1b[2J\x1b[H\x1b[3J"; const reset = SEGMENT_RESET; for (let i = 0; i < newLines.length; i++) { if (i > 0) buffer += "\r\n"; @@ -1056,7 +1060,7 @@ export class TUI extends Container { // Height changes normally need a full re-render to keep the visible viewport aligned, // but Termux changes height when the software keyboard shows or hides. // In that environment, a full redraw causes the entire history to replay on every toggle. - if (heightChanged && !isTermuxSession()) { + if (heightChanged && !isTermuxSession() && !isMultiplexer) { logRedraw(`terminal height changed (${this.#previousHeight} -> ${height})`); fullRender(true); return; From 522f6b0c8d7d54d9f5fbaf8b69becc3608d68f7c Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 1 Apr 2026 15:31:46 +0200 Subject: [PATCH 2/2] docs(coding-agent): add changelog entry for inline image cap --- packages/coding-agent/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0da850e3c..3c0c499c0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed inline image rendering to cap image height and preserve multiplexer scrollback during terminal resizes ([#587](https://github.com/can1357/oh-my-pi/pull/587) by [@smileynet](https://github.com/smileynet)) + ## [13.17.0] - 2026-03-30 ### Added @@ -6451,4 +6455,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections \ No newline at end of file +- HTML export with syntax highlighting and collapsible sections