11import { memo , useState } from "react" ;
2- import { Surface } from "@heroui/react" ;
32import { Trans , useLingui } from "@lingui/react/macro" ;
43import { Brain , ChevronDown } from "lucide-react" ;
54import type { RuntimeChatItem } from "@/renderer/state/slices/runtimeEventSlice" ;
65import { useChatPaneActions } from "../../chatPaneActionsContext" ;
76import { useBrainThinking , useShimmer } from "@/renderer/thinkingAnimator" ;
8- import { chatMessageSurfaceClass } from "./chatMessageSurface" ;
9- import { getReasoningPreview } from "./reasoningPreview" ;
7+ import { getReasoningLastLine , getReasoningPreview } from "./reasoningPreview" ;
108import { ReasoningExpandedBody , ReasoningStreamViewport } from "./ReasoningStreamViewport" ;
119
1210interface ReasoningProps {
@@ -24,55 +22,55 @@ export const Reasoning = memo(function Reasoning({ item }: ReasoningProps) {
2422 const thinkingTextRef = useShimmer < HTMLSpanElement > ( isStreaming ) ;
2523 const brainRef = useBrainThinking ( isStreaming ) ;
2624
27- if ( ! isStreaming ) {
28- const preview = isOpen ? "" : getReasoningPreview ( rawText ) ;
29- // Compact toggle — visually distinct from tool-call accordions: no border
30- // tile, dotted left rule when expanded, italic body. Equal vertical
31- // padding so it doesn't visually bias toward the message above or below.
32- return (
33- < div className = "flex w-full flex-col items-stretch justify-center py-2 pl-6 pr-3 text-[length:var(--lc-chat-font-size-meta)] text-foreground-muted" >
34- < button
35- type = "button"
36- onClick = { ( ) => {
37- setIsOpen ( ( v ) => ! v ) ;
38- actions ?. onContentHeightChange ( ) ;
39- } }
40- aria-expanded = { isOpen }
41- className = "group inline-flex min-w-0 max-w-full items-center gap-1.5 self-start leading-none italic opacity-80 hover:text-foreground hover:opacity-100"
42- >
43- < Brain className = "size-3 shrink-0" />
44- < span className = "shrink-0" >
45- < Trans > Thought</ Trans >
46- </ span >
47- { preview ? < span className = "min-w-0 truncate opacity-70" > { preview } </ span > : null }
48- < ChevronDown
49- className = { `size-3 shrink-0 opacity-100 transition-[transform,opacity] [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 [@media(hover:hover)]:group-focus-visible:opacity-100 ${ isOpen ? "rotate-180" : "" } ` }
50- />
51- </ button >
52- { isOpen ? < ReasoningExpandedBody text = { rawText } className = "mt-2" /> : null }
53- </ div >
54- ) ;
55- }
25+ // Collapsed row is identical while thinking and after: only the label, the
26+ // brain/shimmer animation, and the preview source differ. While streaming the
27+ // trailing meta tracks the model's current line (streamed in line by line);
28+ // on completion it settles into the flattened whole-block preview.
29+ const preview = isOpen
30+ ? ""
31+ : isStreaming
32+ ? getReasoningLastLine ( rawText )
33+ : getReasoningPreview ( rawText ) ;
5634
35+ // Compact toggle — visually distinct from tool-call accordions: no border
36+ // tile, dotted left rule when expanded, italic body. Equal vertical padding so
37+ // it doesn't visually bias toward the message above or below. Expanding while
38+ // streaming reveals the live pinned viewport; after completion, the static body.
5739 return (
58- < Surface variant = "transparent" className = { `${ chatMessageSurfaceClass } pl-6` } >
59- < div className = "flex min-w-0 flex-col gap-1.5 text-[length:var(--lc-chat-font-size-meta)] text-foreground-muted" >
60- < div className = "inline-flex items-center gap-1.5" >
61- < Brain
62- ref = { brainRef }
63- className = "poracode-brain-thinking size-3 shrink-0"
64- aria-label = { t `Thinking` }
65- />
66- < span
67- ref = { thinkingTextRef }
68- className = "poracode-thinking-text"
69- data-poracode-shimmer-text = { t `Thinking` }
70- >
71- < Trans > Thinking</ Trans >
72- </ span >
73- </ div >
74- { hasText ? < ReasoningStreamViewport text = { rawText } className = "pl-4" /> : null }
75- </ div >
76- </ Surface >
40+ < div className = "flex w-full flex-col items-stretch justify-center py-2 pl-6 pr-3 text-[length:var(--lc-chat-font-size-meta)] text-foreground-muted" >
41+ < button
42+ type = "button"
43+ onClick = { ( ) => {
44+ setIsOpen ( ( v ) => ! v ) ;
45+ actions ?. onContentHeightChange ( ) ;
46+ } }
47+ aria-expanded = { isOpen }
48+ className = "group inline-flex min-w-0 max-w-full items-center gap-1.5 self-start leading-none italic opacity-80 hover:text-foreground hover:opacity-100"
49+ >
50+ < Brain
51+ ref = { brainRef }
52+ className = { `size-3 shrink-0 ${ isStreaming ? "poracode-brain-thinking" : "" } ` }
53+ { ...( isStreaming ? { "aria-label" : t `Thinking` } : { } ) }
54+ />
55+ < span
56+ ref = { thinkingTextRef }
57+ className = { `shrink-0 ${ isStreaming ? "poracode-thinking-text" : "" } ` }
58+ { ...( isStreaming ? { "data-poracode-shimmer-text" : t `Thinking` } : { } ) }
59+ >
60+ { isStreaming ? < Trans > Thinking</ Trans > : < Trans > Thought</ Trans > }
61+ </ span >
62+ { preview ? < span className = "min-w-0 truncate opacity-70" > { preview } </ span > : null }
63+ < ChevronDown
64+ className = { `size-3 shrink-0 opacity-100 transition-[transform,opacity] [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 [@media(hover:hover)]:group-focus-visible:opacity-100 ${ isOpen ? "rotate-180" : "" } ` }
65+ />
66+ </ button >
67+ { isOpen && hasText ? (
68+ isStreaming ? (
69+ < ReasoningStreamViewport text = { rawText } className = "mt-2" />
70+ ) : (
71+ < ReasoningExpandedBody text = { rawText } className = "mt-2" />
72+ )
73+ ) : null }
74+ </ div >
7775 ) ;
7876} ) ;
0 commit comments