fix: patch flicker (#459)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-04 21:41:03 -08:00
committed by GitHub
parent e21bfb9b31
commit e424c6ce0c
6 changed files with 704 additions and 31 deletions

View File

@@ -62,6 +62,7 @@ import {
} from "./commands/profile";
import { AgentSelector } from "./components/AgentSelector";
// ApprovalDialog removed - all approvals now render inline
import { ApprovalPreview } from "./components/ApprovalPreview";
import { AssistantMessage } from "./components/AssistantMessageRich";
import { BashCommandMessage } from "./components/BashCommandMessage";
import { CommandMessage } from "./components/CommandMessage";
@@ -74,7 +75,6 @@ import { InlineBashApproval } from "./components/InlineBashApproval";
import { InlineEnterPlanModeApproval } from "./components/InlineEnterPlanModeApproval";
import { InlineFileEditApproval } from "./components/InlineFileEditApproval";
import { InlineGenericApproval } from "./components/InlineGenericApproval";
import { InlinePlanApproval } from "./components/InlinePlanApproval";
import { InlineQuestionApproval } from "./components/InlineQuestionApproval";
import { Input } from "./components/InputRich";
import { McpSelector } from "./components/McpSelector";
@@ -89,6 +89,9 @@ import { PinDialog, validateAgentName } from "./components/PinDialog";
import { ReasoningMessage } from "./components/ReasoningMessageRich";
import { ResumeSelector } from "./components/ResumeSelector";
import { formatUsageStats } from "./components/SessionStats";
// InlinePlanApproval kept for easy rollback if needed
// import { InlinePlanApproval } from "./components/InlinePlanApproval";
import { StaticPlanApproval } from "./components/StaticPlanApproval";
import { StatusMessage } from "./components/StatusMessage";
import { SubagentGroupDisplay } from "./components/SubagentGroupDisplay";
import { SubagentGroupStatic } from "./components/SubagentGroupStatic";
@@ -311,7 +314,7 @@ function planFileExists(): boolean {
}
// Read plan content from the plan file
function readPlanFile(): string {
function _readPlanFile(): string {
const planFilePath = permissionMode.getPlanFilePath();
if (!planFilePath) {
return "No plan file path set.";
@@ -378,6 +381,20 @@ type StaticItem =
error?: string;
}>;
}
| {
// Preview content committed early during approval to enable flicker-free UI
// When an approval's content is tall enough to overflow the viewport,
// we commit the preview to static and only show small approval options in dynamic
kind: "approval_preview";
id: string;
toolCallId: string;
toolName: string;
toolArgs: string;
// Optional precomputed/cached data for rendering
precomputedDiff?: AdvancedDiffSuccess;
planContent?: string; // For ExitPlanMode
planFilePath?: string; // For ExitPlanMode
}
| Line;
export default function App({
@@ -891,6 +908,10 @@ export default function App({
// (needed because plan mode is exited before rendering the result)
const lastPlanFilePathRef = useRef<string | null>(null);
// Track which approval tool call IDs have had their previews eagerly committed
// This prevents double-committing when the approval changes
const eagerCommittedPreviewsRef = useRef<Set<string>>(new Set());
// Recompute UI state from buffers after each streaming chunk
const refreshDerived = useCallback(() => {
const b = buffersRef.current;
@@ -962,6 +983,48 @@ export default function App({
}
}, [loadingState, startupApproval, startupApprovals]);
// Eager commit for ExitPlanMode: Always commit plan preview to staticItems
// This keeps the dynamic area small (just approval options) to avoid flicker
useEffect(() => {
if (!currentApproval) return;
if (currentApproval.toolName !== "ExitPlanMode") return;
const toolCallId = currentApproval.toolCallId;
if (!toolCallId) return;
// Already committed preview for this approval?
if (eagerCommittedPreviewsRef.current.has(toolCallId)) return;
const planFilePath = permissionMode.getPlanFilePath();
if (!planFilePath) return;
try {
const { readFileSync, existsSync } = require("node:fs");
if (!existsSync(planFilePath)) return;
const planContent = readFileSync(planFilePath, "utf-8");
// Commit preview to static area
const previewItem: StaticItem = {
kind: "approval_preview",
id: `approval-preview-${toolCallId}`,
toolCallId,
toolName: currentApproval.toolName,
toolArgs: currentApproval.toolArgs || "{}",
planContent,
planFilePath,
};
setStaticItems((prev) => [...prev, previewItem]);
eagerCommittedPreviewsRef.current.add(toolCallId);
// Also capture plan file path for post-approval rendering
lastPlanFilePathRef.current = planFilePath;
} catch {
// Failed to read plan, don't commit preview
}
}, [currentApproval]);
// Backfill message history when resuming (only once)
useEffect(() => {
if (
@@ -5643,6 +5706,16 @@ Plan file path: ${planFilePath}`;
<CommandMessage line={item} />
) : item.kind === "bash_command" ? (
<BashCommandMessage line={item} />
) : item.kind === "approval_preview" ? (
<ApprovalPreview
toolName={item.toolName}
toolArgs={item.toolArgs}
precomputedDiff={item.precomputedDiff}
allDiffs={precomputedDiffsRef.current}
planContent={item.planContent}
planFilePath={item.planFilePath}
toolCallId={item.toolCallId}
/>
) : null}
</Box>
)}
@@ -5788,10 +5861,10 @@ Plan file path: ${planFilePath}`;
return (
<Box key={ln.id} flexDirection="column" marginTop={1}>
{/* For ExitPlanMode awaiting approval: render InlinePlanApproval */}
{/* For ExitPlanMode awaiting approval: render StaticPlanApproval */}
{/* Plan preview is eagerly committed to staticItems, so this only shows options */}
{isExitPlanModeApproval ? (
<InlinePlanApproval
plan={readPlanFile()}
<StaticPlanApproval
onApprove={() => handlePlanApprove(false)}
onApproveAndAcceptEdits={() =>
handlePlanApprove(true)

View File

@@ -0,0 +1,322 @@
import { Box, Text } from "ink";
import { memo } from "react";
import type { AdvancedDiffSuccess } from "../helpers/diff";
import { parsePatchOperations } from "../helpers/formatArgsDisplay";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer";
import { colors } from "./colors";
import { BashPreview } from "./previews/BashPreview";
import { PlanPreview } from "./previews/PlanPreview";
const SOLID_LINE = "─";
const DOTTED_LINE = "╌";
type Props = {
toolName: string;
toolArgs: string;
precomputedDiff?: AdvancedDiffSuccess;
allDiffs?: Map<string, AdvancedDiffSuccess>;
planContent?: string;
planFilePath?: string;
toolCallId?: string;
};
/**
* Get a human-readable header for file edit tools
*/
function getFileEditHeader(toolName: string, toolArgs: string): string {
const t = toolName.toLowerCase();
try {
const args = JSON.parse(toolArgs);
// Handle patch tools
if (t === "apply_patch" || t === "applypatch") {
if (args.input) {
const operations = parsePatchOperations(args.input);
if (operations.length > 1) {
return `Apply patch to ${operations.length} files?`;
} else if (operations.length === 1) {
const op = operations[0];
if (op) {
const { relative } = require("node:path");
const cwd = process.cwd();
const relPath = relative(cwd, op.path);
const displayPath = relPath.startsWith("..") ? op.path : relPath;
if (op.kind === "add") return `Write to ${displayPath}?`;
if (op.kind === "update") return `Update ${displayPath}?`;
if (op.kind === "delete") return `Delete ${displayPath}?`;
}
}
}
return "Apply patch?";
}
// Handle single-file edit/write tools
const filePath = args.file_path || "";
const { relative } = require("node:path");
const cwd = process.cwd();
const relPath = relative(cwd, filePath);
const displayPath = relPath.startsWith("..") ? filePath : relPath;
if (
t === "write" ||
t === "write_file" ||
t === "writefile" ||
t === "write_file_gemini" ||
t === "writefilegemini"
) {
const { existsSync } = require("node:fs");
try {
if (existsSync(filePath)) {
return `Overwrite ${displayPath}?`;
}
} catch {
// Ignore
}
return `Write to ${displayPath}?`;
}
if (
t === "edit" ||
t === "str_replace_editor" ||
t === "str_replace_based_edit_tool"
) {
return `Update ${displayPath}?`;
}
if (t === "multi_edit" || t === "multiedit") {
return `Apply edits to ${displayPath}?`;
}
} catch {
// Fall through
}
return `${toolName} requires approval`;
}
/**
* ApprovalPreview - Renders the preview content for an eagerly-committed approval
*
* This component renders the "preview" part of an approval that was committed
* early to enable flicker-free approval UI. It ensures visual parity with
* what the inline approval components show.
*/
export const ApprovalPreview = memo(
({
toolName,
toolArgs,
precomputedDiff,
allDiffs,
planContent,
toolCallId,
}: Props) => {
const columns = useTerminalWidth();
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
const dottedLine = DOTTED_LINE.repeat(Math.max(columns - 2, 10));
// ExitPlanMode: Use PlanPreview component
if (toolName === "ExitPlanMode" && planContent) {
return (
<Box flexDirection="column">
<PlanPreview plan={planContent} />
</Box>
);
}
// Bash/Shell: Use BashPreview component
if (
toolName === "Bash" ||
toolName === "shell" ||
toolName === "Shell" ||
toolName === "shell_command"
) {
try {
const args = JSON.parse(toolArgs);
const command =
typeof args.command === "string"
? args.command
: Array.isArray(args.command)
? args.command.join(" ")
: "";
const description = args.description || args.justification || "";
return (
<Box flexDirection="column">
<BashPreview command={command} description={description} />
</Box>
);
} catch {
// Fall through to generic
}
}
// File Edit tools: Render diff preview
if (
toolName === "Edit" ||
toolName === "MultiEdit" ||
toolName === "Write" ||
toolName === "str_replace_editor" ||
toolName === "str_replace_based_edit_tool" ||
toolName === "apply_patch" ||
toolName === "ApplyPatch"
) {
const headerText = getFileEditHeader(toolName, toolArgs);
try {
const args = JSON.parse(toolArgs);
// Handle patch tools (can have multiple files)
if (
args.input &&
(toolName === "apply_patch" || toolName === "ApplyPatch")
) {
const operations = parsePatchOperations(args.input);
return (
<Box flexDirection="column">
<Text dimColor>{solidLine}</Text>
<Text bold color={colors.approval.header}>
{headerText}
</Text>
<Text dimColor>{dottedLine}</Text>
<Box flexDirection="column">
{operations.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;
const diffKey = toolCallId
? `${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>
);
}
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>
);
}
if (op.kind === "delete") {
return (
<Box key={`patch-delete-${op.path}`}>
{idx > 0 && <Box height={1} />}
<Text>
Delete <Text bold>{displayPath}</Text>
</Text>
</Box>
);
}
return null;
})}
</Box>
<Text dimColor>{dottedLine}</Text>
</Box>
);
}
// Single file edit/write
const filePath = args.file_path || "";
return (
<Box flexDirection="column">
<Text dimColor>{solidLine}</Text>
<Text bold color={colors.approval.header}>
{headerText}
</Text>
<Text dimColor>{dottedLine}</Text>
{/* Write */}
{args.content !== undefined && (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="write"
filePath={filePath}
content={args.content}
/>
)}
{/* Multi-edit */}
{args.edits && Array.isArray(args.edits) && (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="multi_edit"
filePath={filePath}
edits={args.edits.map(
(e: { old_string?: string; new_string?: string }) => ({
old_string: e.old_string || "",
new_string: e.new_string || "",
}),
)}
/>
)}
{/* Single edit */}
{args.old_string !== undefined && !args.edits && (
<AdvancedDiffRenderer
precomputed={precomputedDiff}
kind="edit"
filePath={filePath}
oldString={args.old_string || ""}
newString={args.new_string || ""}
replaceAll={args.replace_all}
/>
)}
<Text dimColor>{dottedLine}</Text>
</Box>
);
} catch {
// Fall through to generic
}
}
// Generic fallback
return (
<Box flexDirection="column">
<Text dimColor>{solidLine}</Text>
<Text bold color={colors.approval.header}>
{toolName} requires approval
</Text>
<Text dimColor>{dottedLine}</Text>
</Box>
);
},
);
ApprovalPreview.displayName = "ApprovalPreview";

View File

@@ -0,0 +1,199 @@
import { Box, Text, useInput } from "ink";
import { memo, useState } from "react";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { colors } from "./colors";
type Props = {
onApprove: () => void;
onApproveAndAcceptEdits: () => void;
onKeepPlanning: (reason: string) => void;
isFocused?: boolean;
};
/**
* StaticPlanApproval - Options-only plan approval component
*
* This component renders ONLY the approval options (no plan preview).
* The plan preview is committed separately to the Static area via the
* eager commit pattern, which keeps this component small (~8 lines)
* and flicker-free.
*
* The plan prop was removed because the plan is rendered in the Static
* area by ApprovalPreview, not here.
*/
export const StaticPlanApproval = memo(
({
onApprove,
onApproveAndAcceptEdits,
onKeepPlanning,
isFocused = true,
}: Props) => {
const [selectedOption, setSelectedOption] = useState(0);
const [customReason, setCustomReason] = useState("");
const columns = useTerminalWidth();
const customOptionIndex = 2;
const maxOptionIndex = customOptionIndex;
const isOnCustomOption = selectedOption === customOptionIndex;
const customOptionPlaceholder =
"Type here to tell Letta Code what to change";
useInput(
(input, key) => {
if (!isFocused) return;
// CTRL-C: keep planning with cancel message
if (key.ctrl && input === "c") {
onKeepPlanning("User pressed CTRL-C to cancel");
return;
}
// Arrow navigation always works
if (key.upArrow) {
setSelectedOption((prev) => Math.max(0, prev - 1));
return;
}
if (key.downArrow) {
setSelectedOption((prev) => Math.min(maxOptionIndex, prev + 1));
return;
}
// When on custom input option
if (isOnCustomOption) {
if (key.return) {
if (customReason.trim()) {
onKeepPlanning(customReason.trim());
}
return;
}
if (key.escape) {
if (customReason) {
setCustomReason("");
} else {
onKeepPlanning("User cancelled");
}
return;
}
if (key.backspace || key.delete) {
setCustomReason((prev) => prev.slice(0, -1));
return;
}
if (input && !key.ctrl && !key.meta && input.length === 1) {
setCustomReason((prev) => prev + input);
}
return;
}
// When on regular options
if (key.return) {
if (selectedOption === 0) {
onApproveAndAcceptEdits();
} else if (selectedOption === 1) {
onApprove();
}
return;
}
if (key.escape) {
onKeepPlanning("User cancelled");
}
},
{ isActive: isFocused },
);
// Hint text based on state
const hintText = isOnCustomOption
? customReason
? "Enter to submit · Esc to clear"
: "Type feedback · Esc to cancel"
: "Enter to select · Esc to cancel";
return (
<Box flexDirection="column">
{/* Question */}
<Box>
<Text>Would you like to proceed?</Text>
</Box>
{/* Options */}
<Box marginTop={1} flexDirection="column">
{/* Option 1: Yes, and auto-accept edits */}
<Box flexDirection="row">
<Box width={5} flexShrink={0}>
<Text
color={
selectedOption === 0 ? colors.approval.header : undefined
}
>
{selectedOption === 0 ? "" : " "} 1.
</Text>
</Box>
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
<Text
wrap="wrap"
color={
selectedOption === 0 ? colors.approval.header : undefined
}
>
Yes, and auto-accept edits
</Text>
</Box>
</Box>
{/* Option 2: Yes, and manually approve edits */}
<Box flexDirection="row">
<Box width={5} flexShrink={0}>
<Text
color={
selectedOption === 1 ? colors.approval.header : undefined
}
>
{selectedOption === 1 ? "" : " "} 2.
</Text>
</Box>
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
<Text
wrap="wrap"
color={
selectedOption === 1 ? colors.approval.header : undefined
}
>
Yes, and manually approve edits
</Text>
</Box>
</Box>
{/* Option 3: Custom input */}
<Box flexDirection="row">
<Box width={5} flexShrink={0}>
<Text
color={isOnCustomOption ? colors.approval.header : undefined}
>
{isOnCustomOption ? "" : " "} 3.
</Text>
</Box>
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
{customReason ? (
<Text wrap="wrap">
{customReason}
{isOnCustomOption && "█"}
</Text>
) : (
<Text wrap="wrap" dimColor>
{customOptionPlaceholder}
{isOnCustomOption && "█"}
</Text>
)}
</Box>
</Box>
</Box>
{/* Hint */}
<Box marginTop={1}>
<Text dimColor>{hintText}</Text>
</Box>
</Box>
);
},
);
StaticPlanApproval.displayName = "StaticPlanApproval";

View File

@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from "node:fs";
// existsSync, readFileSync removed - no longer needed since plan content
// is shown via StaticPlanApproval during approval, not in tool result
import { Box, Text } from "ink";
import { memo } from "react";
import { INTERRUPTED_BY_USER } from "../../constants";
@@ -324,40 +325,25 @@ export const ToolCallMessage = memo(
// Fall through to regular handling if parsing fails
}
// Check if this is ExitPlanMode - show plan content (faded) instead of simple message
// Check if this is ExitPlanMode - just show path, not plan content
// The plan content was already shown during approval via StaticPlanApproval
// (rendered via Ink's <Static> and is visible in terminal scrollback)
if (rawName === "ExitPlanMode" && line.resultOk !== false) {
// Read plan file path from ref (captured before plan mode was exited)
const planFilePath = lastPlanFilePath;
let planContent = "";
if (planFilePath && existsSync(planFilePath)) {
try {
planContent = readFileSync(planFilePath, "utf-8");
} catch {
// Fall through to default
}
}
if (planContent) {
if (planFilePath) {
return (
<Box flexDirection="column">
{/* Plan file path */}
<Box flexDirection="row">
<Box width={prefixWidth} flexShrink={0}>
<Text>{prefix}</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text dimColor>Plan saved to: {planFilePath}</Text>
</Box>
<Box flexDirection="row">
<Box width={prefixWidth} flexShrink={0}>
<Text>{prefix}</Text>
</Box>
{/* Plan content (faded) - indent to align with content column */}
<Box paddingLeft={prefixWidth}>
<MarkdownDisplay text={planContent} dimColor={true} />
<Box flexGrow={1} width={contentWidth}>
<Text dimColor>Plan saved to: {planFilePath}</Text>
</Box>
</Box>
);
}
// Fall through to default if no plan content
// Fall through to default if no plan path
}
// Check if this is a file edit tool - show diff instead of success message

View File

@@ -0,0 +1,45 @@
import { Box, Text } from "ink";
import { memo } from "react";
import { useTerminalWidth } from "../../hooks/useTerminalWidth";
import { colors } from "../colors";
const SOLID_LINE = "─";
type Props = {
command: string;
description?: string;
};
/**
* BashPreview - Renders the bash command preview (no interactive options)
*
* Used by:
* - InlineBashApproval for memoized content
* - Static area for eagerly-committed command previews
*/
export const BashPreview = memo(({ command, description }: Props) => {
const columns = useTerminalWidth();
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
return (
<>
{/* 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>{command}</Text>
{description && <Text dimColor>{description}</Text>}
</Box>
</>
);
});
BashPreview.displayName = "BashPreview";

View File

@@ -0,0 +1,48 @@
import { Text } from "ink";
import { memo } from "react";
import { useTerminalWidth } from "../../hooks/useTerminalWidth";
import { colors } from "../colors";
import { MarkdownDisplay } from "../MarkdownDisplay";
const SOLID_LINE = "─";
const DOTTED_LINE = "╌";
type Props = {
plan: string;
};
/**
* PlanPreview - Renders the plan content preview (no interactive options)
*
* Used by:
* - InlinePlanApproval/StaticPlanApproval for memoized content
* - Static area for eagerly-committed plan previews
*/
export const PlanPreview = memo(({ plan }: Props) => {
const columns = useTerminalWidth();
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
const dottedLine = DOTTED_LINE.repeat(Math.max(columns - 2, 10));
return (
<>
{/* 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 */}
<MarkdownDisplay text={plan} />
{/* Dotted separator after plan content */}
<Text dimColor>{dottedLine}</Text>
</>
);
});
PlanPreview.displayName = "PlanPreview";