diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 7d93682bf..0cd4f6c99 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -24,6 +24,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo type Queued = { directory: string; payload: Event } let queue: Array = [] + let buffer: Array = [] const coalesced = new Map() let timer: ReturnType | undefined let last = 0 @@ -41,10 +42,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo if (timer) clearTimeout(timer) timer = undefined + if (queue.length === 0) return + const events = queue - queue = [] + queue = buffer + buffer = events + queue.length = 0 coalesced.clear() - if (events.length === 0) return last = Date.now() batch(() => { @@ -53,6 +57,8 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo emitter.emit(event.directory, event.payload) } }) + + buffer.length = 0 } const schedule = () => { @@ -61,10 +67,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo timer = setTimeout(flush, Math.max(0, 16 - elapsed)) } - const stop = () => { - flush() - } - void (async () => { const events = await eventSdk.global.event() let yielded = Date.now() @@ -87,12 +89,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo await new Promise((resolve) => setTimeout(resolve, 0)) } })() - .finally(stop) + .finally(flush) .catch(() => undefined) onCleanup(() => { abort.abort() - stop() + flush() }) const sdk = createOpencodeClient({ diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index cfbaa3723..37d4d6891 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -119,6 +119,16 @@ type ChildOptions = { bootstrap?: boolean } +function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { + return { + ...input, + all: input.all.map((provider) => ({ + ...provider, + models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")), + })), + } +} + function createGlobalSync() { const globalSDK = useGlobalSDK() const platform = usePlatform() @@ -129,6 +139,21 @@ function createGlobalSync() { const metaCache = new Map() const iconCache = new Map() + const sdkCache = new Map>() + const sdkFor = (directory: string) => { + const cached = sdkCache.get(directory) + if (cached) return cached + + const sdk = createOpencodeClient({ + baseUrl: globalSDK.url, + fetch: platform.fetch, + directory, + throwOnError: true, + }) + sdkCache.set(directory, sdk) + return sdk + } + const [projectCache, setProjectCache, , projectCacheReady] = persisted( Persist.global("globalSync.project", ["globalSync.project.v1"]), createStore({ value: [] as Project[] }), @@ -183,7 +208,7 @@ function createGlobalSync() { setProjectCache("value", projects.map(sanitizeProject)) }) - createEffect(async () => { + createEffect(() => { if (globalStore.reload !== "complete") return if (bootstrapQueue.length) { for (const directory of bootstrapQueue) { @@ -203,14 +228,16 @@ function createGlobalSync() { function ensureChild(directory: string) { if (!directory) console.error("No directory provided") if (!children[directory]) { - const cache = runWithOwner(owner, () => + const vcs = runWithOwner(owner, () => persisted( Persist.workspace(directory, "vcs", ["vcs.v1"]), createStore({ value: undefined as VcsInfo | undefined }), ), ) - if (!cache) throw new Error("Failed to create persisted cache") - vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] }) + if (!vcs) throw new Error("Failed to create persisted cache") + const vcsStore = vcs[0] + const vcsReady = vcs[3] + vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcsReady }) const meta = runWithOwner(owner, () => persisted( @@ -250,7 +277,7 @@ function createGlobalSync() { question: {}, mcp: {}, lsp: [], - vcs: cache[0].value, + vcs: vcsStore.value, limit: 5, message: {}, part: {}, @@ -258,6 +285,13 @@ function createGlobalSync() { children[directory] = child + createEffect(() => { + if (!vcsReady()) return + const cached = vcsStore.value + if (!cached?.branch) return + child[1]("vcs", (value) => value ?? cached) + }) + createEffect(() => { child[1]("projectMeta", meta[0].value) }) @@ -297,7 +331,6 @@ function createGlobalSync() { const nonArchived = (x.data ?? []) .filter((s) => !!s?.id) .filter((s) => !s.time?.archived) - .slice() .sort((a, b) => a.id.localeCompare(b.id)) // Read the current limit at resolve-time so callers that bump the limit while @@ -348,38 +381,18 @@ function createGlobalSync() { if (!cache) return const meta = metaCache.get(directory) if (!meta) return - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, - directory, - throwOnError: true, - }) + const sdk = sdkFor(directory) setStore("status", "loading") - createEffect(() => { - if (!cache.ready()) return - const cached = cache.store.value - if (!cached?.branch) return - setStore("vcs", (value) => value ?? cached) - }) - // projectMeta is synced from persisted storage in ensureChild. + // vcs is seeded from persisted storage in ensureChild. const blockingRequests = { project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), provider: () => sdk.provider.list().then((x) => { - const data = x.data! - setStore("provider", { - ...data, - all: data.all.map((provider) => ({ - ...provider, - models: Object.fromEntries( - Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), - ), - })), - }) + setStore("provider", normalizeProviderList(x.data!)) }), agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), config: () => sdk.config.get().then((x) => setStore("config", x.data!)), @@ -432,10 +445,7 @@ function createGlobalSync() { "permission", sessionID, reconcile( - permissions - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), + permissions.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)), { key: "id" }, ), ) @@ -464,10 +474,7 @@ function createGlobalSync() { "question", sessionID, reconcile( - questions - .filter((q) => !!q?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), + questions.filter((q) => !!q?.id).sort((a, b) => a.id.localeCompare(b.id)), { key: "id" }, ), ) @@ -750,13 +757,9 @@ function createGlobalSync() { break } case "lsp.updated": { - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, - directory, - throwOnError: true, - }) - sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])) + sdkFor(directory) + .lsp.status() + .then((x) => setStore("lsp", x.data ?? [])) break } } @@ -796,16 +799,7 @@ function createGlobalSync() { ), retry(() => globalSDK.client.provider.list().then((x) => { - const data = x.data! - setGlobalStore("provider", { - ...data, - all: data.all.map((provider) => ({ - ...provider, - models: Object.fromEntries( - Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), - ), - })), - }) + setGlobalStore("provider", normalizeProviderList(x.data!)) }), ), retry(() => diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index c24e433d1..1023d8df3 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -209,6 +209,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) const [colors, setColors] = createStore>({}) + const colorRequested = new Map() function pickAvailableColor(used: Set): AvatarColorKey { const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) @@ -324,13 +325,21 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createEffect(() => { const projects = enriched() if (projects.length === 0) return + if (!globalSync.ready) return - if (globalSync.ready) { - for (const project of projects) { - if (!project.id) continue - if (project.id === "global") continue - globalSync.project.icon(project.worktree, project.icon?.override) - } + for (const project of projects) { + if (!project.id) continue + if (project.id === "global") continue + globalSync.project.icon(project.worktree, project.icon?.override) + } + }) + + createEffect(() => { + const projects = enriched() + if (projects.length === 0) return + + for (const project of projects) { + if (project.icon?.color) colorRequested.delete(project.worktree) } const used = new Set() @@ -341,18 +350,29 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( for (const project of projects) { if (project.icon?.color) continue - const existing = colors[project.worktree] + const worktree = project.worktree + const existing = colors[worktree] const color = existing ?? pickAvailableColor(used) if (!existing) { used.add(color) - setColors(project.worktree, color) + setColors(worktree, color) } if (!project.id) continue + + const requested = colorRequested.get(worktree) + if (requested === color) continue + colorRequested.set(worktree, color) + if (project.id === "global") { - globalSync.project.meta(project.worktree, { icon: { color } }) + globalSync.project.meta(worktree, { icon: { color } }) continue } - void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } }) + + void globalSdk.client.project + .update({ projectID: project.id, directory: worktree, icon: { color } }) + .catch(() => { + if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) + }) } }) diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 58744045b..244e5e07b 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -72,7 +72,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const next = items .map((x) => x.info) .filter((m) => !!m?.id) - .slice() .sort((a, b) => a.id.localeCompare(b.id)) batch(() => { @@ -83,10 +82,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ "part", message.info.id, reconcile( - message.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), + message.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)), { key: "id" }, ), ) @@ -146,10 +142,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const result = Binary.search(messages, input.messageID, (m) => m.id) messages.splice(result.index, 0, message) } - draft.part[input.messageID] = input.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) + draft.part[input.messageID] = input.parts.filter((p) => !!p?.id).sort((a, b) => a.id.localeCompare(b.id)) }), ) }, @@ -291,7 +284,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ await client.session.list().then((x) => { const sessions = (x.data ?? []) .filter((s) => !!s?.id) - .slice() .sort((a, b) => a.id.localeCompare(b.id)) .slice(0, store.limit) setStore("session", reconcile(sessions, { key: "id" }))