diff --git a/.opencode/plugins/tui-smoke.tsx b/.opencode/plugins/tui-smoke.tsx index deb3c3e3e4..febfc3e371 100644 --- a/.opencode/plugins/tui-smoke.tsx +++ b/.opencode/plugins/tui-smoke.tsx @@ -1,5 +1,5 @@ /** @jsxImportSource @opentui/solid */ -import { useKeyboard, useTerminalDimensions } from "@opentui/solid" +import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid" import { RGBA, VignetteEffect } from "@opentui/core" import type { TuiKeybindSet, @@ -615,7 +615,7 @@ const Modal = (props: { ) } -const home = (input: Cfg): TuiSlotPlugin => ({ +const home = (api: TuiPluginApi, input: Cfg) => ({ slots: { home_logo(ctx) { const map = ctx.theme.current @@ -649,6 +649,36 @@ const home = (input: Cfg): TuiSlotPlugin => ({ ) }, + home_prompt(ctx, value) { + const skin = look(ctx.theme.current) + type Prompt = (props: { + workspaceID?: string + hint?: JSX.Element + placeholders?: { + normal?: string[] + shell?: string[] + } + }) => JSX.Element + if (!("Prompt" in api.ui)) return null + const view = api.ui.Prompt + if (typeof view !== "function") return null + const Prompt = view as Prompt + const normal = [ + `[SMOKE] route check for ${input.label}`, + "[SMOKE] confirm home_prompt slot override", + "[SMOKE] verify api.ui.Prompt rendering", + ] + const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"] + const Hint = ( + + + smoke home prompt + + + ) + + return + }, home_bottom(ctx) { const skin = look(ctx.theme.current) const text = "extra content in the unified home bottom slot" @@ -706,8 +736,8 @@ const block = (input: Cfg, order: number, title: string, text: string): TuiSlotP }, }) -const slot = (input: Cfg): TuiSlotPlugin[] => [ - home(input), +const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [ + home(api, input), block(input, 50, "Smoke above", "renders above internal sidebar blocks"), block(input, 250, "Smoke between", "renders between internal sidebar blocks"), block(input, 650, "Smoke below", "renders below internal sidebar blocks"), @@ -848,7 +878,7 @@ const tui: TuiPlugin = async (api, options, meta) => { ]) reg(api, value, keys) - for (const item of slot(value)) { + for (const item of slot(api, value)) { api.slots.register(item) } } diff --git a/bun.lock b/bun.lock index 38b5948cc2..d5091c06eb 100644 --- a/bun.lock +++ b/bun.lock @@ -338,8 +338,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "2.3.3", - "@opentui/core": "0.1.91", - "@opentui/solid": "0.1.91", + "@opentui/core": "0.1.92", + "@opentui/solid": "0.1.92", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -428,16 +428,16 @@ "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.91", - "@opentui/solid": "0.1.91", + "@opentui/core": "0.1.92", + "@opentui/solid": "0.1.92", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.1.91", - "@opentui/solid": ">=0.1.91", + "@opentui/core": ">=0.1.92", + "@opentui/solid": ">=0.1.92", }, "optionalPeers": [ "@opentui/core", @@ -1459,21 +1459,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.91", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.91", "@opentui/core-darwin-x64": "0.1.91", "@opentui/core-linux-arm64": "0.1.91", "@opentui/core-linux-x64": "0.1.91", "@opentui/core-win32-arm64": "0.1.91", "@opentui/core-win32-x64": "0.1.91", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-xkuBDChHix3lHESQZTWXnPi0c8aANtg0567te3Am2O9EB3V1afKYdOYRV7RrzC+VBNmkymD8dUN+jzLkEUnAEw=="], + "@opentui/core": ["@opentui/core@0.1.92", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.92", "@opentui/core-darwin-x64": "0.1.92", "@opentui/core-linux-arm64": "0.1.92", "@opentui/core-linux-x64": "0.1.92", "@opentui/core-win32-arm64": "0.1.92", "@opentui/core-win32-x64": "0.1.92", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-c+KdYAIH3M8n24RYaor+t7AQtKZ3l84L7xdP7DEaN4xtuYH8W08E6Gi+wUal4g+HSai3HS9irox68yFf0VPAxw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.91", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WlIMa832vyjHCJsteWtSDsTAOrOPw/LQjYXVPISwwKo5Puyyl9vWNsF+69eYEyFEh15u8JNNrOPK98nlXq8SOA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.92", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NX/qFRuc7My0pazyOrw9fdTXmU7omXcZzQuHcsaVnwssljaT52UYMrJ7mCKhSo69RhHw0lnGCymTorvz3XBdsA=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.91", "", { "os": "darwin", "cpu": "x64" }, "sha512-nFZgQrdGtEzf5GXg4YxtDzxHvSwAig2G4Qf6ySN6sU9f9eaB1NJNhOVYLNJHBVEs5qOamBee+nXYEtG6zInIFQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.92", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zb4jn33hOf167llINKLniOabQIycs14LPOBZnQ6l4khbeeTPVJdG8gy9PhlAyIQygDKmRTFncVlP0RP+L6C7og=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.91", "", { "os": "linux", "cpu": "arm64" }, "sha512-vXAcHZaS3QzEXYyvM9KoE0juSOMPPPdNrV5Fo4HAbI5BXGCkMNQJoN0j0EzoO9xwfsO+EulRSHCLVTNkvI4n8Q=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.92", "", { "os": "linux", "cpu": "arm64" }, "sha512-4VA1A91OTMPJ3LkAyaxKEZVJsk5jIc3Kz0gV2vip8p2aGLPpYHHpkFZpXP/FyzsnJzoSGftBeA6ya1GKa5bkXg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.91", "", { "os": "linux", "cpu": "x64" }, "sha512-rAJ9sOvvI9eoWHjVj6TLPDRqYPYISmfCm2TDxi67BO27+E7naJANHIIxMC7yhPAmwBof7plioL2lwl2UFXAoXw=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.92", "", { "os": "linux", "cpu": "x64" }, "sha512-tr7va8hfKS1uY+TBmulQBoBlwijzJk56K/U/L9/tbHfW7oJctqxPVwEFHIh1HDcOQ3/UhMMWGvMfeG6cFiK8/A=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.91", "", { "os": "win32", "cpu": "arm64" }, "sha512-teLe7uHvPnD/lOwTwZp2lUFfeT27dk6ZSLWk8hrhsAJ/Y0MyoaCUHAsg3nZ/p+I3pie5aZUR1f0vrJfaZ8ukJw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.92", "", { "os": "win32", "cpu": "arm64" }, "sha512-34YM3uPtDjzUVeSnJWIK2J8mxyduzV7f3mYc4Hub0glNpUdM1jjzF2HvvvnrKK5ElzTsIcno3c3lOYT8yvG1Zg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.91", "", { "os": "win32", "cpu": "x64" }, "sha512-Odx9S1NYp3I2jgy5aj5k3/wb3M+yChEK7k8UUxxFt4R37V1/um8n6Cxw4nfid6T2C45KDGJ/0BYe6lGugJlnSg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.92", "", { "os": "win32", "cpu": "x64" }, "sha512-uk442kA2Vn0mmJHHqk5sPM+Zai/AN9sgl7egekhoEOUx2VK3gxftKsVlx2YVpCHTvTE/S+vnD2WpQaJk2SNjww=="], - "@opentui/solid": ["@opentui/solid@0.1.91", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.91", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-phqiOcmTgNy7aG7s3P6zyatrBc1f6DkuLDJmGqy6R9QuoS4Mn9MKdNQe6Ick03xRAZuaS6ZdG3kueNxIlUMTCA=="], + "@opentui/solid": ["@opentui/solid@0.1.92", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.92", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-0Sx1+6zRpmMJ5oDEY0JS9b9+eGd/Q0fPndNllrQNnp7w2FCjpXmvHdBdq+pFI6kFp01MHq2ZOkUU5zX5/9YMSQ=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ca1ecc48a4..5dca50be33 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -102,8 +102,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "2.3.3", - "@opentui/core": "0.1.91", - "@opentui/solid": "0.1.91", + "@opentui/core": "0.1.92", + "@opentui/solid": "0.1.92", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d88747e4c5..96563b884e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -45,6 +45,10 @@ export type PromptProps = { ref?: (ref: PromptRef) => void hint?: JSX.Element showPlaceholder?: boolean + placeholders?: { + normal?: string[] + shell?: string[] + } } export type PromptRef = { @@ -57,13 +61,16 @@ export type PromptRef = { submit(): void } -const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"] -const SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"] const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }) +function randomIndex(count: number) { + if (count <= 0) return 0 + return Math.floor(Math.random() * count) +} + export function Prompt(props: PromptProps) { let input: TextareaRenderable let anchor: BoxRenderable @@ -83,6 +90,8 @@ export function Prompt(props: PromptProps) { const renderer = useRenderer() const { theme, syntax } = useTheme() const kv = useKV() + const list = createMemo(() => props.placeholders?.normal ?? []) + const shell = createMemo(() => props.placeholders?.shell ?? []) function promptModelWarning() { toast.show({ @@ -152,7 +161,7 @@ export function Prompt(props: PromptProps) { interrupt: number placeholder: number }>({ - placeholder: Math.floor(Math.random() * PLACEHOLDERS.length), + placeholder: randomIndex(list().length), prompt: { input: "", parts: [], @@ -166,7 +175,7 @@ export function Prompt(props: PromptProps) { on( () => props.sessionID, () => { - setStore("placeholder", Math.floor(Math.random() * PLACEHOLDERS.length)) + setStore("placeholder", randomIndex(list().length)) }, { defer: true }, ), @@ -801,12 +810,14 @@ export function Prompt(props: PromptProps) { }) const placeholderText = createMemo(() => { - if (props.sessionID) return undefined + if (props.showPlaceholder === false) return undefined if (store.mode === "shell") { - const example = SHELL_PLACEHOLDERS[store.placeholder % SHELL_PLACEHOLDERS.length] + if (!shell().length) return undefined + const example = shell()[store.placeholder % shell().length] return `Run a command... "${example}"` } - return `Ask anything... "${PLACEHOLDERS[store.placeholder % PLACEHOLDERS.length]}"` + if (!list().length) return undefined + return `Ask anything... "${list()[store.placeholder % list().length]}"` }) const spinnerDef = createMemo(() => { @@ -922,7 +933,7 @@ export function Prompt(props: PromptProps) { } } if (e.name === "!" && input.visualCursor.offset === 0) { - setStore("placeholder", Math.floor(Math.random() * SHELL_PLACEHOLDERS.length)) + setStore("placeholder", randomIndex(shell().length)) setStore("mode", "shell") e.preventDefault() return @@ -1097,7 +1108,7 @@ export function Prompt(props: PromptProps) { /> - }> + }> ) }, + Prompt(props) { + return ( + + ) + }, toast(inputToast) { input.toast.show({ title: inputToast.title, diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 07549c6c29..b63bf2d2df 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -15,6 +15,10 @@ import { TuiPluginRuntime } from "../plugin" // TODO: what is the best way to do this? let once = false +const placeholder = { + normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"], + shell: ["ls -la", "git status", "pwd"], +} export function Home() { const sync = useSync() @@ -49,11 +53,12 @@ export function Home() { ) - let prompt: PromptRef + let prompt: PromptRef | undefined const args = useArgs() const local = useLocal() onMount(() => { if (once) return + if (!prompt) return if (route.initialPrompt) { prompt.set(route.initialPrompt) once = true @@ -69,6 +74,7 @@ export function Home() { () => sync.ready && local.model.ready, (ready) => { if (!ready) return + if (!prompt) return if (!args.prompt) return if (prompt.current?.input !== args.prompt) return prompt.submit() @@ -89,14 +95,17 @@ export function Home() { - { - prompt = r - promptRef.set(r) - }} - hint={Hint} - workspaceID={route.workspaceID} - /> + + { + prompt = r + promptRef.set(r) + }} + hint={Hint} + workspaceID={route.workspaceID} + placeholders={placeholder} + /> + diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index c982d129f3..7a34877e97 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -231,6 +231,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { DialogConfirm: () => null, DialogPrompt: () => null, DialogSelect: () => null, + Prompt: () => null, toast: () => {}, dialog: { replace: () => { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 46c6900c35..96fe5cd603 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -21,8 +21,8 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.1.91", - "@opentui/solid": ">=0.1.91" + "@opentui/core": ">=0.1.92", + "@opentui/solid": ">=0.1.92" }, "peerDependenciesMeta": { "@opentui/core": { @@ -33,8 +33,8 @@ } }, "devDependencies": { - "@opentui/core": "0.1.91", - "@opentui/solid": "0.1.91", + "@opentui/core": "0.1.92", + "@opentui/solid": "0.1.92", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index cbb6f62b62..bbf3494909 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -135,6 +135,19 @@ export type TuiDialogSelectProps = { current?: Value } +export type TuiPromptProps = { + workspaceID?: string + visible?: boolean + disabled?: boolean + onSubmit?: () => void + hint?: JSX.Element + showPlaceholder?: boolean + placeholders?: { + normal?: string[] + shell?: string[] + } +} + export type TuiToast = { variant?: "info" | "success" | "warning" | "error" title?: string @@ -279,6 +292,9 @@ export type TuiSidebarFileItem = { export type TuiSlotMap = { app: {} home_logo: {} + home_prompt: { + workspace_id?: string + } home_bottom: {} sidebar_title: { session_id: string @@ -386,6 +402,7 @@ export type TuiPluginApi = { DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element DialogSelect: (props: TuiDialogSelectProps) => JSX.Element + Prompt: (props: TuiPromptProps) => JSX.Element toast: (input: TuiToast) => void dialog: TuiDialogStack }