mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 16:56:33 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1043615df2 | |||
| 96bfd4912d | |||
| 57dd2fba7d | |||
| 684d288b9c |
@@ -30,7 +30,7 @@ export type DataSessionStatus = "idle" | "running"
|
|||||||
|
|
||||||
const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_")
|
const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_")
|
||||||
|
|
||||||
export type FormInfo = FormFormInfo | FormUrlInfo
|
export type FormInfo = (FormFormInfo | FormUrlInfo) & { readonly location?: LocationRef }
|
||||||
|
|
||||||
type LocationData = {
|
type LocationData = {
|
||||||
agent?: AgentV2Info[]
|
agent?: AgentV2Info[]
|
||||||
@@ -56,7 +56,7 @@ type Data = {
|
|||||||
status: Record<string, DataSessionStatus>
|
status: Record<string, DataSessionStatus>
|
||||||
message: Record<string, SessionMessage[]>
|
message: Record<string, SessionMessage[]>
|
||||||
permission: Record<string, PermissionV2Request[]>
|
permission: Record<string, PermissionV2Request[]>
|
||||||
// Pending forms keyed by session ID.
|
// Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel.
|
||||||
form: Record<string, FormInfo[]>
|
form: Record<string, FormInfo[]>
|
||||||
}
|
}
|
||||||
project: {
|
project: {
|
||||||
@@ -592,7 +592,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id)) break
|
if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id)) break
|
||||||
setStore("session", "form", event.data.form.sessionID, [
|
setStore("session", "form", event.data.form.sessionID, [
|
||||||
...(store.session.form[event.data.form.sessionID] ?? []),
|
...(store.session.form[event.data.form.sessionID] ?? []),
|
||||||
mutable(event.data.form),
|
mutable({ ...event.data.form, location: event.location }),
|
||||||
])
|
])
|
||||||
break
|
break
|
||||||
case "form.replied":
|
case "form.replied":
|
||||||
@@ -723,10 +723,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
list(sessionID: string) {
|
list(sessionID: string, ref?: LocationRef) {
|
||||||
return store.session.form[sessionID]
|
const forms = store.session.form[sessionID]
|
||||||
|
if (sessionID !== "global") return forms
|
||||||
|
if (!ref) return
|
||||||
|
const key = locationKey(ref)
|
||||||
|
return forms?.filter((form) => form.location && locationKey(form.location) === key)
|
||||||
},
|
},
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string, ref?: LocationRef) {
|
||||||
|
if (sessionID === "global") {
|
||||||
|
const result = await sdk.api.form.listRequests({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
|
const location = { directory: result.location.directory, workspaceID: result.location.workspaceID }
|
||||||
|
const key = locationKey(location)
|
||||||
|
setStore("session", "form", sessionID, [
|
||||||
|
...(store.session.form[sessionID] ?? []).filter(
|
||||||
|
(form) => form.location && locationKey(form.location) !== key,
|
||||||
|
),
|
||||||
|
...mutable(
|
||||||
|
result.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location })),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
setStore("session", "form", sessionID, mutable(await sdk.api.form.list({ sessionID })))
|
setStore("session", "form", sessionID, mutable(await sdk.api.form.list({ sessionID })))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -871,7 +889,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
directory: defaultLocation().directory,
|
directory: defaultLocation().directory,
|
||||||
workspace: defaultLocation().workspaceID,
|
workspace: defaultLocation().workspaceID,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
setStore(
|
setStore(
|
||||||
"session",
|
"session",
|
||||||
"info",
|
"info",
|
||||||
@@ -880,6 +898,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
for (const session of response.data) registerSession(session.id)
|
for (const session of response.data) registerSession(session.id)
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(
|
||||||
|
new Map(
|
||||||
|
Object.values(store.session.info).map(
|
||||||
|
(session) => [locationKey(session.location), session.location] as const,
|
||||||
|
),
|
||||||
|
).values(),
|
||||||
|
(location) => result.session.form.refresh("global", location),
|
||||||
|
),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
result.location.refresh(),
|
result.location.refresh(),
|
||||||
result.location.agent.refresh(),
|
result.location.agent.refresh(),
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const tui: TuiPlugin = async (api) => {
|
|||||||
const permissions = new Set<string>()
|
const permissions = new Set<string>()
|
||||||
|
|
||||||
api.event.on("form.created", (event) => {
|
api.event.on("form.created", (event) => {
|
||||||
if (event.data.form.sessionID === "global") return
|
|
||||||
if (forms.has(event.data.form.id)) return
|
if (forms.has(event.data.form.id)) return
|
||||||
forms.add(event.data.form.id)
|
forms.add(event.data.form.id)
|
||||||
notify(api, event.data.form.sessionID, "Input needs response", "question")
|
notify(api, event.data.form.sessionID, "Input needs response", "question")
|
||||||
|
|||||||
@@ -130,6 +130,17 @@ function display(field: Field, value: FormValue | undefined) {
|
|||||||
return label(value)
|
return label(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestOptions(form: FormInfo) {
|
||||||
|
if (!form.location) return undefined
|
||||||
|
// Global forms have no session row, so the server needs their location to select the owning Form.Service.
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
"x-opencode-directory": encodeURIComponent(form.location.directory),
|
||||||
|
...(form.location.workspaceID ? { "x-opencode-workspace": form.location.workspaceID } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function FormPrompt(props: { form: FormInfo }) {
|
export function FormPrompt(props: { form: FormInfo }) {
|
||||||
return props.form.mode === "url" ? <UrlPrompt form={props.form} /> : <FieldsPrompt form={props.form} />
|
return props.form.mode === "url" ? <UrlPrompt form={props.form} /> : <FieldsPrompt form={props.form} />
|
||||||
}
|
}
|
||||||
@@ -154,7 +165,10 @@ function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) {
|
|||||||
title: "Dismiss form",
|
title: "Dismiss form",
|
||||||
category: "Form",
|
category: "Form",
|
||||||
run() {
|
run() {
|
||||||
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
|
void sdk.api.form.cancel(
|
||||||
|
{ sessionID: props.form.sessionID, formID: props.form.id },
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -172,7 +186,10 @@ function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) {
|
|||||||
desc: "Dismiss form",
|
desc: "Dismiss form",
|
||||||
group: "Form",
|
group: "Form",
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
|
void sdk.api.form.cancel(
|
||||||
|
{ sessionID: props.form.sessionID, formID: props.form.id },
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -328,11 +345,14 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
|
|||||||
|
|
||||||
function replySingle(field: Field, value: FormValue) {
|
function replySingle(field: Field, value: FormValue) {
|
||||||
sdk.api.form
|
sdk.api.form
|
||||||
.reply({
|
.reply(
|
||||||
sessionID: props.form.sessionID,
|
{
|
||||||
formID: props.form.id,
|
sessionID: props.form.sessionID,
|
||||||
answer: { [field.key]: value },
|
formID: props.form.id,
|
||||||
})
|
answer: { [field.key]: value },
|
||||||
|
},
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
setStore(
|
setStore(
|
||||||
"error",
|
"error",
|
||||||
@@ -532,7 +552,10 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
|
|||||||
group: "Form",
|
group: "Form",
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
if (textual()) {
|
if (textual()) {
|
||||||
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
|
void sdk.api.form.cancel(
|
||||||
|
{ sessionID: props.form.sessionID, formID: props.form.id },
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
@@ -594,7 +617,10 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
|
|||||||
title: "Dismiss form",
|
title: "Dismiss form",
|
||||||
category: "Form",
|
category: "Form",
|
||||||
run() {
|
run() {
|
||||||
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
|
void sdk.api.form.cancel(
|
||||||
|
{ sessionID: props.form.sessionID, formID: props.form.id },
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -638,16 +664,19 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
sdk.api.form
|
sdk.api.form
|
||||||
.reply({
|
.reply(
|
||||||
sessionID: props.form.sessionID,
|
{
|
||||||
formID: props.form.id,
|
sessionID: props.form.sessionID,
|
||||||
answer: Object.fromEntries(
|
formID: props.form.id,
|
||||||
fields().flatMap((field) => {
|
answer: Object.fromEntries(
|
||||||
const value = store.answers[field.key]
|
fields().flatMap((field) => {
|
||||||
return value === undefined ? [] : [[field.key, value] as const]
|
const value = store.answers[field.key]
|
||||||
}),
|
return value === undefined ? [] : [[field.key, value] as const]
|
||||||
),
|
}),
|
||||||
})
|
),
|
||||||
|
},
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
setStore(
|
setStore(
|
||||||
"error",
|
"error",
|
||||||
@@ -666,7 +695,10 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
|
|||||||
desc: "Dismiss form",
|
desc: "Dismiss form",
|
||||||
group: "Form",
|
group: "Form",
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
|
void sdk.api.form.cancel(
|
||||||
|
{ sessionID: props.form.sessionID, formID: props.form.id },
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ key: "up", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(-1) },
|
{ key: "up", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(-1) },
|
||||||
@@ -715,7 +747,10 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) {
|
|||||||
desc: "Dismiss form",
|
desc: "Dismiss form",
|
||||||
group: "Form",
|
group: "Form",
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id })
|
void sdk.api.form.cancel(
|
||||||
|
{ sessionID: props.form.sessionID, formID: props.form.id },
|
||||||
|
requestOptions(props.form),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...tuiConfig.keybinds.get("app.exit"),
|
...tuiConfig.keybinds.get("app.exit"),
|
||||||
|
|||||||
@@ -182,8 +182,10 @@ export function Session() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
const forms = createMemo(() => {
|
const forms = createMemo(() => {
|
||||||
if (session()?.parentID) return []
|
return [
|
||||||
return data.session.form.list(route.sessionID) ?? []
|
...(session()?.parentID ? [] : (data.session.form.list(route.sessionID) ?? [])),
|
||||||
|
...(data.session.form.list("global", location()) ?? []),
|
||||||
|
]
|
||||||
})
|
})
|
||||||
const [composer, setComposer] = createStore({
|
const [composer, setComposer] = createStore({
|
||||||
open: false,
|
open: false,
|
||||||
@@ -258,9 +260,11 @@ export function Session() {
|
|||||||
navigate({ type: "home" })
|
navigate({ type: "home" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await data.session.form.refresh("global", info.location)
|
||||||
|
if (route.sessionID !== sessionID) return
|
||||||
project.workspace.set(info.location.workspaceID)
|
project.workspace.set(info.location.workspaceID)
|
||||||
editor.reconnect(info.location.directory)
|
editor.reconnect(info.location.directory)
|
||||||
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
|
if (scroll) scroll.scrollBy(100_000)
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
if (route.sessionID !== sessionID) return
|
if (route.sessionID !== sessionID) return
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -929,12 +933,12 @@ export function Session() {
|
|||||||
<box flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
<Composer
|
<Composer
|
||||||
sessionID={route.sessionID}
|
sessionID={route.sessionID}
|
||||||
open={composer.open || !!session()?.parentID}
|
open={composer.open || (!!session()?.parentID && forms().length === 0)}
|
||||||
defaultTab={composer.tab ?? (session()?.parentID ? "subagents" : undefined)}
|
defaultTab={composer.tab ?? (session()?.parentID ? "subagents" : undefined)}
|
||||||
onClose={() => setComposer("open", false)}
|
onClose={() => setComposer("open", false)}
|
||||||
/>
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={composer.open || !!session()?.parentID}>{null}</Match>
|
<Match when={composer.open || (!!session()?.parentID && forms().length === 0)}>{null}</Match>
|
||||||
<Match when={permissions().length > 0}>
|
<Match when={permissions().length > 0}>
|
||||||
<PermissionPrompt request={permissions()[0]} directory={session()?.location.directory} />
|
<PermissionPrompt request={permissions()[0]} directory={session()?.location.directory} />
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ const formNotification: TuiAttentionNotifyInput = {
|
|||||||
sound: { name: "question", when: "always" },
|
sound: { name: "question", when: "always" },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalFormNotification: TuiAttentionNotifyInput = {
|
||||||
|
...formNotification,
|
||||||
|
title: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
const permissionNotification: TuiAttentionNotifyInput = {
|
const permissionNotification: TuiAttentionNotifyInput = {
|
||||||
title: "Demo session",
|
title: "Demo session",
|
||||||
message: "Permission needs input",
|
message: "Permission needs input",
|
||||||
@@ -173,12 +178,12 @@ describe("internal notifications TUI plugin", () => {
|
|||||||
expect(harness.notifications).toEqual([formNotification, questionNotification, permissionNotification])
|
expect(harness.notifications).toEqual([formNotification, questionNotification, permissionNotification])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("ignores global forms until the TUI can render them", async () => {
|
test("notifies for global forms once the TUI can render them", async () => {
|
||||||
const harness = await setup()
|
const harness = await setup()
|
||||||
|
|
||||||
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1", "global") } })
|
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1", "global") } })
|
||||||
|
|
||||||
expect(harness.notifications).toEqual([])
|
expect(harness.notifications).toEqual([globalFormNotification])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("dedupes pending forms, questions, and permissions until they are resolved", async () => {
|
test("dedupes pending forms, questions, and permissions until they are resolved", async () => {
|
||||||
|
|||||||
@@ -877,6 +877,196 @@ test("adds, dismisses, and refreshes form requests", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("tracks global forms by location", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
const calls = createFetch(undefined, events)
|
||||||
|
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(() => data.connection.status() === "connected")
|
||||||
|
events.emit({
|
||||||
|
id: "evt_form_created_global",
|
||||||
|
created: 0,
|
||||||
|
location: other,
|
||||||
|
type: "form.created",
|
||||||
|
data: { form: { id: "frm_other", sessionID: "global", mode: "form", fields: [] } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wait(() => data.session.form.list("global", other)?.length === 1)
|
||||||
|
expect(data.session.form.list("global", { directory }) ?? []).toEqual([])
|
||||||
|
|
||||||
|
events.emit({
|
||||||
|
id: "evt_form_created_global_default",
|
||||||
|
created: 1,
|
||||||
|
location: { directory },
|
||||||
|
type: "form.created",
|
||||||
|
data: { form: { id: "frm_default", sessionID: "global", mode: "form", fields: [] } },
|
||||||
|
})
|
||||||
|
await wait(() => data.session.form.list("global", { directory })?.length === 1)
|
||||||
|
|
||||||
|
events.emit({
|
||||||
|
id: "evt_form_replied_global",
|
||||||
|
created: 2,
|
||||||
|
location: other,
|
||||||
|
type: "form.replied",
|
||||||
|
data: { id: "frm_other", sessionID: "global", answer: {} },
|
||||||
|
})
|
||||||
|
await wait(() => data.session.form.list("global", other)?.length === 0)
|
||||||
|
expect(data.session.form.list("global", { directory })?.map((form) => form.id)).toEqual(["frm_default"])
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("refreshes global forms for the requested location", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
const requests: URL[] = []
|
||||||
|
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname !== "/api/form/request") return
|
||||||
|
requests.push(url)
|
||||||
|
const requestedDirectory = url.searchParams.get("location[directory]") ?? directory
|
||||||
|
const requestedWorkspace = url.searchParams.get("location[workspace]") ?? undefined
|
||||||
|
return json({
|
||||||
|
location: {
|
||||||
|
directory: requestedDirectory,
|
||||||
|
workspaceID: requestedWorkspace,
|
||||||
|
project: { id: "proj_test", directory: requestedDirectory },
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: requestedDirectory === other.directory ? "frm_other" : "frm_default",
|
||||||
|
sessionID: "global",
|
||||||
|
mode: "form",
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}, events)
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(() => data.connection.status() === "connected")
|
||||||
|
await data.session.form.refresh("global", { directory })
|
||||||
|
await data.session.form.refresh("global", other)
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(2)
|
||||||
|
expect(requests[1]?.searchParams.get("location[directory]")).toBe(other.directory)
|
||||||
|
expect(requests[1]?.searchParams.get("location[workspace]")).toBe(other.workspaceID)
|
||||||
|
expect(data.session.form.list("global", other)?.map((form) => form.id)).toEqual(["frm_other"])
|
||||||
|
expect(data.session.form.list("global", { directory })?.map((form) => form.id)).toEqual(["frm_default"])
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("refreshes global forms once per loaded location after reconnect", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
const requests: URL[] = []
|
||||||
|
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname === "/api/session" && url.searchParams.has("parentID")) return json({ data: [], cursor: {} })
|
||||||
|
if (url.pathname === "/api/session")
|
||||||
|
return json({
|
||||||
|
data: [
|
||||||
|
{ id: "ses_default", title: "Default", location: { directory }, time: { created: 0, updated: 0 } },
|
||||||
|
{ id: "ses_other_1", title: "Other one", location: other, time: { created: 0, updated: 0 } },
|
||||||
|
{ id: "ses_other_2", title: "Other two", location: other, time: { created: 0, updated: 0 } },
|
||||||
|
],
|
||||||
|
cursor: {},
|
||||||
|
})
|
||||||
|
if (url.pathname !== "/api/form/request") return
|
||||||
|
requests.push(url)
|
||||||
|
const requestedDirectory = url.searchParams.get("location[directory]") ?? directory
|
||||||
|
const requestedWorkspace = url.searchParams.get("location[workspace]") ?? undefined
|
||||||
|
return json({
|
||||||
|
location: {
|
||||||
|
directory: requestedDirectory,
|
||||||
|
workspaceID: requestedWorkspace,
|
||||||
|
project: { id: "proj_test", directory: requestedDirectory },
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
|
}, events)
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(() => requests.length === 2)
|
||||||
|
await Bun.sleep(20)
|
||||||
|
requests.length = 0
|
||||||
|
|
||||||
|
events.disconnect()
|
||||||
|
|
||||||
|
await wait(() => requests.length === 2, 4000)
|
||||||
|
await Bun.sleep(20)
|
||||||
|
expect(requests).toHaveLength(2)
|
||||||
|
expect(
|
||||||
|
requests.map((url) => [
|
||||||
|
url.searchParams.get("location[directory]") ?? directory,
|
||||||
|
url.searchParams.get("location[workspace]") ?? undefined,
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
|
[directory, undefined],
|
||||||
|
[other.directory, other.workspaceID],
|
||||||
|
])
|
||||||
|
expect(data.connection.status()).toBe("connected")
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("settles pending tools when a live failure arrives", async () => {
|
test("settles pending tools when a live failure arrives", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch((url) => {
|
const calls = createFetch((url) => {
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
|||||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||||
if (url.pathname === "/api/mcp")
|
if (url.pathname === "/api/mcp")
|
||||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||||
|
if (url.pathname === "/api/form/request")
|
||||||
|
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||||
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
||||||
if (url.pathname === "/api/session/active") return json({ data: {}, watermarks: {} })
|
if (url.pathname === "/api/session/active") return json({ data: {}, watermarks: {} })
|
||||||
if (/^\/api\/session\/[^/]+\/form$/.test(url.pathname)) return json({ data: [] })
|
if (/^\/api\/session\/[^/]+\/form$/.test(url.pathname)) return json({ data: [] })
|
||||||
|
|||||||
Reference in New Issue
Block a user