Files
opencode/packages/opencode/src/cli/cmd/tui/context/sync.tsx
2025-11-05 15:33:23 -05:00

288 lines
8.7 KiB
TypeScript

import type {
Message,
Agent,
Provider,
Session,
Part,
Config,
Todo,
Command,
Permission,
LspStatus,
McpStatus,
FormatterStatus,
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
import { Binary } from "@/util/binary"
import { createSimpleContext } from "./helper"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
const [store, setStore] = createStore<{
ready: boolean
provider: Provider[]
agent: Agent[]
command: Command[]
permission: {
[sessionID: string]: Permission[]
}
config: Config
session: Session[]
todo: {
[sessionID: string]: Todo[]
}
message: {
[sessionID: string]: Message[]
}
part: {
[messageID: string]: Part[]
}
lsp: LspStatus[]
mcp: {
[key: string]: McpStatus
}
formatter: FormatterStatus[]
}>({
config: {},
ready: false,
agent: [],
permission: {},
command: [],
provider: [],
session: [],
todo: {},
message: {},
part: {},
lsp: [],
mcp: {},
formatter: [],
})
const sdk = useSDK()
sdk.event.listen((e) => {
const event = e.details
switch (event.type) {
case "permission.updated": {
const permissions = store.permission[event.properties.sessionID]
if (!permissions) {
setStore("permission", event.properties.sessionID, [event.properties])
break
}
const match = Binary.search(permissions, event.properties.id, (p) => p.id)
setStore(
"permission",
event.properties.sessionID,
produce((draft) => {
if (match.found) {
draft[match.index] = event.properties
return
}
draft.push(event.properties)
}),
)
break
}
case "permission.replied": {
const permissions = store.permission[event.properties.sessionID]
const match = Binary.search(permissions, event.properties.permissionID, (p) => p.id)
if (!match.found) break
setStore(
"permission",
event.properties.sessionID,
produce((draft) => {
draft.splice(match.index, 1)
}),
)
break
}
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
case "session.deleted": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
case "session.updated":
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
break
}
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
setStore(
"message",
event.properties.info.sessionID,
result.index,
reconcile(event.properties.info),
)
break
}
setStore(
"message",
event.properties.info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "message.removed": {
const messages = store.message[event.properties.sessionID]
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
setStore(
"message",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
case "message.part.updated": {
const parts = store.part[event.properties.part.messageID]
if (!parts) {
setStore("part", event.properties.part.messageID, [event.properties.part])
break
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
setStore(
"part",
event.properties.part.messageID,
result.index,
reconcile(event.properties.part),
)
break
}
setStore(
"part",
event.properties.part.messageID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.part)
}),
)
break
}
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found)
setStore(
"part",
event.properties.messageID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
break
}
case "lsp.updated": {
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!))
break
}
}
})
// blocking
Promise.all([
sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get().then((x) => setStore("config", x.data!)),
]).then(() => setStore("ready", true))
// non-blocking
Promise.all([
sdk.client.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
),
),
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
])
const result = {
data: store,
set: setStore,
get ready() {
return store.ready
},
session: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)
if (match.found) return store.session[match.index]
return undefined
},
status(sessionID: string) {
const session = result.session.get(sessionID)
if (!session) return "idle"
if (session.time.compacting) return "compacting"
const messages = store.message[sessionID] ?? []
const last = messages.at(-1)
if (!last) return "idle"
if (last.role === "user") return "working"
return last.time.completed ? "idle" : "working"
},
async sync(sessionID: string) {
const now = Date.now()
const [session, messages, todo] = await Promise.all([
sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
sdk.client.session.messages({ path: { id: sessionID } }),
sdk.client.session.todo({ path: { id: sessionID } }),
])
console.log("fetched in " + (Date.now() - now), sessionID)
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? []
draft.message[sessionID] = messages.data!.map((x) => x.info)
for (const message of messages.data!) {
draft.part[message.info.id] = message.parts
}
}),
)
console.log("synced in " + (Date.now() - now), sessionID)
},
},
}
return result
},
})