prompt slot (#19563)

This commit is contained in:
Sebastian 2026-03-29 00:27:27 +01:00 committed by GitHub
parent 772059acb5
commit 38af99dcb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 125 additions and 43 deletions

View file

@ -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",

View file

@ -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) {
/>
</box>
<box flexDirection="row" justifyContent="space-between">
<Show when={status().type !== "idle"} fallback={<text />}>
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
<box
flexDirection="row"
gap={1}

View file

@ -14,6 +14,7 @@ import { DialogAlert } from "../ui/dialog-alert"
import { DialogConfirm } from "../ui/dialog-confirm"
import { DialogPrompt } from "../ui/dialog-prompt"
import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dialog-select"
import { Prompt } from "../component/prompt"
import type { useToast } from "../ui/toast"
import { Installation } from "@/installation"
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
@ -287,6 +288,19 @@ export function createTuiApi(input: Input): TuiHostPluginApi {
/>
)
},
Prompt(props) {
return (
<Prompt
workspaceID={props.workspaceID}
visible={props.visible}
disabled={props.disabled}
onSubmit={props.onSubmit}
hint={props.hint}
showPlaceholder={props.showPlaceholder}
placeholders={props.placeholders}
/>
)
},
toast(inputToast) {
input.toast.show({
title: inputToast.title,

View file

@ -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() {
</box>
)
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() {
</box>
<box height={1} minHeight={0} flexShrink={1} />
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
<Prompt
ref={(r) => {
prompt = r
promptRef.set(r)
}}
hint={Hint}
workspaceID={route.workspaceID}
/>
<TuiPluginRuntime.Slot name="home_prompt" mode="replace" workspace_id={route.workspaceID}>
<Prompt
ref={(r) => {
prompt = r
promptRef.set(r)
}}
hint={Hint}
workspaceID={route.workspaceID}
placeholders={placeholder}
/>
</TuiPluginRuntime.Slot>
</box>
<TuiPluginRuntime.Slot name="home_bottom" />
<box flexGrow={1} minHeight={0} />

View file

@ -231,6 +231,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
DialogConfirm: () => null,
DialogPrompt: () => null,
DialogSelect: () => null,
Prompt: () => null,
toast: () => {},
dialog: {
replace: () => {