refactor(tui): remove legacy sdk client

This commit is contained in:
Dax Raad 2026-07-13 16:12:58 -04:00
commit 36f8cb7054
36 changed files with 316 additions and 510 deletions

View file

@ -1,14 +1,19 @@
import { describe, expect, test } from "bun:test"
import Notifications from "../../../../src/feature-plugins/system/notifications"
import type { OpenCodeEvent, PermissionAsked, QuestionAsked } from "@opencode-ai/client"
import type { Session } from "@opencode-ai/sdk/v2"
import type { TuiAttentionNotifyInput } from "@opencode-ai/plugin/tui"
import type { TuiAttentionNotifyInput, TuiPluginApi } from "@opencode-ai/plugin/tui"
import { createTuiPluginApi } from "../../../fixture/tui-plugin"
type Session = NonNullable<ReturnType<TuiPluginApi["state"]["session"]["get"]>>
async function setup() {
const notifications: TuiAttentionNotifyInput[] = []
const handlers = new Map<OpenCodeEvent["type"], ((event: OpenCodeEvent) => void)[]>()
const session = (id: string, title: string, parentID?: string): Session => ({
const session = (
id: string,
title: string,
parentID?: string,
): Session => ({
id,
title,
slug: id,

View file

@ -6,10 +6,10 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
import { EventV2 } from "@opencode-ai/core/event"
import { onMount } from "solid-js"
import { ProjectProvider } from "../../../src/context/project"
import { SDKProvider, useSDK } from "../../../src/context/sdk"
import { ClientProvider, useClient } from "../../../src/context/client"
import { DataProvider, useData } from "../../../src/context/data"
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
import { TestTuiContexts } from "../../fixture/tui-environment"
const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [
@ -90,13 +90,13 @@ test("refreshes resources into reactive getters", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -148,13 +148,13 @@ test("applies absolute usage events to session info", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -233,13 +233,13 @@ test("truncates committed revert messages without changing lifetime usage", asyn
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -371,13 +371,13 @@ test("updates session location when moved", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -430,13 +430,13 @@ test("restores running manual compaction before applying live deltas", async ()
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -514,23 +514,23 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
function Probe() {
data = useData()
sdk = useSDK()
client = useClient()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -539,15 +539,15 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
await wait(() => data.session.status("session-stale") === "running")
await data.session.message.refresh("session-stale")
expect(data.session.message.get("session-stale", "message-stale")?.id).toBe("message-stale")
expect(sdk.connection.status()).toBe("connected")
expect(sdk.connection.attempt()).toBe(0)
expect(client.connection.status()).toBe("connected")
expect(client.connection.attempt()).toBe(0)
events.disconnect()
await wait(() => sdk.connection.status() === "reconnecting")
expect(sdk.connection.attempt()).toBe(1)
expect(sdk.connection.error()).toBe("Event stream disconnected")
await wait(() => client.connection.status() === "reconnecting")
expect(client.connection.attempt()).toBe(1)
expect(client.connection.error()).toBe("Event stream disconnected")
await wait(() => requests.active === 2 && sdk.connection.status() === "connected", 4000)
await wait(() => requests.active === 2 && client.connection.status() === "connected", 4000)
resolveActive(json({ data: { "session-new": { type: "running" } } }))
await wait(() => data.location.model.list()?.[0]?.id === "model-2", 4000)
@ -565,9 +565,9 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
await wait(() => data.session.status("session-new") === "running")
expect(requests.event).toBe(2)
expect(requests.message).toBe(2)
expect(sdk.connection.status()).toBe("connected")
expect(sdk.connection.attempt()).toBe(0)
expect(sdk.connection.error()).toBeUndefined()
expect(client.connection.status()).toBe("connected")
expect(client.connection.attempt()).toBe(0)
expect(client.connection.error()).toBeUndefined()
} finally {
app.renderer.destroy()
}
@ -588,13 +588,13 @@ test("completes exploration when a queued prompt is promoted", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -668,13 +668,13 @@ test("removes committed revert messages from local state", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -733,34 +733,34 @@ test("distinguishes initial connection from reconnection", async () => {
const calls = createFetch((url) => {
if (url.pathname === "/api/event") return eventResponse()
})
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
function Probe() {
sdk = useSDK()
client = useClient()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => stream !== undefined)
expect(sdk.connection.status()).toBe("connecting")
expect(client.connection.status()).toBe("connecting")
connect()
await wait(() => sdk.connection.status() === "connected")
await wait(() => client.connection.status() === "connected")
disconnect()
await wait(() => sdk.connection.status() === "reconnecting")
await wait(() => client.connection.status() === "reconnecting")
} finally {
app.renderer.destroy()
}
@ -811,13 +811,13 @@ test("tracks session status from active sessions and execution events", async ()
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1180,13 +1180,13 @@ test("restores queued compaction from durable pending input", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1295,13 +1295,13 @@ test("refreshes integrations after integration updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1351,13 +1351,13 @@ test("refreshes MCP resources after catalog updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1400,13 +1400,13 @@ test("refreshes effective catalog data after catalog updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<box />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1448,13 +1448,13 @@ test("refreshes agents after agent updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1492,13 +1492,13 @@ test("refreshes references after updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1547,13 +1547,13 @@ test("keeps shell state scoped to location", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1593,28 +1593,28 @@ test("adds and dismisses permission requests from live events", async () => {
const events = createEventStream()
const calls = createFetch(undefined, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
function Probe() {
data = useData()
sdk = useSDK()
client = useClient()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected")
await wait(() => client.connection.status() === "connected")
emitEvent(events, {
id: "evt_permission_asked_1",
created: 0,
@ -1681,13 +1681,13 @@ test("reconciles all pending permission requests when the event stream reconnect
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(fetch.fetch)} api={createApi(fetch.fetch)}>
<ClientProvider api={createApi(fetch.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -1715,28 +1715,28 @@ test("adds, dismisses, and refreshes form requests", async () => {
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
function Probe() {
data = useData()
sdk = useSDK()
client = useClient()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected")
await wait(() => client.connection.status() === "connected")
emitEvent(events, {
id: "evt_form_created_1",
created: 0,
@ -1785,28 +1785,28 @@ test("tracks global forms by location", async () => {
const calls = createFetch(undefined, events)
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
function Probe() {
data = useData()
sdk = useSDK()
client = useClient()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected")
await wait(() => client.connection.status() === "connected")
events.emit({
id: "evt_form_created_global_other",
created: 0,
@ -1871,28 +1871,28 @@ test("refreshes global forms for the requested location", async () => {
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
function Probe() {
data = useData()
sdk = useSDK()
client = useClient()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected" && requests.length > 0)
await wait(() => client.connection.status() === "connected" && requests.length > 0)
requests.length = 0
await data.session.form.refresh("global", { directory })
@ -1957,13 +1957,13 @@ test("refreshes global forms once per loaded location after reconnect", async ()
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -2025,13 +2025,13 @@ test("reconciles all pending form requests when the event stream reconnects", as
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(fetch.fetch)} api={createApi(fetch.fetch)}>
<ClientProvider api={createApi(fetch.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -2078,13 +2078,13 @@ test("settles pending tools when a live failure arrives", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -2225,13 +2225,13 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -2318,13 +2318,13 @@ test("projects live instruction updates with their message ID", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
@ -2387,13 +2387,13 @@ async function mountData(parents: Record<string, string>, costs: Record<string,
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ClientProvider api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
await mounted

View file

@ -4,17 +4,16 @@ import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { DiffRenderable, type Renderable, ScrollBoxRenderable } from "@opentui/core"
import { testRender, useRenderer } from "@opentui/solid"
import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
import type { Session } from "@opencode-ai/sdk/v2"
import { ThemeProvider } from "../../../src/context/theme"
import { ConfigProvider } from "../../../src/config"
import { SDKProvider } from "../../../src/context/sdk"
import { ClientProvider } from "../../../src/context/client"
import { TuiKeybind } from "../../../src/config/keybind"
import { OpencodeKeymapProvider } from "../../../src/keymap"
import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { TestTuiContexts } from "../../fixture/tui-environment"
import { createApi, createClient, createEventStream, createFetch, json } from "../../fixture/tui-sdk"
import { createApi, createEventStream, createFetch, json } from "../../fixture/tui-client"
test("closing the diff viewer returns to the route it opened from", async () => {
const viewer = await renderDiffViewer([])
@ -111,7 +110,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
let current = initialRoute ?? startRoute
let renderDiff: TuiRouteDefinition["render"] | undefined
let vcsDiffInput: unknown
let sessionDiffInput: unknown
const config = createTuiResolvedConfig()
const transport = createFetch((url) => {
if (url.pathname !== "/api/vcs/diff") return
@ -135,14 +133,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
}
const base = createTuiPluginApi({
keymap,
client: {
session: {
diff: async (input: unknown) => {
sessionDiffInput = input
return { data: [] }
},
},
} as unknown as TuiPluginApi["client"],
state: {
session: {
get: () => session,
@ -170,7 +160,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
return (
<TestTuiContexts>
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
<ClientProvider api={createApi(transport.fetch)}>
<OpencodeKeymapProvider keymap={keymap}>
<ConfigProvider config={config}>
<ThemeProvider mode="dark">
@ -178,7 +168,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
</ThemeProvider>
</ConfigProvider>
</OpencodeKeymapProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
)
}
@ -190,7 +180,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
commands,
current: () => current,
vcsDiffInput: () => vcsDiffInput,
sessionDiffInput: () => sessionDiffInput,
}
}
@ -217,7 +206,7 @@ const session = {
created: 0,
updated: 0,
},
} satisfies Session
} satisfies NonNullable<ReturnType<TuiPluginApi["state"]["session"]["get"]>>
test("branch diff source requests branch VCS diff", async () => {
const viewer = await renderDiffViewer([], 20, {
@ -234,24 +223,6 @@ test("branch diff source requests branch VCS diff", async () => {
mode: "branch",
context: "12",
})
expect(viewer.sessionDiffInput()).toBeUndefined()
} finally {
viewer.app.renderer.destroy()
}
})
test("last-turn diff source requests session diff", async () => {
const viewer = await renderDiffViewer([], 20, {
name: "diff",
params: { mode: "last-turn", sessionID: "session-1", messageID: "message-1", returnRoute: startRoute },
})
try {
expect(viewer.current()).toEqual({
name: "diff",
params: { mode: "last-turn", sessionID: "session-1", messageID: "message-1", returnRoute: startRoute },
})
expect(viewer.sessionDiffInput()).toEqual({ sessionID: "session-1", messageID: "message-1" })
expect(viewer.vcsDiffInput()).toBeUndefined()
} finally {
viewer.app.renderer.destroy()
}

View file

@ -7,7 +7,7 @@ import path from "node:path"
import { onCleanup } from "solid-js"
import { ClipboardProvider } from "../../../src/context/clipboard"
import type { FormWithLocation } from "../../../src/context/data"
import { SDKProvider } from "../../../src/context/sdk"
import { ClientProvider } from "../../../src/context/client"
import { ThemeProvider } from "../../../src/context/theme"
import { ConfigProvider } from "../../../src/config"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
@ -15,7 +15,7 @@ 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"
import { createApi, createEventStream, createFetch } from "../../fixture/tui-client"
async function mountForm(root: string, width = 80) {
const state = path.join(root, "state")
@ -75,13 +75,13 @@ async function mountForm(root: string, width = 80) {
>
<OpencodeKeymapProvider keymap={keymap}>
<ConfigProvider config={config}>
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
<ClientProvider api={createApi(transport.fetch)}>
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
<ToastProvider>
<FormPrompt form={form} />
</ToastProvider>
</ThemeProvider>
</SDKProvider>
</ClientProvider>
</ConfigProvider>
</OpencodeKeymapProvider>
</ClipboardProvider>

View file

@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test"
// Before the fix, two concurrent `submit()` calls (e.g. a double-pressed
// Enter, or the input's native onSubmit racing another dispatch) each
// passed the `if (!store.prompt.text) return false` guard, each
// `await sdk.client.session.create(...)`, and each only captured
// `await client.api.session.create(...)`, and each only captured
// `inputText = store.prompt.text` AFTER that await. The first invocation
// finished, sent the prompt, and cleared the store; the second invocation,
// now past its await, read the cleared store and sent an empty prompt to a

View file

@ -2,12 +2,11 @@
import { describe, expect, test } from "bun:test"
import type { OpenCodeClient, OpenCodeEvent } from "@opencode-ai/client"
import { testRender } from "@opentui/solid"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import { onMount } from "solid-js"
import { ProjectProvider, useProject } from "../../../src/context/project"
import { SDKProvider, useSDK } from "../../../src/context/sdk"
import { ClientProvider, useClient } from "../../../src/context/client"
import { useEvent } from "../../../src/context/event"
import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk"
import { createApi, createEventStream, createFetch } from "../../fixture/tui-client"
import { TestTuiContexts } from "../../fixture/tui-environment"
import type { LogLevel, LogSink } from "../../../src/context/log"
@ -54,7 +53,7 @@ function update(version: string): OpenCodeEvent {
}
async function mount(
reconnect?: (attempt: number) => Promise<{ client: OpencodeClient; api: OpenCodeClient }>,
reconnect?: (attempt: number) => Promise<{ api: OpenCodeClient }>,
log?: LogSink,
) {
const events = createEventStream()
@ -62,7 +61,7 @@ async function mount(
const seen: OpenCodeEvent[] = []
const workspaces: Array<string | undefined> = []
let project!: ReturnType<typeof useProject>
let sdk!: ReturnType<typeof useSDK>
let client!: ReturnType<typeof useClient>
let done!: () => void
const ready = new Promise<void>((resolve) => {
done = resolve
@ -70,12 +69,12 @@ async function mount(
const app = await testRender(() => (
<TestTuiContexts log={log}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)} reconnect={reconnect}>
<ClientProvider api={createApi(calls.fetch)} reconnect={reconnect}>
<ProjectProvider>
<Probe
onReady={async (ctx) => {
project = ctx.project
sdk = ctx.sdk
client = ctx.client
await project.sync()
done()
}}
@ -83,21 +82,21 @@ async function mount(
workspaces={workspaces}
/>
</ProjectProvider>
</SDKProvider>
</ClientProvider>
</TestTuiContexts>
))
await ready
return { app, events, emit: events.emit, project, sdk, seen, workspaces }
return { app, events, emit: events.emit, project, client, seen, workspaces }
}
function Probe(props: {
seen: OpenCodeEvent[]
workspaces: Array<string | undefined>
onReady: (ctx: { project: ReturnType<typeof useProject>; sdk: ReturnType<typeof useSDK> }) => void
onReady: (ctx: { project: ReturnType<typeof useProject>; client: ReturnType<typeof useClient> }) => void
}) {
const project = useProject()
const sdk = useSDK()
const client = useClient()
const event = useEvent()
onMount(() => {
@ -105,7 +104,7 @@ function Probe(props: {
props.seen.push(evt)
props.workspaces.push(workspace)
})
props.onReady({ project, sdk })
props.onReady({ project, client })
})
return <box />
@ -137,7 +136,7 @@ describe("useEvent", () => {
{
level: "debug",
message: "event",
tags: { component: "sdk", type: "session.renamed", aggregateID: "ses_test", seq: 1 },
tags: { component: "client", type: "session.renamed", aggregateID: "ses_test", seq: 1 },
},
])
} finally {
@ -194,25 +193,24 @@ describe("useEvent", () => {
const attempts: number[] = []
const replacementEvents = createEventStream()
const replacementCalls = createFetch(undefined, replacementEvents)
const replacement = { client: createClient(replacementCalls.fetch), api: createApi(replacementCalls.fetch) }
const { app, events, sdk, seen } = await mount(async (attempt) => {
const replacement = { api: createApi(replacementCalls.fetch) }
const { app, events, client, seen } = await mount(async (attempt) => {
attempts.push(attempt)
return replacement
})
try {
await wait(() => sdk.connection.status() === "connected")
await wait(() => client.connection.status() === "connected")
// Reconnection only runs when the stream is down, never while connected.
expect(attempts).toEqual([])
events.disconnect()
await wait(() => sdk.connection.status() === "connected" && attempts.length > 0)
await wait(() => client.connection.status() === "connected" && attempts.length > 0)
replacementEvents.emit(event(vcs("rediscovered"), { directory: "/tmp/rediscovered" }))
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "rediscovered"))
expect(sdk.client).toBe(replacement.client)
expect(sdk.api).toBe(replacement.api)
expect(client.api).toBe(replacement.api)
expect(attempts).toEqual([1])
const history = sdk.connection.internal.history()
const history = client.connection.internal.history()
expect(history.map((event) => [event.data.status, event.data.attempt])).toEqual([
["connecting", 0],
["connected", 0],
@ -228,22 +226,22 @@ describe("useEvent", () => {
test("keeps the current client when reconnection fails", async () => {
let calls = 0
const { app, events, sdk, seen } = await mount(async () => {
const { app, events, client, seen } = await mount(async () => {
calls += 1
throw new Error("no server")
})
try {
await wait(() => sdk.connection.status() === "connected")
const original = sdk.client
await wait(() => client.connection.status() === "connected")
const original = client.api
events.disconnect()
// Reconnection rejects; the loop retries against the last known transport,
// which succeeds once the fixture accepts the reconnect.
await wait(() => calls > 0 && sdk.connection.status() === "connected")
await wait(() => calls > 0 && client.connection.status() === "connected")
events.emit(event(vcs("recovered"), { directory: "/tmp/recovered" }))
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "recovered"))
expect(sdk.client).toBe(original)
expect(client.api).toBe(original)
} finally {
app.renderer.destroy()
}