From 44cd984589d685330d3f3cbad3098fd1a1358012 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 28 Jul 2026 14:19:12 -0400 Subject: [PATCH] feat(tui): refine plugin context slots --- packages/plugin/src/tui/context.ts | 23 +++++++++++++-- packages/tui/src/app.tsx | 5 +--- .../tui/src/feature-plugins/home/footer.tsx | 22 +++++++------- .../src/feature-plugins/sidebar/footer.tsx | 11 +++---- .../feature-plugins/system/diff-viewer.tsx | 4 +-- .../tui/src/feature-plugins/system/scrap.tsx | 9 ++---- packages/tui/src/plugin/context.tsx | 29 ++++++++++++++----- packages/tui/src/routes/home.tsx | 3 +- packages/tui/src/routes/session/index.tsx | 3 -- packages/tui/src/routes/session/sidebar.tsx | 4 +-- 10 files changed, 65 insertions(+), 48 deletions(-) diff --git a/packages/plugin/src/tui/context.ts b/packages/plugin/src/tui/context.ts index 511a6d0c5c..0cde1200ec 100644 --- a/packages/plugin/src/tui/context.ts +++ b/packages/plugin/src/tui/context.ts @@ -113,7 +113,22 @@ export interface Page { readonly render: (input: { readonly data?: Record }) => JSX.Element } -export type Slot = (props: Record) => JSX.Element +export interface SlotMap { + readonly app: Readonly> + readonly "home.footer": Readonly> + readonly "sidebar.content": { + readonly sessionID: string + } + readonly "sidebar.footer": Readonly> +} + +export type SlotName = keyof SlotMap +export type Slot = (props: SlotMap[Name]) => JSX.Element + +export interface App { + readonly version: string + readonly channel: string +} export type ToastVariant = "info" | "success" | "warning" | "error" @@ -308,17 +323,21 @@ export interface Keymap { export interface UI { readonly dialog: Dialog readonly toast: Toast + readonly format: { + path(value: string): string + } readonly router: { register(page: Page): () => void navigate(destination: Destination): void current(): Route } - readonly slot: (name: string, render: Slot) => () => void + readonly slot: (name: Name, render: Slot) => () => void } export interface Context { readonly options: Readonly> readonly location: LocationRef | undefined + readonly app: App readonly renderer: CliRenderer readonly client: OpenCodeClient readonly data: Data diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 77e58224ba..73a44854d1 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -1127,10 +1127,7 @@ function App(props: { pair?: DialogPairCredentials }) { - - - - + diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index cbd360fcd1..6042e6e087 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -1,20 +1,17 @@ import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Match, Show, Switch } from "solid-js" import { useTerminalDimensions } from "@opentui/solid" -import { useTuiApp, useTuiPaths } from "../../context/runtime" -import { abbreviateHome } from "../../runtime" import { FilePath } from "../../ui/file-path" import { stringWidth } from "../../util/string-width" function Directory(props: { context: Plugin.Context; maxWidth: number }) { - const paths = useTuiPaths() const directory = createMemo(() => - props.context.location ? abbreviateHome(props.context.location.directory, paths.home) : undefined, + props.context.location ? props.context.ui.format.path(props.context.location.directory) : undefined, ) return ( - {(value) => } + {(value) => } ) } @@ -27,16 +24,18 @@ function Mcp(props: { context: Plugin.Context }) { return ( - + - + 0 ? props.context.theme.text.feedback.success.default : props.context.theme.text.subdued, + count() > 0 + ? props.context.theme.themeV2.text.feedback.success.default + : props.context.theme.themeV2.text.subdued, }} > ⊙{" "} @@ -45,14 +44,13 @@ function Mcp(props: { context: Plugin.Context }) { {count()} MCP - /status + /status ) } function View(props: { context: Plugin.Context }) { - const app = useTuiApp() const dimensions = useTerminalDimensions() const mcpWidth = createMemo(() => { const list = props.context.data.location.mcp.server.list(props.context.location) ?? [] @@ -74,12 +72,12 @@ function View(props: { context: Plugin.Context }) { > - {app.version} + {props.context.app.version} ) diff --git a/packages/tui/src/feature-plugins/sidebar/footer.tsx b/packages/tui/src/feature-plugins/sidebar/footer.tsx index adde20e115..65ff8fecdc 100644 --- a/packages/tui/src/feature-plugins/sidebar/footer.tsx +++ b/packages/tui/src/feature-plugins/sidebar/footer.tsx @@ -1,18 +1,15 @@ import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" -import { useTuiPaths } from "../../context/runtime" -import { useTheme } from "../../context/theme" -import { abbreviateHome } from "../../runtime" import { FilePath } from "../../ui/file-path" function View(props: { context: Plugin.Context }) { - const { themeV2 } = useTheme() - const paths = useTuiPaths() const directory = createMemo(() => - props.context.location ? abbreviateHome(props.context.location.directory, paths.home) : undefined, + props.context.location ? props.context.ui.format.path(props.context.location.directory) : undefined, ) return ( - {(value) => } + + {(value) => } + ) } diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index d72421676c..15cc5860be 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -16,7 +16,6 @@ import path from "path" import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js" import { DiffViewerFileTree } from "./diff-viewer-file-tree" import { Panel, PanelGroup, Separator } from "./diff-viewer-ui" -import { useDialog } from "../../ui/dialog" import { DialogSelect } from "../../ui/dialog-select" import { getScrollAcceleration } from "../../util/scroll" import { useConfig } from "../../config" @@ -1051,7 +1050,6 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context }) { } function Commands(props: { context: Plugin.Context }) { - const dialog = useDialog() props.context.keymap.layer(() => ({ mode: "global", commands: [ @@ -1083,7 +1081,7 @@ function Commands(props: { context: Plugin.Context }) { returnRoute, }, }) - dialog.clear() + props.context.ui.dialog.clear() }, }, ], diff --git a/packages/tui/src/feature-plugins/system/scrap.tsx b/packages/tui/src/feature-plugins/system/scrap.tsx index 1a21a4ca51..9c2921e77d 100644 --- a/packages/tui/src/feature-plugins/system/scrap.tsx +++ b/packages/tui/src/feature-plugins/system/scrap.tsx @@ -1,12 +1,9 @@ import { Plugin } from "@opencode-ai/plugin/tui" import { useTerminalDimensions } from "@opentui/solid" -import { Keymap } from "../../context/keymap" import { useTheme } from "../../context/theme" -import { useDialog } from "../../ui/dialog" function Commands(props: { context: Plugin.Context }) { - const dialog = useDialog() - Keymap.createLayer(() => ({ + props.context.keymap.layer(() => ({ mode: "global", commands: [ { @@ -16,7 +13,7 @@ function Commands(props: { context: Plugin.Context }) { palette: true, run() { props.context.ui.router.navigate({ type: "plugin", name: "scrap" }) - dialog.clear() + props.context.ui.dialog.clear() }, }, ], @@ -29,7 +26,7 @@ function Scrap(props: { context: Plugin.Context }) { const { themeV2 } = useTheme() const { themeV2: elevatedTheme } = useTheme().contextual("elevated") - Keymap.createLayer(() => ({ + props.context.keymap.layer(() => ({ commands: [ { bind: "escape", diff --git a/packages/tui/src/plugin/context.tsx b/packages/tui/src/plugin/context.tsx index 21da40657c..4034348d50 100644 --- a/packages/tui/src/plugin/context.tsx +++ b/packages/tui/src/plugin/context.tsx @@ -13,7 +13,7 @@ import { import path from "path" import { stat } from "fs/promises" import { fileURLToPath, pathToFileURL } from "url" -import type { Context, Dialog, Page, Slot, Toast } from "@opencode-ai/plugin/tui/context" +import type { Context, Dialog, Page, Slot, SlotMap, SlotName, Toast } from "@opencode-ai/plugin/tui/context" import { createStore, produce, reconcile as reconcileStore } from "solid-js/store" import { useRenderer } from "@opentui/solid" import { useConfig } from "../config" @@ -21,7 +21,7 @@ import { useClient } from "../context/client" import { useData } from "../context/data" import { Keymap } from "../context/keymap" import { useRoute } from "../context/route" -import { useTuiLifecycle } from "../context/runtime" +import { useTuiApp, useTuiLifecycle, useTuiPaths } from "../context/runtime" import { useLocation } from "../context/location" import { useTheme } from "../context/theme" import { DialogAlert } from "../ui/dialog-alert" @@ -31,6 +31,7 @@ import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useToast } from "../ui/toast" import { useAttention } from "../context/attention" +import { abbreviateHome } from "../util/path-format" import { builtins } from "./builtins" export interface PackageResolver { @@ -47,7 +48,7 @@ type Value = { readonly ready: () => boolean readonly list: () => ReadonlyArray readonly route: (id: string, name: string) => Page["render"] | undefined - readonly slot: (name: string) => ReadonlyArray + readonly slot: (name: Name) => ReadonlyArray> readonly activate: (id: string) => Promise readonly deactivate: (id: string) => Promise } @@ -75,6 +76,8 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> const shortcuts = Keymap.useShortcuts() const keymapState = Keymap.useState() const lifecycle = useTuiLifecycle() + const app = useTuiApp() + const paths = useTuiPaths() const location = useLocation() const theme = useTheme() const dialog = useDialog() @@ -120,7 +123,6 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> dialog.setCentered(options.centered ?? false) }, clear() { - if (!activeDialog) return dialog.clear() }, alert(options) { @@ -218,11 +220,12 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> get location() { return location.current }, + app: { version: app.version, channel: app.channel }, renderer, client: client.api, data, attention, - theme: theme.themeV2, + theme, keymap: { layer: Keymap.createLayer, dispatch: keymap.dispatch, @@ -235,6 +238,9 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> ui: { dialog: dialogApi, toast: toastApi, + format: { + path: (value) => abbreviateHome(value, paths.home), + }, router: { register(page) { if (store.registrations[item.plugin.id]?.routes[page.name]) @@ -530,7 +536,16 @@ export function PluginRoute(props: { readonly fallback: (id: string, name: strin return <>{content()} } -export function PluginSlot(props: { readonly name: string; readonly input?: Record }) { +export function PluginSlot(props: { + readonly name: Name + readonly input: SlotMap[Name] + readonly mode: "all" | "replace" +}) { const plugins = usePlugin() - return {(render) => render(props.input ?? {})} + const renderers = createMemo(() => { + const items = plugins.slot(props.name) + if (props.mode === "replace") return items.slice(-1) + return items + }) + return {(render) => render(props.input)} } diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index 92640c56b3..a489c9e43b 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -85,11 +85,10 @@ export function Home() { /> - - + {(_) => { diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 917e3a4e83..f487716944 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -77,7 +77,6 @@ import { nextThinkingMode, reasoningSummary, type ThinkingMode } from "../../con import { getScrollAcceleration } from "../../util/scroll" import { collapseToolOutput } from "../../util/collapse-tool-output" import { usePluginRuntime } from "../../plugin/runtime" -import { PluginSlot } from "../../plugin/context" import { Keymap, type KeymapCommand } from "../../context/keymap" import { usePathFormatter } from "../../context/path-format" import { useLocation } from "../../context/location" @@ -915,7 +914,6 @@ export function Session() { > - (scroll = r)} @@ -960,7 +958,6 @@ export function Session() { - - + - +