Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
571856d
feat(opencode): enhanced markdown renderer with tables, themes, and s…
ryanwyler Jan 12, 2026
9cfe157
feat(markdown): add table cell word wrap with bold continuity and lin…
ryanwyler Jan 20, 2026
1084762
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Jan 26, 2026
fb0faba
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Jan 27, 2026
a007400
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Jan 29, 2026
07fa62d
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Jan 29, 2026
c934dec
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Jan 30, 2026
519e05d
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Feb 1, 2026
f63952b
Fix: Restore SDK files accidentally deleted during dev merge
ariane-emory Feb 1, 2026
9ae1310
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Feb 2, 2026
e118c7d
Merge dev into feat/gignit--markdown-renderer
ariane-emory Feb 3, 2026
4a7c23e
Merge dev into feat/gignit--markdown-renderer and resolve conflicts i…
ariane-emory Feb 4, 2026
18a2ed8
Fix syntax error in run.ts introduced during merge resolution
ariane-emory Feb 4, 2026
f74824e
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Feb 4, 2026
b0afce7
Merge dev into feat/gignit--markdown-renderer
ariane-emory Feb 6, 2026
dc24baa
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Feb 6, 2026
181f83e
Merge branch 'dev' into feat/gignit--markdown-renderer
ariane-emory Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SkillTool } from "../../tool/skill"
import { BashTool } from "../../tool/bash"
import { TodoWriteTool } from "../../tool/todo"
import { Locale } from "../../util/locale"
import { loadTheme } from "../theme-loader"

type ToolProps<T extends Tool.Info> = {
input: Tool.InferParameters<T>
Expand Down Expand Up @@ -375,6 +376,15 @@ export const RunCommand = cmd({
}

async function execute(sdk: OpencodeClient) {
let theme
try {
const configResult = await sdk.config.get()
const themeName = configResult.data?.theme
theme = loadTheme(themeName)
} catch {
theme = loadTheme()
}

function tool(part: ToolPart) {
if (part.tool === "bash") return bash(props<typeof BashTool>(part))
if (part.tool === "glob") return glob(props<typeof GlobTool>(part))
Expand Down Expand Up @@ -456,7 +466,7 @@ export const RunCommand = cmd({
continue
}
UI.empty()
UI.println(text)
UI.println(UI.markdown(text, theme))
UI.empty()
}

Expand Down
130 changes: 106 additions & 24 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "solid-js"
import { Dynamic } from "solid-js/web"
import path from "path"

import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
Expand All @@ -26,7 +27,9 @@ import {
type ScrollAcceleration,
TextAttributes,
RGBA,
StyledText,
} from "@opentui/core"
import { Index } from "solid-js"
import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2"
import { useLocal } from "@tui/context/local"
Expand Down Expand Up @@ -77,6 +80,7 @@ import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
import { renderMarkdownThemedStyled, parseMarkdownSegments } from "@/cli/markdown-renderer"
import { UI } from "@/cli/ui.ts"

addDefaultParsers(parsers.parsers)
Expand Down Expand Up @@ -1354,38 +1358,116 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
)
}

// ============================================================================
// Markdown Rendering Components
// ============================================================================

const LANGS: Record<string, string> = {
js: "javascript",
ts: "typescript",
jsx: "typescript",
tsx: "typescript",
py: "python",
rb: "ruby",
sh: "shell",
bash: "shell",
zsh: "shell",
yml: "yaml",
md: "markdown",
}

function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
const ctx = use()
const { theme, syntax } = useTheme()
const tui = useTheme()

// Parse markdown into segments - use Index to prevent recreation
const segments = createMemo(() => parseMarkdownSegments(props.part.text?.trim() ?? ""))

return (
<Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<Switch>
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<markdown
syntaxStyle={syntax()}
streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
</Match>
</Switch>
<Show when={props.part.text?.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0} flexDirection="column">
<Index each={segments()}>
{(segment) => (
<Show
when={segment().type === "code"}
fallback={<Prose segment={segment() as any} theme={tui.theme} width={ctx.width - 3} />}
>
<CodeBlock segment={segment() as any} syntax={tui.syntax()} />
</Show>
)}
</Index>
</box>
</Show>
)
}

// Render text segments with custom renderer (tables, inline formatting)
function Prose(props: { segment: { type: "text"; content: string }; theme: any; width: number }) {
let el: any
const styled = createMemo(() => {
if (!props.segment.content) return new StyledText([])
const result = renderMarkdownThemedStyled(props.segment.content, props.theme, { cols: props.width })
return new StyledText(
result.chunks.map((c) => ({
__isChunk: true as const,
text: c.text,
fg: c.fg ? RGBA.fromInts(c.fg.r, c.fg.g, c.fg.b, c.fg.a) : props.theme.text,
bg: c.bg ? RGBA.fromInts(c.bg.r, c.bg.g, c.bg.b, c.bg.a) : undefined,
attributes: c.attributes,
})),
)
})
createEffect(() => {
if (el) el.content = styled()
})
return <text ref={el} />
}

// Render code blocks with tree-sitter highlighting
function CodeBlock(props: { segment: { type: "code"; content: string; language: string }; syntax: any }) {
const ctx = use()
const lang = () => LANGS[props.segment.language] || props.segment.language

return (
<box paddingLeft={2}>
<code
filetype={lang()}
content={props.segment.content}
syntaxStyle={props.syntax}
drawUnstyledText={true}
streaming={false}
conceal={ctx.conceal()}
/>
</box>
)
}

// Prose and Diff components kept for potential future use with stable rendering
function Diff(props: { content: string; theme: ReturnType<typeof useTheme>["theme"] }) {
let el: any
const styled = createMemo(() => {
const chunks = props.content.split("\n").map((line) => {
const t = line.trim()
const fg = t.startsWith("+")
? props.theme.diffAdded
: t.startsWith("-")
? props.theme.diffRemoved
: props.theme.markdownCodeBlock
return { __isChunk: true as const, text: " " + line + "\n", fg }
})
return new StyledText(chunks)
})
createEffect(() => {
if (el) el.content = styled()
})
// Don't pass fg prop - chunks already have colors
return (
<box paddingLeft={2}>
<text ref={el} />
</box>
)
}

// Pending messages moved to individual tool pending functions

function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) {
Expand Down
Loading