Skip to content

Commit bd55da1

Browse files
kommanderbalcsida
authored andcommitted
prompt slot (anomalyco#19563)
1 parent 47bccf5 commit bd55da1

File tree

9 files changed

+125
-43
lines changed

9 files changed

+125
-43
lines changed

.opencode/plugins/tui-smoke.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @jsxImportSource @opentui/solid */
2-
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
2+
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
33
import { RGBA, VignetteEffect } from "@opentui/core"
44
import type {
55
TuiKeybindSet,
@@ -615,7 +615,7 @@ const Modal = (props: {
615615
)
616616
}
617617

618-
const home = (input: Cfg): TuiSlotPlugin => ({
618+
const home = (api: TuiPluginApi, input: Cfg) => ({
619619
slots: {
620620
home_logo(ctx) {
621621
const map = ctx.theme.current
@@ -649,6 +649,36 @@ const home = (input: Cfg): TuiSlotPlugin => ({
649649
</box>
650650
)
651651
},
652+
home_prompt(ctx, value) {
653+
const skin = look(ctx.theme.current)
654+
type Prompt = (props: {
655+
workspaceID?: string
656+
hint?: JSX.Element
657+
placeholders?: {
658+
normal?: string[]
659+
shell?: string[]
660+
}
661+
}) => JSX.Element
662+
if (!("Prompt" in api.ui)) return null
663+
const view = api.ui.Prompt
664+
if (typeof view !== "function") return null
665+
const Prompt = view as Prompt
666+
const normal = [
667+
`[SMOKE] route check for ${input.label}`,
668+
"[SMOKE] confirm home_prompt slot override",
669+
"[SMOKE] verify api.ui.Prompt rendering",
670+
]
671+
const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
672+
const Hint = (
673+
<box flexShrink={0} flexDirection="row" gap={1}>
674+
<text fg={skin.muted}>
675+
<span style={{ fg: skin.accent }}></span> smoke home prompt
676+
</text>
677+
</box>
678+
)
679+
680+
return <Prompt workspaceID={value.workspace_id} hint={Hint} placeholders={{ normal, shell }} />
681+
},
652682
home_bottom(ctx) {
653683
const skin = look(ctx.theme.current)
654684
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
706736
},
707737
})
708738

709-
const slot = (input: Cfg): TuiSlotPlugin[] => [
710-
home(input),
739+
const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
740+
home(api, input),
711741
block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
712742
block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
713743
block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
@@ -848,7 +878,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
848878
])
849879

850880
reg(api, value, keys)
851-
for (const item of slot(value)) {
881+
for (const item of slot(api, value)) {
852882
api.slots.register(item)
853883
}
854884
}

bun.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/opencode/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@
102102
"@opencode-ai/sdk": "workspace:*",
103103
"@opencode-ai/util": "workspace:*",
104104
"@openrouter/ai-sdk-provider": "2.3.3",
105-
"@opentui/core": "0.1.91",
106-
"@opentui/solid": "0.1.91",
105+
"@opentui/core": "0.1.92",
106+
"@opentui/solid": "0.1.92",
107107
"@parcel/watcher": "2.5.1",
108108
"@pierre/diffs": "catalog:",
109109
"@solid-primitives/event-bus": "1.1.2",

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export type PromptProps = {
4545
ref?: (ref: PromptRef) => void
4646
hint?: JSX.Element
4747
showPlaceholder?: boolean
48+
placeholders?: {
49+
normal?: string[]
50+
shell?: string[]
51+
}
4852
}
4953

5054
export type PromptRef = {
@@ -57,13 +61,16 @@ export type PromptRef = {
5761
submit(): void
5862
}
5963

60-
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
61-
const SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"]
6264
const money = new Intl.NumberFormat("en-US", {
6365
style: "currency",
6466
currency: "USD",
6567
})
6668

69+
function randomIndex(count: number) {
70+
if (count <= 0) return 0
71+
return Math.floor(Math.random() * count)
72+
}
73+
6774
export function Prompt(props: PromptProps) {
6875
let input: TextareaRenderable
6976
let anchor: BoxRenderable
@@ -83,6 +90,8 @@ export function Prompt(props: PromptProps) {
8390
const renderer = useRenderer()
8491
const { theme, syntax } = useTheme()
8592
const kv = useKV()
93+
const list = createMemo(() => props.placeholders?.normal ?? [])
94+
const shell = createMemo(() => props.placeholders?.shell ?? [])
8695

8796
function promptModelWarning() {
8897
toast.show({
@@ -152,7 +161,7 @@ export function Prompt(props: PromptProps) {
152161
interrupt: number
153162
placeholder: number
154163
}>({
155-
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
164+
placeholder: randomIndex(list().length),
156165
prompt: {
157166
input: "",
158167
parts: [],
@@ -166,7 +175,7 @@ export function Prompt(props: PromptProps) {
166175
on(
167176
() => props.sessionID,
168177
() => {
169-
setStore("placeholder", Math.floor(Math.random() * PLACEHOLDERS.length))
178+
setStore("placeholder", randomIndex(list().length))
170179
},
171180
{ defer: true },
172181
),
@@ -801,12 +810,14 @@ export function Prompt(props: PromptProps) {
801810
})
802811

803812
const placeholderText = createMemo(() => {
804-
if (props.sessionID) return undefined
813+
if (props.showPlaceholder === false) return undefined
805814
if (store.mode === "shell") {
806-
const example = SHELL_PLACEHOLDERS[store.placeholder % SHELL_PLACEHOLDERS.length]
815+
if (!shell().length) return undefined
816+
const example = shell()[store.placeholder % shell().length]
807817
return `Run a command... "${example}"`
808818
}
809-
return `Ask anything... "${PLACEHOLDERS[store.placeholder % PLACEHOLDERS.length]}"`
819+
if (!list().length) return undefined
820+
return `Ask anything... "${list()[store.placeholder % list().length]}"`
810821
})
811822

812823
const spinnerDef = createMemo(() => {
@@ -922,7 +933,7 @@ export function Prompt(props: PromptProps) {
922933
}
923934
}
924935
if (e.name === "!" && input.visualCursor.offset === 0) {
925-
setStore("placeholder", Math.floor(Math.random() * SHELL_PLACEHOLDERS.length))
936+
setStore("placeholder", randomIndex(shell().length))
926937
setStore("mode", "shell")
927938
e.preventDefault()
928939
return
@@ -1097,7 +1108,7 @@ export function Prompt(props: PromptProps) {
10971108
/>
10981109
</box>
10991110
<box flexDirection="row" justifyContent="space-between">
1100-
<Show when={status().type !== "idle"} fallback={<text />}>
1111+
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
11011112
<box
11021113
flexDirection="row"
11031114
gap={1}

packages/opencode/src/cli/cmd/tui/plugin/api.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { DialogAlert } from "../ui/dialog-alert"
1414
import { DialogConfirm } from "../ui/dialog-confirm"
1515
import { DialogPrompt } from "../ui/dialog-prompt"
1616
import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dialog-select"
17+
import { Prompt } from "../component/prompt"
1718
import type { useToast } from "../ui/toast"
1819
import { Installation } from "@/installation"
1920
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
@@ -287,6 +288,19 @@ export function createTuiApi(input: Input): TuiHostPluginApi {
287288
/>
288289
)
289290
},
291+
Prompt(props) {
292+
return (
293+
<Prompt
294+
workspaceID={props.workspaceID}
295+
visible={props.visible}
296+
disabled={props.disabled}
297+
onSubmit={props.onSubmit}
298+
hint={props.hint}
299+
showPlaceholder={props.showPlaceholder}
300+
placeholders={props.placeholders}
301+
/>
302+
)
303+
},
290304
toast(inputToast) {
291305
input.toast.show({
292306
title: inputToast.title,

packages/opencode/src/cli/cmd/tui/routes/home.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { TuiPluginRuntime } from "../plugin"
1515

1616
// TODO: what is the best way to do this?
1717
let once = false
18+
const placeholder = {
19+
normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
20+
shell: ["ls -la", "git status", "pwd"],
21+
}
1822

1923
export function Home() {
2024
const sync = useSync()
@@ -49,11 +53,12 @@ export function Home() {
4953
</box>
5054
)
5155

52-
let prompt: PromptRef
56+
let prompt: PromptRef | undefined
5357
const args = useArgs()
5458
const local = useLocal()
5559
onMount(() => {
5660
if (once) return
61+
if (!prompt) return
5762
if (route.initialPrompt) {
5863
prompt.set(route.initialPrompt)
5964
once = true
@@ -69,6 +74,7 @@ export function Home() {
6974
() => sync.ready && local.model.ready,
7075
(ready) => {
7176
if (!ready) return
77+
if (!prompt) return
7278
if (!args.prompt) return
7379
if (prompt.current?.input !== args.prompt) return
7480
prompt.submit()
@@ -89,14 +95,17 @@ export function Home() {
8995
</box>
9096
<box height={1} minHeight={0} flexShrink={1} />
9197
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
92-
<Prompt
93-
ref={(r) => {
94-
prompt = r
95-
promptRef.set(r)
96-
}}
97-
hint={Hint}
98-
workspaceID={route.workspaceID}
99-
/>
98+
<TuiPluginRuntime.Slot name="home_prompt" mode="replace" workspace_id={route.workspaceID}>
99+
<Prompt
100+
ref={(r) => {
101+
prompt = r
102+
promptRef.set(r)
103+
}}
104+
hint={Hint}
105+
workspaceID={route.workspaceID}
106+
placeholders={placeholder}
107+
/>
108+
</TuiPluginRuntime.Slot>
100109
</box>
101110
<TuiPluginRuntime.Slot name="home_bottom" />
102111
<box flexGrow={1} minHeight={0} />

packages/opencode/test/fixture/tui-plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
231231
DialogConfirm: () => null,
232232
DialogPrompt: () => null,
233233
DialogSelect: () => null,
234+
Prompt: () => null,
234235
toast: () => {},
235236
dialog: {
236237
replace: () => {

packages/plugin/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"zod": "catalog:"
2222
},
2323
"peerDependencies": {
24-
"@opentui/core": ">=0.1.91",
25-
"@opentui/solid": ">=0.1.91"
24+
"@opentui/core": ">=0.1.92",
25+
"@opentui/solid": ">=0.1.92"
2626
},
2727
"peerDependenciesMeta": {
2828
"@opentui/core": {
@@ -33,8 +33,8 @@
3333
}
3434
},
3535
"devDependencies": {
36-
"@opentui/core": "0.1.91",
37-
"@opentui/solid": "0.1.91",
36+
"@opentui/core": "0.1.92",
37+
"@opentui/solid": "0.1.92",
3838
"@tsconfig/node22": "catalog:",
3939
"@types/node": "catalog:",
4040
"typescript": "catalog:",

packages/plugin/src/tui.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ export type TuiDialogSelectProps<Value = unknown> = {
135135
current?: Value
136136
}
137137

138+
export type TuiPromptProps = {
139+
workspaceID?: string
140+
visible?: boolean
141+
disabled?: boolean
142+
onSubmit?: () => void
143+
hint?: JSX.Element
144+
showPlaceholder?: boolean
145+
placeholders?: {
146+
normal?: string[]
147+
shell?: string[]
148+
}
149+
}
150+
138151
export type TuiToast = {
139152
variant?: "info" | "success" | "warning" | "error"
140153
title?: string
@@ -279,6 +292,9 @@ export type TuiSidebarFileItem = {
279292
export type TuiSlotMap = {
280293
app: {}
281294
home_logo: {}
295+
home_prompt: {
296+
workspace_id?: string
297+
}
282298
home_bottom: {}
283299
sidebar_title: {
284300
session_id: string
@@ -386,6 +402,7 @@ export type TuiPluginApi = {
386402
DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element
387403
DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element
388404
DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element
405+
Prompt: (props: TuiPromptProps) => JSX.Element
389406
toast: (input: TuiToast) => void
390407
dialog: TuiDialogStack
391408
}

0 commit comments

Comments
 (0)