diff --git a/src/cli/components/InlineBashApproval.tsx b/src/cli/components/InlineBashApproval.tsx
index 9a0d153..289ff92 100644
--- a/src/cli/components/InlineBashApproval.tsx
+++ b/src/cli/components/InlineBashApproval.tsx
@@ -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 */}
+ {solidLine}
+
+ {/* Header */}
+
+ Run this command?
+
+
+
+
+ {/* Command preview */}
+
+ {bashInfo.command}
+ {bashInfo.description && (
+ {bashInfo.description}
+ )}
+
+ >
+ ),
+ [bashInfo.command, bashInfo.description, solidLine],
+ );
+
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -130,21 +157,8 @@ export const InlineBashApproval = memo(
return (
- {/* Top solid line */}
- {solidLine}
-
- {/* Header */}
-
- Run this command?
-
-
-
-
- {/* Command preview */}
-
- {bashInfo.command}
- {bashInfo.description && {bashInfo.description}}
-
+ {/* Static command content - memoized to prevent re-render on keystroke */}
+ {memoizedCommandContent}
{/* Options */}
diff --git a/src/cli/components/InlineFileEditApproval.tsx b/src/cli/components/InlineFileEditApproval.tsx
index 5776606..f6aec8f 100644
--- a/src/cli/components/InlineFileEditApproval.tsx
+++ b/src/cli/components/InlineFileEditApproval.tsx
@@ -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 */}
+ {solidLine}
+
+ {/* Header */}
+
+ {headerText}
+
+
+ {/* Dotted separator before diff content */}
+ {dottedLine}
+
+ {/* Diff preview */}
+
+ {fileEdit.patchInput ? (
+ // Render patch operations (can be multiple files)
+
+ {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 (
+
+ {idx > 0 && }
+ {displayPath}
+
+
+ );
+ } else if (op.kind === "update") {
+ return (
+
+ {idx > 0 && }
+ {displayPath}
+
+
+ );
+ } else if (op.kind === "delete") {
+ return (
+
+ {idx > 0 && }
+ {displayPath}
+ File will be deleted
+
+ );
+ }
+ return null;
+ })}
+
+ ) : diffKind === "write" ? (
+
+ ) : diffKind === "multi_edit" ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Dotted separator after diff content */}
+ {dottedLine}
+ >
+ ),
+ // 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 (
- {/* Top solid line */}
- {solidLine}
-
- {/* Header */}
-
- {headerText}
-
-
- {/* Dotted separator before diff content */}
- {dottedLine}
-
- {/* Diff preview */}
-
- {fileEdit.patchInput ? (
- // Render patch operations (can be multiple files)
-
- {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 (
-
- {idx > 0 && }
- {displayPath}
-
-
- );
- } else if (op.kind === "update") {
- return (
-
- {idx > 0 && }
- {displayPath}
-
-
- );
- } else if (op.kind === "delete") {
- return (
-
- {idx > 0 && }
- {displayPath}
- File will be deleted
-
- );
- }
- return null;
- })}
-
- ) : diffKind === "write" ? (
-
- ) : diffKind === "multi_edit" ? (
-
- ) : (
-
- )}
-
-
- {/* Dotted separator after diff content */}
- {dottedLine}
+ {/* Static diff content - memoized to prevent re-render on keystroke */}
+ {memoizedDiffContent}
{/* Options */}
diff --git a/src/cli/components/InlineGenericApproval.tsx b/src/cli/components/InlineGenericApproval.tsx
index b2ab050..67d43b9 100644
--- a/src/cli/components/InlineGenericApproval.tsx
+++ b/src/cli/components/InlineGenericApproval.tsx
@@ -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 */}
+ {solidLine}
+
+ {/* Header */}
+
+ Run {toolName}?
+
+
+
+
+ {/* Arguments preview */}
+
+ {formattedArgs}
+
+ >
+ ),
+ [toolName, formattedArgs, solidLine],
+ );
+
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -140,20 +164,8 @@ export const InlineGenericApproval = memo(
return (
- {/* Top solid line */}
- {solidLine}
-
- {/* Header */}
-
- Run {toolName}?
-
-
-
-
- {/* Arguments preview */}
-
- {formattedArgs}
-
+ {/* Static tool content - memoized to prevent re-render on keystroke */}
+ {memoizedToolContent}
{/* Options */}
diff --git a/src/cli/components/InlinePlanApproval.tsx b/src/cli/components/InlinePlanApproval.tsx
index d237c24..c2c9f85 100644
--- a/src/cli/components/InlinePlanApproval.tsx
+++ b/src/cli/components/InlinePlanApproval.tsx
@@ -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 */}
+ {solidLine}
+
+ {/* Header */}
+
+ Ready to code? Here is your plan:
+
+
+ {/* Dotted separator before plan content */}
+ {dottedLine}
+
+ {/* Plan content - no indentation, just like Claude Code */}
+
+
+ {/* Dotted separator after plan content */}
+ {dottedLine}
+ >
+ ),
+ [plan, solidLine, dottedLine],
+ );
+
// Hint text based on state
const hintText = isOnCustomOption
? customReason
@@ -118,22 +144,8 @@ export const InlinePlanApproval = memo(
return (
- {/* Top solid line */}
- {solidLine}
-
- {/* Header */}
-
- Ready to code? Here is your plan:
-
-
- {/* Dotted separator before plan content */}
- {dottedLine}
-
- {/* Plan content - no indentation, just like Claude Code */}
-
-
- {/* Dotted separator after plan content */}
- {dottedLine}
+ {/* Static plan content - memoized to prevent re-render on keystroke */}
+ {memoizedPlanContent}
{/* Question */}
diff --git a/src/cli/components/InlineQuestionApproval.tsx b/src/cli/components/InlineQuestionApproval.tsx
index 501b55a..06f59a4 100644
--- a/src/cli/components/InlineQuestionApproval.tsx
+++ b/src/cli/components/InlineQuestionApproval.tsx
@@ -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 */}
+ {solidLine}
+
+ {/* Header label */}
+ {currentQuestion?.header}
+
+
+
+ {/* Question */}
+ {currentQuestion?.question}
+
+
+
+ {/* Progress indicator for multiple questions */}
+ {questions.length > 1 && (
+
+
+ Question {currentQuestionIndex + 1} of {questions.length}
+
+
+ )}
+ >
+ ),
+ [
+ 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 (
- {/* Top solid line */}
- {solidLine}
-
- {/* Header label */}
- {currentQuestion.header}
-
-
-
- {/* Question */}
- {currentQuestion.question}
-
-
-
- {/* Progress indicator for multiple questions */}
- {questions.length > 1 && (
-
-
- Question {currentQuestionIndex + 1} of {questions.length}
-
-
- )}
+ {/* Static header content - memoized to prevent re-render on keystroke */}
+ {memoizedHeaderContent}
{/* Options - Format: ❯ N. [ ] Label (selector, number, checkbox, label) */}