refactor(form): model links as fields (#36129)

This commit is contained in:
Aiden Cline 2026-07-10 00:22:01 -05:00 committed by GitHub
commit a6449cb45c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2409 additions and 1430 deletions

View file

@ -77,16 +77,12 @@ function question(id: string, sessionID = "session"): QuestionRequest {
}
}
function form(
id: string,
sessionID = "session",
): Extract<OpenCodeEvent, { type: "form.created" }>["data"]["form"] {
function form(id: string, sessionID = "session"): Extract<OpenCodeEvent, { type: "form.created" }>["data"]["form"] {
return {
id,
sessionID,
title: "Input requested",
mode: "form",
fields: [],
fields: [{ key: "authorization", type: "external", url: "https://example.com" }],
}
}

View file

@ -12,6 +12,14 @@ import { createSessionRows, type SessionRow } from "../../../src/routes/session/
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
import { TestTuiContexts } from "../../fixture/tui-environment"
const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [
{
key: string
type: "external"
url: string
},
]
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
while (!fn()) {
@ -1510,7 +1518,7 @@ test("adds, dismisses, and refreshes form requests", async () => {
const calls = createFetch((url) => {
if (url.pathname !== "/api/session/ses_1/form") return
return json({
data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] }],
data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", fields: formFields }],
})
}, events)
let data!: ReturnType<typeof useData>
@ -1540,13 +1548,13 @@ test("adds, dismisses, and refreshes form requests", async () => {
id: "evt_form_created_1",
created: 0,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: formFields } },
})
emitEvent(events, {
id: "evt_form_created_duplicate",
created: 1,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: formFields } },
})
await wait(() => data.session.form.list("ses_1")?.length === 1)
@ -1562,7 +1570,7 @@ test("adds, dismisses, and refreshes form requests", async () => {
id: "evt_form_created_2",
created: 3,
type: "form.created",
data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", fields: formFields } },
})
emitEvent(events, {
id: "evt_form_cancelled_2",
@ -1612,7 +1620,7 @@ test("tracks global forms by location", async () => {
location: other,
type: "form.created",
data: {
form: { id: "frm_other", sessionID: "global", title: "Input requested", mode: "form", fields: [] },
form: { id: "frm_other", sessionID: "global", title: "Input requested", fields: formFields },
},
})
@ -1625,7 +1633,7 @@ test("tracks global forms by location", async () => {
location: { directory },
type: "form.created",
data: {
form: { id: "frm_default", sessionID: "global", title: "Input requested", mode: "form", fields: [] },
form: { id: "frm_default", sessionID: "global", title: "Input requested", fields: formFields },
},
})
await wait(() => data.session.form.list("global", { directory })?.length === 1)
@ -1664,8 +1672,7 @@ test("refreshes global forms for the requested location", async () => {
id: requestedDirectory === other.directory ? "frm_other" : "frm_default",
sessionID: "global",
title: "Input requested",
mode: "form",
fields: [],
fields: formFields,
},
],
})
@ -1743,8 +1750,7 @@ test("refreshes global forms once per loaded location after reconnect", async ()
id: `frm_${requestedDirectory === other.directory ? "other" : "default"}_${count}`,
sessionID: "global",
title: "Input requested",
mode: "form",
fields: [],
fields: formFields,
},
],
})
@ -1803,13 +1809,12 @@ test("refreshes global forms once per loaded location after reconnect", async ()
test("reconciles all pending form requests when the event stream reconnects", async () => {
const events = createEventStream()
let requests = [
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", mode: "form" as const, fields: [] },
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", fields: formFields },
{
id: "frm_keep",
sessionID: "ses_keep",
title: "Input requested",
mode: "url" as const,
url: "https://example.com",
fields: [{ key: "authorization", type: "external" as const, url: "https://example.com" }],
},
]
let calls = 0
@ -1841,7 +1846,7 @@ test("reconciles all pending form requests when the event stream reconnects", as
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", mode: "form" as const, fields: [] }]
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", fields: formFields }]
events.disconnect()
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")

View file

@ -0,0 +1,139 @@
/** @jsxImportSource @opentui/solid */
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 { ClipboardProvider } from "../../../src/context/clipboard"
import type { FormWithLocation } from "../../../src/context/data"
import { KVProvider } from "../../../src/context/kv"
import { SDKProvider } from "../../../src/context/sdk"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
import { ToastProvider } from "../../../src/ui/toast"
import { tmpdir } from "../../fixture/fixture"
import { TestTuiContexts } from "../../fixture/tui-environment"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk"
async function mountForm(root: string, width = 80) {
const state = path.join(root, "state")
await mkdir(state, { recursive: true })
await Bun.write(path.join(state, "kv.json"), "{}")
const replies: unknown[] = []
const copied: string[] = []
const events = createEventStream()
const transport = createFetch(
(url, request) =>
url.pathname === "/api/session/ses_test/form/frm_test/reply"
? request.json().then((answer) => {
replies.push(answer)
return new Response(null, { status: 204 })
})
: undefined,
events,
)
const config = createTuiResolvedConfig()
const form = {
id: "frm_test",
sessionID: "ses_test",
title: "Authorization required",
fields: [
{
key: "authorization",
type: "external",
url: "https://example.com/authorize",
title: "Authorize access",
},
],
} satisfies FormWithLocation
const { FormPrompt } = await import("../../../src/routes/session/form")
function Harness() {
const renderer = useRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
const off = registerOpencodeKeymap(keymap, renderer, config)
onCleanup(off)
return (
<TestTuiContexts
directory={root}
paths={{
home: root,
state,
worktree: root,
}}
>
<ClipboardProvider
value={{
write(text) {
copied.push(text)
return Promise.resolve()
},
}}
>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
<KVProvider>
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
<ToastProvider>
<FormPrompt form={form} />
</ToastProvider>
</ThemeProvider>
</KVProvider>
</SDKProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</ClipboardProvider>
</TestTuiContexts>
)
}
const app = await testRender(() => <Harness />, { width, height: 20, kittyKeyboard: true })
app.renderer.start()
await app.waitForFrame((frame) => frame.includes("Authorization required"))
return { app, copied, replies }
}
test("requires explicit acknowledgement before submitting an external field", async () => {
await using tmp = await tmpdir()
const prompt = await mountForm(tmp.path)
try {
prompt.app.mockInput.pressKey("right")
await prompt.app.waitForFrame((frame) => frame.includes("(acknowledgement required)"))
prompt.app.mockInput.pressEnter()
await prompt.app.waitForFrame((frame) => frame.includes("External action must be acknowledged"))
expect(prompt.replies).toEqual([])
prompt.app.mockInput.pressKey("left")
prompt.app.mockInput.pressKey("c")
await prompt.app.waitForFrame((frame) => frame.includes("press enter to confirm"))
expect(prompt.copied).toEqual(["https://example.com/authorize"])
expect(prompt.replies).toEqual([])
prompt.app.mockInput.pressEnter()
await prompt.app.waitForFrame((frame) => frame.includes("Acknowledged"))
expect(prompt.replies).toEqual([])
prompt.app.mockInput.pressEnter()
await prompt.app.waitFor(() => prompt.replies.length === 1)
expect(prompt.replies).toEqual([{ answer: { authorization: true } }])
} finally {
prompt.app.renderer.destroy()
}
})
test("includes external acknowledgements in progress", async () => {
await using tmp = await tmpdir()
const prompt = await mountForm(tmp.path, 32)
try {
expect(prompt.app.captureCharFrame()).toContain("0/1")
expect(prompt.replies).toEqual([])
} finally {
prompt.app.renderer.destroy()
}
})