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.
This commit is contained in:
@@ -406,6 +406,27 @@ logger.debug("LSP fallback triggered", { reason });
|
|||||||
|
|
||||||
Logs go to `~/.omp/logs/omp.YYYY-MM-DD.log` with automatic rotation.
|
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
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [11.4.0] - 2026-02-06
|
||||||
|
|
||||||
@@ -3971,4 +3974,4 @@ Initial public release.
|
|||||||
- Git branch display in footer
|
- Git branch display in footer
|
||||||
- Message queueing during streaming responses
|
- Message queueing during streaming responses
|
||||||
- OAuth integration for Gmail and Google Calendar access
|
- OAuth integration for Gmail and Google Calendar access
|
||||||
- HTML export with syntax highlighting and collapsible sections
|
- HTML export with syntax highlighting and collapsible sections
|
||||||
@@ -265,13 +265,13 @@ export const editToolRenderer = {
|
|||||||
|
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (errorText) {
|
if (errorText) {
|
||||||
text += `\n\n${uiTheme.fg("error", errorText)}`;
|
text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
|
||||||
}
|
}
|
||||||
} else if (result.details?.diff) {
|
} else if (result.details?.diff) {
|
||||||
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
||||||
} else if (editDiffPreview) {
|
} else if (editDiffPreview) {
|
||||||
if ("error" in 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) {
|
} else if (editDiffPreview.diff) {
|
||||||
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { applyHeadTail, normalizeBashCommand } from "./bash-normalize";
|
|||||||
import type { OutputMeta } from "./output-meta";
|
import type { OutputMeta } from "./output-meta";
|
||||||
import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
|
import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
|
||||||
import { resolveToCwd } from "./path-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 { ToolError } from "./tool-errors";
|
||||||
import { toolResult } from "./tool-result";
|
import { toolResult } from "./tool-result";
|
||||||
import { DEFAULT_MAX_BYTES } from "./truncate";
|
import { DEFAULT_MAX_BYTES } from "./truncate";
|
||||||
@@ -271,11 +271,13 @@ export const bashToolRenderer = {
|
|||||||
const hasOutput = displayOutput.trim().length > 0;
|
const hasOutput = displayOutput.trim().length > 0;
|
||||||
if (hasOutput) {
|
if (hasOutput) {
|
||||||
if (expanded) {
|
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 {
|
} else {
|
||||||
const styledOutput = displayOutput
|
const styledOutput = displayOutput
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map(line => uiTheme.fg("toolOutput", line))
|
.map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const textContent = styledOutput;
|
const textContent = styledOutput;
|
||||||
const result = truncateToVisualLines(textContent, previewLines, width);
|
const result = truncateToVisualLines(textContent, previewLines, width);
|
||||||
|
|||||||
Reference in New Issue
Block a user