diff --git a/packages/tui/src/component/dialog-worktree-name.tsx b/packages/tui/src/component/dialog-worktree-name.tsx index 4d54c45208a..889d2508e89 100644 --- a/packages/tui/src/component/dialog-worktree-name.tsx +++ b/packages/tui/src/component/dialog-worktree-name.tsx @@ -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() - 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 ( - - - { - 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} - /> - - - - + props.onConfirm(slugify(value) || Slug.create())} + actions={[ + { + title: "generate one", + command: "dialog.worktree.generate", + commandTitle: "Generate worktree name", + onTrigger: (prompt) => prompt.setValue(Slug.create()), + }, + ]} + /> ) } diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx index 3b4e038e2c0..c7759adba18 100644 --- a/packages/tui/src/ui/dialog-prompt.tsx +++ b/packages/tui/src/ui/dialog-prompt.tsx @@ -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) { processing…}> - + ({ + title: action.title, + command: action.command, + onTrigger: () => action.onTrigger(handle), + })), + ]} + /> diff --git a/packages/tui/test/cli/tui/prompt-move.test.tsx b/packages/tui/test/cli/tui/prompt-move.test.tsx index 159ef16a7a7..ff9feef2522 100644 --- a/packages/tui/test/cli/tui/prompt-move.test.tsx +++ b/packages/tui/test/cli/tui/prompt-move.test.tsx @@ -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) {