Compare commits

...
Author SHA1 Message Date
R44VC0RP 6c724f4b6b fix(tui): preserve sessions during server reconnects 2026-08-25 18:47:11 +00:00
2 changed files with 135 additions and 11 deletions
+16 -11
View File
@@ -1,4 +1,4 @@
import type { LocationGetOutput, LocationRef } from "@opencode-ai/client"
import { ClientError, type LocationGetOutput, type LocationRef } from "@opencode-ai/client"
import { createContext, createMemo, createSignal, onCleanup, useContext, type ParentProps } from "solid-js"
import { useClient } from "./client"
import { useData } from "./data"
@@ -28,16 +28,21 @@ export function LocationProvider(props: ParentProps) {
? undefined
: location
setError(undefined)
void data.location.sync(target).catch((cause) => {
const current = ref()
if (
generation !== attempt ||
current?.directory !== location.directory ||
current.workspaceID !== location.workspaceID
)
return
setError({ location, cause })
})
void data.location
.syncInfo(target)
.then(() => data.location.sync(target).catch(() => undefined))
.catch((cause) => {
const current = ref()
if (
generation !== attempt ||
current?.directory !== location.directory ||
current.workspaceID !== location.workspaceID ||
client.connection.status() !== "connected" ||
(cause instanceof ClientError && cause.reason === "Transport")
)
return
setError({ location, cause })
})
}
function set(location?: LocationRef) {
+119
View File
@@ -241,6 +241,125 @@ test("bootstraps MCP data for the TUI location", async () => {
}
})
test("does not report failed resource syncs as missing locations and retries after reconnecting", async () => {
const events = createEventStream()
let modelRequests = 0
let fail = false
const calls = createFetch((url) => {
if (url.pathname !== "/api/model") return
modelRequests++
if (fail) return json({ message: "server restarting" }, { status: 503 })
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
}, events)
let location!: ReturnType<typeof useLocation>
let data!: ReturnType<typeof useData>
function Probe() {
location = useLocation()
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<ClientProvider api={createApi(calls.fetch)}>
<DataProvider>
<Probe />
</DataProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => modelRequests === 1)
await Bun.sleep(30)
fail = true
data.location.invalidate()
location.set({ directory })
await wait(() => modelRequests === 2)
await Bun.sleep(30)
expect(location.error).toBeUndefined()
fail = false
events.disconnect()
await wait(() => modelRequests === 3)
expect(location.error).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
test("does not report transport failures as missing locations", async () => {
const events = createEventStream()
const unavailable = `${directory}/unavailable`
let requests = 0
const calls = createFetch((url) => {
if (url.pathname !== "/api/location" || url.searchParams.get("location[directory]") !== unavailable) return undefined
requests++
throw new Error("server restarting")
}, events)
let location!: ReturnType<typeof useLocation>
function Probe() {
location = useLocation()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<ClientProvider api={createApi(calls.fetch)}>
<DataProvider>
<Probe />
</DataProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => location.current !== undefined)
location.set({ directory: unavailable })
await wait(() => requests > 0)
await Bun.sleep(30)
expect(location.error).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
test("still reports failures from the location lookup itself", async () => {
const events = createEventStream()
const missing = `${directory}/missing`
const calls = createFetch((url) => {
if (url.pathname !== "/api/location" || url.searchParams.get("location[directory]") !== missing) return
return json({ message: "directory does not exist" }, { status: 500 })
}, events)
let location!: ReturnType<typeof useLocation>
function Probe() {
location = useLocation()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<ClientProvider api={createApi(calls.fetch)}>
<DataProvider>
<Probe />
</DataProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => location.current !== undefined)
location.set({ directory: missing })
await wait(() => location.error !== undefined)
expect(location.error?.location.directory).toBe(missing)
} finally {
app.renderer.destroy()
}
})
test("syncs MCP status when a connection settles during bootstrap", async () => {
const events = createEventStream()
let mcpRequests = 0