refactor(tui): migrate v2 calls to new client

This commit is contained in:
Dax Raad 2026-07-09 16:55:16 -04:00
commit e2eed76101
17 changed files with 2834 additions and 3590 deletions

View file

@ -25,9 +25,8 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
dialog.setCentered(true)
const [server] = createResource(() =>
sdk.client.v2.server
.get({ throwOnError: true })
.then((result) => result.data)
sdk.api.server
.get()
.catch((error) => {
setLoadError(error)
return undefined

View file

@ -44,8 +44,7 @@ export function DialogSessionList() {
directory: location.directory,
workspace: location.workspaceID,
})
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; session list UI reuses legacy mutable session types.
return { query, sessions: structuredClone(response.data) as SessionInfo[] }
return { query, sessions: response.data }
} catch (error) {
// A transient transport failure must degrade search, not crash the TUI
// through the root ErrorBoundary when the errored resource is read.
@ -141,7 +140,7 @@ export function DialogSessionList() {
setToDelete(option.value)
return
}
void sdk.client.v2.session.remove({ sessionID: option.value }, { throwOnError: true }).catch((error) => {
void sdk.api.session.remove({ sessionID: option.value }).catch((error) => {
setToDelete(undefined)
toast.show({
message: `Failed to delete session: ${errorMessage(error)}`,

View file

@ -37,7 +37,7 @@ import { usePromptStash } from "../../prompt/stash"
import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
import type { SessionInfo, UserMessage } from "@opencode-ai/sdk/v2"
import type { UserMessage } from "@opencode-ai/sdk/v2"
import { Locale } from "../../util/locale"
import { errorMessage } from "../../util/error"
import { createColors, createFrames } from "../../ui/spinner"
@ -1027,8 +1027,7 @@ export function Prompt(props: PromptProps) {
}
sessionID = created.id
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; prompt state still uses legacy mutable session types.
session = structuredClone(created) as SessionInfo
session = created
}
const inputText = expandTrackedPastedText(
@ -1064,7 +1063,7 @@ export function Prompt(props: PromptProps) {
if (store.mode === "shell") {
move.startSubmit()
void sdk.client.v2.session.shell({
void sdk.api.session.shell({
sessionID,
command: inputText,
})

View file

@ -24,8 +24,8 @@ import type {
SessionInfo,
Shell,
SkillInfo,
V2Event,
} from "@opencode-ai/sdk/v2"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import { createStore, produce, reconcile } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
@ -82,14 +82,6 @@ function locationQuery(ref?: LocationRef) {
return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined
}
type Mutable<T> =
T extends ReadonlyArray<infer U> ? Mutable<U>[] : T extends object ? { -readonly [K in keyof T]: Mutable<T[K]> } : T
function mutable<T>(value: T): Mutable<T> {
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client data is readonly; the TUI store mutates cloned state.
return structuredClone(value) as Mutable<T>
}
export const { use: useData, provider: DataProvider } = createSimpleContext({
name: "Data",
init: () => {
@ -241,7 +233,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
)
}
function handleEvent(event: V2Event) {
function handleEvent(event: OpenCodeEvent) {
switch (event.type) {
case "session.created":
void result.session.refresh(event.data.sessionID)
@ -300,8 +292,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
.then((item) => {
message.update(event.data.sessionID, (draft, index) => {
const position = index.get(item.id)
if (position === undefined) return message.append(draft, index, mutable(item))
draft[position] = mutable(item)
if (position === undefined) return message.append(draft, index, item)
draft[position] = item
})
})
.catch((error) => console.error("Failed to load projected model switch message", error))
@ -312,7 +304,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
break
case "session.moved":
if (store.session.info[event.data.sessionID]) {
setStore("session", "info", event.data.sessionID, "location", mutable(event.data.location))
setStore("session", "info", event.data.sessionID, "location", event.data.location)
setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath)
}
break
@ -745,11 +737,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id)) break
setStore("session", "form", event.data.form.sessionID, [
...(store.session.form[event.data.form.sessionID] ?? []),
mutable(
event.data.form.sessionID === "global"
? { ...event.data.form, location: event.location }
: event.data.form,
),
event.data.form.sessionID === "global"
? { ...event.data.form, location: event.location }
: event.data.form,
])
break
case "form.replied":
@ -830,7 +820,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
},
},
async refresh(sessionID: string) {
setStore("session", "info", sessionID, mutable(await sdk.api.session.get({ sessionID })))
setStore("session", "info", sessionID, await sdk.api.session.get({ sessionID }))
registerSession(sessionID)
},
message: {
@ -846,9 +836,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return position === undefined ? undefined : messages?.[position]
},
async refresh(sessionID: string) {
const messages = mutable(
(await sdk.api.message.list({ sessionID, limit: 200, order: "desc" })).data,
).toReversed()
const messages = (await sdk.api.message.list({ sessionID, limit: 200, order: "desc" })).data.toReversed()
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
setStore("session", "message", sessionID, reconcile(messages))
},
@ -858,7 +846,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return store.session.permission[sessionID]
},
async refresh(sessionID: string) {
setStore("session", "permission", sessionID, mutable(await sdk.api.permission.list({ sessionID })))
setStore("session", "permission", sessionID, await sdk.api.permission.list({ sessionID }))
},
},
form: {
@ -881,13 +869,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
...(store.session.form[sessionID] ?? []).filter(
(form) => form.location && locationKey(form.location) !== key,
),
...mutable(
response.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location })),
),
...response.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, await sdk.api.form.list({ sessionID }))
},
},
},
@ -897,7 +883,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return store.project.permission[projectID]
},
async refresh(projectID: string) {
setStore("project", "permission", projectID, mutable(await sdk.api.permission.saved.list({ projectID })))
setStore("project", "permission", projectID, await sdk.api.permission.saved.list({ projectID }))
},
},
},
@ -915,7 +901,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
const key = locationKey(result.location)
setStore("location", key, {
...store.location[key],
shell: Object.fromEntries(mutable(result.data).map((info) => [info.id, info])),
shell: Object.fromEntries(result.data.map((info) => [info.id, info])),
})
},
},
@ -936,7 +922,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.agent.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], agent: mutable(result.data) })
setStore("location", key, { ...store.location[key], agent: result.data })
},
},
command: {
@ -946,7 +932,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.command.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], command: mutable(result.data) })
setStore("location", key, { ...store.location[key], command: result.data })
},
},
integration: {
@ -956,7 +942,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.integration.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], integration: mutable(result.data) })
setStore("location", key, { ...store.location[key], integration: result.data })
},
},
mcp: {
@ -964,9 +950,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return store.location[locationKey(location ?? defaultLocation())]?.mcp
},
async refresh(ref?: LocationRef) {
const result = await sdk.client.v2.mcp.list({ location: locationQuery(ref) }, { throwOnError: true })
const key = locationKey(result.data.location)
setStore("location", key, { ...store.location[key], mcp: result.data.data })
const result = await sdk.api["server.mcp"].list({ location: locationQuery(ref) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], mcp: result.data })
},
},
model: {
@ -976,7 +962,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.model.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], model: mutable(result.data) })
setStore("location", key, { ...store.location[key], model: result.data })
},
},
provider: {
@ -986,7 +972,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.provider.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], provider: mutable(result.data) })
setStore("location", key, { ...store.location[key], provider: result.data })
},
},
reference: {
@ -996,7 +982,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.reference.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], reference: mutable(result.data) })
setStore("location", key, { ...store.location[key], reference: result.data })
},
},
skill: {
@ -1006,7 +992,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
async refresh(ref?: LocationRef) {
const result = await sdk.api.skill.list({ location: locationQuery(ref ?? defaultLocation()) })
const key = locationKey(result.location)
setStore("location", key, { ...store.location[key], skill: mutable(result.data) })
setStore("location", key, { ...store.location[key], skill: result.data })
},
},
},
@ -1027,13 +1013,13 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
"session",
"info",
produce((draft) => {
for (const session of response.data) draft[session.id] = mutable(session)
for (const session of response.data) draft[session.id] = session
}),
)
for (const session of response.data) registerSession(session.id)
}),
sdk.api.permission.request.list({ location: locationQuery(defaultLocation()) }).then((response) => {
const permissions = mutable(response.data).reduce<Record<string, PermissionV2Request[]>>(
const permissions = response.data.reduce<Record<string, PermissionV2Request[]>>(
(result, request) => ({
...result,
[request.sessionID]: [...(result[request.sessionID] ?? []), request],
@ -1047,7 +1033,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
directory: response.location.directory,
workspaceID: response.location.workspaceID,
}
const forms = mutable(response.data).reduce<Record<string, FormInfo[]>>(
const forms = response.data.reduce<Record<string, FormInfo[]>>(
(result, form) => ({
...result,
[form.sessionID]: [

View file

@ -1,4 +1,4 @@
import type { V2Event } from "@opencode-ai/sdk/v2"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import { useSDK } from "./sdk"
type EventMetadata = {
@ -9,16 +9,16 @@ type EventMetadata = {
export function useEvent() {
const sdk = useSDK()
function subscribe(handler: (event: V2Event, metadata: EventMetadata) => void) {
function subscribe(handler: (event: OpenCodeEvent, metadata: EventMetadata) => void) {
return sdk.event.listen(({ details }) => {
if (details.type === "server.connected") return
handler(details, { directory: details.location?.directory, workspace: details.location?.workspaceID })
})
}
function on<T extends V2Event["type"]>(
function on<T extends OpenCodeEvent["type"]>(
type: T,
handler: (event: Extract<V2Event, { type: T }>, metadata: EventMetadata) => void,
handler: (event: Extract<OpenCodeEvent, { type: T }>, metadata: EventMetadata) => void,
) {
return sdk.event.on(type, (event) => {
handler(event, { directory: event.location?.directory, workspace: event.location?.workspaceID })

View file

@ -1,4 +1,4 @@
import { batch, onCleanup } from "solid-js"
import { batch } from "solid-js"
import type { Path, Workspace } from "@opencode-ai/sdk/v2"
import { createStore, reconcile } from "solid-js/store"
import { createSimpleContext } from "./helper"
@ -66,12 +66,6 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
})
}
onCleanup(
sdk.event.on("workspace.status", (event) => {
setStore("workspace", "status", event.data.workspaceID, event.data.status)
}),
)
return {
data: store,
project() {

View file

@ -1,5 +1,5 @@
import type { OpenCodeClient } from "@opencode-ai/client/promise"
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
import type { OpenCodeClient, OpenCodeEvent } from "@opencode-ai/client/promise"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
@ -17,7 +17,7 @@ export type SDKConnectionEvent = {
}
}
type SDKEventMap = { [Type in V2Event["type"]]: Extract<V2Event, { type: Type }> }
type SDKEventMap = { [Type in OpenCodeEvent["type"]]: Extract<OpenCodeEvent, { type: Type }> }
const connectTimeout = 2_000
const connectionHistoryLimit = 50
@ -72,12 +72,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
const error = await (async () => {
record(attempt === 0 ? "connecting" : "reconnecting", attempt)
log.info("event stream connecting", { attempt })
const response = await client.v2.event.subscribe({
signal: connection.signal,
sseMaxRetryAttempts: 0,
throwOnError: true,
})
const iterator = response.stream[Symbol.asyncIterator]()
const iterator = api.event.subscribe({ signal: connection.signal })[Symbol.asyncIterator]()
const first = await iterator.next()
if (abort.signal.aborted || controller.signal.aborted) return
if (first.done)

View file

@ -1,10 +1,10 @@
import type { V2Event } from "@opencode-ai/sdk/v2"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
import type { BuiltinTuiPlugin } from "../builtins"
const id = "internal:notifications"
type SessionError = Extract<V2Event, { type: "session.error" }>["data"]["error"]
type SessionError = Extract<OpenCodeEvent, { type: "session.error" }>["data"]["error"]
function notify(
api: TuiPluginApi,