@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
66import { getDirectory , getFilename } from "@opencode-ai/util/path"
77import { createEffect , createMemo , createSignal , For , on , ParentProps , Show } from "solid-js"
88import { Dynamic } from "solid-js/web"
9- import { AssistantParts , Message } from "./message-part"
9+ import { AssistantParts , Message , PART_MAPPING } from "./message-part"
1010import { Card } from "./card"
1111import { Accordion } from "./accordion"
1212import { StickyAccordionHeader } from "./sticky-accordion-header"
@@ -83,22 +83,63 @@ function list<T>(value: T[] | undefined | null, fallback: T[]) {
8383
8484const hidden = new Set ( [ "todowrite" , "todoread" ] )
8585
86- function visible ( part : PartType ) {
86+ function partState ( part : PartType , showReasoningSummaries : boolean ) {
8787 if ( part . type === "tool" ) {
88- if ( hidden . has ( part . tool ) ) return false
89- if ( part . tool === "question" ) return part . state . status !== "pending" && part . state . status !== "running"
90- return true
88+ if ( hidden . has ( part . tool ) ) return
89+ if ( part . tool === "question" && ( part . state . status === "pending" || part . state . status === "running" ) ) return
90+ return "visible" as const
91+ }
92+ if ( part . type === "text" ) return part . text ?. trim ( ) ? ( "visible" as const ) : undefined
93+ if ( part . type === "reasoning" ) {
94+ if ( showReasoningSummaries ) return "visible" as const
95+ return
96+ }
97+ if ( PART_MAPPING [ part . type ] ) return "visible" as const
98+ return
99+ }
100+
101+ function clean ( value : string ) {
102+ return value
103+ . replace ( / ` ( [ ^ ` ] + ) ` / g, "$1" )
104+ . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ \) ] + \) / g, "$1" )
105+ . replace ( / [ * _ ~ ] + / g, "" )
106+ . trim ( )
107+ }
108+
109+ function heading ( text : string ) {
110+ const markdown = text . replace ( / \r \n ? / g, "\n" )
111+
112+ const html = markdown . match ( / < h [ 1 - 6 ] [ ^ > ] * > ( [ \s \S ] * ?) < \/ h [ 1 - 6 ] > / i)
113+ if ( html ?. [ 1 ] ) {
114+ const value = clean ( html [ 1 ] . replace ( / < [ ^ > ] + > / g, " " ) )
115+ if ( value ) return value
116+ }
117+
118+ const atx = markdown . match ( / ^ \s { 0 , 3 } # { 1 , 6 } [ \t ] + ( .+ ?) (?: [ \t ] + # + [ \t ] * ) ? $ / m)
119+ if ( atx ?. [ 1 ] ) {
120+ const value = clean ( atx [ 1 ] )
121+ if ( value ) return value
122+ }
123+
124+ const setext = markdown . match ( / ^ ( [ ^ \n ] + ) \n (?: = + | - + ) \s * $ / m)
125+ if ( setext ?. [ 1 ] ) {
126+ const value = clean ( setext [ 1 ] )
127+ if ( value ) return value
128+ }
129+
130+ const strong = markdown . match ( / ^ \s * (?: \* \* | _ _ ) ( .+ ?) (?: \* \* | _ _ ) \s * $ / m)
131+ if ( strong ?. [ 1 ] ) {
132+ const value = clean ( strong [ 1 ] )
133+ if ( value ) return value
91134 }
92- if ( part . type === "text" ) return ! ! part . text ?. trim ( )
93- if ( part . type === "reasoning" ) return ! ! part . text ?. trim ( )
94- return false
95135}
96136
97137export function SessionTurn (
98138 props : ParentProps < {
99139 sessionID : string
100140 messageID : string
101141 lastUserMessageID ?: string
142+ showReasoningSummaries ?: boolean
102143 onUserInteracted ?: ( ) => void
103144 classes ?: {
104145 root ?: string
@@ -242,6 +283,7 @@ export function SessionTurn(
242283
243284 const status = createMemo ( ( ) => data . store . session_status [ props . sessionID ] ?? idle )
244285 const working = createMemo ( ( ) => status ( ) . type !== "idle" && isLastUserMessage ( ) )
286+ const showReasoningSummaries = createMemo ( ( ) => props . showReasoningSummaries ?? true )
245287
246288 const assistantCopyPartID = createMemo ( ( ) => {
247289 if ( working ( ) ) return null
@@ -265,9 +307,33 @@ export function SessionTurn(
265307 const assistantVisible = createMemo ( ( ) =>
266308 assistantMessages ( ) . reduce ( ( count , message ) => {
267309 const parts = list ( data . store . part ?. [ message . id ] , emptyParts )
268- return count + parts . filter ( visible ) . length
310+ return count + parts . filter ( ( part ) => partState ( part , showReasoningSummaries ( ) ) === " visible" ) . length
269311 } , 0 ) ,
270312 )
313+ const assistantTailVisible = createMemo ( ( ) =>
314+ assistantMessages ( )
315+ . flatMap ( ( message ) => list ( data . store . part ?. [ message . id ] , emptyParts ) )
316+ . flatMap ( ( part ) => {
317+ if ( partState ( part , showReasoningSummaries ( ) ) !== "visible" ) return [ ]
318+ if ( part . type === "text" ) return [ "text" as const ]
319+ return [ "other" as const ]
320+ } )
321+ . at ( - 1 ) ,
322+ )
323+ const reasoningHeading = createMemo ( ( ) =>
324+ assistantMessages ( )
325+ . flatMap ( ( message ) => list ( data . store . part ?. [ message . id ] , emptyParts ) )
326+ . filter ( ( part ) : part is PartType & { type : "reasoning" ; text : string } => part . type === "reasoning" )
327+ . map ( ( part ) => heading ( part . text ) )
328+ . filter ( ( text ) : text is string => ! ! text )
329+ . at ( - 1 ) ,
330+ )
331+ const showThinking = createMemo ( ( ) => {
332+ if ( ! working ( ) || ! ! error ( ) ) return false
333+ if ( showReasoningSummaries ( ) ) return assistantVisible ( ) === 0
334+ if ( assistantTailVisible ( ) === "text" ) return false
335+ return true
336+ } )
271337
272338 const autoScroll = createAutoScroll ( {
273339 working,
@@ -295,21 +361,25 @@ export function SessionTurn(
295361 < div data-slot = "session-turn-message-content" aria-live = "off" >
296362 < Message message = { msg ( ) } parts = { parts ( ) } interrupted = { interrupted ( ) } />
297363 </ div >
298- < Show when = { working ( ) && assistantVisible ( ) === 0 && ! error ( ) } >
299- < div data-slot = "session-turn-thinking" >
300- < TextShimmer text = { i18n . t ( "ui.sessionTurn.status.thinking" ) } />
301- </ div >
302- </ Show >
303364 < Show when = { assistantMessages ( ) . length > 0 } >
304365 < div data-slot = "session-turn-assistant-content" aria-hidden = { working ( ) } >
305366 < AssistantParts
306367 messages = { assistantMessages ( ) }
307368 showAssistantCopyPartID = { assistantCopyPartID ( ) }
308369 turnDurationMs = { turnDurationMs ( ) }
309370 working = { working ( ) }
371+ showReasoningSummaries = { showReasoningSummaries ( ) }
310372 />
311373 </ div >
312374 </ Show >
375+ < Show when = { showThinking ( ) } >
376+ < div data-slot = "session-turn-thinking" >
377+ < TextShimmer text = { i18n . t ( "ui.sessionTurn.status.thinking" ) } />
378+ < Show when = { ! showReasoningSummaries ( ) && reasoningHeading ( ) } >
379+ { ( text ) => < span data-slot = "session-turn-thinking-heading" > { text ( ) } </ span > }
380+ </ Show >
381+ </ div >
382+ </ Show >
313383 < Show when = { edited ( ) > 0 && ! working ( ) } >
314384 < div data-slot = "session-turn-diffs" >
315385 < Collapsible open = { open ( ) } onOpenChange = { setOpen } variant = "ghost" >
0 commit comments