diff options
| author | Sebastian <[email protected]> | 2026-03-30 14:33:01 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-30 14:33:01 +0200 |
| commit | 14f9e21d5c3f4e853dee8ca133693dd3b915b634 (patch) | |
| tree | 1e5d8a42793d0551c6ade9bb29a37b749df6d2b9 /packages | |
| parent | 8e4bab51812fccf3b69713904159a4394b3a29ab (diff) | |
| download | opencode-14f9e21d5c3f4e853dee8ca133693dd3b915b634.tar.gz opencode-14f9e21d5c3f4e853dee8ca133693dd3b915b634.zip | |
pluggable home footer (#20057)
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx | 93 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/plugin/internal.ts | 2 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/routes/home.tsx | 61 | ||||
| -rw-r--r-- | packages/plugin/src/tui.ts | 1 |
4 files changed, 99 insertions, 58 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx new file mode 100644 index 000000000..8047c2645 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx @@ -0,0 +1,93 @@ +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" +import { createMemo, Match, Show, Switch } from "solid-js" +import { Global } from "@/global" + +const id = "internal:home-footer" + +function Directory(props: { api: TuiPluginApi }) { + const theme = () => props.api.theme.current + const dir = createMemo(() => { + const dir = props.api.state.path.directory || process.cwd() + const out = dir.replace(Global.Path.home, "~") + const branch = props.api.state.vcs?.branch + if (branch) return out + ":" + branch + return out + }) + + return <text fg={theme().textMuted}>{dir()}</text> +} + +function Mcp(props: { api: TuiPluginApi }) { + const theme = () => props.api.theme.current + const list = createMemo(() => props.api.state.mcp()) + const has = createMemo(() => list().length > 0) + const err = createMemo(() => list().some((item) => item.status === "failed")) + const count = createMemo(() => list().filter((item) => item.status === "connected").length) + + return ( + <Show when={has()}> + <box gap={1} flexDirection="row" flexShrink={0}> + <text fg={theme().text}> + <Switch> + <Match when={err()}> + <span style={{ fg: theme().error }}>⊙ </span> + </Match> + <Match when={true}> + <span style={{ fg: count() > 0 ? theme().success : theme().textMuted }}>⊙ </span> + </Match> + </Switch> + {count()} MCP + </text> + <text fg={theme().textMuted}>/status</text> + </box> + </Show> + ) +} + +function Version(props: { api: TuiPluginApi }) { + const theme = () => props.api.theme.current + + return ( + <box flexShrink={0}> + <text fg={theme().textMuted}>{props.api.app.version}</text> + </box> + ) +} + +function View(props: { api: TuiPluginApi }) { + return ( + <box + width="100%" + paddingTop={1} + paddingBottom={1} + paddingLeft={2} + paddingRight={2} + flexDirection="row" + flexShrink={0} + gap={2} + > + <Directory api={props.api} /> + <Mcp api={props.api} /> + <box flexGrow={1} /> + <Version api={props.api} /> + </box> + ) +} + +const tui: TuiPlugin = async (api) => { + api.slots.register({ + order: 100, + slots: { + home_footer() { + return <View api={api} /> + }, + }, + }) +} + +const plugin: TuiPluginModule & { id: string } = { + id, + tui, +} + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts index 9e28bbd2e..856ee0ebb 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts @@ -1,3 +1,4 @@ +import HomeFooter from "../feature-plugins/home/footer" import HomeTips from "../feature-plugins/home/tips" import SidebarContext from "../feature-plugins/sidebar/context" import SidebarMcp from "../feature-plugins/sidebar/mcp" @@ -14,6 +15,7 @@ export type InternalTuiPlugin = TuiPluginModule & { } export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [ + HomeFooter, HomeTips, SidebarContext, SidebarMcp, diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index b63bf2d2d..8826df314 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,15 +1,11 @@ import { Prompt, type PromptRef } from "@tui/component/prompt" -import { createEffect, createMemo, Match, on, onMount, Show, Switch } from "solid-js" -import { useTheme } from "@tui/context/theme" +import { createEffect, on, onMount } from "solid-js" import { Logo } from "../component/logo" -import { Locale } from "@/util/locale" import { useSync } from "../context/sync" import { Toast } from "../ui/toast" import { useArgs } from "../context/args" -import { useDirectory } from "../context/directory" import { useRouteData } from "@tui/context/route" import { usePromptRef } from "../context/prompt" -import { Installation } from "@/installation" import { useLocal } from "../context/local" import { TuiPluginRuntime } from "../plugin" @@ -22,37 +18,8 @@ const placeholder = { export function Home() { const sync = useSync() - const { theme } = useTheme() const route = useRouteData("home") const promptRef = usePromptRef() - const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0) - const mcpError = createMemo(() => { - return Object.values(sync.data.mcp).some((x) => x.status === "failed") - }) - - const connectedMcpCount = createMemo(() => { - return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length - }) - - const Hint = ( - <box flexShrink={0} flexDirection="row" gap={1}> - <Show when={connectedMcpCount() > 0}> - <text fg={theme.text}> - <Switch> - <Match when={mcpError()}> - <span style={{ fg: theme.error }}>•</span> mcp errors{" "} - <span style={{ fg: theme.textMuted }}>ctrl+x s</span> - </Match> - <Match when={true}> - <span style={{ fg: theme.success }}>•</span>{" "} - {Locale.pluralize(connectedMcpCount(), "{} mcp server", "{} mcp servers")} - </Match> - </Switch> - </text> - </Show> - </box> - ) - let prompt: PromptRef | undefined const args = useArgs() const local = useLocal() @@ -81,7 +48,6 @@ export function Home() { }, ), ) - const directory = useDirectory() return ( <> @@ -101,7 +67,6 @@ export function Home() { prompt = r promptRef.set(r) }} - hint={Hint} workspaceID={route.workspaceID} placeholders={placeholder} /> @@ -111,28 +76,8 @@ export function Home() { <box flexGrow={1} minHeight={0} /> <Toast /> </box> - <box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}> - <text fg={theme.textMuted}>{directory()}</text> - <box gap={1} flexDirection="row" flexShrink={0}> - <Show when={mcp()}> - <text fg={theme.text}> - <Switch> - <Match when={mcpError()}> - <span style={{ fg: theme.error }}>⊙ </span> - </Match> - <Match when={true}> - <span style={{ fg: connectedMcpCount() > 0 ? theme.success : theme.textMuted }}>⊙ </span> - </Match> - </Switch> - {connectedMcpCount()} MCP - </text> - <text fg={theme.textMuted}>/status</text> - </Show> - </box> - <box flexGrow={1} /> - <box flexShrink={0}> - <text fg={theme.textMuted}>{Installation.VERSION}</text> - </box> + <box width="100%" flexShrink={0}> + <TuiPluginRuntime.Slot name="home_footer" mode="single_winner" /> </box> </> ) diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index bbf349490..b082f6abe 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -296,6 +296,7 @@ export type TuiSlotMap = { workspace_id?: string } home_bottom: {} + home_footer: {} sidebar_title: { session_id: string title: string |
