@@ -1513,70 +1513,63 @@ const PART_MAPPING = {
15131513function ReasoningPart ( props : { last : boolean ; part : ReasoningPart ; message : AssistantMessage } ) {
15141514 const { theme, subtleSyntax } = useTheme ( )
15151515 const ctx = use ( )
1516- // `userExpanded` is the user's explicit choice. `undefined` means "no choice
1517- // yet, follow the auto behavior". Once they click, it pins to true/false.
1518- const [ userExpanded , setUserExpanded ] = createSignal < boolean | undefined > ( undefined )
1519- // Flips to true after the grace period elapses post-finalization.
1520- const [ autoFolded , setAutoFolded ] = createSignal ( false )
1516+ // In minimal mode the part stays a single line throughout — closed by default
1517+ // so the layout never shifts. Clicking expands into the full markdown block.
1518+ const [ expanded , setExpanded ] = createSignal ( false )
1519+ // Flips after the grace window: the closed line switches from a live tail
1520+ // preview of the reasoning to the static "Thought for Xs" stamp.
1521+ const [ graceElapsed , setGraceElapsed ] = createSignal ( false )
15211522
15221523 const content = createMemo ( ( ) => {
1523- // Filter out redacted reasoning chunks from OpenRouter
1524- // OpenRouter sends encrypted reasoning data that appears as [REDACTED]
1524+ // OpenRouter encrypts some reasoning blocks; drop the placeholder.
15251525 return props . part . text . replace ( "[REDACTED]" , "" ) . trim ( )
15261526 } )
15271527 // Reasoning is finalized when the server sets `time.end` (see processor.ts).
1528- // This flips independently of the parent message completing, so the
1529- // collapse happens as soon as thinking ends — even while text/tools stream.
1528+ // This flips independently of the parent message completing.
15301529 const isDone = createMemo ( ( ) => props . part . time . end !== undefined )
1531- const collapsible = createMemo ( ( ) => ctx . thinkingMode ( ) === "minimal" && isDone ( ) )
1530+ const inMinimal = createMemo ( ( ) => ctx . thinkingMode ( ) === "minimal" )
15321531 const duration = createMemo ( ( ) => {
15331532 const end = props . part . time . end
1534- if ( end === undefined ) return 0
1535- return Math . max ( 0 , end - props . part . time . start )
1533+ return end === undefined ? 0 : Math . max ( 0 , end - props . part . time . start )
1534+ } )
1535+ // Live single-line preview of the streaming reasoning. Whitespace is
1536+ // collapsed so multi-paragraph reasoning shows as one flowing line, and we
1537+ // right-truncate so the most recent words remain visible as deltas arrive.
1538+ const tailPreview = createMemo ( ( ) => {
1539+ const flat = content ( ) . replace ( / \s + / g, " " ) . trim ( )
1540+ // Budget for paddingLeft (3) + "▼ Thinking: " (12) + a couple cells slack.
1541+ const max = Math . max ( 20 , ctx . width - 18 )
1542+ return Locale . truncateLeft ( flat , max )
15361543 } )
15371544
1538- // Schedule auto-collapse a short delay after reasoning finalizes in minimal
1539- // mode, so the fold doesn't snap the instant streaming stops. A manual
1540- // toggle or leaving minimal mode cancels the pending timer. For reasoning
1541- // that finished before this component mounted (e.g. loading a past session)
1542- // we skip the grace and collapse immediately.
1545+ // Grace timer: after finalization the closed view holds the tail preview
1546+ // for a beat, then switches to the timestamp. Historical sessions (loaded
1547+ // later) snap directly past the grace.
15431548 createEffect ( ( ) => {
1544- if ( ! collapsible ( ) ) {
1545- setAutoFolded ( false )
1546- return
1547- }
1548- if ( userExpanded ( ) !== undefined ) return
1549- if ( autoFolded ( ) ) return
1549+ if ( ! inMinimal ( ) ) return
1550+ if ( ! isDone ( ) ) return
1551+ if ( graceElapsed ( ) ) return
15501552 const end = props . part . time . end
15511553 if ( end === undefined ) return
15521554 const remaining = MINIMAL_AUTO_COLLAPSE_MS - ( Date . now ( ) - end )
15531555 if ( remaining <= 0 ) {
1554- setAutoFolded ( true )
1556+ setGraceElapsed ( true )
15551557 return
15561558 }
1557- const timer = setTimeout ( ( ) => setAutoFolded ( true ) , remaining )
1559+ const timer = setTimeout ( ( ) => setGraceElapsed ( true ) , remaining )
15581560 onCleanup ( ( ) => clearTimeout ( timer ) )
15591561 } )
15601562
1561- // Effective expansion: stay expanded while streaming and during grace; fold
1562- // when auto-fold fires; user clicks override everything.
1563- const expanded = createMemo ( ( ) => {
1564- if ( ! collapsible ( ) ) return true
1565- const choice = userExpanded ( )
1566- if ( choice !== undefined ) return choice
1567- return ! autoFolded ( )
1568- } )
1569- const collapsed = createMemo ( ( ) => collapsible ( ) && ! expanded ( ) )
15701563 const toggle = ( ) => {
1571- if ( ! collapsible ( ) ) return
1572- setUserExpanded ( ! expanded ( ) )
1564+ if ( ! inMinimal ( ) ) return
1565+ setExpanded ( ( prev ) => ! prev )
15731566 }
15741567
15751568 return (
15761569 < Show when = { content ( ) && ctx . thinkingMode ( ) !== "hide" } >
1577- < Show
1578- when = { collapsed ( ) }
1579- fallback = {
1570+ < Switch >
1571+ < Match when = { ! inMinimal ( ) || expanded ( ) } >
1572+ { /* Full markdown block: `show` mode, or `minimal` after the user clicks open. */ }
15801573 < box
15811574 id = { "text-" + props . part . id }
15821575 paddingLeft = { 2 }
@@ -1592,24 +1585,29 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
15921585 drawUnstyledText = { false }
15931586 streaming = { true }
15941587 syntaxStyle = { subtleSyntax ( ) }
1595- content = { ( collapsible ( ) ? "▼ " : "" ) + "_Thinking:_ " + content ( ) }
1588+ content = { ( inMinimal ( ) ? "▼ " : "" ) + "_Thinking:_ " + content ( ) }
15961589 conceal = { ctx . conceal ( ) }
15971590 fg = { theme . textMuted }
15981591 />
15991592 </ box >
1600- }
1601- >
1602- < box id = { "text-" + props . part . id } paddingLeft = { 3 } marginTop = { 1 } flexShrink = { 0 } onMouseUp = { toggle } >
1603- < code
1604- filetype = "markdown"
1605- drawUnstyledText = { false }
1606- syntaxStyle = { subtleSyntax ( ) }
1607- content = { "▶ _Thought for " + Locale . duration ( duration ( ) ) + "_" }
1608- conceal = { ctx . conceal ( ) }
1609- fg = { theme . textMuted }
1610- />
1611- </ box >
1612- </ Show >
1593+ </ Match >
1594+ < Match when = { isDone ( ) && graceElapsed ( ) } >
1595+ { /* Settled: timestamp. */ }
1596+ < box id = { "text-" + props . part . id } paddingLeft = { 3 } marginTop = { 1 } flexShrink = { 0 } onMouseUp = { toggle } >
1597+ < text fg = { theme . textMuted } wrapMode = "none" >
1598+ { "▶ Thought for " + Locale . duration ( duration ( ) ) }
1599+ </ text >
1600+ </ box >
1601+ </ Match >
1602+ < Match when = { true } >
1603+ { /* Still streaming or inside the grace window: live tail preview. */ }
1604+ < box id = { "text-" + props . part . id } paddingLeft = { 3 } marginTop = { 1 } flexShrink = { 0 } onMouseUp = { toggle } >
1605+ < text fg = { theme . textMuted } wrapMode = "none" >
1606+ { "▼ Thinking: " + tailPreview ( ) }
1607+ </ text >
1608+ </ box >
1609+ </ Match >
1610+ </ Switch >
16131611 </ Show >
16141612 )
16151613}
0 commit comments