From 4eef01bdaa4672d7eb435e53caf780129066cac2 Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 6 Feb 2026 07:38:46 +0100 Subject: [PATCH] fix: fixed tab character display holes in UI by sanitizing output - Fixed tab character display in error messages and bash tool output by applying replaceTabs() sanitization. - Added TUI rendering sanitization documentation to AGENTS.md with guidelines for sanitizing text in tool renderers. - Applied replaceTabs() function to error text in patch shared module to ensure proper tab-to-space conversion. - Applied replaceTabs() function to bash tool output lines in both expanded and collapsed display modes. --- AGENTS.md | 21 +++++++++++++++++++++ packages/coding-agent/CHANGELOG.md | 5 ++++- packages/coding-agent/src/patch/shared.ts | 4 ++-- packages/coding-agent/src/tools/bash.ts | 8 +++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a02d655de..032c3baf2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -406,6 +406,27 @@ logger.debug("LSP fallback triggered", { reason }); Logs go to `~/.omp/logs/omp.YYYY-MM-DD.log` with automatic rotation. +## TUI Rendering Sanitization + +All text displayed in tool renderers must be sanitized before output. Raw content (file contents, error messages, tool output) can contain characters that break terminal rendering — tabs cause visual holes, long lines overflow, and unsanitized paths leak home directories. + +### Rules + +- **Tabs → spaces**: Always pass displayed text through `replaceTabs()` before rendering. Tabs produce variable-width gaps in terminals and cause visual holes in the TUI. Import from `@oh-my-pi/pi-tui` or `../tools/render-utils`. +- **Line truncation**: Truncate displayed lines with `truncateToWidth()` or `ui.truncate()` to prevent horizontal overflow. Use constants from `TRUNCATE_LENGTHS` for consistency. +- **Path shortening**: Use `shortenPath()` for file paths shown to users — replaces home directory prefix with `~`. +- **Content preview limits**: Use `PREVIEW_LIMITS` constants for collapsed/expanded line counts. Don't invent ad-hoc limits. + +### Where to apply + +Sanitization applies to **every** code path that renders text to the TUI, including: +- Success output (file previews, command output, search results) +- **Error messages** — these often embed file content (e.g., patch failure messages include the lines that failed to match) +- Diff content (both added/removed lines) +- Streaming previews + +A common mistake is sanitizing the happy path but forgetting error paths. If a message includes file content, it needs `replaceTabs()`. + ## Commands | Command | Description | diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 91c331806..211ca691b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Fixed + +- Fixed tab character display in error messages and bash tool output by properly replacing tabs with spaces ## [11.4.0] - 2026-02-06 @@ -3971,4 +3974,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 +- HTML export with syntax highlighting and collapsible sections \ No newline at end of file diff --git a/packages/coding-agent/src/patch/shared.ts b/packages/coding-agent/src/patch/shared.ts index cd206d318..802caa568 100644 --- a/packages/coding-agent/src/patch/shared.ts +++ b/packages/coding-agent/src/patch/shared.ts @@ -265,13 +265,13 @@ export const editToolRenderer = { if (result.isError) { if (errorText) { - text += `\n\n${uiTheme.fg("error", errorText)}`; + text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`; } } else if (result.details?.diff) { text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn); } else if (editDiffPreview) { if ("error" in editDiffPreview) { - text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`; + text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`; } else if (editDiffPreview.diff) { text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn); } diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index d0ee147fd..64935b477 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -17,7 +17,7 @@ import { applyHeadTail, normalizeBashCommand } from "./bash-normalize"; import type { OutputMeta } from "./output-meta"; import { allocateOutputArtifact, createTailBuffer } from "./output-utils"; import { resolveToCwd } from "./path-utils"; -import { formatBytes, wrapBrackets } from "./render-utils"; +import { formatBytes, replaceTabs, wrapBrackets } from "./render-utils"; import { ToolError } from "./tool-errors"; import { toolResult } from "./tool-result"; import { DEFAULT_MAX_BYTES } from "./truncate"; @@ -271,11 +271,13 @@ export const bashToolRenderer = { const hasOutput = displayOutput.trim().length > 0; if (hasOutput) { if (expanded) { - outputLines.push(...displayOutput.split("\n").map(line => uiTheme.fg("toolOutput", line))); + outputLines.push( + ...displayOutput.split("\n").map(line => uiTheme.fg("toolOutput", replaceTabs(line))), + ); } else { const styledOutput = displayOutput .split("\n") - .map(line => uiTheme.fg("toolOutput", line)) + .map(line => uiTheme.fg("toolOutput", replaceTabs(line))) .join("\n"); const textContent = styledOutput; const result = truncateToVisualLines(textContent, previewLines, width);