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
}