feat(tui): add plugin context hook

This commit is contained in:
Dax Raad 2026-07-28 14:57:28 -04:00
commit 5bcc0016a6
16 changed files with 216 additions and 148 deletions

View file

@ -4,10 +4,15 @@ import { testRender } from "@opentui/solid"
import type { JSX } from "solid-js"
import { onMount, type ParentProps } from "solid-js"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { ThemeProvider } from "../../../src/context/theme"
import { ThemeProvider, useTheme } from "../../../src/context/theme"
import type { Plugin } from "@opencode-ai/plugin/tui"
import { ConfigProvider } from "../../../src/config"
import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import {
DiffViewerFileTree,
type DiffViewerFileTreeProps,
} from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import { TestTuiContexts } from "../../fixture/tui-environment"
import { createPluginTheme } from "../../../src/plugin/context"
import {
allExpandedFileTreeDirectories,
buildFileTree,
@ -17,7 +22,7 @@ describe("DiffViewerFileTree", () => {
test.skip("renders sorted hierarchical file rows", async () => {
const lines = visibleLines(
await renderFrame(() => (
<DiffViewerFileTree
<ThemedDiffViewerFileTree
width={32}
files={[
{ file: "z-file.ts" },
@ -45,13 +50,13 @@ describe("DiffViewerFileTree", () => {
test("keeps loading and error quiet while rendering an empty settled state", async () => {
const loading = await renderFrame(() => (
<DiffViewerFileTree width={32} files={[]} loading={true} error={undefined} />
<ThemedDiffViewerFileTree width={32} files={[]} loading={true} error={undefined} />
))
const failed = await renderFrame(() => (
<DiffViewerFileTree width={32} files={[]} loading={false} error={new Error("nope")} />
<ThemedDiffViewerFileTree width={32} files={[]} loading={false} error={new Error("nope")} />
))
const empty = await renderFrame(() => (
<DiffViewerFileTree width={32} files={[]} loading={false} error={undefined} />
<ThemedDiffViewerFileTree width={32} files={[]} loading={false} error={undefined} />
))
expect(loading).not.toContain("Loading diff...")
@ -67,7 +72,7 @@ describe("DiffViewerFileTree", () => {
const focused = visibleLines(
await renderFrame(() => (
<DiffViewerFileTree
<ThemedDiffViewerFileTree
width={32}
files={files}
loading={false}
@ -78,7 +83,7 @@ describe("DiffViewerFileTree", () => {
)),
)
const unfocused = visibleLines(
await renderFrame(() => <DiffViewerFileTree width={32} files={files} loading={false} error={undefined} />),
await renderFrame(() => <ThemedDiffViewerFileTree width={32} files={files} loading={false} error={undefined} />),
)
expect(focused).toContain("▾ src/config")
@ -97,7 +102,13 @@ describe("DiffViewerFileTree", () => {
expect(
visibleLines(
await renderFrame(() => (
<DiffViewerFileTree width={32} files={files} loading={false} error={undefined} expandedNodes={collapsed} />
<ThemedDiffViewerFileTree
width={32}
files={files}
loading={false}
error={undefined}
expandedNodes={collapsed}
/>
)),
),
).toEqual(["▸ src/config"])
@ -105,7 +116,7 @@ describe("DiffViewerFileTree", () => {
expect(
visibleLines(
await renderFrame(() => (
<DiffViewerFileTree
<ThemedDiffViewerFileTree
files={files}
width={32}
loading={false}
@ -118,6 +129,10 @@ describe("DiffViewerFileTree", () => {
})
})
function ThemedDiffViewerFileTree(props: Omit<DiffViewerFileTreeProps, "context">) {
return <DiffViewerFileTree {...props} context={{ theme: createPluginTheme(useTheme()) } as Plugin.Context} />
}
async function renderFrame(component: () => JSX.Element) {
const mounted = Promise.withResolvers<void>()
const app = await testRender(() => withTheme(component, mounted.resolve), { width: 40, height: 10 })

View file

@ -11,7 +11,7 @@ import type {
Route,
Slot,
} from "@opencode-ai/plugin/tui/context"
import { ThemeProvider } from "../../../src/context/theme"
import { ThemeProvider, useTheme } from "../../../src/context/theme"
import { ConfigProvider } from "../../../src/config"
import { TuiKeybind } from "../../../src/config/keybind"
import { Keymap } from "../../../src/context/keymap"
@ -21,6 +21,7 @@ import { TestTuiContexts } from "../../fixture/tui-environment"
import { createApi, createEventStream, createFetch, json } from "../../fixture/tui-client"
import { DialogProvider } from "../../../src/ui/dialog"
import { ToastProvider } from "../../../src/ui/toast"
import { createPluginTheme } from "../../../src/plugin/context"
test("closing the diff viewer returns to the route it opened from", async () => {
const viewer = await renderDiffViewer([])
@ -157,6 +158,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
})
}, createEventStream())
function Harness() {
let theme: ReturnType<typeof createPluginTheme>
const context = {
options: {},
client: createApi(transport.fetch),
@ -164,6 +166,9 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
session: { get: () => session },
location: { default: () => ({ directory: "/repo/default" }) },
},
get theme() {
return theme
},
keymap: {
layer(input: () => KeymapLayer) {
input().commands?.forEach((command) => {
@ -202,6 +207,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
void diffViewerPlugin.setup(context)
function Content() {
theme = createPluginTheme(useTheme())
const commandView = renderCommands?.({})
if (current.type !== "plugin") commands.get("diff.open")?.run()
return (