From 3342ab0d062d911d9b7d13a94cadf098f4d7ce9e Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Fri, 9 Jan 2026 14:56:52 -0800 Subject: [PATCH] fix: reduce footer flicker in Ghostty by memoizing high-frequency renders (#508) Co-authored-by: Letta --- src/cli/components/InputRich.tsx | 145 +++++++++++++++++++---------- src/cli/components/ShimmerText.tsx | 8 +- 2 files changed, 100 insertions(+), 53 deletions(-) diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index 2d114c1..047a788 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -5,7 +5,14 @@ import { stdin } from "node:process"; import chalk from "chalk"; import { Box, Text, useInput } from "ink"; import SpinnerLib from "ink-spinner"; -import { type ComponentType, useEffect, useRef, useState } from "react"; +import { + type ComponentType, + memo, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { LETTA_CLOUD_API_URL } from "../../auth/oauth"; import { ELAPSED_DISPLAY_THRESHOLD_MS, @@ -30,6 +37,69 @@ const Spinner = SpinnerLib as ComponentType<{ type?: string }>; // Window for double-escape to clear input const ESC_CLEAR_WINDOW_MS = 2500; +/** + * Memoized footer component to prevent re-renders during high-frequency + * shimmer/timer updates. Only updates when its specific props change. + */ +const InputFooter = memo(function InputFooter({ + ctrlCPressed, + escapePressed, + isBashMode, + modeName, + modeColor, + showExitHint, + agentName, + currentModel, + isAnthropicProvider, +}: { + ctrlCPressed: boolean; + escapePressed: boolean; + isBashMode: boolean; + modeName: string | null; + modeColor: string | null; + showExitHint: boolean; + agentName: string | null | undefined; + currentModel: string | null | undefined; + isAnthropicProvider: boolean; +}) { + return ( + + {ctrlCPressed ? ( + Press CTRL-C again to exit + ) : escapePressed ? ( + Press Esc again to clear + ) : isBashMode ? ( + + ⏵⏵ bash mode + + {" "} + (backspace to exit) + + + ) : modeName && modeColor ? ( + + ⏵⏵ {modeName} + + {" "} + (shift+tab to {showExitHint ? "exit" : "cycle"}) + + + ) : ( + Press / for commands + )} + + {agentName || "Unnamed"} + + {` [${currentModel ?? "unknown"}]`} + + + + ); +}); + // Increase max listeners to accommodate multiple useInput hooks // (5 in this component + autocomplete components) stdin.setMaxListeners(20); @@ -586,7 +656,8 @@ export function Input({ }; // Get display name and color for permission mode (ralph modes take precedence) - const getModeInfo = () => { + // Memoized to prevent unnecessary footer re-renders + const modeInfo = useMemo(() => { // Check ralph pending first (waiting for task input) if (ralphPending) { if (ralphPendingYolo) { @@ -635,9 +706,7 @@ export function Input({ default: return null; } - }; - - const modeInfo = getModeInfo(); + }, [ralphPending, ralphPendingYolo, ralphActive, currentMode]); const estimatedTokens = charsToTokens(tokenCount); const shouldShowTokenCount = @@ -647,8 +716,8 @@ export function Input({ const elapsedMinutes = Math.floor(elapsedMs / 60000); // Build the status hint text (esc to interrupt · 2m · 1.2k ↑) - // In ralph mode, also show "shift+tab to exit" - const statusHintText = (() => { + // Memoized to prevent unnecessary re-renders during shimmer updates + const statusHintText = useMemo(() => { const hintColor = chalk.hex(colors.subagent.hint); const hintBold = hintColor.bold; const suffix = @@ -661,10 +730,17 @@ export function Input({ return ( hintColor(" (") + hintBold("esc") + hintColor(` to interrupt${suffix}`) ); - })(); + }, [ + shouldShowElapsed, + elapsedMinutes, + shouldShowTokenCount, + estimatedTokens, + interruptRequested, + ]); // Create a horizontal line using box-drawing characters - const horizontalLine = "─".repeat(columns); + // Memoized since it only changes when terminal width changes + const horizontalLine = useMemo(() => "─".repeat(columns), [columns]); // If not visible, render nothing but keep component mounted to preserve state if (!visible) { @@ -749,46 +825,17 @@ export function Input({ workingDirectory={process.cwd()} /> - - {ctrlCPressed ? ( - Press CTRL-C again to exit - ) : escapePressed ? ( - Press Esc again to clear - ) : isBashMode ? ( - - ⏵⏵ bash mode - - {" "} - (backspace to exit) - - - ) : modeInfo ? ( - - ⏵⏵ {modeInfo.name} - - {" "} - (shift+tab to {ralphActive || ralphPending ? "exit" : "cycle"}) - - - ) : ( - Press / for commands - )} - - - {agentName || "Unnamed"} - - - {` [${currentModel ?? "unknown"}]`} - - - + ); diff --git a/src/cli/components/ShimmerText.tsx b/src/cli/components/ShimmerText.tsx index f64e354..6d043ff 100644 --- a/src/cli/components/ShimmerText.tsx +++ b/src/cli/components/ShimmerText.tsx @@ -1,6 +1,6 @@ import chalk from "chalk"; import { Text } from "ink"; -import type React from "react"; +import { memo } from "react"; import { colors } from "./colors.js"; interface ShimmerTextProps { @@ -10,12 +10,12 @@ interface ShimmerTextProps { shimmerOffset: number; } -export const ShimmerText: React.FC = ({ +export const ShimmerText = memo(function ShimmerText({ color = colors.status.processing, boldPrefix, message, shimmerOffset, -}) => { +}: ShimmerTextProps) { const fullText = `${boldPrefix ? `${boldPrefix} ` : ""}${message}…`; const prefixLength = boldPrefix ? boldPrefix.length + 1 : 0; // +1 for space @@ -37,4 +37,4 @@ export const ShimmerText: React.FC = ({ .join(""); return {shimmerText}; -}; +});