fix(tui): honor padded markdown width budgets

This commit is contained in:
usr-bin-roygbiv
2026-07-28 07:41:59 +00:00
parent f32dd6eff4
commit a28ca5a268
6 changed files with 50 additions and 10 deletions
+9 -2
View File
@@ -12,7 +12,14 @@ import { normalizeToLF } from "../edit/normalize";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
import { Ellipsis, fileHyperlink, framedBlock, renderStatusLine, truncateToWidth } from "../tui";
import {
Ellipsis,
fileHyperlink,
framedBlock,
outputBlockContentWidth,
renderStatusLine,
truncateToWidth,
} from "../tui";
import { resolveFileDisplayMode } from "../utils/file-display-mode";
import type { ToolSession } from ".";
import { truncateForPrompt } from "./approval";
@@ -695,7 +702,7 @@ export const astEditToolRenderer = {
}
return framedBlock(uiTheme, width => {
const changeLines = buildChangeBody(changeGroups, Boolean(options.expanded), COLLAPSED_CHANGE_LIMIT, uiTheme);
const innerWidth = Math.max(1, width - 3);
const innerWidth = outputBlockContentWidth(width);
const bodyLines = [...changeLines, ...extraLines].map(l => truncateToWidth(l, innerWidth, Ellipsis.Omit));
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
return {
@@ -1,7 +1,7 @@
import { type Component, padding, Text, visibleWidth } from "@oh-my-pi/pi-tui";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme, ThemeColor } from "../modes/theme/theme";
import { framedBlock, renderStatusLine } from "../tui";
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../tui";
import type {
GhRunWatchFailedLogDetails,
GhRunWatchJobDetails,
@@ -364,7 +364,7 @@ function renderFallbackComponent(
}
return framedBlock(theme, width => {
const lineWidth = Math.max(1, (width || FALLBACK_WIDTH) - 3);
const lineWidth = outputBlockContentWidth(width || FALLBACK_WIDTH);
const expanded = options.expanded;
const limit = expanded ? allLines.length : Math.min(allLines.length, PREVIEW_LIMITS.OUTPUT_EXPANDED);
const visible = allLines.slice(0, limit);
@@ -464,7 +464,7 @@ export const githubToolRenderer = {
uiTheme,
);
return framedBlock(uiTheme, width => {
const innerWidth = Math.max(1, (width || FALLBACK_WIDTH) - 3);
const innerWidth = outputBlockContentWidth(width || FALLBACK_WIDTH);
const sections = buildWatchSections(watch, uiTheme, options, innerWidth);
return {
header,
+4 -4
View File
@@ -10,7 +10,7 @@ import {
formatStatusIcon,
replaceTabs,
} from "../tools/render-utils";
import { renderOutputBlock } from "./output-block";
import { outputBlockContentWidth, renderOutputBlock } from "./output-block";
import type { State } from "./types";
export interface CodeCellOptions {
@@ -230,9 +230,9 @@ export function renderMarkdownCell(options: MarkdownCellOptions, theme: Theme):
const { title, meta } = formatHeader(codeOptions, theme);
const state = getState(options.status);
// Markdown component manages its own wrapping at the inner content width.
// `renderOutputBlock` adds a `│ ` prefix + `│` suffix → 3 visible columns.
const innerWidth = Math.max(20, width - 3);
// Markdown component manages its own wrapping at the same inner width as
// `renderOutputBlock`, so collapsed row caps are applied after final wrapping.
const innerWidth = Math.max(20, outputBlockContentWidth(width));
const allLines = content.trim() ? new Markdown(content, 0, 0, getMarkdownTheme()).render(innerWidth) : [];
const maxContentLines = expanded ? allLines.length : Math.min(allLines.length, contentMaxLines);
const contentLines = allLines.slice(0, maxContentLines);
@@ -1,6 +1,7 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { stripVTControlCharacters } from "node:util";
import { getThemeByName, initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { renderMarkdownCell } from "@oh-my-pi/pi-coding-agent/tui/code-cell";
import { renderOutputBlock } from "@oh-my-pi/pi-coding-agent/tui/output-block";
describe("renderOutputBlock", () => {
@@ -36,4 +37,21 @@ describe("renderOutputBlock", () => {
expect(lines.filter(line => line.startsWith("│"))).toEqual(["│abcdefghijklmn│"]);
});
it("budgets collapsed Markdown rows against the padded block width", async () => {
const theme = (await getThemeByName("dark"))!;
const lines = renderMarkdownCell(
{
content: "x".repeat(27),
contentMaxLines: 1,
status: "complete",
title: "Read",
width: 30,
},
theme,
).map(line => stripVTControlCharacters(line));
expect(lines[1]).toBe(`│ ${"x".repeat(26)} │`);
expect(lines[2]).toStartWith("│ … 1 more line");
});
});
+7 -1
View File
@@ -2596,7 +2596,13 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
// Use the list's start property (defaults to 1 for ordered lists)
const startNumber = token.start ?? 1;
const pushWrapped = (text: string, firstPrefix: string, continuationPrefix: string): void => {
const bodyWidth = Math.max(1, width - visibleWidth(firstPrefix));
const prefixWidth = visibleWidth(firstPrefix);
if (prefixWidth >= width) {
lines.push(truncateToWidth(firstPrefix, width, Ellipsis.Omit));
lines.push(...wrapTextWithAnsi(text, Math.max(1, width)));
return;
}
const bodyWidth = width - prefixWidth;
const wrapped = wrapTextWithAnsi(text, bodyWidth);
if (wrapped.length === 0) {
lines.push(firstPrefix);
+9
View File
@@ -160,6 +160,15 @@ describe("Markdown component", () => {
expect(plainLines.every(line => visibleWidth(line) <= 16)).toBe(true);
});
it("keeps list rows within width when the marker consumes the line", () => {
const markdown = new Markdown("123456789. x", 0, 0, defaultMarkdownTheme);
const plainLines = markdown.render(8).map(line => stripVTControlCharacters(line).trimEnd());
expect(plainLines.every(line => visibleWidth(line) <= 8)).toBe(true);
expect(plainLines.join("")).toContain("x");
});
it("should maintain numbering when code blocks are not indented (LLM output)", () => {
// When code blocks aren't indented, marked parses each item as a separate list.
// We use token.start to preserve the original numbering.