Skip to content

Commit 4d9ebca

Browse files
authored
pluggable home footer (anomalyco#20057)
1 parent a908309 commit 4d9ebca

4 files changed

Lines changed: 99 additions & 58 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
2+
import { createMemo, Match, Show, Switch } from "solid-js"
3+
import { Global } from "@/global"
4+
5+
const id = "internal:home-footer"
6+
7+
function Directory(props: { api: TuiPluginApi }) {
8+
const theme = () => props.api.theme.current
9+
const dir = createMemo(() => {
10+
const dir = props.api.state.path.directory || process.cwd()
11+
const out = dir.replace(Global.Path.home, "~")
12+
const branch = props.api.state.vcs?.branch
13+
if (branch) return out + ":" + branch
14+
return out
15+
})
16+
17+
return <text fg={theme().textMuted}>{dir()}</text>
18+
}
19+
20+
function Mcp(props: { api: TuiPluginApi }) {
21+
const theme = () => props.api.theme.current
22+
const list = createMemo(() => props.api.state.mcp())
23+
const has = createMemo(() => list().length > 0)
24+
const err = createMemo(() => list().some((item) => item.status === "failed"))
25+
const count = createMemo(() => list().filter((item) => item.status === "connected").length)
26+
27+
return (
28+
<Show when={has()}>
29+
<box gap={1} flexDirection="row" flexShrink={0}>
30+
<text fg={theme().text}>
31+
<Switch>
32+
<Match when={err()}>
33+
<span style={{ fg: theme().error }}></span>
34+
</Match>
35+
<Match when={true}>
36+
<span style={{ fg: count() > 0 ? theme().success : theme().textMuted }}></span>
37+
</Match>
38+
</Switch>
39+
{count()} MCP
40+
</text>
41+
<text fg={theme().textMuted}>/status</text>
42+
</box>
43+
</Show>
44+
)
45+
}
46+
47+
function Version(props: { api: TuiPluginApi }) {
48+
const theme = () => props.api.theme.current
49+
50+
return (
51+
<box flexShrink={0}>
52+
<text fg={theme().textMuted}>{props.api.app.version}</text>
53+
</box>
54+
)
55+
}
56+
57+
function View(props: { api: TuiPluginApi }) {
58+
return (
59+
<box
60+
width="100%"
61+
paddingTop={1}
62+
paddingBottom={1}
63+
paddingLeft={2}
64+
paddingRight={2}
65+
flexDirection="row"
66+
flexShrink={0}
67+
gap={2}
68+
>
69+
<Directory api={props.api} />
70+
<Mcp api={props.api} />
71+
<box flexGrow={1} />
72+
<Version api={props.api} />
73+
</box>
74+
)
75+
}
76+
77+
const tui: TuiPlugin = async (api) => {
78+
api.slots.register({
79+
order: 100,
80+
slots: {
81+
home_footer() {
82+
return <View api={api} />
83+
},
84+
},
85+
})
86+
}
87+
88+
const plugin: TuiPluginModule & { id: string } = {
89+
id,
90+
tui,
91+
}
92+
93+
export default plugin

packages/opencode/src/cli/cmd/tui/plugin/internal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import HomeFooter from "../feature-plugins/home/footer"
12
import HomeTips from "../feature-plugins/home/tips"
23
import SidebarContext from "../feature-plugins/sidebar/context"
34
import SidebarMcp from "../feature-plugins/sidebar/mcp"
@@ -14,6 +15,7 @@ export type InternalTuiPlugin = TuiPluginModule & {
1415
}
1516

1617
export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
18+
HomeFooter,
1719
HomeTips,
1820
SidebarContext,
1921
SidebarMcp,

packages/opencode/src/cli/cmd/tui/routes/home.tsx

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { Prompt, type PromptRef } from "@tui/component/prompt"
2-
import { createEffect, createMemo, Match, on, onMount, Show, Switch } from "solid-js"
3-
import { useTheme } from "@tui/context/theme"
2+
import { createEffect, on, onMount } from "solid-js"
43
import { Logo } from "../component/logo"
5-
import { Locale } from "@/util/locale"
64
import { useSync } from "../context/sync"
75
import { Toast } from "../ui/toast"
86
import { useArgs } from "../context/args"
9-
import { useDirectory } from "../context/directory"
107
import { useRouteData } from "@tui/context/route"
118
import { usePromptRef } from "../context/prompt"
12-
import { Installation } from "@/installation"
139
import { useLocal } from "../context/local"
1410
import { TuiPluginRuntime } from "../plugin"
1511

@@ -22,37 +18,8 @@ const placeholder = {
2218

2319
export function Home() {
2420
const sync = useSync()
25-
const { theme } = useTheme()
2621
const route = useRouteData("home")
2722
const promptRef = usePromptRef()
28-
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
29-
const mcpError = createMemo(() => {
30-
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
31-
})
32-
33-
const connectedMcpCount = createMemo(() => {
34-
return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length
35-
})
36-
37-
const Hint = (
38-
<box flexShrink={0} flexDirection="row" gap={1}>
39-
<Show when={connectedMcpCount() > 0}>
40-
<text fg={theme.text}>
41-
<Switch>
42-
<Match when={mcpError()}>
43-
<span style={{ fg: theme.error }}></span> mcp errors{" "}
44-
<span style={{ fg: theme.textMuted }}>ctrl+x s</span>
45-
</Match>
46-
<Match when={true}>
47-
<span style={{ fg: theme.success }}></span>{" "}
48-
{Locale.pluralize(connectedMcpCount(), "{} mcp server", "{} mcp servers")}
49-
</Match>
50-
</Switch>
51-
</text>
52-
</Show>
53-
</box>
54-
)
55-
5623
let prompt: PromptRef | undefined
5724
const args = useArgs()
5825
const local = useLocal()
@@ -81,7 +48,6 @@ export function Home() {
8148
},
8249
),
8350
)
84-
const directory = useDirectory()
8551

8652
return (
8753
<>
@@ -101,7 +67,6 @@ export function Home() {
10167
prompt = r
10268
promptRef.set(r)
10369
}}
104-
hint={Hint}
10570
workspaceID={route.workspaceID}
10671
placeholders={placeholder}
10772
/>
@@ -111,28 +76,8 @@ export function Home() {
11176
<box flexGrow={1} minHeight={0} />
11277
<Toast />
11378
</box>
114-
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
115-
<text fg={theme.textMuted}>{directory()}</text>
116-
<box gap={1} flexDirection="row" flexShrink={0}>
117-
<Show when={mcp()}>
118-
<text fg={theme.text}>
119-
<Switch>
120-
<Match when={mcpError()}>
121-
<span style={{ fg: theme.error }}></span>
122-
</Match>
123-
<Match when={true}>
124-
<span style={{ fg: connectedMcpCount() > 0 ? theme.success : theme.textMuted }}></span>
125-
</Match>
126-
</Switch>
127-
{connectedMcpCount()} MCP
128-
</text>
129-
<text fg={theme.textMuted}>/status</text>
130-
</Show>
131-
</box>
132-
<box flexGrow={1} />
133-
<box flexShrink={0}>
134-
<text fg={theme.textMuted}>{Installation.VERSION}</text>
135-
</box>
79+
<box width="100%" flexShrink={0}>
80+
<TuiPluginRuntime.Slot name="home_footer" mode="single_winner" />
13681
</box>
13782
</>
13883
)

packages/plugin/src/tui.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ export type TuiSlotMap = {
296296
workspace_id?: string
297297
}
298298
home_bottom: {}
299+
home_footer: {}
299300
sidebar_title: {
300301
session_id: string
301302
title: string

0 commit comments

Comments
 (0)