From f6101aef8a813e0649b3a707803645ea15b4efe6 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Fri, 22 May 2026 14:40:56 +0800 Subject: [PATCH] refactor(app): consolidate sdk and sync contexts (#28782) --- packages/app/src/components/titlebar.tsx | 24 ++++++--- packages/app/src/context/global-sdk.tsx | 55 +++++++++++++++++++ packages/app/src/context/sdk.tsx | 42 +-------------- packages/app/src/context/sync.tsx | 60 ++------------------- packages/app/src/pages/directory-layout.tsx | 8 ++- 5 files changed, 82 insertions(+), 107 deletions(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index e9b723d518..1df7c43528 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -23,6 +23,8 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx" import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers" import { makeEventListener } from "@solid-primitives/event-listener" +import { StatusPopover } from "./status-popover" +import { SDKProvider } from "@/context/sdk" type TauriDesktopWindow = { startDragging?: () => Promise @@ -296,14 +298,13 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { const currentSessionTab = () => { if (!params.dir || !params.id) return const href = makeSessionHref(params.dir, params.id) - if (!tabsStore.some((tab) => tab.href === href)) return - return href + return tabsStore.find((tab) => tab.href === href) } const closeCurrentSessionTab = () => { - const href = currentSessionTab() - if (!href) return false - tabsStoreActions.removeTab(href) + const tab = currentSessionTab() + if (!tab) return false + tabsStoreActions.removeTab(tab.href) return true } @@ -338,7 +339,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { keybind: `mod+option+ArrowLeft`, hidden: true, onSelect: () => { - let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()) + let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href) if (index === -1) return index -= 1 @@ -355,7 +356,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { keybind: `mod+option+ArrowRight`, hidden: true, onSelect: () => { - let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()) + let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href) if (index === -1) return index += 1 @@ -464,6 +465,15 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
+ + {(dir) => ( + + + + + + )} +
diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 001b90b42e..18ad5ab677 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -232,6 +232,9 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo throwOnError: true, }) + const dirSyncContexts = new Map>() + const dirSdkContextRefCounts = new Map() + return { url: currentServer.http.url, client: sdk, @@ -249,6 +252,58 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo ...opts, }) }, + createDirSyncContext: (directory: string) => { + onCleanup(() => { + dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) - 1) + if (dirSdkContextRefCounts.get(directory) === 0) { + dirSyncContexts.delete(directory) + dirSdkContextRefCounts.delete(directory) + } + }) + + const cached = dirSyncContexts.get(directory) + if (cached) { + dirSdkContextRefCounts.set(directory, (dirSdkContextRefCounts.get(directory) ?? 0) + 1) + return cached + } + const ctx = createDirSdkContext(directory) + dirSyncContexts.set(directory, ctx) + dirSdkContextRefCounts.set(directory, 1) + + return ctx + }, } }, }) + +type SDKEventMap = { + [key in Event["type"]]: Extract +} + +function createDirSdkContext(directory: string) { + const globalSDK = useGlobalSDK() + + const client = globalSDK.createClient({ + directory, + throwOnError: true, + }) + + const emitter = createGlobalEmitter() + + const unsub = globalSDK.event.on(directory, (event) => { + emitter.emit(event.type, event) + }) + onCleanup(unsub) + + return { + directory, + client, + event: emitter, + get url() { + return globalSDK.url + }, + createClient(opts: Parameters[0]) { + return globalSDK.createClient(opts) + }, + } +} diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx index bc97ea13ac..7f9edf365d 100644 --- a/packages/app/src/context/sdk.tsx +++ b/packages/app/src/context/sdk.tsx @@ -1,49 +1,11 @@ -import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" -import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js" import { useGlobalSDK } from "./global-sdk" -type SDKEventMap = { - [key in Event["type"]]: Extract -} - export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", - init: (props: { directory: Accessor }) => { + init: (props: { directory: string }) => { const globalSDK = useGlobalSDK() - const directory = createMemo(props.directory) - const client = createMemo(() => - globalSDK.createClient({ - directory: directory(), - throwOnError: true, - }), - ) - - const emitter = createGlobalEmitter() - - createEffect(() => { - const unsub = globalSDK.event.on(directory(), (event) => { - emitter.emit(event.type, event) - }) - onCleanup(unsub) - }) - - return { - get directory() { - return directory() - }, - get client() { - return client() - }, - event: emitter, - get url() { - return globalSDK.url - }, - createClient(opts: Parameters[0]) { - return globalSDK.createClient(opts) - }, - } + return globalSDK.createDirSyncContext(props.directory) }, }) diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index ba97d14282..e5bf1ccfae 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,5 +1,4 @@ import { Binary } from "@opencode-ai/core/util/binary" -import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" import { useSDK } from "./sdk" import type { Message, Part } from "@opencode-ai/sdk/v2/client" @@ -10,26 +9,8 @@ function sortParts(parts: Part[]) { return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) } -function runInflight(map: Map>, key: string, task: () => Promise) { - const pending = map.get(key) - if (pending) return pending - const promise = task().finally(() => { - map.delete(key) - }) - map.set(key, promise) - return promise -} - -const keyFor = (directory: string, id: string) => `${directory}\n${id}` - const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) -function merge(a: readonly T[], b: readonly T[]) { - const map = new Map(a.map((item) => [item.id, item] as const)) - for (const item of b) map.set(item.id, item) - return [...map.values()].sort((x, y) => cmp(x.id, y.id)) -} - type OptimisticStore = { message: Record part: Record @@ -127,40 +108,9 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR delete draft.part[input.messageID] } -function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) { - setStore("message", input.sessionID, (messages: Message[] | undefined) => { - if (!messages) return [input.message] - const result = Binary.search(messages, input.message.id, (m) => m.id) - const next = [...messages] - next.splice(result.index, 0, input.message) - return next - }) - setStore("part", input.message.id, sortParts(input.parts)) +export const useSync = () => { + const globalSync = useGlobalSync() + const sdk = useSDK() + + return globalSync.createDirSyncContext(sdk.directory) } - -function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) { - setStore("message", input.sessionID, (messages: Message[] | undefined) => { - if (!messages) return messages - const result = Binary.search(messages, input.messageID, (m) => m.id) - if (!result.found) return messages - const next = [...messages] - next.splice(result.index, 1) - return next - }) - setStore("part", (part: Record) => { - if (!(input.messageID in part)) return part - const next = { ...part } - delete next[input.messageID] - return next - }) -} - -export const { use: useSync, provider: SyncProvider } = createSimpleContext({ - name: "Sync", - init: () => { - const globalSync = useGlobalSync() - const sdk = useSDK() - - return globalSync.createDirSyncContext(sdk.directory) - }, -}) diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 59f454ff57..4759f44c3b 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -6,7 +6,7 @@ import { createEffect, createMemo, createResource, type ParentProps, Show } from import { useLanguage } from "@/context/language" import { LocalProvider } from "@/context/local" import { SDKProvider } from "@/context/sdk" -import { SyncProvider, useSync } from "@/context/sync" +import { useSync } from "@/context/sync" import { decode64 } from "@/utils/base64" import { Schema } from "effect" @@ -81,10 +81,8 @@ export default function Layout(props: ParentProps) { return ( {(resolved) => ( - resolved}> - - {props.children} - + + {props.children} )}