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};
-};
+});