feat(cli): enhance DiffRenderer for uncolored layout preservation

Adds a disableColor mode to DiffRenderer and CodeColorizer. This allows for rendering uncolored diffs (for rejected tool calls) while maintaining full layout features like line numbers and indentation.
This commit is contained in:
Jarrod Whelan
2026-02-10 20:27:24 -08:00
parent 2db8fb20fa
commit 78ec824838
2 changed files with 54 additions and 31 deletions

View File

@@ -88,6 +88,7 @@ interface DiffRendererProps {
availableTerminalHeight?: number;
terminalWidth: number;
theme?: Theme;
disableColor?: boolean;
}
const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
@@ -99,6 +100,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
availableTerminalHeight,
terminalWidth,
theme,
disableColor = false,
}) => {
const settings = useSettings();
@@ -169,6 +171,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
maxWidth: terminalWidth,
theme,
settings,
disableColor,
});
} else {
return renderDiffContent(
@@ -177,6 +180,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
tabWidth,
availableTerminalHeight,
terminalWidth,
disableColor,
);
}
}, [
@@ -190,6 +194,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
theme,
settings,
tabWidth,
disableColor,
]);
return renderedOutput;
@@ -201,6 +206,7 @@ const renderDiffContent = (
tabWidth = DEFAULT_TAB_WIDTH,
availableTerminalHeight: number | undefined,
terminalWidth: number,
disableColor = false,
) => {
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
const normalizedLines = parsedLines.map((line) => ({
@@ -321,12 +327,26 @@ const renderDiffContent = (
const displayContent = line.content.substring(baseIndentation);
const backgroundColor =
line.type === 'add'
const backgroundColor = disableColor
? undefined
: line.type === 'add'
? semanticTheme.background.diff.added
: line.type === 'del'
? semanticTheme.background.diff.removed
: undefined;
const gutterColor = disableColor
? undefined
: semanticTheme.text.secondary;
const symbolColor = disableColor
? undefined
: line.type === 'add'
? semanticTheme.status.success
: line.type === 'del'
? semanticTheme.status.error
: undefined;
acc.push(
<Box key={lineKey} flexDirection="row">
<Box
@@ -336,32 +356,24 @@ const renderDiffContent = (
backgroundColor={backgroundColor}
justifyContent="flex-end"
>
<Text color={semanticTheme.text.secondary}>{gutterNumStr}</Text>
<Text color={gutterColor}>{gutterNumStr}</Text>
</Box>
{line.type === 'context' ? (
<>
<Text>{prefixSymbol} </Text>
<Text wrap="wrap">{colorizeLine(displayContent, language)}</Text>
<Text wrap="wrap">
{colorizeLine(
displayContent,
language,
undefined,
disableColor,
)}
</Text>
</>
) : (
<Text
backgroundColor={
line.type === 'add'
? semanticTheme.background.diff.added
: semanticTheme.background.diff.removed
}
wrap="wrap"
>
<Text
color={
line.type === 'add'
? semanticTheme.status.success
: semanticTheme.status.error
}
>
{prefixSymbol}
</Text>{' '}
{colorizeLine(displayContent, language)}
<Text backgroundColor={backgroundColor} wrap="wrap">
<Text color={symbolColor}>{prefixSymbol}</Text>{' '}
{colorizeLine(displayContent, language, undefined, disableColor)}
</Text>
)}
</Box>,

View File

@@ -116,7 +116,11 @@ export function colorizeLine(
line: string,
language: string | null,
theme?: Theme,
disableColor = false,
): React.ReactNode {
if (disableColor) {
return <Text>{line}</Text>;
}
const activeTheme = theme || themeManager.getActiveTheme();
return highlightAndRenderLine(line, language, activeTheme);
}
@@ -129,6 +133,7 @@ export interface ColorizeCodeOptions {
theme?: Theme | null;
settings: LoadedSettings;
hideLineNumbers?: boolean;
disableColor?: boolean;
}
/**
@@ -145,6 +150,7 @@ export function colorizeCode({
theme = null,
settings,
hideLineNumbers = false,
disableColor = false,
}: ColorizeCodeOptions): React.ReactNode {
const codeToHighlight = code.replace(/\n$/, '');
const activeTheme = theme || themeManager.getActiveTheme();
@@ -172,11 +178,9 @@ export function colorizeCode({
}
const renderedLines = lines.map((line, index) => {
const contentToRender = highlightAndRenderLine(
line,
language,
activeTheme,
);
const contentToRender = disableColor
? line
: highlightAndRenderLine(line, language, activeTheme);
return (
<Box key={index} minHeight={1}>
@@ -188,12 +192,15 @@ export function colorizeCode({
alignItems="flex-start"
justifyContent="flex-end"
>
<Text color={activeTheme.colors.Gray}>
<Text color={disableColor ? undefined : activeTheme.colors.Gray}>
{`${index + 1 + hiddenLinesCount}`}
</Text>
</Box>
)}
<Text color={activeTheme.defaultColor} wrap="wrap">
<Text
color={disableColor ? undefined : activeTheme.defaultColor}
wrap="wrap"
>
{contentToRender}
</Text>
</Box>
@@ -237,10 +244,14 @@ export function colorizeCode({
alignItems="flex-start"
justifyContent="flex-end"
>
<Text color={activeTheme.defaultColor}>{`${index + 1}`}</Text>
<Text color={disableColor ? undefined : activeTheme.defaultColor}>
{`${index + 1}`}
</Text>
</Box>
)}
<Text color={activeTheme.colors.Gray}>{line}</Text>
<Text color={disableColor ? undefined : activeTheme.colors.Gray}>
{line}
</Text>
</Box>
));