Merge pull request #587 from smileynet/fix/puppeteer-excessive-scrolling

fix(tui): cap inline image height and preserve multiplexer scrollback
This commit is contained in:
Can Bölük
2026-04-01 15:35:22 +02:00
committed by GitHub
6 changed files with 39 additions and 6 deletions
+3
View File
@@ -8,6 +8,9 @@
- `--scope user|project` flag added to `/marketplace uninstall`, `/marketplace upgrade`, `/plugins enable`, and `/plugins disable` to disambiguate when a plugin is installed in both scopes
- `omp plugin upgrade --scope project` with no plugin ID warns that `--scope` is ignored for bulk upgrades
### 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.1] - 2026-04-01
### Removed
@@ -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",
@@ -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;
@@ -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);
@@ -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 */
+6 -2
View File
@@ -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;