/** @jsxImportSource @opentui/solid */ import { TextareaRenderable } from "@opentui/core" import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" import { testRender, useRenderer } from "@opentui/solid" import { expect, test } from "bun:test" import { mkdir } from "node:fs/promises" import path from "node:path" import { onCleanup } from "solid-js" import { tmpdir } from "../../fixture/fixture" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" import type { TuiKeybind } from "../../../src/cli/cmd/tui/config/keybind" async function wait(fn: () => boolean, timeout = 2000) { const start = Date.now() while (!fn()) { if (Date.now() - start > timeout) throw new Error("timed out waiting for condition") await Bun.sleep(10) } } async function mountPrompt(input: { root: string keybinds: Partial onConfirm: (value: string) => void }) { const { Global } = await import("@opencode-ai/core/global") const previous = { config: Global.Path.config, state: Global.Path.state, } Global.Path.config = path.join(input.root, "config") Global.Path.state = path.join(input.root, "state") await mkdir(Global.Path.config, { recursive: true }) await mkdir(Global.Path.state, { recursive: true }) await Bun.write(path.join(Global.Path.state, "kv.json"), "{}") const [ { DialogProvider }, { DialogPrompt }, { KVProvider }, { ThemeProvider }, { TuiConfigProvider }, { ToastProvider }, { OpencodeKeymapProvider, registerOpencodeKeymap }, ] = await Promise.all([ import("../../../src/cli/cmd/tui/ui/dialog"), import("../../../src/cli/cmd/tui/ui/dialog-prompt"), import("../../../src/cli/cmd/tui/context/kv"), import("../../../src/cli/cmd/tui/context/theme"), import("../../../src/cli/cmd/tui/context/tui-config"), import("../../../src/cli/cmd/tui/ui/toast"), import("../../../src/cli/cmd/tui/keymap"), ]) function Harness() { const renderer = useRenderer() const keymap = createDefaultOpenTuiKeymap(renderer) const resolvedConfig = createTuiResolvedConfig({ keybinds: input.keybinds, leader_timeout: 1000, }) const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig) onCleanup(off) return ( ) } const app = await testRender(() => , { kittyKeyboard: true }) return { app, async cleanup() { app.renderer.destroy() Global.Path.config = previous.config Global.Path.state = previous.state }, } } test("dialog prompt submit wins when return is also input newline", async () => { await using tmp = await tmpdir() const confirmed: string[] = [] const prompt = await mountPrompt({ root: tmp.path, keybinds: { input_submit: "super+return", input_newline: "return,shift+return,alt+return,ctrl+j", }, onConfirm: (value) => confirmed.push(value), }) try { await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable) const textarea = prompt.app.renderer.currentFocusedEditor if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea") prompt.app.mockInput.pressEnter() expect(confirmed).toEqual(["draft"]) expect(textarea.plainText).toBe("draft") } finally { await prompt.cleanup() } }) test("dialog prompt submit can be rebound separately from input submit", async () => { await using tmp = await tmpdir() const confirmed: string[] = [] const prompt = await mountPrompt({ root: tmp.path, keybinds: { input_submit: "return", "dialog.prompt.submit": "ctrl+y", }, onConfirm: (value) => confirmed.push(value), }) try { await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable) const textarea = prompt.app.renderer.currentFocusedEditor if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea") prompt.app.mockInput.pressEnter() expect(confirmed).toEqual([]) expect(textarea.plainText).toBe("draft") prompt.app.mockInput.pressKey("y", { ctrl: true }) expect(confirmed).toEqual(["draft"]) } finally { await prompt.cleanup() } })