refactor(tui): build prompt-style dialogs on DialogPrompt (#55518)

This commit is contained in:
James Long
2026-10-15 13:17:19 -04:00
committed by GitHub
Unverified
parent 6d570f1d92
commit 61bfcbdaff
3 changed files with 51 additions and 72 deletions
@@ -1,75 +1,22 @@
import { InputRenderable } from "@opentui/core"
import { Slug } from "@opencode/core/util/slug"
import { createSignal, onMount } from "solid-js"
import { Keymap } from "../context/keymap"
import { useTheme } from "../context/theme"
import { DIALOG_INSET, DialogFooter, DialogHeader, useDialog, type DialogContext } from "../ui/dialog"
import { useConfig } from "../config"
import { type DialogContext } from "../ui/dialog"
import { DialogPrompt } from "../ui/dialog-prompt"
export function DialogWorktreeName(props: { onConfirm: (name: string) => void }) {
const dialog = useDialog()
const theme = useTheme().surface("dialog")
const config = useConfig().data
const [inputTarget, setInputTarget] = createSignal<InputRenderable>()
let input: InputRenderable
function generate() {
input.value = Slug.create()
input.gotoLineEnd()
}
function confirm() {
props.onConfirm(slugify(input.value) || Slug.create())
}
Keymap.createLayer(() => ({
mode: "modal",
target: inputTarget,
enabled: inputTarget() !== undefined,
priority: 1,
commands: [
{
id: "dialog.worktree.generate",
title: "Generate worktree name",
group: "Dialog",
run: generate,
},
],
}))
onMount(() => {
dialog.setSize("medium")
setTimeout(() => {
if (!input || input.isDestroyed) return
input.focus()
}, 1)
})
return (
<box paddingLeft={DIALOG_INSET} paddingRight={DIALOG_INSET} gap={1}>
<DialogHeader title="Name worktree" />
<input
cursorStyle={config.cursor}
ref={(value: InputRenderable) => {
input = value
setInputTarget(value)
}}
onSubmit={confirm}
placeholder="Worktree name"
placeholderColor={theme.text.muted}
textColor={theme.text.formfield.base}
focusedTextColor={theme.text.formfield.base}
cursorColor={theme.text.formfield.base}
/>
<box paddingBottom={1} flexDirection="row">
<DialogFooter
actions={[
{ title: "submit", key: "enter", onTrigger: confirm },
{ title: "generate one", command: "dialog.worktree.generate", onTrigger: generate },
]}
/>
</box>
</box>
<DialogPrompt
title="Name worktree"
placeholder="Worktree name"
onConfirm={(value) => props.onConfirm(slugify(value) || Slug.create())}
actions={[
{
title: "generate one",
command: "dialog.worktree.generate",
commandTitle: "Generate worktree name",
onTrigger: (prompt) => prompt.setValue(Slug.create()),
},
]}
/>
)
}
+33 -1
View File
@@ -7,6 +7,15 @@ import { Spinner } from "../component/spinner"
import { useConfig } from "../config"
import { useRenderer } from "@opentui/solid"
export type DialogPromptAction = {
title: string
/** Keymap command id, bound while the prompt input is focused and shown in the footer. */
command: string
/** Keymap command title, e.g. for help listings. Defaults to `title`. */
commandTitle?: string
onTrigger: (prompt: { setValue: (value: string) => void }) => void
}
export type DialogPromptProps = {
title: string
size?: DialogSize
@@ -15,6 +24,7 @@ export type DialogPromptProps = {
value?: string
busy?: boolean
busyText?: string
actions?: DialogPromptAction[]
onConfirm?: (value: string) => void
onCancel?: () => void
}
@@ -32,6 +42,13 @@ export function DialogPrompt(props: DialogPromptProps) {
props.onConfirm?.(textarea.plainText)
}
const handle = {
setValue(value: string) {
textarea.setText(value)
textarea.gotoLineEnd()
},
}
Keymap.createLayer(() => ({
mode: "modal",
target: textareaTarget,
@@ -45,6 +62,12 @@ export function DialogPrompt(props: DialogPromptProps) {
group: "Dialog",
run: confirm,
},
...(props.actions ?? []).map((action) => ({
id: action.command,
title: action.commandTitle ?? action.title,
group: "Dialog",
run: () => action.onTrigger(handle),
})),
],
}))
@@ -125,7 +148,16 @@ export function DialogPrompt(props: DialogPromptProps) {
</box>
<box paddingBottom={1} flexDirection="row">
<Show when={!props.busy} fallback={<text fg={theme.text.muted}>processing…</text>}>
<DialogFooter actions={[{ title: "submit", command: "dialog.prompt.submit", onTrigger: confirm }]} />
<DialogFooter
actions={[
{ title: "submit", command: "dialog.prompt.submit", onTrigger: confirm },
...(props.actions ?? []).map((action) => ({
title: action.title,
command: action.command,
onTrigger: () => action.onTrigger(handle),
})),
]}
/>
</Show>
</box>
</box>
@@ -1,6 +1,6 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { InputRenderable } from "@opentui/core"
import { InputRenderable, TextareaRenderable } from "@opentui/core"
import { testRender } from "@opentui/solid"
import { usePromptMove } from "../../../src/component/prompt/move"
import { ConfigProvider } from "../../../src/config"
@@ -204,7 +204,7 @@ test("creating a worktree recovers the session without reading its removed locat
await fixture.app.waitForFrame((frame) => frame.includes("Worktrees") && frame.includes(linked))
fixture.app.mockInput.pressKey("a", { ctrl: true })
await fixture.app.waitForFrame((frame) => frame.includes("Name worktree"))
await fixture.app.waitFor(() => fixture.app.renderer.currentFocusedEditor instanceof InputRenderable)
await fixture.app.waitFor(() => fixture.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
await fixture.app.mockInput.typeText("fresh")
fixture.app.mockInput.pressEnter()
await fixture.app.waitFor(() => fixture.moves.length === 1)
@@ -431,7 +431,7 @@ async function renderMove(input: {
)
app.mockInput.pressKey("a", { ctrl: true })
await app.waitForFrame((frame) => frame.includes("Name worktree"))
await app.waitFor(() => app.renderer.currentFocusedEditor instanceof InputRenderable)
await app.waitFor(() => app.renderer.currentFocusedEditor instanceof TextareaRenderable)
await app.mockInput.typeText("fresh")
app.mockInput.pressEnter()
if (input.home) {