From c9536d721e6007e69f9344023c4ae357b4f9f1c6 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 17 Apr 2026 19:27:35 +1000 Subject: [PATCH] fix(ui/text): stop idle 300ms re-render loop that could OOM the TUI (#8616) Signed-off-by: Michael Neale --- ui/text/src/tui.tsx | 53 +++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/ui/text/src/tui.tsx b/ui/text/src/tui.tsx index fde633ed..b82cf4c5 100644 --- a/ui/text/src/tui.tsx +++ b/ui/text/src/tui.tsx @@ -1,5 +1,5 @@ #!/usr/bin/env node -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Box, Text, render, useApp, useInput, useStdout } from "ink"; import { MultilineInput } from "ink-multiline-input"; import meow from "meow"; @@ -574,13 +574,18 @@ function App({ const queueRef = useRef([]); const isProcessingRef = useRef(false); + // Only run the animation tick when something is actually animating: + // the splash goose while the banner is up, or the spinner while loading. + // Otherwise we were re-rendering the entire viewport every 300ms forever, + // which rebuilds every turn's markdown and can OOM long-running sessions. useEffect(() => { + if (!bannerVisible && !loading) return; const t = setInterval(() => { - setSpinIdx((i) => (i + 1) % SPINNER_FRAMES.length); - setGooseFrame((f) => f + 1); + if (loading) setSpinIdx((i) => (i + 1) % SPINNER_FRAMES.length); + if (bannerVisible) setGooseFrame((f) => (f + 1) % GOOSE_FRAMES.length); }, 300); return () => clearInterval(t); - }, []); + }, [bannerVisible, loading]); useEffect(() => { if (turns.length > 0) setBannerVisible(false); @@ -1046,18 +1051,34 @@ function App({ 3, ); - const contentLines = buildContentLines({ - turn: currentTurn, - turnIndex: effectiveTurnIdx, - width: contentWidth, - loading: isLatest && loading, - status, - spinIdx, - pendingPermission: isLatest ? pendingPermission : null, - permissionIdx, - toolCallsExpanded, - queuedMessages: isLatest ? queuedMessages : [], - }); + const contentLines = useMemo( + () => + buildContentLines({ + turn: currentTurn, + turnIndex: effectiveTurnIdx, + width: contentWidth, + loading: isLatest && loading, + status, + spinIdx, + pendingPermission: isLatest ? pendingPermission : null, + permissionIdx, + toolCallsExpanded, + queuedMessages: isLatest ? queuedMessages : [], + }), + [ + currentTurn, + effectiveTurnIdx, + contentWidth, + isLatest, + loading, + status, + spinIdx, + pendingPermission, + permissionIdx, + toolCallsExpanded, + queuedMessages, + ], + ); if (needsOnboarding && clientRef.current) { return (