/**
* ReasoningPart — the model's thinking trace, collapsible (item 6; opencode's
* ReasoningPart/ReasoningHeader). Auto-EXPANDED while the turn streams (so you
* watch it think), then COLLAPSES to a one-line `▶ Thought:
` once the
* turn settles. Click the header to override either way.
*
* ▼ Thinking: ← live (streaming), body shown
* ▶ Thought: ← settled (collapsed), click to reopen
* │ ← dim body in a left-bordered block
*
* Title is the model's leading `**bold**` line when present (opencode's
* reasoningSummary). Dim throughout — it's secondary to the answer.
*/
import { createMemo, createSignal, Show } from 'solid-js'
import type { ThemeColors } from '../logic/theme.ts'
import { Markdown } from './markdown.tsx'
import { useScrollAnchor } from './scrollAnchor.tsx'
import { useTheme } from './theme.tsx'
const GUTTER = 2
/**
* Header label style — reasoning is the MOST secondary tier, so the word
* Thinking/Thought + its title preview stay muted but render ITALIC: a
* `▶ Thought` row reads as a different KIND of row than a tool at a glance
* (tools carry a bold text-colored name; reasoning stays quiet, just
* recognizably distinct). Exported so tests can pin the selection logic
* (char frames carry no color/attribute info).
*/
export function reasoningLabelStyle(color: ThemeColors): { fg: string; italic: boolean } {
return { fg: color.muted, italic: true }
}
/** Split a leading `**Title**\n\n body` into {title, body} (opencode reasoningSummary). */
function reasoningSummary(text: string): { title?: string; body: string } {
const s = (text ?? '').replace('[REDACTED]', '').trim()
const m = s.match(/^\*\*([^*\n]+)\*\*(?:\r?\n\r?\n|$)/)
const title = m?.[1]?.trim()
if (!m || !title) return { body: s }
return { title, body: s.slice(m[0].length).trimStart() }
}
export function ReasoningPart(props: { text: string; streaming?: boolean }) {
const theme = useTheme()
const anchor = useScrollAnchor()
const [override, setOverride] = createSignal(undefined)
// live → expanded so you see it think; settled → collapsed. Click overrides.
const expanded = () => override() ?? !!props.streaming
const toggle = () => anchor(() => setOverride(e => !(e ?? !!props.streaming)))
const summary = createMemo(() => reasoningSummary(props.text))
const label = () => (props.streaming ? 'Thinking' : 'Thought')
return (
{expanded() ? '▼' : '▶'}
{/* the header is a collapsible-section LABEL (Thinking/Thought + title)
— chrome, not the reasoning body — so a free-form drag yields only
the markdown body below, not the section label (item 4). */}
{/* accent chevron marks it; muted ITALIC label keeps reasoning the
most secondary tier AND visibly a different kind of row than a
tool (bold name) — see reasoningLabelStyle. */}
{label()}
{`: ${summary().title}`}
)
}