Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../component/border"
import { useTuiConfig } from "../../context/tui-config"
import { useBindings, useOpencodeModeStack } from "../../keymap"
import { useProject } from "../../context/project"

const QUESTION_MODE = "question"

export function QuestionPrompt(props: { request: QuestionRequest }) {
const sdk = useSDK()
const project = useProject()
Comment on lines 16 to +17
const { theme } = useTheme()
const renderer = useRenderer()
const tuiConfig = useTuiConfig()
Expand Down Expand Up @@ -50,12 +52,14 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
void sdk.client.question.reply({
requestID: props.request.id,
answers,
workspace: project.workspace.current(),
})
Comment on lines 52 to 56
}

function reject() {
void sdk.client.question.reject({
requestID: props.request.id,
workspace: project.workspace.current(),
})
Comment on lines 60 to 63
}

Expand All @@ -72,6 +76,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
void sdk.client.question.reply({
requestID: props.request.id,
answers: [[answer]],
workspace: project.workspace.current(),
})
Comment on lines 76 to 80
return
}
Expand Down
267 changes: 267 additions & 0 deletions packages/opencode/test/cli/tui/question-prompt-routing.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/** @jsxImportSource @opentui/solid */
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test"
import { Global } from "@opencode-ai/core/global"
import { onCleanup, onMount } from "solid-js"
import { mkdir } from "node:fs/promises"
import path from "node:path"
import { ProjectProvider, useProject } from "../../../src/cli/cmd/tui/context/project"
import { SDKProvider } from "../../../src/cli/cmd/tui/context/sdk"
import { KVProvider } from "../../../src/cli/cmd/tui/context/kv"
import { ThemeProvider } from "../../../src/cli/cmd/tui/context/theme"
import { TuiConfigProvider } from "../../../src/cli/cmd/tui/context/tui-config"
import {
OpencodeKeymapProvider,
registerOpencodeKeymap,
} from "../../../src/cli/cmd/tui/keymap"
import { QuestionPrompt } from "../../../src/cli/cmd/tui/routes/session/question"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { createFetch, directory, json } from "../../fixture/tui-sdk"
import type { QuestionRequest } from "@opencode-ai/sdk/v2"

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)
}
}

const request: QuestionRequest = {
id: "que_test",
sessionID: "ses_test",
questions: [
{
header: "Routing",
question: "Pick one",
options: [
{
label: "A",
description: "First option",
},
],
custom: false,
},
],
}

const multipleRequest: QuestionRequest = {
id: "que_test",
sessionID: "ses_test",
questions: [
{
header: "Routing",
question: "Pick many",
options: [
{
label: "A",
description: "First option",
},
{
label: "B",
description: "Second option",
},
],
multiple: true,
custom: false,
},
],
}

test("question prompt replies through the active workspace", async () => {
await mkdir(Global.Path.state, { recursive: true })
await Bun.write(path.join(Global.Path.state, "kv.json"), "{}")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f2cef90: the tests now use a temporary Global.Path.state per test and restore the previous value in finally.

Re-verified in the container:

bun test test/cli/tui/question-prompt-routing.test.tsx test/cli/tui/use-event.test.tsx test/cli/cmd/tui/sync.test.tsx
bun typecheck
git diff --check


const replies: URL[] = []
const calls = createFetch((url) => {
if (url.pathname === "/question/que_test/reply") {
replies.push(url)
return json(true)
}
})
Comment on lines +78 to +83

let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})

function Harness() {
const renderer = useRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
const config = createTuiResolvedConfig()
const off = registerOpencodeKeymap(keymap, renderer, config)
onCleanup(off)

return (
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch}>
<ProjectProvider>
<Probe onReady={ready} />
<QuestionPrompt request={request} />
</ProjectProvider>
</SDKProvider>
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
)
}

function Probe(props: { onReady: () => void }) {
const project = useProject()
onMount(async () => {
await project.sync()
project.workspace.set("ws_question")
props.onReady()
})
return <box />
}

const app = await testRender(() => <Harness />, { kittyKeyboard: true })
try {
await mounted

app.mockInput.pressEnter()
await wait(() => replies.length === 1)

expect(replies[0]?.searchParams.get("workspace")).toBe("ws_question")
} finally {
app.renderer.destroy()
}
})

test("multiple question prompt replies through the active workspace", async () => {
await mkdir(Global.Path.state, { recursive: true })
await Bun.write(path.join(Global.Path.state, "kv.json"), "{}")

const replies: URL[] = []
const calls = createFetch((url) => {
if (url.pathname === "/question/que_test/reply") {
replies.push(url)
return json(true)
}
})

let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})

function Harness() {
const renderer = useRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
const config = createTuiResolvedConfig()
const off = registerOpencodeKeymap(keymap, renderer, config)
onCleanup(off)

return (
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch}>
<ProjectProvider>
<Probe onReady={ready} />
<QuestionPrompt request={multipleRequest} />
</ProjectProvider>
</SDKProvider>
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
)
}

function Probe(props: { onReady: () => void }) {
const project = useProject()
onMount(async () => {
await project.sync()
project.workspace.set("ws_question")
props.onReady()
})
return <box />
}

const app = await testRender(() => <Harness />, { kittyKeyboard: true })
try {
await mounted

app.mockInput.pressEnter()
app.mockInput.pressKey("l")
app.mockInput.pressEnter()
await wait(() => replies.length === 1)

expect(replies[0]?.searchParams.get("workspace")).toBe("ws_question")
} finally {
app.renderer.destroy()
}
})

test("question prompt rejects through the active workspace", async () => {
await mkdir(Global.Path.state, { recursive: true })
await Bun.write(path.join(Global.Path.state, "kv.json"), "{}")

const rejects: URL[] = []
const calls = createFetch((url) => {
if (url.pathname === "/question/que_test/reject") {
rejects.push(url)
return json(true)
}
})

let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})

function Harness() {
const renderer = useRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
const config = createTuiResolvedConfig()
const off = registerOpencodeKeymap(keymap, renderer, config)
onCleanup(off)

return (
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch}>
<ProjectProvider>
<Probe onReady={ready} />
<QuestionPrompt request={request} />
</ProjectProvider>
</SDKProvider>
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
)
}

function Probe(props: { onReady: () => void }) {
const project = useProject()
onMount(async () => {
await project.sync()
project.workspace.set("ws_question")
props.onReady()
})
return <box />
}

const app = await testRender(() => <Harness />, { kittyKeyboard: true })
try {
await mounted

app.mockInput.pressEscape()
await wait(() => rejects.length === 1)

expect(rejects[0]?.searchParams.get("workspace")).toBe("ws_question")
} finally {
app.renderer.destroy()
}
})
Loading