diff --git a/ui-tui-opentui-v2/src/test/render.test.tsx b/ui-tui-opentui-v2/src/test/render.test.tsx index 5d7ad99a7e6..3560ecc0957 100644 --- a/ui-tui-opentui-v2/src/test/render.test.tsx +++ b/ui-tui-opentui-v2/src/test/render.test.tsx @@ -121,6 +121,28 @@ describe('App render (Phase 1, themed)', () => { expect(frame).toContain('(2 lines)') // output line count (collapsed) }) + test('a settled reasoning part collapses to a one-line "Thought:
. `drawUnstyledText={false}` avoids the
// raw→styled flash per delta (the streaming flicker); `streaming` re-tokenizes
- // incrementally rather than reparsing the whole buffer each repaint.
+ // incrementally rather than reparsing the whole buffer each repaint. `fg`
+ // overrides the base text color (e.g. muted for reasoning bodies — item 6).
return (
)
}
diff --git a/ui-tui-opentui-v2/src/view/messageLine.tsx b/ui-tui-opentui-v2/src/view/messageLine.tsx
index 620659dd651..8ceb46f355f 100644
--- a/ui-tui-opentui-v2/src/view/messageLine.tsx
+++ b/ui-tui-opentui-v2/src/view/messageLine.tsx
@@ -12,6 +12,7 @@ import { For, Match, Show, Switch } from 'solid-js'
import type { Message } from '../logic/store.ts'
import { Markdown } from './markdown.tsx'
+import { ReasoningPart } from './reasoningPart.tsx'
import { useTheme } from './theme.tsx'
import { ToolPart } from './toolPart.tsx'
@@ -59,11 +60,7 @@ export function MessageLine(props: { message: Message }) {
{tool => }
- {r => (
-
- {r().text}
-
- )}
+ {r => }
{t => }
diff --git a/ui-tui-opentui-v2/src/view/reasoningPart.tsx b/ui-tui-opentui-v2/src/view/reasoningPart.tsx
new file mode 100644
index 00000000000..528b69521fb
--- /dev/null
+++ b/ui-tui-opentui-v2/src/view/reasoningPart.tsx
@@ -0,0 +1,69 @@
+/**
+ * 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 { Markdown } from './markdown.tsx'
+import { useTheme } from './theme.tsx'
+
+const GUTTER = 2
+
+/** 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 (!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 [override, setOverride] = createSignal(undefined)
+ // live → expanded so you see it think; settled → collapsed. Click overrides.
+ const expanded = () => override() ?? !!props.streaming
+ const summary = createMemo(() => reasoningSummary(props.text))
+ const label = () => (props.streaming ? 'Thinking' : 'Thought')
+
+ return (
+
+
+ setOverride(e => !(e ?? !!props.streaming))}
+ >
+
+
+ {expanded() ? '▼' : '▶'}
+
+
+
+ {label()}
+
+ {`: ${summary().title}`}
+
+
+
+
+
+
+
+
+
+
+ )
+}