refactor(tui): wire generated client reads (#34381)
This commit is contained in:
parent
e8ac44430b
commit
381d67572e
28 changed files with 509 additions and 427 deletions
|
|
@ -61,6 +61,14 @@ 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: () => {
|
||||
|
|
@ -288,13 +296,19 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
break
|
||||
case "session.next.text.delta":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID), event.data.textID)
|
||||
const match = message.latestText(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.textID,
|
||||
)
|
||||
if (match) match.text += event.data.delta
|
||||
})
|
||||
break
|
||||
case "session.next.text.ended":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID), event.data.textID)
|
||||
const match = message.latestText(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.textID,
|
||||
)
|
||||
if (match) match.text = event.data.text
|
||||
})
|
||||
break
|
||||
|
|
@ -311,19 +325,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
break
|
||||
case "session.next.tool.input.delta":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (match?.state.status === "pending") match.state.input += event.data.delta
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.ended":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (match?.state.status === "pending") match.state.input = event.data.text
|
||||
})
|
||||
break
|
||||
case "session.next.tool.called":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (!match) return
|
||||
match.time.ran = event.data.timestamp
|
||||
match.provider = event.data.provider
|
||||
|
|
@ -332,7 +355,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
break
|
||||
case "session.next.tool.progress":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state.structured = event.data.structured
|
||||
match.state.content = [...event.data.content]
|
||||
|
|
@ -340,7 +366,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
break
|
||||
case "session.next.tool.success":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state = {
|
||||
status: "completed",
|
||||
|
|
@ -359,7 +388,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
break
|
||||
case "session.next.tool.failed":
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
)
|
||||
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
||||
match.state = {
|
||||
status: "error",
|
||||
|
|
@ -522,8 +554,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.session.status[sessionID] ?? "idle"
|
||||
},
|
||||
async refresh(sessionID: string) {
|
||||
const result = await sdk.client.v2.session.get({ sessionID }, { throwOnError: true })
|
||||
setStore("session", "info", sessionID, result.data.data)
|
||||
setStore("session", "info", sessionID, mutable(await sdk.api.sessions.get({ sessionID })))
|
||||
},
|
||||
message: {
|
||||
ids(sessionID: string) {
|
||||
|
|
@ -540,11 +571,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
async refresh(sessionID: string) {
|
||||
setStore("session", "message", sessionID, [])
|
||||
messageIndex.set(sessionID, new Map())
|
||||
const response = await sdk.client.v2.session.messages(
|
||||
{ sessionID, limit: 200, order: "desc" },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
const loaded = response.data.data.toReversed()
|
||||
const loaded = mutable(
|
||||
(await sdk.api.messages.list({ sessionID, limit: 200, order: "desc" })).data,
|
||||
).toReversed()
|
||||
const live = store.session.message[sessionID] ?? []
|
||||
const liveByID = new Map(live.map((message) => [message.id, message]))
|
||||
const messages = [...loaded.map((message) => liveByID.get(message.id) ?? message), ...live]
|
||||
|
|
@ -559,8 +588,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.session.permission[sessionID]
|
||||
},
|
||||
async refresh(sessionID: string) {
|
||||
const result = await sdk.client.v2.session.permission.list({ sessionID }, { throwOnError: true })
|
||||
setStore("session", "permission", sessionID, result.data.data)
|
||||
setStore("session", "permission", sessionID, mutable(await sdk.api.permissions.list({ sessionID })))
|
||||
},
|
||||
},
|
||||
question: {
|
||||
|
|
@ -568,8 +596,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.session.question[sessionID]
|
||||
},
|
||||
async refresh(sessionID: string) {
|
||||
const result = await sdk.client.v2.session.question.list({ sessionID }, { throwOnError: true })
|
||||
setStore("session", "question", sessionID, result.data.data)
|
||||
setStore("session", "question", sessionID, mutable(await sdk.api.questions.list({ sessionID })))
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -579,8 +606,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.project.permission[projectID]
|
||||
},
|
||||
async refresh(projectID: string) {
|
||||
const result = await sdk.client.v2.permission.saved.list({ projectID }, { throwOnError: true })
|
||||
setStore("project", "permission", projectID, result.data.data)
|
||||
setStore("project", "permission", projectID, mutable(await sdk.api.permissions.listSaved({ projectID })))
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -606,8 +632,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return defaultLocation()
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const response = await sdk.client.v2.location.get({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const location = response.data
|
||||
const location = await sdk.api.location.get({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(location)
|
||||
if (!store.location[key]) setStore("location", key, {})
|
||||
if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID })
|
||||
|
|
@ -617,9 +642,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.agent.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], agent: result.data.data })
|
||||
const result = await sdk.api.agents.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], agent: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
command: {
|
||||
|
|
@ -627,9 +652,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.command
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.command.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], command: result.data.data })
|
||||
const result = await sdk.api.commands.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], command: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
|
|
@ -637,12 +662,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.integration
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.integration.list(
|
||||
{ location: locationQuery(ref) },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], integration: result.data.data })
|
||||
const result = await sdk.api.integrations.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], integration: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
model: {
|
||||
|
|
@ -650,9 +672,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.model
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.model.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], model: result.data.data })
|
||||
const result = await sdk.api.models.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], model: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
|
|
@ -660,9 +682,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.provider
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.provider.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], provider: result.data.data })
|
||||
const result = await sdk.api.providers.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], provider: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
reference: {
|
||||
|
|
@ -670,9 +692,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.reference
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.reference.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], reference: result.data.data })
|
||||
const result = await sdk.api.references.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], reference: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
skill: {
|
||||
|
|
@ -680,9 +702,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
return store.location[locationKey(location ?? defaultLocation())]?.skill
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.client.v2.skill.list({ location: locationQuery(ref) }, { throwOnError: true })
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], skill: result.data.data })
|
||||
const result = await sdk.api.skills.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], skill: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -690,24 +712,31 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
|
||||
async function bootstrap() {
|
||||
const settled = await Promise.allSettled([
|
||||
sdk.client.v2.session
|
||||
.list({ limit: 50, order: "desc" }, { throwOnError: true })
|
||||
sdk.api.sessions
|
||||
.list({
|
||||
limit: 50,
|
||||
order: "desc",
|
||||
directory: defaultLocation().directory,
|
||||
workspace: defaultLocation().workspaceID,
|
||||
})
|
||||
.then((response) =>
|
||||
setStore(
|
||||
"session",
|
||||
"info",
|
||||
produce((draft) => {
|
||||
for (const session of response.data.data) draft[session.id] = session
|
||||
for (const session of response.data) draft[session.id] = mutable(session)
|
||||
}),
|
||||
),
|
||||
),
|
||||
sdk.client.v2.session.active({ throwOnError: true }).then((response) =>
|
||||
setStore(
|
||||
"session",
|
||||
"status",
|
||||
Object.fromEntries(Object.keys(response.data.data).map((sessionID) => [sessionID, "running" as const])),
|
||||
sdk.api.sessions
|
||||
.active()
|
||||
.then((active) =>
|
||||
setStore(
|
||||
"session",
|
||||
"status",
|
||||
Object.fromEntries(Object.keys(active).map((sessionID) => [sessionID, "running" as const])),
|
||||
),
|
||||
),
|
||||
),
|
||||
result.location.refresh(),
|
||||
result.location.agent.refresh(),
|
||||
result.location.integration.refresh(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { OpenCodeClient } from "@opencode-ai/client"
|
||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
|
|
@ -11,9 +12,14 @@ const connectTimeout = 2_000
|
|||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { client: OpencodeClient; reload?: () => Promise<OpencodeClient> }) => {
|
||||
init: (props: {
|
||||
client: OpencodeClient
|
||||
api: OpenCodeClient
|
||||
reload?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
|
||||
}) => {
|
||||
const abort = new AbortController()
|
||||
let client = props.client
|
||||
let api = props.api
|
||||
const events = createGlobalEmitter<SDKEventMap>()
|
||||
const [connection, setConnection] = createStore<{
|
||||
status: SDKConnectionStatus
|
||||
|
|
@ -40,7 +46,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||
const connection = new AbortController()
|
||||
const cancel = () => connection.abort(controller.signal.reason)
|
||||
const timeout = setTimeout(() => connection.abort(new Error("Timed out connecting to server")), connectTimeout)
|
||||
const timeout = setTimeout(
|
||||
() => connection.abort(new Error("Timed out connecting to server")),
|
||||
connectTimeout,
|
||||
)
|
||||
controller.signal.addEventListener("abort", cancel, { once: true })
|
||||
const error = await (async () => {
|
||||
const response = await current.v2.event.subscribe({
|
||||
|
|
@ -91,7 +100,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
pending = Promise.resolve()
|
||||
.then(props.reload)
|
||||
.then(async (next) => {
|
||||
client = next
|
||||
client = next.client
|
||||
api = next.api
|
||||
if (!abort.signal.aborted) await start()
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -112,6 +122,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
get client() {
|
||||
return client
|
||||
},
|
||||
get api() {
|
||||
return api
|
||||
},
|
||||
event: {
|
||||
on: events.on,
|
||||
listen: events.listen,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue