opencode/run: refresh themes after terminal reloads (#30917)

This commit is contained in:
Simon Klee
2026-06-05 11:47:17 +02:00
committed by GitHub
parent b278e49e96
commit 0c0d193474
12 changed files with 499 additions and 71 deletions
@@ -1,8 +1,9 @@
import { afterEach, expect, test } from "bun:test"
import type { ToolPart } from "@opencode-ai/sdk/v2"
import { RGBA, SyntaxStyle } from "@opentui/core"
import { MockTreeSitterClient, createTestRenderer, type TestRenderer } from "@opentui/core/testing"
import { RunScrollbackStream } from "@/cli/cmd/run/scrollback.surface"
import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme"
import { RUN_THEME_FALLBACK, type RunTheme } from "@/cli/cmd/run/theme"
import type { StreamCommit } from "@/cli/cmd/run/types"
type ClaimedCommit = {
@@ -62,6 +63,8 @@ async function setup(
input: {
width?: number
wrote?: boolean
theme?: RunTheme
onThemeRelease?: (theme: RunTheme) => void
} = {},
) {
const out = await createTestRenderer({
@@ -78,9 +81,10 @@ async function setup(
return {
renderer: out.renderer,
scrollback: new RunScrollbackStream(out.renderer, RUN_THEME_FALLBACK, {
scrollback: new RunScrollbackStream(out.renderer, input.theme ?? RUN_THEME_FALLBACK, {
treeSitterClient,
wrote: input.wrote ?? false,
onThemeRelease: input.onThemeRelease,
}),
}
}
@@ -107,6 +111,79 @@ function reasoning(text: string, phase: StreamCommit["phase"] = "progress"): Str
}
}
test("theme swaps restyle active reasoning without resetting the stream", async () => {
const previousSyntax = SyntaxStyle.fromStyles({ default: { fg: "#123456" } })
const nextSyntax = SyntaxStyle.fromStyles({ default: { fg: "#abcdef" } })
const released: RunTheme[] = []
const previous = {
...RUN_THEME_FALLBACK,
block: {
...RUN_THEME_FALLBACK.block,
subtleSyntax: previousSyntax,
},
}
const next = {
...RUN_THEME_FALLBACK,
block: {
...RUN_THEME_FALLBACK.block,
subtleSyntax: nextSyntax,
},
}
const out = await setup({ theme: previous, onThemeRelease: (theme) => released.push(theme) })
try {
await out.scrollback.append(reasoning("before"))
expect(activeSyntax(out.scrollback)).toBe(previousSyntax)
out.scrollback.setTheme(next)
expect(activeSyntax(out.scrollback)).toBe(nextSyntax)
expect(released).toEqual([])
await out.scrollback.append(reasoning("after"))
expect(activeSyntax(out.scrollback)).toBe(nextSyntax)
expect(released).toEqual([previous])
} finally {
out.scrollback.destroy()
destroy(claim(out.renderer))
previousSyntax.destroy()
nextSyntax.destroy()
}
})
function activeSyntax(scrollback: RunScrollbackStream) {
const entry = Reflect.get(scrollback, "active") as { renderable?: { syntaxStyle?: SyntaxStyle } } | undefined
return entry?.renderable?.syntaxStyle
}
test("theme swaps preserve streamed markdown parser state", async () => {
const out = await setup()
const next = {
...RUN_THEME_FALLBACK,
footer: {
...RUN_THEME_FALLBACK.footer,
surface: RGBA.fromHex("#123456"),
},
}
try {
await out.scrollback.append(assistant("```ts\nconst answer ="))
out.scrollback.setTheme(next)
await out.scrollback.append(assistant(" 42\n```"))
await out.scrollback.complete()
const commits = claim(out.renderer)
try {
const output = render(commits)
expect(output).toContain("const answer = 42")
expect(output).not.toContain("```")
} finally {
destroy(commits)
}
} finally {
out.scrollback.destroy()
}
})
function user(text: string): StreamCommit {
return {
kind: "user",