diff --git a/bun.lock b/bun.lock index e9c7917910..3ca21b9284 100644 --- a/bun.lock +++ b/bun.lock @@ -596,17 +596,20 @@ "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", + "solid-js": "catalog:", "typescript": "catalog:", }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5", + "solid-js": ">=1.9.0", }, "optionalPeers": [ "@opentui/core", "@opentui/keymap", "@opentui/solid", + "solid-js", ], }, "packages/protocol": { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index afc828e686..8f1ec0da3e 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -32,7 +32,8 @@ "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", - "@opentui/solid": ">=0.4.5" + "@opentui/solid": ">=0.4.5", + "solid-js": ">=1.9.0" }, "peerDependenciesMeta": { "@opentui/core": { @@ -43,6 +44,9 @@ }, "@opentui/solid": { "optional": true + }, + "solid-js": { + "optional": true } }, "devDependencies": { @@ -52,6 +56,7 @@ "@tsconfig/bun": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", + "solid-js": "catalog:", "typescript": "catalog:", "@typescript/native-preview": "catalog:" } diff --git a/packages/plugin/src/tui/index.ts b/packages/plugin/src/tui/index.ts index 8020216abc..8772be00e8 100644 --- a/packages/plugin/src/tui/index.ts +++ b/packages/plugin/src/tui/index.ts @@ -1 +1,2 @@ export * as Plugin from "./plugin.js" +export { PluginContextProvider, usePlugin } from "./solid.js" diff --git a/packages/plugin/src/tui/solid.ts b/packages/plugin/src/tui/solid.ts new file mode 100644 index 0000000000..37eacf1035 --- /dev/null +++ b/packages/plugin/src/tui/solid.ts @@ -0,0 +1,19 @@ +import { createComponent, createContext, useContext, type JSX } from "solid-js" +import type { Context } from "./context.js" + +const PluginContext = createContext() + +export function PluginContextProvider(props: { readonly value: Context; readonly children: JSX.Element }) { + return createComponent(PluginContext.Provider, { + value: props.value, + get children() { + return props.children + }, + }) +} + +export function usePlugin() { + const context = useContext(PluginContext) + if (!context) throw new Error("PluginContextProvider is missing") + return context +} diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index 6042e6e087..15cf984182 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -11,7 +11,7 @@ function Directory(props: { context: Plugin.Context; maxWidth: number }) { return ( - {(value) => } + {(value) => } ) } @@ -24,18 +24,16 @@ function Mcp(props: { context: Plugin.Context }) { return ( - + - + 0 - ? props.context.theme.themeV2.text.feedback.success.default - : props.context.theme.themeV2.text.subdued, + count() > 0 ? props.context.theme.text.feedback.success.default : props.context.theme.text.subdued, }} > ⊙{" "} @@ -44,7 +42,7 @@ function Mcp(props: { context: Plugin.Context }) { {count()} MCP - /status + /status ) @@ -77,7 +75,7 @@ function View(props: { context: Plugin.Context }) { - {props.context.app.version} + {props.context.app.version} ) diff --git a/packages/tui/src/feature-plugins/sidebar/context.tsx b/packages/tui/src/feature-plugins/sidebar/context.tsx index 52655add51..7c11825eb7 100644 --- a/packages/tui/src/feature-plugins/sidebar/context.tsx +++ b/packages/tui/src/feature-plugins/sidebar/context.tsx @@ -1,6 +1,5 @@ import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" -import { useTheme } from "../../context/theme" import { contextUsage } from "../../util/session" const money = new Intl.NumberFormat("en-US", { @@ -9,7 +8,7 @@ const money = new Intl.NumberFormat("en-US", { }) function View(props: { context: Plugin.Context; sessionID: string }) { - const { themeV2 } = useTheme() + const theme = props.context.theme const msg = createMemo(() => props.context.data.session.message.list(props.sessionID)) const session = createMemo(() => props.context.data.session.get(props.sessionID)) const cost = createMemo(() => props.context.data.session.cost(props.sessionID)) @@ -20,20 +19,20 @@ function View(props: { context: Plugin.Context; sessionID: string }) { return ( - + Context - Not measured}> + Not measured}> {(value) => ( <> - {value().tokens.toLocaleString()} tokens + {value().tokens.toLocaleString()} tokens - {value().percent}% used + {value().percent}% used )} - {money.format(cost())} spent + {money.format(cost())} spent ) } diff --git a/packages/tui/src/feature-plugins/sidebar/footer.tsx b/packages/tui/src/feature-plugins/sidebar/footer.tsx index 65ff8fecdc..c233a47848 100644 --- a/packages/tui/src/feature-plugins/sidebar/footer.tsx +++ b/packages/tui/src/feature-plugins/sidebar/footer.tsx @@ -8,7 +8,7 @@ function View(props: { context: Plugin.Context }) { ) return ( - {(value) => } + {(value) => } ) } diff --git a/packages/tui/src/feature-plugins/sidebar/lsp.tsx b/packages/tui/src/feature-plugins/sidebar/lsp.tsx index 5eedcc12b2..792a05cfde 100644 --- a/packages/tui/src/feature-plugins/sidebar/lsp.tsx +++ b/packages/tui/src/feature-plugins/sidebar/lsp.tsx @@ -1,14 +1,14 @@ -import { Plugin } from "@opencode-ai/plugin/tui" -import { useTheme } from "../../context/theme" +import { Plugin, usePlugin } from "@opencode-ai/plugin/tui" function View() { - const { themeV2 } = useTheme() + const context = usePlugin() + const theme = context.theme return ( - + LSP - LSP status unavailable + LSP status unavailable ) } diff --git a/packages/tui/src/feature-plugins/sidebar/mcp.tsx b/packages/tui/src/feature-plugins/sidebar/mcp.tsx index 842da4c310..977867795a 100644 --- a/packages/tui/src/feature-plugins/sidebar/mcp.tsx +++ b/packages/tui/src/feature-plugins/sidebar/mcp.tsx @@ -1,10 +1,9 @@ import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js" -import { useTheme } from "../../context/theme" function View(props: { context: Plugin.Context; sessionID: string }) { const [open, setOpen] = createSignal(true) - const { themeV2 } = useTheme() + const theme = props.context.theme const session = createMemo(() => props.context.data.session.get(props.sessionID)) const list = createMemo(() => props.context.data.location.mcp.server.list(session()?.location) ?? []) const on = createMemo(() => list().filter((item) => item.status.status === "connected").length) @@ -19,12 +18,12 @@ function View(props: { context: Plugin.Context; sessionID: string }) { ) const dot = (status: string) => { - if (status === "connected") return themeV2.text.feedback.success.default - if (status === "failed") return themeV2.text.feedback.error.default - if (status === "disabled") return themeV2.text.subdued - if (status === "needs_auth") return themeV2.text.feedback.warning.default - if (status === "needs_client_registration") return themeV2.text.feedback.error.default - return themeV2.text.subdued + if (status === "connected") return theme.text.feedback.success.default + if (status === "failed") return theme.text.feedback.error.default + if (status === "disabled") return theme.text.subdued + if (status === "needs_auth") return theme.text.feedback.warning.default + if (status === "needs_client_registration") return theme.text.feedback.error.default + return theme.text.subdued } return ( @@ -32,12 +31,12 @@ function View(props: { context: Plugin.Context; sessionID: string }) { list().length > 2 && setOpen((x) => !x)}> 2}> - {open() ? "▼" : "▶"} + {open() ? "▼" : "▶"} - + MCP - + {" "} ({on()} active{bad() > 0 ? `, ${bad()} error${bad() > 1 ? "s" : ""}` : ""}) @@ -56,9 +55,9 @@ function View(props: { context: Plugin.Context; sessionID: string }) { > • - + {item.name}{" "} - + Connected diff --git a/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx b/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx index 2a34f73755..8e36d5b2df 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx @@ -1,15 +1,16 @@ /** @jsxImportSource @opentui/solid */ import type { ScrollBoxRenderable } from "@opentui/core" +import type { Plugin } from "@opencode-ai/plugin/tui" import { Locale } from "../../util/locale" import { tint } from "../../theme/color" import { createEffect, createMemo, For, Match, Switch } from "solid-js" import { buildFileTree, flattenFileTree, type FileTreeItem, type FileTreeRow } from "./diff-viewer-file-tree-utils" import { Panel } from "./diff-viewer-ui" -import { useTheme } from "../../context/theme" const FILE_TREE_STATUS_WIDTH = 2 export type DiffViewerFileTreeProps = { + readonly context: Plugin.Context readonly width: number readonly files: readonly FileTreeItem[] readonly loading: boolean @@ -23,7 +24,7 @@ export type DiffViewerFileTreeProps = { } export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { - const { themeV2 } = useTheme() + const theme = props.context.theme const tree = createMemo(() => buildFileTree(props.files)) const rows = createMemo(() => flattenFileTree(tree(), props.expandedNodes)) let scroll: ScrollBoxRenderable | undefined @@ -38,10 +39,10 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { requestAnimationFrame(scrollSelectedIntoView) }) - const fadedColor = () => tint(themeV2.text.default, themeV2.background.default, 0.75) + const fadedColor = () => tint(theme.text.default, theme.background.default, 0.75) return ( - + (scroll = element)} verticalScrollbarOptions={{ visible: false }} @@ -52,7 +53,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { - No files + No files 0}> @@ -71,11 +72,11 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { props.onRowClick?.(row)} > @@ -85,12 +86,12 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { @@ -98,7 +99,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) { diff --git a/packages/tui/src/feature-plugins/system/diff-viewer-ui.tsx b/packages/tui/src/feature-plugins/system/diff-viewer-ui.tsx index ca0e05aeed..2336a07ce8 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer-ui.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer-ui.tsx @@ -1,13 +1,13 @@ import type { BorderSides, ColorInput } from "@opentui/core" +import type { Plugin } from "@opencode-ai/plugin/tui" import type { JSX } from "@opentui/solid" -import { useTheme } from "../../context/theme" import { createContext, Show, splitProps, useContext } from "solid-js" export type Axis = "x" | "y" export type SeparatorEdge = "edge" | "edge-in" | "edge-out" export type PanelBorder = "start" | "end" | "both" | "none" -const PanelGroupContext = createContext<{ axis: Axis }>() +const PanelGroupContext = createContext<{ axis: Axis; context: Plugin.Context }>() function crossAxis(axis: Axis) { return axis === "x" ? "y" : "x" @@ -17,10 +17,10 @@ function usePanelGroup() { return useContext(PanelGroupContext) } -export function PanelGroup(props: JSX.IntrinsicElements["box"] & { axis: Axis }) { - const [local, boxProps] = splitProps(props, ["axis", "children"]) +export function PanelGroup(props: JSX.IntrinsicElements["box"] & { axis: Axis; context: Plugin.Context }) { + const [local, boxProps] = splitProps(props, ["axis", "context", "children"]) return ( - + {local.children} @@ -28,24 +28,28 @@ export function PanelGroup(props: JSX.IntrinsicElements["box"] & { axis: Axis }) ) } -export function Panel(props: Omit & { border?: PanelBorder }) { +export function Panel( + props: Omit & { border?: PanelBorder; context?: Plugin.Context }, +) { const group = usePanelGroup() - const { themeV2 } = useTheme() - const [local, boxProps] = splitProps(props, ["border"]) + const [local, boxProps] = splitProps(props, ["border", "context"]) + const context = local.context ?? group?.context + if (!context) throw new Error("Panel context is missing") + const theme = context.theme const border = local.border ?? "start" const borderProps = border === "none" ? {} : { border: panelBorderSides(group?.axis ?? "y", border), - borderColor: themeV2.border.default, + borderColor: theme.border.default, } return ( @@ -59,9 +63,10 @@ function panelBorderSides(axis: Axis, border: Exclude): Bor export function Separator(props: { axis?: Axis; color?: ColorInput; start?: SeparatorEdge; end?: SeparatorEdge }) { const group = usePanelGroup() - const { themeV2 } = useTheme() - const color = () => props.color ?? themeV2.border.default - const axis = () => props.axis ?? crossAxis(group?.axis ?? "y") + if (!group) throw new Error("PanelGroup is missing") + const theme = group.context.theme + const color = () => props.color ?? theme.border.default + const axis = () => props.axis ?? crossAxis(group.axis) if (axis() === "y") { return ( { const route = props.context.ui.router.current() return (route.type === "plugin" ? route.data : undefined) as @@ -738,13 +736,13 @@ function DiffViewer(props: { context: Plugin.Context }) { return ( - + - Diff - {diffSourceLabel(mode())} + Diff + {diffSourceLabel(mode())} - + {files().length} {files().length === 1 ? "file" : "files"} @@ -755,13 +753,13 @@ function DiffViewer(props: { context: Plugin.Context }) { - Loading diff… + Loading diff… - + Could not load diff. Reopen the diff viewer to try again. @@ -769,13 +767,14 @@ function DiffViewer(props: { context: Plugin.Context }) { - No changes to show + No changes to show - + - - {entry.file.file} - + {entry.file.file} - + +{entry.file.additions} - + -{entry.file.deletions} No patch available for this file.} + fallback={No patch available for this file.} > {(patch) => ( - + diffNodeByFileIndex.set(entry.fileIndex, element)} diff={patch()} view={view()} filetype={reviewed() ? PLAIN_TEXT_FILETYPE : filetype(entry.file.file)} - syntaxStyle={themeState.syntax()} + syntaxStyle={theme.syntaxStyle()} showLineNumbers={true} width="100%" wrapMode="char" - fg={reviewed() ? themeV2.text.subdued : themeV2.text.default} + fg={reviewed() ? theme.text.subdued : theme.text.default} addedBg={ - reviewed() ? themeV2.background.surface.overlay : themeV2.diff.background.added + reviewed() ? theme.background.surface.overlay : theme.diff.background.added } removedBg={ - reviewed() ? themeV2.background.surface.overlay : themeV2.diff.background.removed + reviewed() ? theme.background.surface.overlay : theme.diff.background.removed } - addedSignColor={reviewed() ? themeV2.text.subdued : themeV2.diff.highlight.added} - removedSignColor={ - reviewed() ? themeV2.text.subdued : themeV2.diff.highlight.removed - } - lineNumberFg={themeV2.diff.lineNumber.text} + addedSignColor={reviewed() ? theme.text.subdued : theme.diff.highlight.added} + removedSignColor={reviewed() ? theme.text.subdued : theme.diff.highlight.removed} + lineNumberFg={theme.diff.lineNumber.text} addedLineNumberBg={ reviewed() - ? themeV2.background.surface.overlay - : themeV2.diff.lineNumber.background.added + ? theme.background.surface.overlay + : theme.diff.lineNumber.background.added } removedLineNumberBg={ reviewed() - ? themeV2.background.surface.overlay - : themeV2.diff.lineNumber.background.removed + ? theme.background.surface.overlay + : theme.diff.lineNumber.background.removed } /> @@ -872,11 +867,7 @@ function DiffViewer(props: { context: Plugin.Context }) { }} 0}> - + @@ -889,57 +880,57 @@ function DiffViewer(props: { context: Plugin.Context }) { {(shortcut) => ( - - {shortcut()} focus file tree + + {shortcut()} focus file tree )} {(shortcut) => ( - - {shortcut()} next file + + {shortcut()} next file )} {(shortcut) => ( - - {shortcut()} next hunk + + {shortcut()} next hunk )} {(shortcut) => ( - - {shortcut()} previous hunk + + {shortcut()} previous hunk )} {(shortcut) => ( - - {shortcut()} previous file + + {shortcut()} previous file )} {(shortcut) => ( - - {shortcut()} switch source + + {shortcut()} switch source )} {(shortcut) => ( - - {shortcut()} mark reviewed + + {shortcut()} mark reviewed )} {(shortcut) => ( - - {shortcut()} all + + {shortcut()} all )} @@ -950,7 +941,7 @@ function DiffViewer(props: { context: Plugin.Context }) { } function DiffViewerHelpDialog(props: { context: Plugin.Context }) { - const { themeV2 } = useTheme().contextual("elevated") + const theme = props.context.theme.contextual("elevated") const shortcut = (id: string) => () => props.context.keymap.shortcuts(id)[0] const rows = [ { @@ -1018,30 +1009,30 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context }) { return ( - + Diff shortcuts - esc + esc - + Key - + Action - Description + Description {(row) => ( - + {row.shortcut() || "-"} - + {row.action} - {row.description} + {row.description} )} diff --git a/packages/tui/src/feature-plugins/system/scrap.tsx b/packages/tui/src/feature-plugins/system/scrap.tsx index 9c2921e77d..351afaab15 100644 --- a/packages/tui/src/feature-plugins/system/scrap.tsx +++ b/packages/tui/src/feature-plugins/system/scrap.tsx @@ -1,6 +1,5 @@ import { Plugin } from "@opencode-ai/plugin/tui" import { useTerminalDimensions } from "@opentui/solid" -import { useTheme } from "../../context/theme" function Commands(props: { context: Plugin.Context }) { props.context.keymap.layer(() => ({ @@ -23,8 +22,8 @@ function Commands(props: { context: Plugin.Context }) { function Scrap(props: { context: Plugin.Context }) { const dimensions = useTerminalDimensions() - const { themeV2 } = useTheme() - const { themeV2: elevatedTheme } = useTheme().contextual("elevated") + const theme = props.context.theme + const elevatedTheme = props.context.theme.contextual("elevated") props.context.keymap.layer(() => ({ commands: [ @@ -40,7 +39,7 @@ function Scrap(props: { context: Plugin.Context }) { })) return ( - + const paths = useTuiPaths() const location = useLocation() const theme = useTheme() + const pluginTheme = createPluginTheme(theme) const dialog = useDialog() const toast = useToast() const attention = useAttention() @@ -100,18 +101,22 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> setStore("registrations", id, "cleanups", []) }) const owned: Dispose[] = [] + let context: Context let activeDialog: symbol | undefined const dialogApi: Dialog = { show(render, onClose) { const token = Symbol() let closed = false activeDialog = token - dialog.replace(render, () => { - if (closed) return - closed = true - if (activeDialog === token) activeDialog = undefined - onClose?.() - }) + dialog.replace( + () => {render()}, + () => { + if (closed) return + closed = true + if (activeDialog === token) activeDialog = undefined + onClose?.() + }, + ) return () => { if (closed || activeDialog !== token) return dialog.clear() @@ -215,7 +220,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> }, } owned.push(async () => dialogApi.clear()) - const context: Context = { + context = { options: item.options ?? {}, get location() { return location.current @@ -225,7 +230,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> client: client.api, data, attention, - theme, + theme: pluginTheme, keymap: { layer: Keymap.createLayer, dispatch: keymap.dispatch, @@ -245,7 +250,10 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> register(page) { if (store.registrations[item.plugin.id]?.routes[page.name]) throw new Error(`Route already registered: ${page.name}`) - setStore("registrations", item.plugin.id, "routes", page.name, page) + setStore("registrations", item.plugin.id, "routes", page.name, { + ...page, + render: (input) => {page.render(input)}, + }) let registered = true const unregister = () => { if (!registered) return @@ -275,7 +283,9 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> }, slot(name, render) { if (store.registrations[item.plugin.id]?.slots[name]) throw new Error(`Slot already registered: ${name}`) - setStore("registrations", item.plugin.id, "slots", name, () => render) + setStore("registrations", item.plugin.id, "slots", name, () => (input: SlotMap[typeof name]) => ( + {render(input)} + )) let registered = true const unregister = () => { if (!registered) return @@ -518,6 +528,23 @@ function isPlugin(value: unknown): value is Plugin.Definition { ) } +type PluginTheme = ReturnType["themeV2"] & { + contextual(context: "elevated" | "overlay"): PluginTheme + syntaxStyle(): ReturnType["syntax"]> +} + +export function createPluginTheme(theme: ReturnType): PluginTheme { + return new Proxy(theme.themeV2 as PluginTheme, { + get(target, property, receiver) { + if (property === "contextual") + return (context: "elevated" | "overlay") => createPluginTheme(theme.contextual(context)) + if (property === "syntaxStyle") return theme.syntax + if (Reflect.has(target, property)) return Reflect.get(target, property, receiver) + return Reflect.get(theme, property, theme) + }, + }) +} + export function usePlugin() { const value = useContext(PluginContext) if (!value) throw new Error("PluginProvider is missing") diff --git a/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx b/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx index 68113f65b4..f85f5ddbb7 100644 --- a/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx @@ -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(() => ( - { test("keeps loading and error quiet while rendering an empty settled state", async () => { const loading = await renderFrame(() => ( - + )) const failed = await renderFrame(() => ( - + )) const empty = await renderFrame(() => ( - + )) expect(loading).not.toContain("Loading diff...") @@ -67,7 +72,7 @@ describe("DiffViewerFileTree", () => { const focused = visibleLines( await renderFrame(() => ( - { )), ) const unfocused = visibleLines( - await renderFrame(() => ), + await renderFrame(() => ), ) expect(focused).toContain("▾ src/config") @@ -97,7 +102,13 @@ describe("DiffViewerFileTree", () => { expect( visibleLines( await renderFrame(() => ( - + )), ), ).toEqual(["▸ src/config"]) @@ -105,7 +116,7 @@ describe("DiffViewerFileTree", () => { expect( visibleLines( await renderFrame(() => ( - { }) }) +function ThemedDiffViewerFileTree(props: Omit) { + return +} + async function renderFrame(component: () => JSX.Element) { const mounted = Promise.withResolvers() const app = await testRender(() => withTheme(component, mounted.resolve), { width: 40, height: 10 }) diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index 524e1c2b84..f640310b52 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -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 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 (