fix: patch approval rendering to reduce churn (#453)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-02 17:22:28 -08:00
committed by GitHub
parent cd28bab41a
commit d2e0fc3bc5
5 changed files with 265 additions and 173 deletions

View File

@@ -1,5 +1,5 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { memo, useMemo, useState } from "react";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
@@ -121,6 +121,33 @@ export const InlineBashApproval = memo(
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
// Memoize the static command content so it doesn't re-render on keystroke
// This prevents flicker when typing feedback in the custom input field
const memoizedCommandContent = useMemo(
() => (
<>
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
Run this command?
</Text>
<Box height={1} />
{/* Command preview */}
<Box paddingLeft={2} flexDirection="column">
<Text>{bashInfo.command}</Text>
{bashInfo.description && (
<Text dimColor>{bashInfo.description}</Text>
)}
</Box>
</>
),
[bashInfo.command, bashInfo.description, solidLine],
);
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -130,21 +157,8 @@ export const InlineBashApproval = memo(
return (
<Box flexDirection="column">
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
Run this command?
</Text>
<Box height={1} />
{/* Command preview */}
<Box paddingLeft={2} flexDirection="column">
<Text>{bashInfo.command}</Text>
{bashInfo.description && <Text dimColor>{bashInfo.description}</Text>}
</Box>
{/* Static command content - memoized to prevent re-render on keystroke */}
{memoizedCommandContent}
{/* Options */}
<Box marginTop={1} flexDirection="column">

View File

@@ -282,6 +282,143 @@ export const InlineFileEditApproval = memo(
const headerText = getHeaderText(fileEdit);
const diffKind = getDiffKind(fileEdit.toolName);
// Memoize the static diff content so it doesn't re-render on keystroke
// This prevents flicker when typing feedback in the custom input field
// biome-ignore lint/correctness/useExhaustiveDependencies: JSON.stringify(fileEdit.edits) provides stable value comparison for arrays
const memoizedDiffContent = useMemo(
() => (
<>
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
{headerText}
</Text>
{/* Dotted separator before diff content */}
<Text dimColor>{dottedLine}</Text>
{/* Diff preview */}
<Box paddingLeft={0}>
{fileEdit.patchInput ? (
// Render patch operations (can be multiple files)
<Box flexDirection="column">
{parsePatchOperations(fileEdit.patchInput).map((op, idx) => {
const { relative } = require("node:path");
const cwd = process.cwd();
const relPath = relative(cwd, op.path);
const displayPath = relPath.startsWith("..")
? op.path
: relPath;
// Look up precomputed diff using toolCallId:path key
const diffKey = fileEdit.toolCallId
? `${fileEdit.toolCallId}:${op.path}`
: undefined;
const opDiff =
diffKey && allDiffs ? allDiffs.get(diffKey) : undefined;
if (op.kind === "add") {
return (
<Box key={`patch-add-${op.path}`} flexDirection="column">
{idx > 0 && <Box height={1} />}
<Text dimColor>{displayPath}</Text>
<AdvancedDiffRenderer
precomputed={opDiff}
kind="write"
filePath={op.path}
content={op.content}
showHeader={false}
/>
</Box>
);
} else if (op.kind === "update") {
return (
<Box
key={`patch-update-${op.path}`}
flexDirection="column"
>
{idx > 0 && <Box height={1} />}
<Text dimColor>{displayPath}</Text>
<AdvancedDiffRenderer
precomputed={opDiff}
kind="edit"
filePath={op.path}
oldString={op.oldString}
newString={op.newString}
showHeader={false}
/>
</Box>
);
} else if (op.kind === "delete") {
return (
<Box
key={`patch-delete-${op.path}`}
flexDirection="column"
>
{idx > 0 && <Box height={1} />}
<Text dimColor>{displayPath}</Text>
<Text color="red">File will be deleted</Text>
</Box>
);
}
return null;
})}
</Box>
) : diffKind === "write" ? (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="write"
filePath={fileEdit.filePath}
content={fileEdit.content || ""}
showHeader={false}
/>
) : diffKind === "multi_edit" ? (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="multi_edit"
filePath={fileEdit.filePath}
edits={fileEdit.edits || []}
showHeader={false}
/>
) : (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="edit"
filePath={fileEdit.filePath}
oldString={fileEdit.oldString || ""}
newString={fileEdit.newString || ""}
replaceAll={fileEdit.replaceAll}
showHeader={false}
/>
)}
</Box>
{/* Dotted separator after diff content */}
<Text dimColor>{dottedLine}</Text>
</>
),
// Use primitive values to avoid memo invalidation when parent re-renders.
// Arrays/objects are compared by reference, so we stringify edits for stable comparison.
[
fileEdit.filePath,
fileEdit.content,
fileEdit.oldString,
fileEdit.newString,
fileEdit.replaceAll,
fileEdit.patchInput,
fileEdit.toolCallId,
JSON.stringify(fileEdit.edits),
precomputedDiff,
allDiffs,
solidLine,
dottedLine,
headerText,
diffKind,
],
);
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -291,109 +428,8 @@ export const InlineFileEditApproval = memo(
return (
<Box flexDirection="column">
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
{headerText}
</Text>
{/* Dotted separator before diff content */}
<Text dimColor>{dottedLine}</Text>
{/* Diff preview */}
<Box paddingLeft={0}>
{fileEdit.patchInput ? (
// Render patch operations (can be multiple files)
<Box flexDirection="column">
{parsePatchOperations(fileEdit.patchInput).map((op, idx) => {
const { relative } = require("node:path");
const cwd = process.cwd();
const relPath = relative(cwd, op.path);
const displayPath = relPath.startsWith("..")
? op.path
: relPath;
// Look up precomputed diff using toolCallId:path key
const diffKey = fileEdit.toolCallId
? `${fileEdit.toolCallId}:${op.path}`
: undefined;
const opDiff =
diffKey && allDiffs ? allDiffs.get(diffKey) : undefined;
if (op.kind === "add") {
return (
<Box key={`patch-add-${op.path}`} flexDirection="column">
{idx > 0 && <Box height={1} />}
<Text dimColor>{displayPath}</Text>
<AdvancedDiffRenderer
precomputed={opDiff}
kind="write"
filePath={op.path}
content={op.content}
showHeader={false}
/>
</Box>
);
} else if (op.kind === "update") {
return (
<Box key={`patch-update-${op.path}`} flexDirection="column">
{idx > 0 && <Box height={1} />}
<Text dimColor>{displayPath}</Text>
<AdvancedDiffRenderer
precomputed={opDiff}
kind="edit"
filePath={op.path}
oldString={op.oldString}
newString={op.newString}
showHeader={false}
/>
</Box>
);
} else if (op.kind === "delete") {
return (
<Box key={`patch-delete-${op.path}`} flexDirection="column">
{idx > 0 && <Box height={1} />}
<Text dimColor>{displayPath}</Text>
<Text color="red">File will be deleted</Text>
</Box>
);
}
return null;
})}
</Box>
) : diffKind === "write" ? (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="write"
filePath={fileEdit.filePath}
content={fileEdit.content || ""}
showHeader={false}
/>
) : diffKind === "multi_edit" ? (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="multi_edit"
filePath={fileEdit.filePath}
edits={fileEdit.edits || []}
showHeader={false}
/>
) : (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="edit"
filePath={fileEdit.filePath}
oldString={fileEdit.oldString || ""}
newString={fileEdit.newString || ""}
replaceAll={fileEdit.replaceAll}
showHeader={false}
/>
)}
</Box>
{/* Dotted separator after diff content */}
<Text dimColor>{dottedLine}</Text>
{/* Static diff content - memoized to prevent re-render on keystroke */}
{memoizedDiffContent}
{/* Options */}
<Box marginTop={1} flexDirection="column">

View File

@@ -1,5 +1,5 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { memo, useMemo, useState } from "react";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
@@ -131,6 +131,30 @@ export const InlineGenericApproval = memo(
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
const formattedArgs = formatArgs(toolArgs);
// Memoize the static tool content so it doesn't re-render on keystroke
// This prevents flicker when typing feedback in the custom input field
const memoizedToolContent = useMemo(
() => (
<>
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
Run {toolName}?
</Text>
<Box height={1} />
{/* Arguments preview */}
<Box paddingLeft={2} flexDirection="column">
<Text dimColor>{formattedArgs}</Text>
</Box>
</>
),
[toolName, formattedArgs, solidLine],
);
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -140,20 +164,8 @@ export const InlineGenericApproval = memo(
return (
<Box flexDirection="column">
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
Run {toolName}?
</Text>
<Box height={1} />
{/* Arguments preview */}
<Box paddingLeft={2} flexDirection="column">
<Text dimColor>{formattedArgs}</Text>
</Box>
{/* Static tool content - memoized to prevent re-render on keystroke */}
{memoizedToolContent}
{/* Options */}
<Box marginTop={1} flexDirection="column">

View File

@@ -1,5 +1,5 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { memo, useMemo, useState } from "react";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
import { MarkdownDisplay } from "./MarkdownDisplay";
@@ -109,6 +109,32 @@ export const InlinePlanApproval = memo(
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
const dottedLine = DOTTED_LINE.repeat(Math.max(columns - 2, 10));
// Memoize the static plan content so it doesn't re-render on keystroke
// This prevents flicker when typing feedback in the custom input field
const memoizedPlanContent = useMemo(
() => (
<>
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
Ready to code? Here is your plan:
</Text>
{/* Dotted separator before plan content */}
<Text dimColor>{dottedLine}</Text>
{/* Plan content - no indentation, just like Claude Code */}
<MarkdownDisplay text={plan} />
{/* Dotted separator after plan content */}
<Text dimColor>{dottedLine}</Text>
</>
),
[plan, solidLine, dottedLine],
);
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -118,22 +144,8 @@ export const InlinePlanApproval = memo(
return (
<Box flexDirection="column" marginTop={1}>
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header */}
<Text bold color={colors.approval.header}>
Ready to code? Here is your plan:
</Text>
{/* Dotted separator before plan content */}
<Text dimColor>{dottedLine}</Text>
{/* Plan content - no indentation, just like Claude Code */}
<MarkdownDisplay text={plan} />
{/* Dotted separator after plan content */}
<Text dimColor>{dottedLine}</Text>
{/* Static plan content - memoized to prevent re-render on keystroke */}
{memoizedPlanContent}
{/* Question */}
<Box marginTop={1}>

View File

@@ -1,5 +1,5 @@
import { Box, Text, useInput } from "ink";
import { Fragment, memo, useState } from "react";
import { Fragment, memo, useMemo, useState } from "react";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
@@ -251,6 +251,43 @@ export const InlineQuestionApproval = memo(
// Generate horizontal line
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
// Memoize the static header content so it doesn't re-render on keystroke
// This prevents flicker when typing in the custom input field
const memoizedHeaderContent = useMemo(
() => (
<>
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header label */}
<Text>{currentQuestion?.header}</Text>
<Box height={1} />
{/* Question */}
<Text bold>{currentQuestion?.question}</Text>
<Box height={1} />
{/* Progress indicator for multiple questions */}
{questions.length > 1 && (
<Box marginBottom={1}>
<Text dimColor>
Question {currentQuestionIndex + 1} of {questions.length}
</Text>
</Box>
)}
</>
),
[
currentQuestion?.header,
currentQuestion?.question,
currentQuestionIndex,
questions.length,
solidLine,
],
);
// Hint text based on state - keep consistent to avoid jarring changes
const hintText = currentQuestion?.multiSelect
? "Enter to toggle · Arrow to navigate · Esc to cancel"
@@ -260,27 +297,8 @@ export const InlineQuestionApproval = memo(
return (
<Box flexDirection="column">
{/* Top solid line */}
<Text dimColor>{solidLine}</Text>
{/* Header label */}
<Text>{currentQuestion.header}</Text>
<Box height={1} />
{/* Question */}
<Text bold>{currentQuestion.question}</Text>
<Box height={1} />
{/* Progress indicator for multiple questions */}
{questions.length > 1 && (
<Box marginBottom={1}>
<Text dimColor>
Question {currentQuestionIndex + 1} of {questions.length}
</Text>
</Box>
)}
{/* Static header content - memoized to prevent re-render on keystroke */}
{memoizedHeaderContent}
{/* Options - Format: N. [ ] Label (selector, number, checkbox, label) */}
<Box flexDirection="column">