mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-09 22:25:02 +03:00
refactor(tui): build prompt-style dialogs on DialogPrompt (#55518)
This commit is contained in:
committed by
GitHub
Unverified
parent
6d570f1d92
commit
61bfcbdaff
@@ -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()),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user