/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/tui/src/context/data.tsx
569 строк
22 KB
Dax
fix(sdk): preserve V2Event name for SSE streams (#34171)
28 июн 2026, 04:05
Не верифицирован
28 июн 2026, 04:05
6446b8a
Код
Авторство
О чём код?
import type { AgentV2Info, CommandV2Info, IntegrationInfo, LocationRef, ModelV2Info, PermissionSavedInfo, PermissionV2Request, ProviderV2Info, QuestionV2Request, ReferenceInfo, SessionMessage, SessionMessageAssistant, SessionMessageAssistantReasoning, SessionMessageAssistantText, SessionMessageAssistantTool, SessionV2Info, SkillV2Info, V2Event, } from "@opencode-ai/sdk/v2" import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "./helper" import { useSDK } from "./sdk" import { useEvent } from "./event" import { createSignal, onCleanup, onMount } from "solid-js" type LocationData = { agent?: AgentV2Info[] command?: CommandV2Info[] integration?: IntegrationInfo[] model?: ModelV2Info[] provider?: ProviderV2Info[] reference?: ReferenceInfo[] skill?: SkillV2Info[] } type Data = { session: { info: Record<string, SessionV2Info> message: Record<string, SessionMessage[]> permission: Record<string, PermissionV2Request[]> question: Record<string, QuestionV2Request[]> } project: { permission: Record<string, PermissionSavedInfo[]> } location: Record<string, LocationData> } function locationKey(location: LocationRef) { return JSON.stringify([location.directory, location.workspaceID]) } function locationQuery(ref?: LocationRef) { return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined } export const { use: useData, provider: DataProvider } = createSimpleContext({ name: "Data", init: () => { const [store, setStore] = createStore<Data>({ session: { info: {}, message: {}, permission: {}, question: {}, }, project: { permission: {}, }, location: {}, }) const sdk = useSDK() const events = useEvent() const [defaultLocation, setDefaultLocation] = createSignal<LocationRef>({ directory: sdk.directory ?? process.cwd(), }) const message = { update(sessionID: string, fn: (messages: SessionMessage[]) => void) { setStore( "session", "message", produce((draft) => { fn((draft[sessionID] ??= [])) }), ) }, prepend(messages: SessionMessage[], item: SessionMessage) { if (messages.some((existing) => existing.id === item.id)) return messages.unshift(item) }, activeAssistant(messages: SessionMessage[]) { const item = messages.find((item) => item.type === "assistant" && !item.time.completed) return item?.type === "assistant" ? item : undefined }, assistant(messages: SessionMessage[], messageID: string) { const item = messages.find((item) => item.type === "assistant" && item.id === messageID) return item?.type === "assistant" ? item : undefined }, activeShell(messages: SessionMessage[], callID: string) { const item = messages.find((item) => item.type === "shell" && item.callID === callID) return item?.type === "shell" ? 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, textID: string) { return assistant?.content.findLast( (item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID, ) }, latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) { return assistant?.content.findLast( (item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID, ) }, } function handleEvent(event: V2Event) { switch (event.type) { case "catalog.updated": void Promise.all([ result.location.model.refresh(event.location), result.location.provider.refresh(event.location), ]) break case "session.next.agent.switched": message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "agent-switched", agent: event.data.agent, time: { created: event.data.timestamp }, }) }) break case "session.next.model.switched": message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "model-switched", model: event.data.model, time: { created: event.data.timestamp }, }) }) break case "session.next.prompted": { message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "user", text: event.data.prompt.text, files: event.data.prompt.files, agents: event.data.prompt.agents, time: { created: event.data.timestamp }, }) }) break } case "session.next.prompt.admitted": break case "session.next.context.updated": message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "system", text: event.data.text, time: { created: event.data.timestamp }, }) }) break case "session.next.synthetic": message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "synthetic", sessionID: event.data.sessionID, text: event.data.text, time: { created: event.data.timestamp }, }) }) break case "session.next.shell.started": message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "shell", callID: event.data.callID, command: event.data.command, output: "", time: { created: event.data.timestamp }, }) }) break case "session.next.shell.ended": message.update(event.data.sessionID, (draft) => { const match = message.activeShell(draft, event.data.callID) if (!match) return match.output = event.data.output match.time.completed = event.data.timestamp }) break case "session.next.step.started": message.update(event.data.sessionID, (draft) => { if (draft.some((message) => message.id === event.data.assistantMessageID)) return const currentAssistant = message.activeAssistant(draft) if (currentAssistant) currentAssistant.time.completed = event.data.timestamp message.prepend(draft, { id: event.data.assistantMessageID, type: "assistant", agent: event.data.agent, model: event.data.model, content: [], snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, time: { created: event.data.timestamp }, }) }) break case "session.next.step.ended": message.update(event.data.sessionID, (draft) => { const currentAssistant = message.assistant(draft, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.data.timestamp 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.next.step.failed": message.update(event.data.sessionID, (draft) => { const currentAssistant = message.assistant(draft, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.data.timestamp currentAssistant.finish = "error" currentAssistant.error = event.data.error }) break case "session.next.text.started": message.update(event.data.sessionID, (draft) => { message.assistant(draft, event.data.assistantMessageID)?.content.push({ type: "text", id: event.data.textID, text: "", }) }) break case "session.next.text.delta": message.update(event.data.sessionID, (draft) => { const match = message.latestText(message.assistant(draft, 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) => { const match = message.latestText(message.assistant(draft, event.data.assistantMessageID), event.data.textID) if (match) match.text = event.data.text }) break case "session.next.tool.input.started": message.update(event.data.sessionID, (draft) => { message.assistant(draft, event.data.assistantMessageID)?.content.push({ type: "tool", id: event.data.callID, name: event.data.name, time: { created: event.data.timestamp }, state: { status: "pending", input: "" }, }) }) break case "session.next.tool.input.delta": message.update(event.data.sessionID, (draft) => { const match = message.latestTool(message.assistant(draft, 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) => { const match = message.latestTool(message.assistant(draft, 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) => { const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID) if (!match) return match.time.ran = event.data.timestamp match.provider = event.data.provider match.state = { status: "running", input: event.data.input, structured: {}, content: [] } }) break case "session.next.tool.progress": message.update(event.data.sessionID, (draft) => { const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID) if (match?.state.status !== "running") return match.state.structured = event.data.structured match.state.content = [...event.data.content] }) break case "session.next.tool.success": message.update(event.data.sessionID, (draft) => { const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID) if (match?.state.status !== "running") return match.state = { status: "completed", input: match.state.input, structured: event.data.structured, content: [...event.data.content], result: event.data.result, } match.provider = { executed: event.data.provider.executed || match.provider?.executed === true, metadata: match.provider?.metadata, resultMetadata: event.data.provider.metadata, } match.time.completed = event.data.timestamp }) break case "session.next.tool.failed": message.update(event.data.sessionID, (draft) => { const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID) if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return match.state = { status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, structured: match.state.status === "running" ? match.state.structured : {}, content: match.state.status === "running" ? match.state.content : [], result: event.data.result, } match.provider = { executed: event.data.provider.executed || match.provider?.executed === true, metadata: match.provider?.metadata, resultMetadata: event.data.provider.metadata, } match.time.completed = event.data.timestamp }) break case "session.next.reasoning.started": message.update(event.data.sessionID, (draft) => { message.assistant(draft, event.data.assistantMessageID)?.content.push({ type: "reasoning", id: event.data.reasoningID, text: "", providerMetadata: event.data.providerMetadata, }) }) break case "session.next.reasoning.delta": message.update(event.data.sessionID, (draft) => { const match = message.latestReasoning( message.assistant(draft, event.data.assistantMessageID), event.data.reasoningID, ) if (match) match.text += event.data.delta }) break case "session.next.reasoning.ended": message.update(event.data.sessionID, (draft) => { const match = message.latestReasoning( message.assistant(draft, event.data.assistantMessageID), event.data.reasoningID, ) if (match) { match.text = event.data.text if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata } }) break case "session.next.retried": case "session.next.compaction.started": case "session.next.compaction.delta": break case "session.next.compaction.ended": message.update(event.data.sessionID, (draft) => { message.prepend(draft, { id: event.data.messageID, type: "compaction", reason: event.data.reason, summary: event.data.text, recent: event.data.recent, time: { created: event.data.timestamp }, }) }) break case "reference.updated": void result.location.reference.refresh() break case "integration.updated": void Promise.all([ result.location.integration.refresh(event.location), result.location.model.refresh(event.location), result.location.provider.refresh(event.location), ]) break } } onMount(() => { const unsub = events.subscribe((event, metadata) => { handleEvent({ ...event, data: event.properties, location: { directory: metadata.directory, workspaceID: metadata.workspace }, } as V2Event) }) onCleanup(unsub) }) const result = { session: { get(sessionID: string) { return store.session.info[sessionID] }, async refresh(sessionID: string) { const result = await sdk.client.v2.session.get({ sessionID }, { throwOnError: true }) setStore("session", "info", sessionID, result.data.data) }, message: { list(sessionID: string) { return store.session.message[sessionID] }, async refresh(sessionID: string) { const result = await sdk.client.v2.session.messages({ sessionID }, { throwOnError: true }) setStore("session", "message", sessionID, result.data.data) }, }, permission: { list(sessionID: string) { 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) }, }, question: { list(sessionID: string) { 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) }, }, }, project: { permission: { list(projectID: string) { 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) }, }, }, location: { default() { return defaultLocation() }, async refresh(ref?: LocationRef) { const response = await sdk.client.v2.location.get({ location: locationQuery(ref) }, { throwOnError: true }) const location = response.data const key = locationKey(location) if (!store.location[key]) setStore("location", key, {}) if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID }) }, agent: { list(location?: LocationRef) { 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, "agent", result.data.data) }, }, command: { list(location?: LocationRef) { 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, "command", result.data.data) }, }, integration: { list(location?: LocationRef) { 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, "integration", result.data.data) }, }, model: { list(location?: LocationRef) { 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, "model", result.data.data) }, }, provider: { list(location?: LocationRef) { 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, "provider", result.data.data) }, }, reference: { list(location?: LocationRef) { 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, "reference", result.data.data) }, }, skill: { list(location?: LocationRef) { 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, "skill", result.data.data) }, }, }, } onMount(() => { void Promise.allSettled([ result.location.refresh(), result.location.agent.refresh(), result.location.integration.refresh(), result.location.model.refresh(), result.location.provider.refresh(), result.location.reference.refresh(), result.location.command.refresh(), result.location.skill.refresh(), ]).then((settled) => { for (const failure of settled.filter((item) => item.status === "rejected")) console.error("Failed to refresh default location data", failure.reason) }) }) return result }, })