From 2e07d757ce322bed51e91d12ac554674fa54dc1c Mon Sep 17 00:00:00 2001 From: DarkSkyXD Date: Wed, 25 Mar 2026 13:28:43 -0500 Subject: [PATCH] fix: resolve chat input lag caused by SSE-driven React re-renders Convert WebChatPanel and CortexChatPanel text inputs from controlled (useState) to uncontrolled (ref-based) textareas so keystrokes bypass React's render cycle entirely. Wrap input components and message lists in React.memo to prevent expensive re-renders of long chat histories on every SSE event. Co-Authored-By: Claude Opus 4.6 (1M context) --- interface/src/components/CortexChatPanel.tsx | 136 +++++++++-------- interface/src/components/WebChatPanel.tsx | 148 +++++++++++-------- 2 files changed, 162 insertions(+), 122 deletions(-) diff --git a/interface/src/components/CortexChatPanel.tsx b/interface/src/components/CortexChatPanel.tsx index 5ec03b031..f27721532 100644 --- a/interface/src/components/CortexChatPanel.tsx +++ b/interface/src/components/CortexChatPanel.tsx @@ -1,8 +1,8 @@ -import {useCallback, useEffect, useRef, useState} from "react"; +import {memo, useCallback, useEffect, useRef, useState} from "react"; import {useCortexChat, type ToolActivity} from "@/hooks/useCortexChat"; import {Markdown} from "@/components/Markdown"; import {ToolCall, type ToolCallPair} from "@/components/ToolCall"; -import {api, type CortexChatToolCall, type CortexChatThread} from "@/api/client"; +import {api, type CortexChatMessage, type CortexChatToolCall, type CortexChatThread} from "@/api/client"; import {Button} from "@/ui"; import {Popover, PopoverContent, PopoverTrigger} from "@/ui/Popover"; import {PlusSignIcon, Cancel01Icon, Clock01Icon, Delete02Icon} from "@hugeicons/core-free-icons"; @@ -164,44 +164,51 @@ function ThinkingIndicator() { ); } -function CortexChatInput({ - value, - onChange, +const CortexChatInput = memo(function CortexChatInput({ onSubmit, isStreaming, }: { - value: string; - onChange: (value: string) => void; - onSubmit: () => void; + onSubmit: (text: string) => void; isStreaming: boolean; }) { const textareaRef = useRef(null); + const [hasText, setHasText] = useState(false); useEffect(() => { textareaRef.current?.focus(); }, []); - useEffect(() => { + const adjustHeight = () => { const textarea = textareaRef.current; if (!textarea) return; + textarea.style.height = "auto"; + const scrollHeight = textarea.scrollHeight; + const maxHeight = 160; + textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`; + textarea.style.overflowY = scrollHeight > maxHeight ? "auto" : "hidden"; + }; - const adjustHeight = () => { - textarea.style.height = "auto"; - const scrollHeight = textarea.scrollHeight; - const maxHeight = 160; - textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`; - textarea.style.overflowY = scrollHeight > maxHeight ? "auto" : "hidden"; - }; + const doSubmit = () => { + const textarea = textareaRef.current; + if (!textarea) return; + const trimmed = textarea.value.trim(); + if (!trimmed) return; + textarea.value = ""; + setHasText(false); + adjustHeight(); + onSubmit(trimmed); + }; + const handleInput = () => { + const value = textareaRef.current?.value ?? ""; + setHasText(value.trim().length > 0); adjustHeight(); - textarea.addEventListener("input", adjustHeight); - return () => textarea.removeEventListener("input", adjustHeight); - }, [value]); + }; const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); - onSubmit(); + doSubmit(); } }; @@ -210,8 +217,7 @@ function CortexChatInput({