tips as plugin

This commit is contained in:
Sebastian Herrlinger
2026-03-25 01:46:02 +01:00
parent b178de68f0
commit 9d4b720c4f
9 changed files with 66 additions and 82 deletions

View File

@@ -636,28 +636,12 @@ const home = (input: Cfg): TuiSlotPlugin => ({
</box>
)
},
home_tips(ctx, value) {
if (!value.show_tips) return null
home_bottom(ctx) {
const skin = look(ctx.theme.current)
const text = "extra content in the unified home bottom slot"
return (
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
<text fg={skin.muted}>
<span style={{ fg: skin.accent }}>{input.label}</span> replaces the built-in home tips slot
</text>
</box>
)
},
home_below_tips(ctx, value) {
const skin = look(ctx.theme.current)
const text = value.first_time_user
? "first-time user state"
: value.tips_hidden
? "tips are hidden"
: "extra content below tips"
return (
<box width="100%" maxWidth={75} alignItems="center" paddingTop={1} flexShrink={0}>
<box width="100%" maxWidth={75} alignItems="center" paddingTop={1} flexShrink={0} gap={1}>
<box
border
borderColor={skin.border}

View File

@@ -1,4 +1,4 @@
import { createMemo, createSignal, For } from "solid-js"
import { For } from "solid-js"
import { DEFAULT_THEMES, useTheme } from "@tui/context/theme"
const themeCount = Object.keys(DEFAULT_THEMES).length

View File

@@ -0,0 +1,46 @@
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
import { createMemo, Show } from "solid-js"
import { Tips } from "./tips-view"
function View(props: { show: boolean }) {
return (
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
<Show when={props.show}>
<Tips />
</Show>
</box>
)
}
const tui: TuiPlugin = async (api) => {
const hidden = createMemo(() => api.kv.get("tips_hidden", false))
const first = createMemo(() => api.state.session.count() === 0)
const show = createMemo(() => !first() && !hidden())
api.command.register(() => [
{
title: hidden() ? "Show tips" : "Hide tips",
value: "tips.toggle",
keybind: "tips_toggle",
category: "System",
hidden: api.route.current.name !== "home",
onSelect() {
api.kv.set("tips_hidden", !hidden())
api.ui.dialog.clear()
},
},
])
api.slots.register({
order: 100,
slots: {
home_bottom() {
return <View show={show()} />
},
},
})
}
export default {
tui,
}

View File

@@ -131,6 +131,9 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiApi["state"] {
return sync.data.provider
},
session: {
count() {
return sync.data.session.length
},
diff(sessionID) {
return sync.data.session_diff[sessionID] ?? []
},

View File

@@ -1,3 +1,4 @@
import * as HomeTips from "../feature-plugins/home/tips"
import * as SidebarTitle from "../feature-plugins/sidebar/title"
import * as SidebarContext from "../feature-plugins/sidebar/context"
import * as SidebarMcp from "../feature-plugins/sidebar/mcp"
@@ -13,6 +14,10 @@ export type InternalTuiPlugin = {
}
export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
{
name: "home-tips",
module: HomeTips,
},
{
name: "sidebar-title",
module: SidebarTitle,

View File

@@ -1,9 +1,7 @@
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 { useKeybind } from "@tui/context/keybind"
import { Logo } from "../component/logo"
import { Tips } from "../component/tips"
import { Locale } from "@/util/locale"
import { useSync } from "../context/sync"
import { Toast } from "../ui/toast"
@@ -12,8 +10,6 @@ import { useDirectory } from "../context/directory"
import { useRouteData } from "@tui/context/route"
import { usePromptRef } from "../context/prompt"
import { Installation } from "@/installation"
import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command"
import { useLocal } from "../context/local"
import { TuiPluginRuntime } from "../plugin"
@@ -22,11 +18,9 @@ let once = false
export function Home() {
const sync = useSync()
const kv = useKV()
const { theme } = useTheme()
const route = useRouteData("home")
const promptRef = usePromptRef()
const command = useCommandDialog()
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
const mcpError = createMemo(() => {
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
@@ -36,27 +30,6 @@ export function Home() {
return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length
})
const isFirstTimeUser = createMemo(() => sync.data.session.length === 0)
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
const showTips = createMemo(() => {
// Don't show tips for first-time users
if (isFirstTimeUser()) return false
return !tipsHidden()
})
command.register(() => [
{
title: tipsHidden() ? "Show tips" : "Hide tips",
value: "tips.toggle",
keybind: "tips_toggle",
category: "System",
onSelect: (dialog) => {
kv.set("tips_hidden", !tipsHidden())
dialog.clear()
},
},
])
const Hint = (
<box flexShrink={0} flexDirection="row" gap={1}>
<Show when={connectedMcpCount() > 0}>
@@ -104,8 +77,6 @@ export function Home() {
)
const directory = useDirectory()
const keybind = useKeybind()
return (
<>
<box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
@@ -127,25 +98,7 @@ export function Home() {
workspaceID={route.workspaceID}
/>
</box>
<TuiPluginRuntime.Slot
name="home_tips"
mode="replace"
show_tips={showTips()}
tips_hidden={tipsHidden()}
first_time_user={isFirstTimeUser()}
>
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
<Show when={showTips()}>
<Tips />
</Show>
</box>
</TuiPluginRuntime.Slot>
<TuiPluginRuntime.Slot
name="home_below_tips"
show_tips={showTips()}
tips_hidden={tipsHidden()}
first_time_user={isFirstTimeUser()}
/>
<TuiPluginRuntime.Slot name="home_bottom" />
<box flexGrow={1} minHeight={0} />
<Toast />
</box>

View File

@@ -81,20 +81,20 @@ test("disposes tracked event, route, and command hooks", async () => {
expect(count.event_drop).toBe(0)
expect(count.route_add).toBe(1)
expect(count.route_drop).toBe(0)
expect(count.command_add).toBe(1)
expect(count.command_add).toBe(2)
expect(count.command_drop).toBe(1)
await TuiPluginRuntime.dispose()
expect(count.event_drop).toBe(1)
expect(count.route_drop).toBe(1)
expect(count.command_drop).toBe(1)
expect(count.command_drop).toBe(2)
await TuiPluginRuntime.dispose()
expect(count.event_drop).toBe(1)
expect(count.route_drop).toBe(1)
expect(count.command_drop).toBe(1)
expect(count.command_drop).toBe(2)
const marker = await fs.readFile(tmp.extra.marker, "utf8")
expect(marker).toContain("custom")
@@ -231,7 +231,7 @@ export default {
mark("two")
},
slots: {
home_tips() {
home_bottom() {
return null
},
},

View File

@@ -244,6 +244,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
return opts.state?.provider ?? []
},
session: {
count: opts.state?.session?.count ?? (() => 0),
diff: opts.state?.session?.diff ?? (() => []),
todo: opts.state?.session?.todo ?? (() => []),
messages: opts.state?.session?.messages ?? (() => []),

View File

@@ -212,6 +212,7 @@ export type TuiState = {
readonly config: SdkConfig
readonly provider: ReadonlyArray<Provider>
session: {
count: () => number
diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem>
todo: (sessionID: string) => ReadonlyArray<TuiSidebarTodoItem>
messages: (sessionID: string) => ReadonlyArray<Message>
@@ -285,16 +286,7 @@ export type TuiSidebarFileItem = {
export type TuiSlotMap = {
app: {}
home_logo: {}
home_tips: {
show_tips: boolean
tips_hidden: boolean
first_time_user: boolean
}
home_below_tips: {
show_tips: boolean
tips_hidden: boolean
first_time_user: boolean
}
home_bottom: {}
sidebar_title: {
session_id: string
title: string