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 => } )}