diff --git a/ui-tui-opentui-v2/src/test/render.test.tsx b/ui-tui-opentui-v2/src/test/render.test.tsx
index 54a454c856b..a1f8b24ac68 100644
--- a/ui-tui-opentui-v2/src/test/render.test.tsx
+++ b/ui-tui-opentui-v2/src/test/render.test.tsx
@@ -29,7 +29,7 @@ describe('App render (Phase 1, themed)', () => {
),
- { width: 60, height: 16 }
+ { until: 'Hi there, glitch!', width: 60, height: 16 }
)
expect(frame).toContain('Hermes Agent') // default brand.name
@@ -74,7 +74,7 @@ describe('App render (Phase 1, themed)', () => {
),
- { width: 60, height: 16 }
+ { until: 'Listing files:', width: 60, height: 16 }
)
expect(frame).toContain('Listing files:') // text part
diff --git a/ui-tui-opentui-v2/src/view/markdown.tsx b/ui-tui-opentui-v2/src/view/markdown.tsx
new file mode 100644
index 00000000000..1f560830670
--- /dev/null
+++ b/ui-tui-opentui-v2/src/view/markdown.tsx
@@ -0,0 +1,67 @@
+/**
+ * Markdown — assistant/reasoning text rendered with the NATIVE renderable, never
+ * a hand-rolled parser (spec v4 §7). Uses ``
+ * (`CodeRenderable`) — opencode's v2 text path (`session-v2.tsx:358` AssistantText)
+ * — backed by the same markdown tokenizer + Tree-sitter as ``, but it
+ * paints reliably (incl. headless): `drawUnstyledText` draws the raw text
+ * immediately while highlighting settles, `conceal` hides the `**`/backtick
+ * markers, `streaming` feeds incremental deltas.
+ *
+ * The `SyntaxStyle` is derived from the active theme (no hardcoded styles — §7.5)
+ * and cached by theme-object identity, so all text parts share ONE instance and
+ * it's rebuilt only when the skin changes (a new `Theme` object).
+ */
+import { RGBA, SyntaxStyle } from '@opentui/core'
+
+import type { Theme } from '../logic/theme.ts'
+import { useTheme } from './theme.tsx'
+
+const FALLBACK = RGBA.fromHex('#E6EDF3')
+const HEX6 = /^#[0-9a-fA-F]{6}$/
+
+/** Theme colors are usually hex but may be `ansi256(n)`/`rgb(...)` after light-mode
+ * normalization — only hand hex to RGBA.fromHex, else fall back. */
+function rgba(color: string): RGBA {
+ return HEX6.test(color) ? RGBA.fromHex(color) : FALLBACK
+}
+
+function buildSyntaxStyle(theme: Theme): SyntaxStyle {
+ const c = theme.color
+ return SyntaxStyle.fromStyles({
+ default: { fg: rgba(c.text) },
+ 'markup.heading': { bold: true, fg: rgba(c.primary) },
+ 'markup.heading.1': { bold: true, fg: rgba(c.primary) },
+ 'markup.heading.2': { bold: true, fg: rgba(c.accent) },
+ 'markup.heading.3': { bold: true, fg: rgba(c.accent) },
+ 'markup.bold': { bold: true, fg: rgba(c.text) },
+ 'markup.italic': { fg: rgba(c.text), italic: true },
+ 'markup.list': { fg: rgba(c.accent) },
+ 'markup.quote': { fg: rgba(c.muted) },
+ 'markup.link': { fg: rgba(c.accent) },
+ 'markup.raw': { fg: rgba(c.label) },
+ 'markup.raw.block': { fg: rgba(c.label) }
+ })
+}
+
+let cache: { theme: Theme; style: SyntaxStyle } | undefined
+function syntaxStyleFor(theme: Theme): SyntaxStyle {
+ if (cache && cache.theme === theme) return cache.style
+ const style = buildSyntaxStyle(theme)
+ cache = { style, theme }
+ return style
+}
+
+export function Markdown(props: { text: string; streaming?: boolean }) {
+ const theme = useTheme()
+ return (
+
+ )
+}
diff --git a/ui-tui-opentui-v2/src/view/messageLine.tsx b/ui-tui-opentui-v2/src/view/messageLine.tsx
index f96d0d8c047..34d559db186 100644
--- a/ui-tui-opentui-v2/src/view/messageLine.tsx
+++ b/ui-tui-opentui-v2/src/view/messageLine.tsx
@@ -11,6 +11,7 @@
import { For, Match, Show, Switch } from 'solid-js'
import type { Message } from '../logic/store.ts'
+import { Markdown } from './markdown.tsx'
import { useTheme } from './theme.tsx'
import { ToolPart } from './toolPart.tsx'
@@ -52,11 +53,7 @@ export function MessageLine(props: { message: Message }) {
)}
- {t => (
-
- {t().text}
-
- )}
+ {t => }
)}