feat(tui): add v2 plugin runtime
This commit is contained in:
parent
5c5579e90c
commit
4a93972a78
63 changed files with 1722 additions and 1701 deletions
|
|
@ -1,16 +1,9 @@
|
|||
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||
import type { PluginRuntime } from "../plugin/runtime"
|
||||
import HomeFooter from "./home/footer"
|
||||
import HomeTips from "./home/tips"
|
||||
import SidebarContext from "./sidebar/context"
|
||||
import SidebarFooter from "./sidebar/footer"
|
||||
import SidebarLsp from "./sidebar/lsp"
|
||||
import SidebarMcp from "./sidebar/mcp"
|
||||
import DiffViewer from "./system/diff-viewer"
|
||||
import Notifications from "./system/notifications"
|
||||
import PluginManager from "./system/plugins"
|
||||
import WhichKey from "./system/which-key"
|
||||
import Scrap from "./system/scrap"
|
||||
|
||||
export type BuiltinTuiPlugin = Omit<TuiPluginModule, "id"> & {
|
||||
id: string
|
||||
|
|
@ -19,25 +12,10 @@ export type BuiltinTuiPlugin = Omit<TuiPluginModule, "id"> & {
|
|||
}
|
||||
|
||||
export function createBuiltinPlugins(): BuiltinTuiPlugin[] {
|
||||
return [
|
||||
HomeFooter,
|
||||
HomeTips,
|
||||
SidebarContext,
|
||||
SidebarMcp,
|
||||
SidebarLsp,
|
||||
SidebarFooter,
|
||||
Notifications,
|
||||
PluginManager,
|
||||
WhichKey,
|
||||
Scrap,
|
||||
DiffViewer,
|
||||
]
|
||||
return [Notifications, PluginManager, WhichKey, DiffViewer]
|
||||
}
|
||||
|
||||
export async function loadBuiltinPlugins(
|
||||
api: TuiPluginApi,
|
||||
runtime: PluginRuntime,
|
||||
) {
|
||||
export async function loadBuiltinPlugins(api: TuiPluginApi, runtime: PluginRuntime) {
|
||||
const slots = runtime.setupSlots(api)
|
||||
const dispose: Array<() => void> = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1,98 +1,66 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||
import { abbreviateHome } from "../../runtime"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
||||
import { FilePath } from "../../ui/file-path"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
||||
import { abbreviateHome } from "../../runtime"
|
||||
import { FilePath } from "../../ui/file-path"
|
||||
|
||||
const id = "internal:home-footer"
|
||||
|
||||
function Directory(props: { api: TuiPluginApi; maxWidth: number }) {
|
||||
const theme = () => props.api.theme.current
|
||||
function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
||||
const { theme } = useTheme()
|
||||
const destination = useHomeSessionDestination()
|
||||
const paths = useTuiPaths()
|
||||
const dir = createMemo(() => {
|
||||
const directory = createMemo(() => {
|
||||
const selected = destination?.destination()
|
||||
if (!selected || selected.type === "new") return
|
||||
const branch =
|
||||
selected.directory === (props.api.state.path.directory || paths.cwd) ? props.api.state.vcs?.branch : undefined
|
||||
return { path: abbreviateHome(selected.directory, paths.home), branch }
|
||||
return abbreviateHome(selected.directory || props.context.data.location.default().directory, paths.home)
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={dir()}>
|
||||
{(value) => {
|
||||
const suffix = () => (value().branch ? `:${value().branch}` : "")
|
||||
const suffixWidth = () => Math.min(Bun.stringWidth(suffix()), Math.max(0, props.maxWidth - 2))
|
||||
return (
|
||||
<box flexDirection="row" minWidth={0}>
|
||||
<FilePath
|
||||
value={value().path}
|
||||
maxWidth={Math.max(2, props.maxWidth - suffixWidth())}
|
||||
fg={theme().textMuted}
|
||||
/>
|
||||
<Show when={suffix()}>
|
||||
<text width={suffixWidth()} wrapMode="none" truncate fg={theme().textMuted}>
|
||||
{suffix()}
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
<Show when={directory()}>
|
||||
{(value) => <FilePath value={value()} maxWidth={props.maxWidth} fg={theme.textMuted} />}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
function Mcp(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const list = createMemo(() => props.context.data.location.mcp.server.list() ?? [])
|
||||
const failed = createMemo(() => list().some((item) => item.status.status === "failed"))
|
||||
const count = createMemo(() => list().filter((item) => item.status.status === "connected").length)
|
||||
|
||||
return (
|
||||
<Show when={has()}>
|
||||
<Show when={list().length}>
|
||||
<box gap={1} flexDirection="row" flexShrink={0}>
|
||||
<text fg={theme().text}>
|
||||
<text fg={theme.text}>
|
||||
<Switch>
|
||||
<Match when={err()}>
|
||||
<span style={{ fg: theme().error }}>⊙ </span>
|
||||
<Match when={failed()}>
|
||||
<span style={{ fg: theme.error }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: count() > 0 ? theme().success : theme().textMuted }}>⊙ </span>
|
||||
<span style={{ fg: count() > 0 ? theme.success : theme.textMuted }}>⊙ </span>
|
||||
</Match>
|
||||
</Switch>
|
||||
{count()} MCP
|
||||
</text>
|
||||
<text fg={theme().textMuted}>/status</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 }) {
|
||||
function View(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const mcpWidth = createMemo(() => {
|
||||
const list = props.api.state.mcp()
|
||||
const list = props.context.data.location.mcp.server.list() ?? []
|
||||
if (list.length === 0) return 0
|
||||
const count = list.filter((item) => item.status === "connected").length
|
||||
const count = list.filter((item) => item.status.status === "connected").length
|
||||
return Bun.stringWidth(`⊙ ${count} MCP /status`) + 2
|
||||
})
|
||||
const directoryWidth = createMemo(() =>
|
||||
Math.max(2, dimensions().width - 8 - Bun.stringWidth(props.api.app.version) - mcpWidth()),
|
||||
)
|
||||
|
||||
return (
|
||||
<box
|
||||
width="100%"
|
||||
|
|
@ -104,28 +72,22 @@ function View(props: { api: TuiPluginApi }) {
|
|||
flexShrink={0}
|
||||
gap={2}
|
||||
>
|
||||
<Directory api={props.api} maxWidth={directoryWidth()} />
|
||||
<Mcp api={props.api} />
|
||||
<Directory
|
||||
context={props.context}
|
||||
maxWidth={Math.max(2, dimensions().width - 8 - Bun.stringWidth(InstallationVersion) - mcpWidth())}
|
||||
/>
|
||||
<Mcp context={props.context} />
|
||||
<box flexGrow={1} />
|
||||
<Version api={props.api} />
|
||||
<box flexShrink={0}>
|
||||
<text fg={theme.textMuted}>{InstallationVersion}</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.slots.register({
|
||||
order: 100,
|
||||
slots: {
|
||||
home_footer() {
|
||||
return <View api={api} />
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "opencode.home-footer",
|
||||
setup(context) {
|
||||
context.ui.slot("home.footer", () => <View context={context} />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import { createMemo, For, type Accessor } from "solid-js"
|
||||
import { DEFAULT_THEMES, useTheme } from "../../context/theme"
|
||||
import { useCommandShortcut } from "../../keymap"
|
||||
import { Keymap } from "../../context/keymap"
|
||||
|
||||
const themeCount = Object.keys(DEFAULT_THEMES).length
|
||||
|
||||
type TipPart = { text: string; highlight: boolean }
|
||||
type TipShortcut = Accessor<string>
|
||||
type TipShortcut = Accessor<string | undefined>
|
||||
type Shortcuts = {
|
||||
agentCycle: TipShortcut
|
||||
childFirst: TipShortcut
|
||||
|
|
@ -74,61 +73,54 @@ function shortcutText(value: string) {
|
|||
return `{highlight}${value}{/highlight}`
|
||||
}
|
||||
|
||||
function commandText(command: string, shortcut: string) {
|
||||
function commandText(command: string, shortcut: string | undefined) {
|
||||
if (!shortcut) return shortcutText(command)
|
||||
return `${shortcutText(command)} or ${shortcutText(shortcut)}`
|
||||
}
|
||||
|
||||
function press(shortcut: string, text: string) {
|
||||
function press(shortcut: string | undefined, text: string) {
|
||||
if (!shortcut) return undefined
|
||||
return `Press ${shortcutText(shortcut)} ${text}`
|
||||
}
|
||||
|
||||
function configShortcut(api: TuiPluginApi, command: string): TipShortcut {
|
||||
return () =>
|
||||
api.tuiConfig.keybinds
|
||||
.get(command)
|
||||
.map((binding) => api.keys.formatSequence(Array.from(api.keymap.parseKeySequence(binding.key))))
|
||||
.filter(Boolean)
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
|
||||
export function Tips(props: { connected?: boolean }) {
|
||||
const theme = useTheme().theme
|
||||
const keymap = Keymap.useShortcuts()
|
||||
const tipOffset = Math.random()
|
||||
const shortcut = (id: string) => () => keymap.get(id)
|
||||
const shortcuts: Shortcuts = {
|
||||
agentCycle: useCommandShortcut("agent.cycle"),
|
||||
childFirst: configShortcut(props.api, "session.child.first"),
|
||||
childNext: configShortcut(props.api, "session.child.next"),
|
||||
childPrevious: configShortcut(props.api, "session.child.previous"),
|
||||
commandList: useCommandShortcut("command.palette.show"),
|
||||
editorOpen: useCommandShortcut("prompt.editor"),
|
||||
helpShow: useCommandShortcut("help.show"),
|
||||
inputClear: useCommandShortcut("prompt.clear"),
|
||||
inputNewline: useCommandShortcut("input.newline"),
|
||||
inputPaste: useCommandShortcut("prompt.paste"),
|
||||
inputUndo: useCommandShortcut("input.undo"),
|
||||
leader: configShortcut(props.api, "leader"),
|
||||
messagesCopy: configShortcut(props.api, "messages.copy"),
|
||||
messagesFirst: configShortcut(props.api, "session.first"),
|
||||
messagesLast: configShortcut(props.api, "session.last"),
|
||||
messagesPageDown: configShortcut(props.api, "session.page.down"),
|
||||
messagesPageUp: configShortcut(props.api, "session.page.up"),
|
||||
modelCycleRecent: useCommandShortcut("model.cycle_recent"),
|
||||
modelList: useCommandShortcut("model.list"),
|
||||
sessionExport: configShortcut(props.api, "session.export"),
|
||||
sessionInterrupt: configShortcut(props.api, "session.interrupt"),
|
||||
sessionList: useCommandShortcut("session.list"),
|
||||
sessionNew: useCommandShortcut("session.new"),
|
||||
sessionParent: configShortcut(props.api, "session.parent"),
|
||||
sessionPinToggle: configShortcut(props.api, "session.pin.toggle"),
|
||||
sessionQuickSwitch1: useCommandShortcut("session.quick_switch.1"),
|
||||
sessionQuickSwitch9: useCommandShortcut("session.quick_switch.9"),
|
||||
sessionSidebarToggle: configShortcut(props.api, "session.sidebar.toggle"),
|
||||
sessionTimeline: configShortcut(props.api, "session.timeline"),
|
||||
statusView: useCommandShortcut("opencode.status"),
|
||||
terminalSuspend: useCommandShortcut("terminal.suspend"),
|
||||
themeList: useCommandShortcut("theme.switch"),
|
||||
agentCycle: shortcut("agent.cycle"),
|
||||
childFirst: shortcut("session.child.first"),
|
||||
childNext: shortcut("session.child.next"),
|
||||
childPrevious: shortcut("session.child.previous"),
|
||||
commandList: shortcut("command.palette.show"),
|
||||
editorOpen: shortcut("prompt.editor"),
|
||||
helpShow: shortcut("help.show"),
|
||||
inputClear: shortcut("prompt.clear"),
|
||||
inputNewline: shortcut("input.newline"),
|
||||
inputPaste: shortcut("prompt.paste"),
|
||||
inputUndo: shortcut("input.undo"),
|
||||
leader: shortcut("leader"),
|
||||
messagesCopy: shortcut("messages.copy"),
|
||||
messagesFirst: shortcut("session.first"),
|
||||
messagesLast: shortcut("session.last"),
|
||||
messagesPageDown: shortcut("session.page.down"),
|
||||
messagesPageUp: shortcut("session.page.up"),
|
||||
modelCycleRecent: shortcut("model.cycle_recent"),
|
||||
modelList: shortcut("model.list"),
|
||||
sessionExport: shortcut("session.export"),
|
||||
sessionInterrupt: shortcut("session.interrupt"),
|
||||
sessionList: shortcut("session.list"),
|
||||
sessionNew: shortcut("session.new"),
|
||||
sessionParent: shortcut("session.parent"),
|
||||
sessionPinToggle: shortcut("session.pin.toggle"),
|
||||
sessionQuickSwitch1: shortcut("session.quick_switch.1"),
|
||||
sessionQuickSwitch9: shortcut("session.quick_switch.9"),
|
||||
sessionSidebarToggle: shortcut("session.sidebar.toggle"),
|
||||
sessionTimeline: shortcut("session.timeline"),
|
||||
statusView: shortcut("opencode.status"),
|
||||
terminalSuspend: shortcut("terminal.suspend"),
|
||||
themeList: shortcut("theme.switch"),
|
||||
}
|
||||
const tip = createMemo(() => {
|
||||
if (props.connected === false) return NO_MODELS_TIP
|
||||
|
|
@ -175,22 +167,30 @@ const TIPS: Tip[] = [
|
|||
(shortcuts) => `Use ${commandText("/new", shortcuts.sessionNew())} to start a fresh conversation session`,
|
||||
(shortcuts) => `Use ${commandText("/sessions", shortcuts.sessionList())} to list, pin, and continue sessions`,
|
||||
(shortcuts) => press(shortcuts.sessionPinToggle(), "in the session list to pin one at the top"),
|
||||
(shortcuts) =>
|
||||
shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9()
|
||||
? `Use ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} to switch pinned sessions`
|
||||
: undefined,
|
||||
(shortcuts) => {
|
||||
const first = shortcuts.sessionQuickSwitch1()
|
||||
const last = shortcuts.sessionQuickSwitch9()
|
||||
if (!first || !last) return undefined
|
||||
return `Use ${shortcutText(first)} through ${shortcutText(last)} to switch pinned sessions`
|
||||
},
|
||||
"Run {highlight}/compact{/highlight} to summarize long sessions near context limits",
|
||||
(shortcuts) => `Use ${commandText("/export", shortcuts.sessionExport())} to save the conversation as Markdown`,
|
||||
(shortcuts) => press(shortcuts.messagesCopy(), "to copy the assistant's last message to clipboard"),
|
||||
(shortcuts) => press(shortcuts.commandList(), "to see all available actions and commands"),
|
||||
"Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers",
|
||||
(shortcuts) => `The leader key is ${shortcutText(shortcuts.leader())}; combine with other keys for quick actions`,
|
||||
(shortcuts) => {
|
||||
const leader = shortcuts.leader()
|
||||
if (!leader) return undefined
|
||||
return `The leader key is ${shortcutText(leader)}; combine with other keys for quick actions`
|
||||
},
|
||||
(shortcuts) => press(shortcuts.modelCycleRecent(), "to quickly switch between recently used models"),
|
||||
(shortcuts) => press(shortcuts.sessionSidebarToggle(), "in a session to show or hide the sidebar panel"),
|
||||
(shortcuts) =>
|
||||
shortcuts.messagesPageUp() && shortcuts.messagesPageDown()
|
||||
? `Use ${shortcutText(shortcuts.messagesPageUp())}/${shortcutText(shortcuts.messagesPageDown())} to navigate through conversation history`
|
||||
: undefined,
|
||||
(shortcuts) => {
|
||||
const up = shortcuts.messagesPageUp()
|
||||
const down = shortcuts.messagesPageDown()
|
||||
if (!up || !down) return undefined
|
||||
return `Use ${shortcutText(up)}/${shortcutText(down)} to navigate through conversation history`
|
||||
},
|
||||
(shortcuts) => press(shortcuts.messagesFirst(), "to jump to the beginning of the conversation"),
|
||||
(shortcuts) => press(shortcuts.messagesLast(), "to jump to the most recent message"),
|
||||
(shortcuts) => press(shortcuts.inputNewline(), "to add newlines in your prompt"),
|
||||
|
|
@ -204,7 +204,7 @@ const TIPS: Tip[] = [
|
|||
shortcuts.childFirst(),
|
||||
shortcuts.childPrevious(),
|
||||
shortcuts.childNext(),
|
||||
].filter(Boolean)
|
||||
].filter((item): item is string => Boolean(item))
|
||||
if (!items.length) return undefined
|
||||
return `Use ${items.map(shortcutText).join(" / ")} for parent/child sessions`
|
||||
},
|
||||
|
|
@ -267,10 +267,12 @@ const TIPS: Tip[] = [
|
|||
(shortcuts) => `Use ${commandText("/timeline", shortcuts.sessionTimeline())} to jump to specific messages`,
|
||||
(shortcuts) => `Use ${commandText("/status", shortcuts.statusView())} to see system status info`,
|
||||
"Enable {highlight}scroll.acceleration{/highlight} in {highlight}cli.json{/highlight} for smooth scrolling",
|
||||
(shortcuts) =>
|
||||
shortcuts.commandList()
|
||||
? `Toggle username display in chat via the command palette (${shortcutText(shortcuts.commandList())})`
|
||||
: "Toggle username display in chat via the command palette",
|
||||
(shortcuts) => {
|
||||
const commandList = shortcuts.commandList()
|
||||
return commandList
|
||||
? `Toggle username display in chat via the command palette (${shortcutText(commandList)})`
|
||||
: "Toggle username display in chat via the command palette"
|
||||
},
|
||||
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} in a container",
|
||||
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",
|
||||
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing",
|
||||
|
|
|
|||
|
|
@ -1,66 +1,51 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { Tips } from "./tips-view"
|
||||
import { useBindings } from "../../keymap"
|
||||
import { Keymap } from "../../context/keymap"
|
||||
import { useData } from "../../context/data"
|
||||
import { hasConnectedProvider } from "../../util/connected-provider"
|
||||
import { useConfig } from "../../config"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
|
||||
const id = "internal:home-tips"
|
||||
|
||||
function View(props: { api: TuiPluginApi; hidden: boolean; show: boolean; connected: boolean }) {
|
||||
function View() {
|
||||
const config = useConfig()
|
||||
useBindings(() => ({
|
||||
const data = useData()
|
||||
const dialog = useDialog()
|
||||
const hidden = createMemo(() => !(config.data.hints?.tips ?? true))
|
||||
const first = createMemo(() => data.session.list().length === 0)
|
||||
const connected = createMemo(() => hasConnectedProvider(data.location.integration.list() ?? []))
|
||||
const show = createMemo(() => (!first() || !connected()) && !hidden())
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
commands: [
|
||||
{
|
||||
name: "tips.toggle",
|
||||
title: props.hidden ? "Show tips" : "Hide tips",
|
||||
category: "System",
|
||||
namespace: "palette",
|
||||
hidden: true,
|
||||
id: "tips.toggle",
|
||||
title: hidden() ? "Show tips" : "Hide tips",
|
||||
group: "System",
|
||||
run() {
|
||||
void config
|
||||
.update((draft) => {
|
||||
draft.hints = { ...draft.hints, tips: props.hidden }
|
||||
draft.hints = { ...draft.hints, tips: hidden() }
|
||||
})
|
||||
.catch(() => {})
|
||||
props.api.ui.dialog.clear()
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: props.api.tuiConfig.keybinds.get("tips.toggle"),
|
||||
}))
|
||||
|
||||
return (
|
||||
<box width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
|
||||
<Show when={props.show}>
|
||||
<Tips api={props.api} connected={props.connected} />
|
||||
<Show when={show()}>
|
||||
<Tips connected={connected()} />
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.slots.register({
|
||||
order: 100,
|
||||
slots: {
|
||||
home_bottom() {
|
||||
const data = useData()
|
||||
const config = useConfig().data
|
||||
const hidden = createMemo(() => !(config.hints?.tips ?? true))
|
||||
const first = createMemo(() => api.state.session.count() === 0)
|
||||
const connected = createMemo(() => hasConnectedProvider(data.location.integration.list() ?? []))
|
||||
const show = createMemo(() => (!first() || !connected()) && !hidden())
|
||||
return <View api={api} hidden={hidden()} show={show()} connected={connected()} />
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "internal:home-tips",
|
||||
setup(context) {
|
||||
context.ui.slot("home.bottom", () => <View />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,59 +1,46 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useData } from "../../context/data"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { contextUsage } from "../../util/session"
|
||||
|
||||
const id = "internal:sidebar-context"
|
||||
|
||||
const money = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
function View(props: { api: TuiPluginApi; session_id: string }) {
|
||||
const data = useData()
|
||||
const theme = () => props.api.theme.current
|
||||
const msg = createMemo(() => data.session.message.list(props.session_id))
|
||||
const session = createMemo(() => data.session.get(props.session_id))
|
||||
const cost = createMemo(() => data.session.cost(props.session_id))
|
||||
function View(props: { context: Plugin.Context; sessionID: string }) {
|
||||
const { theme } = useTheme()
|
||||
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))
|
||||
|
||||
const state = createMemo(() => contextUsage(msg(), data.location.model.list(session()?.location), session()?.revert?.messageID))
|
||||
const state = createMemo(() =>
|
||||
contextUsage(msg(), props.context.data.location.model.list(session()?.location), session()?.revert?.messageID),
|
||||
)
|
||||
|
||||
return (
|
||||
<box>
|
||||
<text fg={theme().text}>
|
||||
<text fg={theme.text}>
|
||||
<b>Context</b>
|
||||
</text>
|
||||
<Show when={state()} fallback={<text fg={theme().textMuted}>Not measured</text>}>
|
||||
<Show when={state()} fallback={<text fg={theme.textMuted}>Not measured</text>}>
|
||||
{(value) => (
|
||||
<>
|
||||
<text fg={theme().textMuted}>{value().tokens.toLocaleString()} tokens</text>
|
||||
<text fg={theme.textMuted}>{value().tokens.toLocaleString()} tokens</text>
|
||||
<Show when={value().percent !== undefined}>
|
||||
<text fg={theme().textMuted}>{value().percent}% used</text>
|
||||
<text fg={theme.textMuted}>{value().percent}% used</text>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<text fg={theme().textMuted}>{money.format(cost())} spent</text>
|
||||
<text fg={theme.textMuted}>{money.format(cost())} spent</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.slots.register({
|
||||
order: 100,
|
||||
slots: {
|
||||
sidebar_content(_ctx, props) {
|
||||
return <View api={api} session_id={props.session_id} />
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "internal:sidebar-context",
|
||||
setup(context) {
|
||||
context.ui.slot("sidebar.content", (props) => <View context={context} sessionID={props.sessionID} />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,113 +1,14 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { abbreviateHome } from "../../runtime"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { FilePath } from "../../ui/file-path"
|
||||
import { useConfig } from "../../config"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { useTheme } from "../../context/theme"
|
||||
|
||||
const id = "internal:sidebar-footer"
|
||||
|
||||
function View(props: { api: TuiPluginApi; directory: string }) {
|
||||
const paths = useTuiPaths()
|
||||
const config = useConfig()
|
||||
const theme = () => props.api.theme.current
|
||||
const has = createMemo(() =>
|
||||
props.api.state.provider.some(
|
||||
(item) => item.id !== "opencode" || Object.values(item.models).some((model) => model.cost?.input !== 0),
|
||||
),
|
||||
)
|
||||
const done = createMemo(() => !(config.data.hints?.onboarding ?? true))
|
||||
const show = createMemo(() => !has() && !done())
|
||||
const location = createMemo(() => {
|
||||
const branch = props.directory === props.api.state.path.directory ? props.api.state.vcs?.branch : undefined
|
||||
return { path: abbreviateHome(props.directory, paths.home), branch }
|
||||
})
|
||||
const suffix = createMemo(() => (location().branch ? `:${location().branch}` : ""))
|
||||
const suffixWidth = createMemo(() => Math.min(Bun.stringWidth(suffix()), 36))
|
||||
|
||||
return (
|
||||
<box gap={1}>
|
||||
<Show when={show()}>
|
||||
<box
|
||||
backgroundColor={theme().backgroundElement}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
>
|
||||
<text flexShrink={0} fg={theme().text}>
|
||||
⬖
|
||||
</text>
|
||||
<box flexGrow={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme().text}>
|
||||
<b>Getting started</b>
|
||||
</text>
|
||||
<text
|
||||
fg={theme().textMuted}
|
||||
onMouseDown={() =>
|
||||
void config
|
||||
.update((draft) => {
|
||||
draft.hints = { ...draft.hints, onboarding: false }
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
>
|
||||
✕
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme().textMuted}>OpenCode includes free models so you can start immediately.</text>
|
||||
<text fg={theme().textMuted}>
|
||||
Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
|
||||
</text>
|
||||
<box flexDirection="row" gap={1} justifyContent="space-between">
|
||||
<text fg={theme().text}>Connect provider</text>
|
||||
<text fg={theme().textMuted}>/connect</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
<box flexDirection="row" minWidth={0}>
|
||||
<FilePath
|
||||
value={location().path}
|
||||
maxWidth={Math.max(2, 38 - suffixWidth())}
|
||||
fg={theme().textMuted}
|
||||
basenameFg={theme().text}
|
||||
/>
|
||||
<Show when={suffix()}>
|
||||
<text width={suffixWidth()} wrapMode="none" truncate fg={theme().textMuted}>
|
||||
{suffix()}
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
<text fg={theme().textMuted}>
|
||||
<span style={{ fg: theme().success }}>•</span> <b>Open</b>
|
||||
<span style={{ fg: theme().text }}>
|
||||
<b>Code</b>
|
||||
</span>{" "}
|
||||
<span>{props.api.app.version}</span>
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
function View() {
|
||||
const { theme } = useTheme()
|
||||
return <text fg={theme.textMuted}>Sidebar footer unavailable</text>
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.slots.register({
|
||||
order: 100,
|
||||
slots: {
|
||||
sidebar_footer(_ctx, props) {
|
||||
return <View api={api} directory={props.directory} />
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "opencode.sidebar-footer",
|
||||
setup(context) {
|
||||
context.ui.slot("sidebar.footer", () => <View />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,65 +1,21 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { createMemo, For, Show, createSignal } from "solid-js"
|
||||
|
||||
const id = "internal:sidebar-lsp"
|
||||
|
||||
function View(props: { api: TuiPluginApi }) {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
const theme = () => props.api.theme.current
|
||||
const list = createMemo(() => props.api.state.lsp())
|
||||
const off = createMemo(() => !props.api.state.config.lsp)
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { useTheme } from "../../context/theme"
|
||||
|
||||
function View() {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<box>
|
||||
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
||||
<Show when={list().length > 2}>
|
||||
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme().text}>
|
||||
<b>LSP</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={list().length <= 2 || open()}>
|
||||
<Show when={list().length === 0}>
|
||||
<text fg={theme().textMuted}>{off() ? "LSPs are disabled" : "LSPs will activate as files are read"}</text>
|
||||
</Show>
|
||||
<For each={list()}>
|
||||
{(item) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text
|
||||
flexShrink={0}
|
||||
style={{
|
||||
fg: item.status === "connected" ? theme().success : theme().error,
|
||||
}}
|
||||
>
|
||||
•
|
||||
</text>
|
||||
<text fg={theme().textMuted}>
|
||||
{item.id} {item.root}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>LSP</b>
|
||||
</text>
|
||||
<text fg={theme.textMuted}>LSP status unavailable</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.slots.register({
|
||||
order: 300,
|
||||
slots: {
|
||||
sidebar_content() {
|
||||
return <View api={api} />
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "opencode.sidebar-lsp",
|
||||
setup(context) {
|
||||
context.ui.slot("sidebar.content", () => <View />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js"
|
||||
import { useTheme } from "../../context/theme"
|
||||
|
||||
const id = "internal:sidebar-mcp"
|
||||
|
||||
function View(props: { api: TuiPluginApi }) {
|
||||
function View(props: { context: Plugin.Context; sessionID: string }) {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
const theme = () => props.api.theme.current
|
||||
const list = createMemo(() => props.api.state.mcp())
|
||||
const on = createMemo(() => list().filter((item) => item.status === "connected").length)
|
||||
const { theme } = useTheme()
|
||||
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)
|
||||
const bad = createMemo(
|
||||
() =>
|
||||
list().filter(
|
||||
(item) =>
|
||||
item.status === "failed" || item.status === "needs_auth" || item.status === "needs_client_registration",
|
||||
item.status.status === "failed" ||
|
||||
item.status.status === "needs_auth" ||
|
||||
item.status.status === "needs_client_registration",
|
||||
).length,
|
||||
)
|
||||
|
||||
const dot = (status: string) => {
|
||||
if (status === "connected") return theme().success
|
||||
if (status === "failed") return theme().error
|
||||
if (status === "disabled") return theme().textMuted
|
||||
if (status === "needs_auth") return theme().warning
|
||||
if (status === "needs_client_registration") return theme().error
|
||||
return theme().textMuted
|
||||
if (status === "connected") return theme.success
|
||||
if (status === "failed") return theme.error
|
||||
if (status === "disabled") return theme.textMuted
|
||||
if (status === "needs_auth") return theme.warning
|
||||
if (status === "needs_client_registration") return theme.error
|
||||
return theme.textMuted
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -31,12 +32,12 @@ function View(props: { api: TuiPluginApi }) {
|
|||
<box>
|
||||
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
||||
<Show when={list().length > 2}>
|
||||
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
||||
<text fg={theme.text}>{open() ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme().text}>
|
||||
<text fg={theme.text}>
|
||||
<b>MCP</b>
|
||||
<Show when={!open()}>
|
||||
<span style={{ fg: theme().textMuted }}>
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
{" "}
|
||||
({on()} active{bad() > 0 ? `, ${bad()} error${bad() > 1 ? "s" : ""}` : ""})
|
||||
</span>
|
||||
|
|
@ -50,22 +51,22 @@ function View(props: { api: TuiPluginApi }) {
|
|||
<text
|
||||
flexShrink={0}
|
||||
style={{
|
||||
fg: dot(item.status),
|
||||
fg: dot(item.status.status),
|
||||
}}
|
||||
>
|
||||
•
|
||||
</text>
|
||||
<text fg={theme().text} wrapMode="word">
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
{item.name}{" "}
|
||||
<span style={{ fg: theme().textMuted }}>
|
||||
<Switch fallback={item.status}>
|
||||
<Match when={item.status === "connected"}>Connected</Match>
|
||||
<Match when={item.status === "failed"}>
|
||||
<i>{item.error}</i>
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
<Switch fallback={item.status.status}>
|
||||
<Match when={item.status.status === "connected"}>Connected</Match>
|
||||
<Match when={item.status.status === "failed"}>
|
||||
<i>{item.status.status === "failed" ? item.status.error : undefined}</i>
|
||||
</Match>
|
||||
<Match when={item.status === "disabled"}>Disabled</Match>
|
||||
<Match when={item.status === "needs_auth"}>Needs auth</Match>
|
||||
<Match when={item.status === "needs_client_registration"}>Needs client ID</Match>
|
||||
<Match when={item.status.status === "disabled"}>Disabled</Match>
|
||||
<Match when={item.status.status === "needs_auth"}>Needs auth</Match>
|
||||
<Match when={item.status.status === "needs_client_registration"}>Needs client ID</Match>
|
||||
</Switch>
|
||||
</span>
|
||||
</text>
|
||||
|
|
@ -78,20 +79,9 @@ function View(props: { api: TuiPluginApi }) {
|
|||
)
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.slots.register({
|
||||
order: 200,
|
||||
slots: {
|
||||
sidebar_content() {
|
||||
return <View api={api} />
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "internal:sidebar-mcp",
|
||||
setup(context) {
|
||||
context.ui.slot("sidebar.content", (props) => <View context={context} sessionID={props.sessionID} />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -732,10 +732,7 @@ function DiffViewer(props: { api: TuiPluginApi }) {
|
|||
{ key: "pagedown,ctrl+f", cmd: "diff.page.down", desc: "Page diff viewer down" },
|
||||
{ key: "pageup,ctrl+b", cmd: "diff.page.up", desc: "Page diff viewer up" },
|
||||
{ key: "m", cmd: "diff.mark_reviewed", desc: "Mark selected file reviewed" },
|
||||
...props.api.tuiConfig.keybinds.gather(
|
||||
"diff",
|
||||
commands.map((command) => command.name),
|
||||
),
|
||||
...commands.flatMap((command) => props.api.tuiConfig.keybinds.get(command.name)),
|
||||
],
|
||||
}))
|
||||
|
||||
|
|
@ -1047,7 +1044,7 @@ const tui: TuiPlugin = async (api) => {
|
|||
{
|
||||
name: "diff.open",
|
||||
title: "Open diff viewer",
|
||||
slashName: "diff",
|
||||
slash: { name: "diff" },
|
||||
category: "VCS",
|
||||
namespace: "palette",
|
||||
run() {
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ const tui: TuiPlugin = async (api) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
bindings: api.tuiConfig.keybinds.gather("plugins.palette", ["plugins.list", "plugins.install"]),
|
||||
bindings: ["plugins.list", "plugins.install"].flatMap((command) => api.tuiConfig.keybinds.get(command)),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,41 @@
|
|||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { Keymap } from "../../context/keymap"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useBindings } from "../../keymap"
|
||||
import type { BuiltinTuiPlugin } from "../builtins"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
|
||||
const id = "internal:scrap"
|
||||
const route = "scrap"
|
||||
function Commands(props: { context: Plugin.Context }) {
|
||||
const dialog = useDialog()
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "global",
|
||||
commands: [
|
||||
{
|
||||
id: "app.scrap",
|
||||
title: "Open scrap screen",
|
||||
group: "Debug",
|
||||
palette: true,
|
||||
run() {
|
||||
props.context.ui.router.navigate({ type: "plugin", name: "scrap" })
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
return null
|
||||
}
|
||||
|
||||
function Scrap(props: { api: TuiPluginApi }) {
|
||||
function Scrap(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme } = useTheme()
|
||||
|
||||
useBindings(() => ({
|
||||
bindings: [
|
||||
Keymap.createLayer(() => ({
|
||||
commands: [
|
||||
{
|
||||
key: "escape",
|
||||
desc: "Back home",
|
||||
bind: "escape",
|
||||
title: "Back home",
|
||||
group: "Scrap",
|
||||
cmd() {
|
||||
props.api.route.navigate("home")
|
||||
run() {
|
||||
props.context.ui.router.navigate({ type: "home" })
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -43,24 +60,10 @@ function Scrap(props: { api: TuiPluginApi }) {
|
|||
)
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.route.register([{ name: route, render: () => <Scrap api={api} /> }])
|
||||
api.keymap.registerLayer({
|
||||
commands: [
|
||||
{
|
||||
name: "app.scrap",
|
||||
title: "Open scrap screen",
|
||||
category: "Debug",
|
||||
namespace: "palette",
|
||||
run() {
|
||||
api.route.navigate(route)
|
||||
api.ui.dialog.clear()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const plugin: BuiltinTuiPlugin = { id, tui }
|
||||
|
||||
export default plugin
|
||||
export default Plugin.define({
|
||||
id: "opencode.scrap",
|
||||
setup(context) {
|
||||
context.ui.router.register({ name: "scrap", render: () => <Scrap context={context} /> })
|
||||
context.ui.slot("app", () => <Commands context={context} />)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -358,9 +358,9 @@ function WhichKeyPanel(props: {
|
|||
},
|
||||
},
|
||||
],
|
||||
bindings: pendingMode()
|
||||
? props.api.tuiConfig.keybinds.gather("which-key.scroll", scrollCommands)
|
||||
: props.api.tuiConfig.keybinds.gather("which-key.panel", panelCommands),
|
||||
bindings: (pendingMode() ? scrollCommands : panelCommands).flatMap((command) =>
|
||||
props.api.tuiConfig.keybinds.get(command),
|
||||
),
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -568,7 +568,7 @@ const tui: TuiPlugin = async (api) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
bindings: api.tuiConfig.keybinds.gather("which-key.toggle", toggleCommands),
|
||||
bindings: toggleCommands.flatMap((command) => api.tuiConfig.keybinds.get(command)),
|
||||
})
|
||||
|
||||
api.slots.register({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue