// Client data layer: apply server events and cache API reads into a Solid store. // Prefer straightforward projection. Do not add generation counters, stale-response // merges, live/history overlays, or other race machinery here—last write wins. // Reconnect invalidates cached reads; active UI owners decide what to sync again. import type { AgentInfo, CommandInfo, FormInfo, IntegrationInfo, LocationRef, LocationGetOutput, McpResource, McpServer, ModelInfo, PermissionSavedInfo, PermissionV2Request, ProviderV2Info, ReferenceInfo, SessionMessageInfo, SessionMessageAssistant, SessionMessageAssistantReasoning, SessionMessageAssistantText, SessionMessageAssistantTool, SessionInfo, SessionPendingInfo, ShellInfo, SkillInfo, OpenCodeEvent, } from "@opencode-ai/client" import type { Plugin } from "@opencode-ai/plugin/v2/tui" import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useClient } from "./client" import { nonEmptyToolContent } from "../util/tool-display" import { createEffect, createSignal, onCleanup } from "solid-js" export type DataSessionStatus = "idle" | "running" const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_") // Global MCP elicitations temporarily use "global" instead of a real session ID, so the // server cannot recover their Location when settling them. Preserve the event Location // until MCP elicitations carry session ownership. export type FormWithLocation = FormInfo & { readonly location?: LocationRef } type LocationData = { info?: LocationGetOutput agent?: AgentInfo[] command?: CommandInfo[] integration?: IntegrationInfo[] mcp?: { server?: McpServer[] resource?: McpResource[] } model?: ModelInfo[] provider?: ProviderV2Info[] reference?: ReferenceInfo[] // Currently running shell commands for this location, keyed by shell id. Entries are removed // once the command exits or is deleted, so this only ever holds in-flight shells. shell?: Record skill?: SkillInfo[] } type Store = { session: { info: Record // Family index keyed by a family's root (or furthest-known-ancestor when the // true root is not yet loaded). The value is a flat deduplicated list of every // session ID in that family, including the key itself once its info arrives. family: Record active: Record message: Record pending: Record input: Record permission: Record // Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel. form: Record } project: { permission: Record } location: Record } function locationKey(location: LocationRef) { return JSON.stringify([location.directory, location.workspaceID]) } function locationQuery(ref?: LocationRef) { return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined } function createSync() { const state = new Map>() return { run(key: string, load: () => Promise) { const active = state.get(key) if (active === true) return Promise.resolve() if (active) return active const pending = load() .then(() => { if (state.get(key) === pending) state.set(key, true) }) .finally(() => { if (state.get(key) === pending) state.delete(key) }) state.set(key, pending) return pending }, complete(key: string) { if (state.has(key)) return state.set(key, true) }, invalidate(key?: string) { if (key) { state.delete(key) return } state.clear() }, } } export const { use: useData, provider: DataProvider } = createSimpleContext({ name: "Data", init: () => { const [store, setStore] = createStore({ session: { info: {}, family: {}, active: {}, message: {}, pending: {}, input: {}, permission: {}, form: {}, }, project: { permission: {}, }, location: {}, }) const client = useClient() const [defaultLocation, setDefaultLocation] = createSignal({ directory: process.cwd(), }) const messageIndex = new Map>() const sync = createSync() function setSessionActive(sessionID: string, status: DataSessionStatus) { setStore("session", "active", sessionID, status) } function addPending(item: SessionPendingInfo) { if (store.session.pending[item.sessionID]?.some((pending) => pending.id === item.id)) return setStore("session", "pending", item.sessionID, [...(store.session.pending[item.sessionID] ?? []), item]) } function removePending(sessionID: string, inputID?: string) { if (!inputID) return setStore( "session", "pending", sessionID, (store.session.pending[sessionID] ?? []).filter((item) => item.id !== inputID), ) } const message = { update(sessionID: string, fn: (messages: SessionMessageInfo[], index: Map) => void) { setStore( "session", "message", produce((draft) => { fn((draft[sessionID] ??= []), index(sessionID)) }), ) }, append(messages: SessionMessageInfo[], index: Map, item: SessionMessageInfo) { if (index.has(item.id)) return index.set(item.id, messages.length) messages.push(item) }, activeAssistant(messages: SessionMessageInfo[]) { const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed) return item?.type === "assistant" ? item : undefined }, assistant(messages: SessionMessageInfo[], index: Map, messageID: string) { const position = index.get(messageID) const item = position === undefined ? undefined : messages[position] return item?.type === "assistant" ? item : undefined }, shell(messages: SessionMessageInfo[], shellID: string) { const item = messages.findLast((item) => item.type === "shell" && item.shellID === shellID) return item?.type === "shell" ? item : undefined }, compaction(messages: SessionMessageInfo[]) { const item = messages.findLast((item) => item.type === "compaction" && item.status === "running") return item?.type === "compaction" ? item : undefined }, latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { return assistant?.content.findLast( (item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID), ) }, latestText(assistant: SessionMessageAssistant | undefined) { return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text") }, latestReasoning(assistant: SessionMessageAssistant | undefined) { return assistant?.content.findLast( (item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && !item.time?.completed, ) }, } function index(sessionID: string) { const existing = messageIndex.get(sessionID) if (existing) return existing const created = new Map() messageIndex.set(sessionID, created) return created } // Walk parentID upward through loaded session info to the family root. When a // parent's info is missing, that missing ID is the furthest-known ancestor and // is returned so orphan subtrees group under it until the parent arrives. A // seen set guards against parent cycles, stopping at the last non-repeating // ancestor. function resolveRoot(sessionID: string) { let current = sessionID let parentID = store.session.info[sessionID]?.parentID const seen = new Set([sessionID]) while (parentID) { if (seen.has(parentID)) break seen.add(parentID) current = parentID parentID = store.session.info[parentID]?.parentID } return current } // Register one session into the family index. Idempotent: syncing an // existing session never duplicates its ID. When a tentative family keyed by // sessionID exists (descendants arrived while sessionID's own info was // absent) but sessionID turns out to have a parent, fold the orphan subtree // into the resolved root's family and drop the tentative entry. function registerSession(sessionID: string) { const info = store.session.info[sessionID] if (!info) return const rootID = resolveRoot(sessionID) setStore( "session", "family", produce((draft) => { if (sessionID !== rootID && draft[sessionID]) { const members = (draft[rootID] ??= []) for (const id of draft[sessionID]) { if (!members.includes(id)) members.push(id) } delete draft[sessionID] } const family = (draft[rootID] ??= []) if (!family.includes(sessionID)) family.push(sessionID) }), ) } function removeSession(sessionID: string) { messageIndex.delete(sessionID) sync.invalidate(`session:${sessionID}`) sync.invalidate(`session.pending:${sessionID}`) sync.invalidate(`session.message:${sessionID}`) sync.invalidate(`session.permission:${sessionID}`) sync.invalidate(`session.form:${sessionID}:`) setStore( "session", produce((draft) => { delete draft.info[sessionID] delete draft.active[sessionID] delete draft.message[sessionID] delete draft.pending[sessionID] delete draft.input[sessionID] delete draft.permission[sessionID] delete draft.form[sessionID] for (const [rootID, family] of Object.entries(draft.family)) { const next = family.filter((id) => id !== sessionID) if (next.length === 0) delete draft.family[rootID] else draft.family[rootID] = next } }), ) } function handleEvent(event: OpenCodeEvent) { switch (event.type) { case "session.created": result.session.invalidate(event.data.sessionID) void result.session.sync(event.data.sessionID) // Band-aid: a newly created session starts empty, so live events can be its source of truth. // Fetching pending inputs and projected messages separately lets promotion move an input between snapshots, // causing both requests to miss it and overwrite event-built state. Skip those racy initial reads until // hydration can load pending and projected messages atomically. sync.complete(`session.pending:${event.data.sessionID}`) sync.complete(`session.message:${event.data.sessionID}`) break case "session.deleted": removeSession(event.data.sessionID) break case "session.usage.updated": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens, }) break case "catalog.updated": result.location.model.invalidate(event.location) result.location.provider.invalidate(event.location) void Promise.all([result.location.model.sync(event.location), result.location.provider.sync(event.location)]) break case "agent.updated": result.location.agent.invalidate(event.location) void result.location.agent.sync(event.location) break case "command.updated": result.location.command.invalidate(event.location) void result.location.command.sync(event.location) break case "skill.updated": result.location.skill.invalidate(event.location) void result.location.skill.sync(event.location) break case "session.agent.selected": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "agent", event.data.agent) message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "agent-switched", agent: event.data.agent, time: { created: event.created }, }) }) break case "session.model.selected": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "model", event.data.model) if (!store.session.message[event.data.sessionID]) break message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "model-switched", model: event.data.model, time: { created: event.created }, }) }) void client.api.session .message({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) }) .then((item) => { message.update(event.data.sessionID, (draft, index) => { const position = index.get(item.id) if (position === undefined) return message.append(draft, index, item) draft[position] = item }) }) .catch((error) => console.error("Failed to load projected model switch message", error)) break case "session.renamed": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "title", event.data.title) break case "session.moved": if (store.session.info[event.data.sessionID]) { setStore("session", "info", event.data.sessionID, "location", event.data.location) if (event.data.projectID) setStore("session", "info", event.data.sessionID, "projectID", event.data.projectID) setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath) } break case "session.input.promoted": { removePending(event.data.sessionID, event.data.inputID) message.update(event.data.sessionID, (draft, index) => { const position = index.get(event.data.inputID) if (position === undefined) return const existing = draft[position] if (!existing || !store.session.input[event.data.sessionID]?.includes(event.data.inputID)) return existing.time.created = event.created draft.splice(position, 1) draft.push(existing) index.clear() draft.forEach((message, indexValue) => index.set(message.id, indexValue)) }) setStore( "session", "input", event.data.sessionID, (store.session.input[event.data.sessionID] ?? []).filter((id) => id !== event.data.inputID), ) break } case "session.input.admitted": addPending({ id: event.data.inputID, sessionID: event.data.sessionID, admittedSeq: event.durable.seq, timeCreated: event.created, ...event.data.input, }) if (!store.session.input[event.data.sessionID]?.includes(event.data.inputID)) setStore("session", "input", event.data.sessionID, [ ...(store.session.input[event.data.sessionID] ?? []), event.data.inputID, ]) message.update(event.data.sessionID, (draft, index) => { message.append( draft, index, event.data.input.type === "user" ? { id: event.data.inputID, type: "user", ...event.data.input.data, time: { created: event.created }, } : { id: event.data.inputID, type: "synthetic", ...event.data.input.data, time: { created: event.created }, }, ) }) break case "session.instructions.updated": const instructions = event.metadata?.instructions if ( typeof instructions === "object" && instructions !== null && "initial" in instructions && instructions.initial === true ) break message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "system", text: `Instructions updated: ${Object.keys(event.data.delta).join(", ")}`, metadata: event.metadata, time: { created: event.created }, }) }) break case "session.synthetic": message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "synthetic", text: event.data.text, description: event.data.description, metadata: event.data.metadata, time: { created: event.created }, }) }) break case "session.shell.started": message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "shell", shellID: event.data.shell.id, command: event.data.shell.command, status: event.data.shell.status, exit: event.data.shell.exit, metadata: event.metadata, time: { created: event.created }, }) }) break case "session.shell.ended": message.update(event.data.sessionID, (draft) => { const match = message.shell(draft, event.data.shell.id) if (!match) return match.status = event.data.shell.status match.exit = event.data.shell.exit match.output = event.data.output match.time.completed = event.created }) break case "session.step.started": message.update(event.data.sessionID, (draft, index) => { const position = index.get(event.data.assistantMessageID) const existing = position === undefined ? undefined : draft[position] if (existing?.type === "assistant") { existing.agent = event.data.agent existing.model = event.data.model existing.retry = undefined existing.error = undefined existing.finish = undefined existing.time.completed = undefined if (event.data.snapshot) existing.snapshot = { ...existing.snapshot, start: event.data.snapshot } return } const currentAssistant = message.activeAssistant(draft) if (currentAssistant) { currentAssistant.retry = undefined currentAssistant.time.completed = event.created } message.append(draft, index, { id: event.data.assistantMessageID, type: "assistant", agent: event.data.agent, model: event.data.model, metadata: event.metadata, content: [], snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, time: { created: event.created }, }) }) break case "session.step.ended": { message.update(event.data.sessionID, (draft, index) => { const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.created currentAssistant.finish = event.data.finish currentAssistant.cost = event.data.cost currentAssistant.tokens = event.data.tokens if (event.data.snapshot) currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot } }) break } case "session.step.failed": message.update(event.data.sessionID, (draft, index) => { const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.created currentAssistant.finish = "error" currentAssistant.error = event.data.error currentAssistant.retry = undefined if (event.data.cost !== undefined && event.data.tokens !== undefined) { currentAssistant.cost = event.data.cost currentAssistant.tokens = event.data.tokens } }) break case "session.text.started": message.update(event.data.sessionID, (draft, index) => { message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ type: "text", text: "", }) }) break case "session.text.delta": message.update(event.data.sessionID, (draft, index) => { const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID)) if (match) match.text += event.data.delta }) break case "session.text.ended": message.update(event.data.sessionID, (draft, index) => { const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID)) if (match) match.text = event.data.text }) break case "session.tool.input.started": message.update(event.data.sessionID, (draft, index) => { message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ type: "tool", id: event.data.callID, name: event.data.name, time: { created: event.created }, state: { status: "streaming", input: "" }, }) }) break case "session.tool.input.delta": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) if (match?.state.status === "streaming") match.state.input += event.data.delta }) break case "session.tool.input.ended": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) if (match?.state.status === "streaming") match.state.input = event.data.text }) break case "session.tool.called": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) if (!match) return match.time.ran = event.created match.executed = event.data.executed match.providerState = event.data.state match.state = { status: "running", input: event.data.input, metadata: {} } }) break case "session.tool.progress": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) if (match?.state.status !== "running") return match.state.metadata = event.data.metadata }) break case "session.tool.success": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) if (match?.state.status !== "running") return match.state = { status: "completed", input: match.state.input, metadata: event.data.metadata, content: [...event.data.content], } match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState match.time.completed = event.created }) break case "session.tool.failed": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) if (!match || (match.state.status !== "streaming" && match.state.status !== "running")) return match.state = { status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, metadata: event.data.metadata, content: event.data.content, } match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState match.time.completed = event.created }) break case "session.reasoning.started": message.update(event.data.sessionID, (draft, index) => { message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ type: "reasoning", text: "", state: event.data.state, time: { created: event.created }, }) }) break case "session.reasoning.delta": message.update(event.data.sessionID, (draft, index) => { const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID)) if (match) match.text += event.data.delta }) break case "session.reasoning.ended": message.update(event.data.sessionID, (draft, index) => { const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID)) if (match) { match.text = event.data.text match.time = { created: match.time?.created ?? event.created, completed: event.created } if (event.data.state !== undefined) match.state = event.data.state } }) break case "session.retry.scheduled": message.update(event.data.sessionID, (draft, index) => { const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.retry = { attempt: event.data.attempt, at: event.data.at, error: event.data.error, } }) break case "session.execution.started": setSessionActive(event.data.sessionID, "running") break case "session.compaction.admitted": addPending({ id: event.data.inputID, sessionID: event.data.sessionID, admittedSeq: event.durable.seq, timeCreated: event.created, type: "compaction", }) break case "session.compaction.started": removePending(event.data.sessionID, event.data.inputID) message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: event.data.inputID ?? messageIDFromEvent(event.id), type: "compaction", status: "running", reason: event.data.reason, summary: "", recent: event.data.recent ?? "", time: { created: event.created }, }) }) break case "session.execution.succeeded": case "session.execution.failed": case "session.execution.interrupted": setSessionActive(event.data.sessionID, "idle") message.update(event.data.sessionID, (draft) => { const currentAssistant = message.activeAssistant(draft) if (currentAssistant) currentAssistant.retry = undefined }) break case "session.revert.staged": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "revert", event.data.revert) break case "session.revert.cleared": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "revert", undefined) break case "session.revert.committed": if (store.session.info[event.data.sessionID]) { setStore("session", "info", event.data.sessionID, "revert", undefined) } setStore( "session", "input", event.data.sessionID, (store.session.input[event.data.sessionID] ?? []).filter((id) => id < event.data.to), ) message.update(event.data.sessionID, (draft, index) => { const position = draft.findIndex((item) => item.id >= event.data.to) if (position === -1) return for (const item of draft.splice(position)) index.delete(item.id) }) break case "session.compaction.delta": message.update(event.data.sessionID, (draft) => { const current = message.compaction(draft) if (current?.status === "running") current.summary += event.data.text }) break case "session.compaction.ended": message.update(event.data.sessionID, (draft, index) => { const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running") const current = draft[position] if (current?.type === "compaction") { Object.assign(current, { status: "completed", reason: event.data.reason, summary: event.data.text, recent: event.data.recent, }) return } message.append(draft, index, { id: messageIDFromEvent(event.id), type: "compaction", status: "completed", reason: event.data.reason, summary: event.data.text, recent: event.data.recent, time: { created: event.created }, }) }) break case "session.compaction.failed": removePending(event.data.sessionID, event.data.inputID) message.update(event.data.sessionID, (draft, index) => { const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running") const current = draft[position] const failed: Extract = { id: current?.id ?? event.data.inputID ?? messageIDFromEvent(event.id), type: "compaction", status: "failed", reason: event.data.reason ?? "manual", error: event.data.error ?? { type: "compaction.failed", message: "Compaction failed before recording an error", }, metadata: current?.type === "compaction" ? current.metadata : event.metadata, time: current?.type === "compaction" ? current.time : { created: event.created }, } if (current?.type === "compaction") { draft[position] = failed return } message.append(draft, index, failed) }) break case "permission.v2.asked": if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id)) break setStore("session", "permission", event.data.sessionID, [ ...(store.session.permission[event.data.sessionID] ?? []), event.data, ]) break case "permission.v2.replied": setStore( "session", "permission", event.data.sessionID, (store.session.permission[event.data.sessionID] ?? []).filter( (request) => request.id !== event.data.requestID, ), ) break case "form.created": 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] ?? []), event.data.form.sessionID === "global" ? { ...event.data.form, location: event.location } : event.data.form, ]) break case "form.replied": case "form.cancelled": setStore( "session", "form", event.data.sessionID, (store.session.form[event.data.sessionID] ?? []).filter((form) => form.id !== event.data.id), ) break case "shell.created": setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({ ...data, shell: { ...data?.shell, [event.data.info.id]: event.data.info }, })) break case "shell.exited": case "shell.deleted": if (event.location) { setStore("location", locationKey(event.location), (data) => ({ ...data, shell: Object.fromEntries(Object.entries(data?.shell ?? {}).filter(([id]) => id !== event.data.id)), })) break } setStore( "location", produce((draft) => { for (const data of Object.values(draft)) delete data.shell?.[event.data.id] }), ) break case "reference.updated": result.location.reference.invalidate() void result.location.reference.sync() break case "integration.updated": result.location.integration.invalidate(event.location) result.location.model.invalidate(event.location) result.location.provider.invalidate(event.location) void Promise.all([ result.location.integration.sync(event.location), result.location.model.sync(event.location), result.location.provider.sync(event.location), ]) break // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed, // so the mcp list syncs here rather than off integration.updated. case "mcp.status.changed": result.location.mcp.server.invalidate(event.location) void result.location.mcp.server.sync(event.location) break case "mcp.resources.changed": result.location.mcp.resource.invalidate(event.location) void result.location.mcp.resource.sync(event.location) break } } const result = { on: client.event.on, listen: client.event.listen, session: { list() { return Object.values(store.session.info).toSorted((a, b) => b.time.updated - a.time.updated) }, get(sessionID: string) { return store.session.info[sessionID] }, root(sessionID: string) { return resolveRoot(sessionID) }, family(sessionID: string) { return store.session.family[resolveRoot(sessionID)] ?? [] }, cost(sessionID: string) { const session = store.session.info[sessionID] if (!session) return 0 if (session.parentID) return session.cost return (store.session.family[sessionID] ?? [sessionID]).reduce( (total, id) => total + (store.session.info[id]?.cost ?? 0), 0, ) }, status(sessionID: string) { return store.session.active[sessionID] ?? "idle" }, input: { list(sessionID: string) { return store.session.input[sessionID] ?? [] }, has(sessionID: string, inputID: string) { return store.session.input[sessionID]?.includes(inputID) ?? false }, }, pending: { list(sessionID: string) { return store.session.pending[sessionID] ?? [] }, sync(sessionID: string) { return sync.run(`session.pending:${sessionID}`, async () => { const pending = await client.api.session.pending.list({ sessionID }) setStore("session", "pending", sessionID, reconcile(pending)) setStore( "session", "input", sessionID, reconcile(pending.filter((item) => item.type !== "compaction").map((item) => item.id)), ) }) }, invalidate(sessionID: string) { sync.invalidate(`session.pending:${sessionID}`) }, }, sync(sessionID: string) { return sync.run(`session:${sessionID}`, async () => { setStore("session", "info", sessionID, await client.api.session.get({ sessionID })) registerSession(sessionID) }) }, invalidate(sessionID: string) { sync.invalidate(`session:${sessionID}`) }, message: { list(sessionID: string) { return store.session.message[sessionID] ?? [] }, get(sessionID: string, messageID: string) { const messages = store.session.message[sessionID] const position = messageIndex.get(sessionID)?.get(messageID) return position === undefined ? undefined : messages?.[position] }, sync(sessionID: string) { return sync.run(`session.message:${sessionID}`, async () => { const messages = ( await client.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)) }) }, invalidate(sessionID: string) { sync.invalidate(`session.message:${sessionID}`) }, }, permission: { list(sessionID: string) { return store.session.permission[sessionID] }, sync(sessionID: string) { return sync.run(`session.permission:${sessionID}`, async () => { setStore("session", "permission", sessionID, await client.api.permission.list({ sessionID })) }) }, invalidate(sessionID: string) { sync.invalidate(`session.permission:${sessionID}`) }, }, form: { list(sessionID: string, ref?: LocationRef) { const forms = store.session.form[sessionID] if (sessionID !== "global") return forms if (!ref) return const key = locationKey(ref) return forms?.filter((form) => form.location && locationKey(form.location) === key) }, sync(sessionID: string, ref?: LocationRef) { const key = `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}` return sync.run(key, async () => { if (sessionID === "global") { const response = await client.api.form.request.list({ location: locationQuery(ref ?? defaultLocation()), }) const location = { directory: response.location.directory, workspaceID: response.location.workspaceID, } const locationID = locationKey(location) setStore("session", "form", sessionID, [ ...(store.session.form[sessionID] ?? []).filter( (form) => form.location && locationKey(form.location) !== locationID, ), ...response.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location })), ]) return } setStore("session", "form", sessionID, await client.api.form.list({ sessionID })) }) }, invalidate(sessionID: string, ref?: LocationRef) { sync.invalidate( `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`, ) }, }, }, project: { permission: { list(projectID: string) { return store.project.permission[projectID] }, sync(projectID: string) { return sync.run(`project.permission:${projectID}`, async () => { setStore("project", "permission", projectID, await client.api.permission.saved.list({ projectID })) }) }, invalidate(projectID: string) { sync.invalidate(`project.permission:${projectID}`) }, }, }, shell: { list(location?: LocationRef) { return Object.values(store.location[locationKey(location ?? defaultLocation())]?.shell ?? {}) }, get(id: string) { return Object.values(store.location) .map((data) => data.shell?.[id]) .find((shell) => shell !== undefined) }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.shell:${id}`, async () => { const response = await client.api.shell.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], shell: Object.fromEntries(response.data.map((info) => [info.id, info])), }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.shell:${locationKey(ref ?? defaultLocation())}`) }, }, location: { info(ref?: LocationRef) { return store.location[locationKey(ref ?? defaultLocation())]?.info }, default() { return defaultLocation() }, async sync(ref?: LocationRef) { const current = ref ?? defaultLocation() await sync.run(`location:${locationKey(current)}`, async () => { const location = await client.api.location.get({ location: locationQuery(current) }) const key = locationKey(location) if (!store.location[key]) setStore("location", key, {}) setStore("location", key, "info", location) if (!ref) { setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID }) } }) const location = ref ?? defaultLocation() await Promise.all([ result.location.agent.sync(location), result.location.command.sync(location), result.location.integration.sync(location), result.location.mcp.server.sync(location), result.location.mcp.resource.sync(location), result.location.model.sync(location), result.location.provider.sync(location), result.location.reference.sync(location), result.location.skill.sync(location), result.shell.sync(location), result.session.form.sync("global", location), ]) }, invalidate(ref?: LocationRef) { const location = ref ?? defaultLocation() sync.invalidate(`location:${locationKey(location)}`) result.location.agent.invalidate(location) result.location.command.invalidate(location) result.location.integration.invalidate(location) result.location.mcp.server.invalidate(location) result.location.mcp.resource.invalidate(location) result.location.model.invalidate(location) result.location.provider.invalidate(location) result.location.reference.invalidate(location) result.location.skill.invalidate(location) result.shell.invalidate(location) result.session.form.invalidate("global", location) }, agent: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.agent }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.agent:${id}`, async () => { const response = await client.api.agent.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], agent: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.agent:${locationKey(ref ?? defaultLocation())}`) }, }, command: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.command }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.command:${id}`, async () => { const response = await client.api.command.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], command: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.command:${locationKey(ref ?? defaultLocation())}`) }, }, integration: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.integration }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.integration:${id}`, async () => { const response = await client.api.integration.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], integration: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.integration:${locationKey(ref ?? defaultLocation())}`) }, }, mcp: { server: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.mcp?.server }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.mcp.server:${id}`, async () => { const response = await client.api.mcp.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], mcp: { ...store.location[key]?.mcp, server: response.data }, }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.mcp.server:${locationKey(ref ?? defaultLocation())}`) }, }, resource: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.mcp?.resource }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.mcp.resource:${id}`, async () => { const response = await client.api.mcp.resource.catalog({ location: locationQuery(ref ?? defaultLocation()), }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], mcp: { ...store.location[key]?.mcp, resource: response.data.resources }, }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.mcp.resource:${locationKey(ref ?? defaultLocation())}`) }, }, }, model: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.model }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.model:${id}`, async () => { const response = await client.api.model.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], model: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.model:${locationKey(ref ?? defaultLocation())}`) }, }, provider: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.provider }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.provider:${id}`, async () => { const response = await client.api.provider.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], provider: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.provider:${locationKey(ref ?? defaultLocation())}`) }, }, reference: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.reference }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.reference:${id}`, async () => { const response = await client.api.reference.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], reference: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.reference:${locationKey(ref ?? defaultLocation())}`) }, }, skill: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.skill }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.skill:${id}`, async () => { const response = await client.api.skill.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], skill: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.skill:${locationKey(ref ?? defaultLocation())}`) }, }, }, } result satisfies Plugin.Context["data"] createEffect(() => { if (client.connection.status() === "connected") return sync.invalidate() }) onCleanup( client.event.listen(({ details }) => { if (details.type === "server.connected") { void client.api.session .active() .then((active) => { setStore( "session", "active", reconcile(Object.fromEntries(Object.keys(active).map((sessionID) => [sessionID, "running" as const]))), ) }) .catch(() => undefined) void client.api.location .get({ location: locationQuery(defaultLocation()) }) .then((location) => { const key = locationKey(location) setStore("location", key, { ...store.location[key], info: location }) return client.api.session.list({ project: location.project.id, limit: 50, order: "desc", parentID: null, }) }) .then((response) => { setStore( "session", "info", produce((draft) => { for (const session of response.data) draft[session.id] = session }), ) for (const session of response.data) { sync.complete(`session:${session.id}`) registerSession(session.id) } }) .catch((error) => console.error("Failed to preload sessions", error)) return } handleEvent(details) }), ) return result }, })