feat(tui): add plugin context hook
This commit is contained in:
parent
771174b5c3
commit
5bcc0016a6
16 changed files with 216 additions and 148 deletions
3
bun.lock
3
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": {
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * as Plugin from "./plugin.js"
|
||||
export { PluginContextProvider, usePlugin } from "./solid.js"
|
||||
|
|
|
|||
19
packages/plugin/src/tui/solid.ts
Normal file
19
packages/plugin/src/tui/solid.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { createComponent, createContext, useContext, type JSX } from "solid-js"
|
||||
import type { Context } from "./context.js"
|
||||
|
||||
const PluginContext = createContext<Context>()
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
|||
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
{(value) => <FilePath value={value()} maxWidth={props.maxWidth} fg={props.context.theme.themeV2.text.subdued} />}
|
||||
{(value) => <FilePath value={value()} maxWidth={props.maxWidth} fg={props.context.theme.text.subdued} />}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
@ -24,18 +24,16 @@ function Mcp(props: { context: Plugin.Context }) {
|
|||
return (
|
||||
<Show when={list().length}>
|
||||
<box gap={1} flexDirection="row" flexShrink={0}>
|
||||
<text fg={props.context.theme.themeV2.text.default}>
|
||||
<text fg={props.context.theme.text.default}>
|
||||
<Switch>
|
||||
<Match when={failed()}>
|
||||
<span style={{ fg: props.context.theme.themeV2.text.feedback.error.default }}>⊙ </span>
|
||||
<span style={{ fg: props.context.theme.text.feedback.error.default }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span
|
||||
style={{
|
||||
fg:
|
||||
count() > 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 }) {
|
|||
</Switch>
|
||||
{count()} MCP
|
||||
</text>
|
||||
<text fg={props.context.theme.themeV2.text.subdued}>/status</text>
|
||||
<text fg={props.context.theme.text.subdued}>/status</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
|
|
@ -77,7 +75,7 @@ function View(props: { context: Plugin.Context }) {
|
|||
<Mcp context={props.context} />
|
||||
<box flexGrow={1} />
|
||||
<box flexShrink={0}>
|
||||
<text fg={props.context.theme.themeV2.text.subdued}>{props.context.app.version}</text>
|
||||
<text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<box>
|
||||
<text fg={themeV2.text.default}>
|
||||
<text fg={theme.text.default}>
|
||||
<b>Context</b>
|
||||
</text>
|
||||
<Show when={state()} fallback={<text fg={themeV2.text.subdued}>Not measured</text>}>
|
||||
<Show when={state()} fallback={<text fg={theme.text.subdued}>Not measured</text>}>
|
||||
{(value) => (
|
||||
<>
|
||||
<text fg={themeV2.text.subdued}>{value().tokens.toLocaleString()} tokens</text>
|
||||
<text fg={theme.text.subdued}>{value().tokens.toLocaleString()} tokens</text>
|
||||
<Show when={value().percent !== undefined}>
|
||||
<text fg={themeV2.text.subdued}>{value().percent}% used</text>
|
||||
<text fg={theme.text.subdued}>{value().percent}% used</text>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<text fg={themeV2.text.subdued}>{money.format(cost())} spent</text>
|
||||
<text fg={theme.text.subdued}>{money.format(cost())} spent</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function View(props: { context: Plugin.Context }) {
|
|||
)
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.themeV2.text.subdued} />}
|
||||
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.text.subdued} />}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<box>
|
||||
<text fg={themeV2.text.default}>
|
||||
<text fg={theme.text.default}>
|
||||
<b>LSP</b>
|
||||
</text>
|
||||
<text fg={themeV2.text.subdued}>LSP status unavailable</text>
|
||||
<text fg={theme.text.subdued}>LSP status unavailable</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
|||
<box>
|
||||
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
||||
<Show when={list().length > 2}>
|
||||
<text fg={themeV2.text.default}>{open() ? "▼" : "▶"}</text>
|
||||
<text fg={theme.text.default}>{open() ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={themeV2.text.default}>
|
||||
<text fg={theme.text.default}>
|
||||
<b>MCP</b>
|
||||
<Show when={!open()}>
|
||||
<span style={{ fg: themeV2.text.subdued }}>
|
||||
<span style={{ fg: theme.text.subdued }}>
|
||||
{" "}
|
||||
({on()} active{bad() > 0 ? `, ${bad()} error${bad() > 1 ? "s" : ""}` : ""})
|
||||
</span>
|
||||
|
|
@ -56,9 +55,9 @@ function View(props: { context: Plugin.Context; sessionID: string }) {
|
|||
>
|
||||
•
|
||||
</text>
|
||||
<text fg={themeV2.text.default} wrapMode="word">
|
||||
<text fg={theme.text.default} wrapMode="word">
|
||||
{item.name}{" "}
|
||||
<span style={{ fg: themeV2.text.subdued }}>
|
||||
<span style={{ fg: theme.text.subdued }}>
|
||||
<Switch fallback={item.status.status}>
|
||||
<Match when={item.status.status === "connected"}>Connected</Match>
|
||||
<Match when={item.status.status === "failed"}>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Panel border="both" width={props.width}>
|
||||
<Panel border="both" width={props.width} context={props.context}>
|
||||
<scrollbox
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
verticalScrollbarOptions={{ visible: false }}
|
||||
|
|
@ -52,7 +53,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
<text />
|
||||
</Match>
|
||||
<Match when={props.files.length === 0}>
|
||||
<text fg={themeV2.text.default}>No files</text>
|
||||
<text fg={theme.text.default}>No files</text>
|
||||
</Match>
|
||||
<Match when={props.files.length > 0}>
|
||||
<For each={rows()}>
|
||||
|
|
@ -71,11 +72,11 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
<box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
backgroundColor={highlighted() ? themeV2.background.action.primary.focused : undefined}
|
||||
backgroundColor={highlighted() ? theme.background.action.primary.focused : undefined}
|
||||
onMouseUp={() => props.onRowClick?.(row)}
|
||||
>
|
||||
<text
|
||||
fg={highlighted() ? themeV2.text.action.primary.focused : fadedColor()}
|
||||
fg={highlighted() ? theme.text.action.primary.focused : fadedColor()}
|
||||
wrapMode="none"
|
||||
flexShrink={0}
|
||||
>
|
||||
|
|
@ -85,12 +86,12 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
<text
|
||||
fg={
|
||||
highlighted()
|
||||
? themeV2.text.action.primary.focused
|
||||
? theme.text.action.primary.focused
|
||||
: selected()
|
||||
? themeV2.text.formfield.selected
|
||||
? theme.text.formfield.selected
|
||||
: reviewed() || row.kind === "directory"
|
||||
? themeV2.text.subdued
|
||||
: themeV2.text.default
|
||||
? theme.text.subdued
|
||||
: theme.text.default
|
||||
}
|
||||
wrapMode="none"
|
||||
>
|
||||
|
|
@ -98,7 +99,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
</text>
|
||||
</box>
|
||||
<text
|
||||
fg={highlighted() ? themeV2.text.action.primary.focused : themeV2.text.subdued}
|
||||
fg={highlighted() ? theme.text.action.primary.focused : theme.text.subdued}
|
||||
wrapMode="none"
|
||||
flexShrink={0}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<PanelGroupContext.Provider value={{ axis: local.axis }}>
|
||||
<PanelGroupContext.Provider value={{ axis: local.axis, context: local.context }}>
|
||||
<box minWidth={0} minHeight={0} padding={0} flexDirection={local.axis === "x" ? "row" : "column"} {...boxProps}>
|
||||
{local.children}
|
||||
</box>
|
||||
|
|
@ -28,24 +28,28 @@ export function PanelGroup(props: JSX.IntrinsicElements["box"] & { axis: Axis })
|
|||
)
|
||||
}
|
||||
|
||||
export function Panel(props: Omit<JSX.IntrinsicElements["box"], "border"> & { border?: PanelBorder }) {
|
||||
export function Panel(
|
||||
props: Omit<JSX.IntrinsicElements["box"], "border"> & { 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 (
|
||||
<box
|
||||
minWidth={0}
|
||||
minHeight={0}
|
||||
flexDirection={crossAxis(group?.axis || "y") === "x" ? "row" : "column"}
|
||||
flexDirection={crossAxis(group?.axis ?? "y") === "x" ? "row" : "column"}
|
||||
{...borderProps}
|
||||
{...boxProps}
|
||||
/>
|
||||
|
|
@ -59,9 +63,10 @@ function panelBorderSides(axis: Axis, border: Exclude<PanelBorder, "none">): 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 (
|
||||
<Show
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
type ScrollBoxRenderable,
|
||||
} from "@opentui/core"
|
||||
import { LANGUAGE_EXTENSIONS } from "../../util/filetype"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import path from "path"
|
||||
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
|
|
@ -83,8 +82,7 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
const dimensions = useTerminalDimensions()
|
||||
const config = useConfig()
|
||||
const dialog = props.context.ui.dialog
|
||||
const themeState = useTheme()
|
||||
const themeV2 = themeState.themeV2
|
||||
const theme = props.context.theme
|
||||
const params = () => {
|
||||
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 (
|
||||
<box position="absolute" zIndex={2500} left={0} top={0} width={dimensions().width} height={dimensions().height}>
|
||||
<PanelGroup axis="y" width="100%" height="100%">
|
||||
<PanelGroup axis="y" context={props.context} width="100%" height="100%">
|
||||
<Panel border="none" flexShrink={0} padding={0} paddingLeft={1}>
|
||||
<text fg={themeV2.text.default}>Diff </text>
|
||||
<text fg={themeV2.text.subdued}>{diffSourceLabel(mode())}</text>
|
||||
<text fg={theme.text.default}>Diff </text>
|
||||
<text fg={theme.text.subdued}>{diffSourceLabel(mode())}</text>
|
||||
<box flexGrow={1} />
|
||||
<Show when={!diff.loading && !diff.error}>
|
||||
<text fg={themeV2.text.subdued}>
|
||||
<text fg={theme.text.subdued}>
|
||||
{files().length} {files().length === 1 ? "file" : "files"}
|
||||
</text>
|
||||
</Show>
|
||||
|
|
@ -755,13 +753,13 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
<Match when={diff.loading}>
|
||||
<Separator axis="x" />
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<text fg={themeV2.text.subdued}>Loading diff…</text>
|
||||
<text fg={theme.text.subdued}>Loading diff…</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={!diff.loading && diff.error}>
|
||||
<Separator axis="x" />
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<text fg={themeV2.text.feedback.error.default}>
|
||||
<text fg={theme.text.feedback.error.default}>
|
||||
Could not load diff. Reopen the diff viewer to try again.
|
||||
</text>
|
||||
</box>
|
||||
|
|
@ -769,13 +767,14 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
<Match when={!diff.loading && files().length === 0}>
|
||||
<Separator axis="x" />
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<text fg={themeV2.text.subdued}>No changes to show</text>
|
||||
<text fg={theme.text.subdued}>No changes to show</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={!diff.loading}>
|
||||
<PanelGroup axis="x">
|
||||
<PanelGroup axis="x" context={props.context}>
|
||||
<Show when={showFileTree()}>
|
||||
<DiffViewerFileTree
|
||||
context={props.context}
|
||||
files={files()}
|
||||
loading={diff.loading}
|
||||
error={diff.error}
|
||||
|
|
@ -812,56 +811,52 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
border={patchLeftBorder()}
|
||||
borderColor={themeV2.border.default}
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<text fg={reviewed() ? themeV2.text.subdued : themeV2.text.default}>
|
||||
{entry.file.file}
|
||||
</text>
|
||||
<text fg={reviewed() ? theme.text.subdued : theme.text.default}>{entry.file.file}</text>
|
||||
<box flexGrow={1} />
|
||||
<text fg={reviewed() ? themeV2.text.subdued : themeV2.diff.text.added}>
|
||||
<text fg={reviewed() ? theme.text.subdued : theme.diff.text.added}>
|
||||
+{entry.file.additions}
|
||||
</text>
|
||||
<text fg={reviewed() ? themeV2.text.subdued : themeV2.diff.text.removed}>
|
||||
<text fg={reviewed() ? theme.text.subdued : theme.diff.text.removed}>
|
||||
-{entry.file.deletions}
|
||||
</text>
|
||||
</box>
|
||||
<Separator axis="x" start={showFileTree() ? "edge" : undefined} />
|
||||
<Show
|
||||
when={entry.file.patch}
|
||||
fallback={<text fg={themeV2.text.subdued}>No patch available for this file.</text>}
|
||||
fallback={<text fg={theme.text.subdued}>No patch available for this file.</text>}
|
||||
>
|
||||
{(patch) => (
|
||||
<box border={patchLeftBorder()} borderColor={themeV2.border.default}>
|
||||
<box border={patchLeftBorder()} borderColor={theme.border.default}>
|
||||
<diff
|
||||
ref={(element: DiffRenderable) => 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
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
|
|
@ -872,11 +867,7 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
}}
|
||||
</For>
|
||||
<Show when={patchFillerHeight() > 0}>
|
||||
<box
|
||||
height={patchFillerHeight()}
|
||||
border={patchLeftBorder()}
|
||||
borderColor={themeV2.border.default}
|
||||
/>
|
||||
<box height={patchFillerHeight()} border={patchLeftBorder()} borderColor={theme.border.default} />
|
||||
</Show>
|
||||
</scrollbox>
|
||||
<Separator axis="x" start={showFileTree() ? "edge-in" : undefined} />
|
||||
|
|
@ -889,57 +880,57 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
<Panel flexShrink={0} gap={2} paddingLeft={1} border="none">
|
||||
<Show when={switchFocusShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>focus file tree</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>focus file tree</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={nextFileShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>next file</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>next file</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={nextHunkShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>next hunk</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>next hunk</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={previousHunkShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>previous hunk</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>previous hunk</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={previousFileShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>previous file</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>previous file</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={switchSourceShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>switch source</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>switch source</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={markReviewedShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>mark reviewed</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>mark reviewed</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={helpShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>all</span>
|
||||
<text fg={theme.text.default}>
|
||||
{shortcut()} <span style={{ fg: theme.text.subdued }}>all</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
|
|
@ -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 (
|
||||
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
|
||||
Diff shortcuts
|
||||
</text>
|
||||
<text fg={themeV2.text.subdued}>esc</text>
|
||||
<text fg={theme.text.subdued}>esc</text>
|
||||
</box>
|
||||
<box flexDirection="row">
|
||||
<text fg={themeV2.text.subdued} width={5} wrapMode="none">
|
||||
<text fg={theme.text.subdued} width={5} wrapMode="none">
|
||||
Key
|
||||
</text>
|
||||
<text fg={themeV2.text.subdued} width={22} wrapMode="none">
|
||||
<text fg={theme.text.subdued} width={22} wrapMode="none">
|
||||
Action
|
||||
</text>
|
||||
<text fg={themeV2.text.subdued}>Description</text>
|
||||
<text fg={theme.text.subdued}>Description</text>
|
||||
</box>
|
||||
<For each={rows}>
|
||||
{(row) => (
|
||||
<box flexDirection="row">
|
||||
<text fg={themeV2.text.default} width={5} wrapMode="none">
|
||||
<text fg={theme.text.default} width={5} wrapMode="none">
|
||||
{row.shortcut() || "-"}
|
||||
</text>
|
||||
<text fg={themeV2.text.default} width={22} wrapMode="none">
|
||||
<text fg={theme.text.default} width={22} wrapMode="none">
|
||||
{row.action}
|
||||
</text>
|
||||
<text fg={themeV2.text.subdued}>{row.description}</text>
|
||||
<text fg={theme.text.subdued}>{row.description}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<box width={dimensions().width} height={dimensions().height} backgroundColor={themeV2.background.default}>
|
||||
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background.default}>
|
||||
<box flexGrow={1} />
|
||||
<box
|
||||
height={1}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { PluginContextProvider, type Plugin } from "@opencode-ai/plugin/tui"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
|
|
@ -80,6 +80,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
|
|||
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(
|
||||
() => <PluginContextProvider value={context}>{render()}</PluginContextProvider>,
|
||||
() => {
|
||||
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) => <PluginContextProvider value={context}>{page.render(input)}</PluginContextProvider>,
|
||||
})
|
||||
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]) => (
|
||||
<PluginContextProvider value={context}>{render(input)}</PluginContextProvider>
|
||||
))
|
||||
let registered = true
|
||||
const unregister = () => {
|
||||
if (!registered) return
|
||||
|
|
@ -518,6 +528,23 @@ function isPlugin(value: unknown): value is Plugin.Definition {
|
|||
)
|
||||
}
|
||||
|
||||
type PluginTheme = ReturnType<typeof useTheme>["themeV2"] & {
|
||||
contextual(context: "elevated" | "overlay"): PluginTheme
|
||||
syntaxStyle(): ReturnType<ReturnType<typeof useTheme>["syntax"]>
|
||||
}
|
||||
|
||||
export function createPluginTheme(theme: ReturnType<typeof useTheme>): 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")
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue