Compare commits

...
Author SHA1 Message Date
Test 44f4211357 feat(tui): route pasted question answers 2026-06-24 13:58:21 +00:00
2 changed files with 97 additions and 2 deletions
+15 -2
View File
@@ -1,7 +1,7 @@
import { createStore } from "solid-js/store"
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
import { useRenderer } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core"
import { usePaste, useRenderer } from "@opentui/solid"
import { decodePasteBytes, type TextareaRenderable } from "@opentui/core"
import { selectedForeground, tint, useTheme } from "../../context/theme"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
import { useSDK } from "../../context/sdk"
@@ -130,6 +130,19 @@ export function QuestionPrompt(props: { request: QuestionRequest; directory?: st
onCleanup(popMode)
})
usePaste((event) => {
if (store.editing) return
// Question options do not own a text input. Capture bracketed paste at the
// renderer level and use it to start editing the custom answer.
event.preventDefault()
if (confirm() || !custom() || !decodePasteBytes(event.bytes).trim()) return
setStore("selected", options().length)
setStore("editing", true)
queueMicrotask(() => textarea?.handlePaste(event))
})
useBindings(() => ({
mode: QUESTION_MODE,
enabled: store.editing && !confirm(),
@@ -0,0 +1,82 @@
/** @jsxImportSource @opentui/solid */
import { TextareaRenderable } from "@opentui/core"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { testRender, useRenderer } from "@opentui/solid"
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
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 { TestTuiContexts } from "../../fixture/tui-environment"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { TuiConfigProvider } from "../../../src/config"
import { KVProvider } from "../../../src/context/kv"
import { SDKProvider } from "../../../src/context/sdk"
import { ThemeProvider } from "../../../src/context/theme"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
import { QuestionPrompt } from "../../../src/routes/session/question"
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)
}
}
test("pasting while selecting starts a custom answer", async () => {
await using tmp = await tmpdir()
const state = path.join(tmp.path, "state")
await mkdir(state, { recursive: true })
await Bun.write(path.join(state, "kv.json"), "{}")
const request = {
id: "question-1",
sessionID: "session-1",
questions: [
{
header: "Approach",
question: "How should this be implemented?",
options: [{ label: "Suggested", description: "Use the suggested approach" }],
},
],
} satisfies QuestionRequest
function Harness() {
const renderer = useRenderer()
const config = createTuiResolvedConfig()
const keymap = createDefaultOpenTuiKeymap(renderer)
const off = registerOpencodeKeymap(keymap, renderer, config)
onCleanup(off)
return (
<TestTuiContexts directory={tmp.path} paths={{ state }}>
<SDKProvider url="http://test" events={{ subscribe: async () => () => {} }}>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
<QuestionPrompt request={request} />
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</SDKProvider>
</TestTuiContexts>
)
}
const app = await testRender(() => <Harness />, { width: 80, height: 20 })
try {
await wait(() => app.captureCharFrame().includes("How should this be implemented?"))
await app.mockInput.pasteBracketedText("Use the existing state machine")
await wait(() => app.renderer.currentFocusedEditor instanceof TextareaRenderable)
const textarea = app.renderer.currentFocusedEditor
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused custom answer textarea")
expect(textarea.plainText).toBe("Use the existing state machine")
} finally {
app.renderer.destroy()
}
})