diff --git a/.opencode/plugins/tui-smoke.tsx b/.opencode/plugins/tui-smoke.tsx index 9bc0459cd8..34ca7359b4 100644 --- a/.opencode/plugins/tui-smoke.tsx +++ b/.opencode/plugins/tui-smoke.tsx @@ -53,40 +53,6 @@ const current = (api: TuiApi, route: ReturnType) => { return parse(value.params) } -const nav = (api: TuiApi, name: string, params: Record | undefined, from: string) => { - console.log("[smoke] nav", { - from, - to: name, - params, - current: api.route.current, - }) - api.route.navigate(name, params) -} - -const key = (api: TuiApi, where: string, evt: any) => { - console.log("[smoke] key", { - where, - current: api.route.current.name, - name: evt.name, - ctrl: evt.ctrl, - meta: evt.meta, - shift: evt.shift, - leader: evt.leader, - defaultPrevented: evt.defaultPrevented, - eventType: evt.eventType, - }) -} - -const Probe = (props: { api: TuiApi; route: ReturnType }) => { - useKeyboard((evt) => { - const name = props.api.route.current.name - if (name !== props.route.screen && name !== props.route.modal) return - key(props.api, "probe", evt) - }) - - return null -} - const Screen = (props: { api: TuiApi input: ReturnType @@ -96,14 +62,7 @@ const Screen = (props: { const dim = useTerminalDimensions() const value = parse(props.params) - console.log("[smoke] render", { - view: "screen", - current: props.api.route.current, - params: props.params, - }) - useKeyboard((evt) => { - key(props.api, "screen", evt) if (props.api.route.current.name !== props.route.screen) return const next = current(props.api, props.route) @@ -111,42 +70,42 @@ const Screen = (props: { if (evt.name === "escape" || (evt.ctrl && evt.name === "h")) { evt.preventDefault() evt.stopPropagation() - nav(props.api, "home", undefined, "screen:escape") + props.api.route.navigate("home") return } if (evt.name === "left" || evt.name === "h") { evt.preventDefault() evt.stopPropagation() - nav(props.api, props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length }, "screen:left") + props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length }) return } if (evt.name === "right" || evt.name === "l") { evt.preventDefault() evt.stopPropagation() - nav(props.api, props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length }, "screen:right") + props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length }) return } if (evt.name === "up" || evt.name === "k") { evt.preventDefault() evt.stopPropagation() - nav(props.api, props.route.screen, { ...next, count: next.count + 1 }, "screen:up") + props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 }) return } if (evt.name === "down" || evt.name === "j") { evt.preventDefault() evt.stopPropagation() - nav(props.api, props.route.screen, { ...next, count: next.count - 1 }, "screen:down") + props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 }) return } if (evt.ctrl && evt.name === "m") { evt.preventDefault() evt.stopPropagation() - nav(props.api, props.route.modal, next, "screen:ctrl+m") + props.api.route.navigate(props.route.modal, next) } }) @@ -174,7 +133,7 @@ const Screen = (props: { const on = value.tab === i return ( nav(props.api, props.route.screen, { ...value, tab: i }, "screen:click-tab")} + onMouseUp={() => props.api.route.navigate(props.route.screen, { ...value, tab: i })} backgroundColor={on ? ui.accent : ui.border} paddingLeft={1} paddingRight={1} @@ -219,7 +178,7 @@ const Screen = (props: { nav(props.api, "home", undefined, "screen:click-home")} + onMouseUp={() => props.api.route.navigate("home")} backgroundColor={ui.border} paddingLeft={1} paddingRight={1} @@ -227,7 +186,7 @@ const Screen = (props: { go home nav(props.api, props.route.modal, value, "screen:click-modal")} + onMouseUp={() => props.api.route.navigate(props.route.modal, value)} backgroundColor={ui.accent} paddingLeft={1} paddingRight={1} @@ -249,33 +208,26 @@ const Modal = (props: { const Dialog = props.api.ui.Dialog const value = parse(props.params) - console.log("[smoke] render", { - view: "modal", - current: props.api.route.current, - params: props.params, - }) - useKeyboard((evt) => { - key(props.api, "modal", evt) if (props.api.route.current.name !== props.route.modal) return if (evt.name === "return" || evt.name === "enter") { evt.preventDefault() evt.stopPropagation() - nav(props.api, props.route.screen, { ...value, source: "modal" }, "modal:enter") + props.api.route.navigate(props.route.screen, { ...value, source: "modal" }) return } if (evt.name === "escape") { evt.preventDefault() evt.stopPropagation() - nav(props.api, "home", undefined, "modal:escape") + props.api.route.navigate("home") } }) return ( - nav(props.api, "home", undefined, "modal:onClose")}> + props.api.route.navigate("home")}> {props.input.label} modal @@ -285,7 +237,7 @@ const Modal = (props: { enter opens screen ยท esc closes nav(props.api, props.route.screen, { ...value, source: "modal" }, "modal:click-open")} + onMouseUp={() => props.api.route.navigate(props.route.screen, { ...value, source: "modal" })} backgroundColor={ui.accent} paddingLeft={1} paddingRight={1} @@ -293,7 +245,7 @@ const Modal = (props: { open screen nav(props.api, "home", undefined, "modal:click-cancel")} + onMouseUp={() => props.api.route.navigate("home")} backgroundColor={ui.border} paddingLeft={1} paddingRight={1} @@ -307,12 +259,9 @@ const Modal = (props: { ) } -const slot = (api: TuiApi, input: ReturnType, route: ReturnType) => ({ +const slot = (input: ReturnType) => ({ id: "workspace-smoke", slots: { - app() { - return - }, home_logo() { return plugin logo:{input.label} }, @@ -338,8 +287,7 @@ const reg = (api: TuiApi, input: ReturnType) => { name: "smoke", }, onSelect: () => { - console.log("[smoke] command", { value: "plugin.smoke.modal", current: api.route.current }) - nav(api, route.modal, { source: "command" }, "command:modal") + api.route.navigate(route.modal, { source: "command" }) }, }, { @@ -351,8 +299,7 @@ const reg = (api: TuiApi, input: ReturnType) => { name: "smoke-screen", }, onSelect: () => { - console.log("[smoke] command", { value: "plugin.smoke.screen", current: api.route.current }) - nav(api, route.screen, { source: "command", tab: 0, count: 0 }, "command:screen") + api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 }) }, }, { @@ -361,8 +308,7 @@ const reg = (api: TuiApi, input: ReturnType) => { category: "Plugin", enabled: api.route.current.name !== "home", onSelect: () => { - console.log("[smoke] command", { value: "plugin.smoke.home", current: api.route.current }) - nav(api, "home", undefined, "command:home") + api.route.navigate("home") }, }, { @@ -370,7 +316,6 @@ const reg = (api: TuiApi, input: ReturnType) => { value: "plugin.smoke.toast", category: "Plugin", onSelect: () => { - console.log("[smoke] command", { value: "plugin.smoke.toast", current: api.route.current }) api.ui.toast({ variant: "info", title: "Smoke", @@ -392,14 +337,6 @@ const tui = async (input: TuiPluginInput, options?: Record) => const value = cfg(options) const route = names(value) - console.log("[smoke] init", { - route, - keybind: { - modal: value.modal, - screen: value.screen, - }, - }) - input.api.route.register([ { name: route.screen, @@ -411,10 +348,8 @@ const tui = async (input: TuiPluginInput, options?: Record) => }, ]) - console.log("[smoke] routes registered", route) - reg(input.api, value) - input.slots.register(slot(input.api, value, route)) + input.slots.register(slot(value)) } export default { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index b9d2da5ad2..28538370d1 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -41,6 +41,9 @@ import { PromptHistoryProvider } from "./component/prompt/history" import { FrecencyProvider } from "./component/prompt/frecency" import { PromptStashProvider } from "./component/prompt/stash" import { DialogAlert } from "./ui/dialog-alert" +import { DialogConfirm } from "./ui/dialog-confirm" +import { DialogPrompt } from "./ui/dialog-prompt" +import { DialogSelect } from "./ui/dialog-select" import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider, useExit } from "./context/exit" import { Session as SessionApi } from "@/session" @@ -239,7 +242,7 @@ function App() { return routes.get(name)?.at(-1)?.render } - const api: TuiApi = { + const api: TuiApi = { command: { register(cb) { command.register(() => cb()) @@ -311,6 +314,59 @@ function App() { ) }, + DialogAlert(props) { + return + }, + DialogConfirm(props) { + return + }, + DialogPrompt(props) { + return JSX.Element) | undefined} /> + }, + DialogSelect(props) { + const list = props.options.map((item) => ({ + ...item, + footer: item.footer as JSX.Element | string | undefined, + onSelect: () => item.onSelect?.(), + })) + return ( + + props.onMove?.({ + title: item.title, + value: item.value, + description: item.description, + footer: item.footer, + category: item.category, + disabled: item.disabled, + }) + : undefined + } + onFilter={props.onFilter} + onSelect={ + props.onSelect + ? (item) => + props.onSelect?.({ + title: item.title, + value: item.value, + description: item.description, + footer: item.footer, + category: item.category, + disabled: item.disabled, + }) + : undefined + } + skipFilter={props.skipFilter} + current={props.current} + /> + ) + }, toast(input) { toast.show({ title: input.title, diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index e45545c286..41d13b9bec 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -111,6 +111,10 @@ test("ignores function-only tui exports and loads object exports", async () => { }, ui: { Dialog: () => null, + DialogAlert: () => null, + DialogConfirm: () => null, + DialogPrompt: () => null, + DialogSelect: () => null, toast: () => {}, }, keybind: { diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 56a4195afa..90b07f42d7 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -74,6 +74,50 @@ export type TuiDialogProps = { children?: Node } +export type TuiDialogAlertProps = { + title: string + message: string + onConfirm?: () => void +} + +export type TuiDialogConfirmProps = { + title: string + message: string + onConfirm?: () => void + onCancel?: () => void +} + +export type TuiDialogPromptProps = { + title: string + description?: () => Node + placeholder?: string + value?: string + onConfirm?: (value: string) => void + onCancel?: () => void +} + +export type TuiDialogSelectOption = { + title: string + value: Value + description?: string + footer?: Node | string + category?: string + disabled?: boolean + onSelect?: () => void +} + +export type TuiDialogSelectProps = { + title: string + placeholder?: string + options: TuiDialogSelectOption[] + flat?: boolean + onMove?: (option: TuiDialogSelectOption) => void + onFilter?: (query: string) => void + onSelect?: (option: TuiDialogSelectOption) => void + skipFilter?: boolean + current?: Value +} + export type TuiToast = { variant?: "info" | "success" | "warning" | "error" title?: string @@ -93,6 +137,10 @@ export type TuiApi = { } ui: { Dialog: (props: TuiDialogProps) => Node + DialogAlert: (props: TuiDialogAlertProps) => Node + DialogConfirm: (props: TuiDialogConfirmProps) => Node + DialogPrompt: (props: TuiDialogPromptProps) => Node + DialogSelect: (props: TuiDialogSelectProps) => Node toast: (input: TuiToast) => void } keybind: {