fix: use ref for reliable streaming content accumulation in UI

- Replace fragile setState callback with contentAccRef for tracking streamed content
- Save intermediate assistant content when tool calls arrive
- Remove unused useTerminalDimensions import
- Fix HelpPanel <br/> tag (must be inside text node in OpenTUI)

Co-authored-by: Ismail Pelaseyed <homanp@users.noreply.github.com>
This commit is contained in:
Cursor AgentandIsmail Pelaseyed committed 2026-03-17 23:17:41 +00:00
1 parent 15c762a000
commit dc2ffe6f11
1 file changed
+21 -16
+21 -16
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
import { useKeyboard } from "@opentui/react";
import type { InputRenderable, ScrollBoxRenderable } from "@opentui/core";
import type { Agent } from "../agent/agent.js";
import type { ChatEntry, ToolCall, StreamChunk } from "../types/index.js";
@@ -16,7 +16,6 @@ export function App({ agent, initialMessage }: AppProps) {
const [streamContent, setStreamContent] = useState("");
const [streamReasoning, setStreamReasoning] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [inputValue, setInputValue] = useState("");
const [model, setModel] = useState(agent.getModel());
const [showModelPicker, setShowModelPicker] = useState(false);
const [modelPickerIndex, setModelPickerIndex] = useState(0);
@@ -24,8 +23,8 @@ export function App({ agent, initialMessage }: AppProps) {
const [activeToolCalls, setActiveToolCalls] = useState<ToolCall[]>([]);
const inputRef = useRef<InputRenderable>(null);
const scrollRef = useRef<ScrollBoxRenderable>(null);
const { width, height } = useTerminalDimensions();
const processedInitial = useRef(false);
const contentAccRef = useRef("");
const scrollToBottom = useCallback(() => {
try {
@@ -43,6 +42,7 @@ export function App({ agent, initialMessage }: AppProps) {
setStreamContent("");
setStreamReasoning("");
setActiveToolCalls([]);
contentAccRef.current = "";
setMessages((prev) => [
...prev,
@@ -55,7 +55,8 @@ export function App({ agent, initialMessage }: AppProps) {
for await (const chunk of agent.processMessage(text.trim())) {
switch (chunk.type) {
case "content":
setStreamContent((prev) => prev + (chunk.content || ""));
contentAccRef.current += chunk.content || "";
setStreamContent(contentAccRef.current);
setTimeout(scrollToBottom, 10);
break;
case "reasoning":
@@ -63,6 +64,15 @@ export function App({ agent, initialMessage }: AppProps) {
break;
case "tool_calls":
if (chunk.toolCalls) {
if (contentAccRef.current.trim()) {
const savedContent = contentAccRef.current;
setMessages((prev) => [
...prev,
{ type: "assistant", content: savedContent, timestamp: new Date() },
]);
contentAccRef.current = "";
setStreamContent("");
}
setActiveToolCalls(chunk.toolCalls);
}
break;
@@ -85,32 +95,27 @@ export function App({ agent, initialMessage }: AppProps) {
}
break;
case "error":
setStreamContent((prev) => prev + `\n${chunk.content || "Unknown error"}`);
contentAccRef.current += `\n${chunk.content || "Unknown error"}`;
setStreamContent(contentAccRef.current);
break;
case "done":
break;
}
}
} catch {
setStreamContent((prev) => prev + "\nAn unexpected error occurred.");
contentAccRef.current += "\nAn unexpected error occurred.";
setStreamContent(contentAccRef.current);
}
const finalContent = (() => {
let c = "";
setStreamContent((prev) => {
c = prev;
return prev;
});
return c;
})();
if (finalContent.trim()) {
if (contentAccRef.current.trim()) {
const finalContent = contentAccRef.current;
setMessages((prev) => [
...prev,
{ type: "assistant", content: finalContent, timestamp: new Date() },
]);
}
contentAccRef.current = "";
setStreamContent("");
setStreamReasoning("");
setActiveToolCalls([]);