diff --git a/src/app/chat/ChatMessageList.tsx b/src/app/chat/ChatMessageList.tsx
index 64bea2a..9963169 100644
--- a/src/app/chat/ChatMessageList.tsx
+++ b/src/app/chat/ChatMessageList.tsx
@@ -342,7 +342,10 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
}
}, [displayParts.length]);
- const firstThinkingIdx = displayParts.findIndex((p) => p.type === "thinking");
+ const activeThinkingIdx =
+ !displayDone && displayParts[displayParts.length - 1]?.type === "thinking"
+ ? displayParts.length - 1
+ : -1;
return (
@@ -364,12 +367,12 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
{displayParts.map((part, i) => {
if (part.type === "thinking") {
- const isActivelyThinking = !displayDone && i === firstThinkingIdx;
return (
-
-
- {part.content}
-
+
);
}
if (part.type === "tool_call") {
@@ -407,6 +410,59 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
);
}
+function StreamingReasoningBlock({
+ content,
+ isActivelyThinking,
+}: {
+ content: string;
+ isActivelyThinking: boolean;
+}) {
+ const [manualOpen, setManualOpen] = useState(false);
+ const [autoCollapsed, setAutoCollapsed] = useState(false);
+ const wasActivelyThinkingRef = useRef(isActivelyThinking);
+
+ useEffect(() => {
+ const wasActivelyThinking = wasActivelyThinkingRef.current;
+
+ if (isActivelyThinking && !wasActivelyThinking) {
+ setAutoCollapsed(false);
+ setManualOpen(false);
+ }
+
+ if (!isActivelyThinking && wasActivelyThinking) {
+ setAutoCollapsed(false);
+ setManualOpen(false);
+ }
+
+ wasActivelyThinkingRef.current = isActivelyThinking;
+ }, [isActivelyThinking]);
+
+ const isOpen = isActivelyThinking ? !autoCollapsed : manualOpen;
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ if (isActivelyThinking) {
+ setAutoCollapsed(!nextOpen);
+ return;
+ }
+ setManualOpen(nextOpen);
+ },
+ [isActivelyThinking]
+ );
+
+ return (
+
+
+ {content}
+
+ );
+}
+
function StreamingCursor() {
return (
& {
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
duration?: number;
+ autoLifecycle?: boolean;
};
const AUTO_CLOSE_DELAY = 1000;
@@ -64,6 +65,7 @@ export const Reasoning = memo(
defaultOpen,
onOpenChange,
duration: durationProp,
+ autoLifecycle = true,
children,
...props
}: ReasoningProps) => {
@@ -100,15 +102,16 @@ export const Reasoning = memo(
// Auto-open when streaming starts (unless explicitly closed)
useEffect(() => {
- if (isStreaming && !isOpen && !isExplicitlyClosed) {
+ if (autoLifecycle && isStreaming && !isOpen && !isExplicitlyClosed) {
setIsOpen(true);
}
- }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
+ }, [autoLifecycle, isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
// Auto-close when streaming ends (once only, and only if it ever streamed)
useEffect(() => {
if (
hasEverStreamedRef.current &&
+ autoLifecycle &&
!isStreaming &&
isOpen &&
!hasAutoClosed
@@ -120,7 +123,7 @@ export const Reasoning = memo(
return () => clearTimeout(timer);
}
- }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
+ }, [autoLifecycle, isStreaming, isOpen, setIsOpen, hasAutoClosed]);
const handleOpenChange = useCallback(
(newOpen: boolean) => {