Просмотр исходного кода

refactor(app): consolidate sdk and sync contexts (#28782)

Brendan Allan 3 месяцев назад
Родитель
Сommit
f6101aef8a

+ 17 - 7
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<void>
@@ -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 }) {
                   </Show>
                   <div class="min-w-0 flex-1" />
                 </div>
+                <Show when={currentSessionTab()?.dir} keyed>
+                  {(dir) => (
+                    <SDKProvider directory={dir}>
+                      <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
+                        <StatusPopover />
+                      </Tooltip>
+                    </SDKProvider>
+                  )}
+                </Show>
                 <TitlebarUpdatePill update={props.update} />
                 <Show when={windows() && !electronWindows()}>
                   <div data-tauri-decorum-tb class="flex flex-row" />

+ 55 - 0
packages/app/src/context/global-sdk.tsx

@@ -232,6 +232,9 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
       throwOnError: true,
     })
 
+    const dirSyncContexts = new Map<string, ReturnType<typeof createDirSdkContext>>()
+    const dirSdkContextRefCounts = new Map<string, number>()
+
     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<Event, { type: key }>
+}
+
+function createDirSdkContext(directory: string) {
+  const globalSDK = useGlobalSDK()
+
+  const client = globalSDK.createClient({
+    directory,
+    throwOnError: true,
+  })
+
+  const emitter = createGlobalEmitter<SDKEventMap>()
+
+  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<typeof globalSDK.createClient>[0]) {
+      return globalSDK.createClient(opts)
+    },
+  }
+}

+ 2 - 40
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<Event, { type: key }>
-}
-
 export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
   name: "SDK",
-  init: (props: { directory: Accessor<string> }) => {
+  init: (props: { directory: string }) => {
     const globalSDK = useGlobalSDK()
 
-    const directory = createMemo(props.directory)
-    const client = createMemo(() =>
-      globalSDK.createClient({
-        directory: directory(),
-        throwOnError: true,
-      }),
-    )
-
-    const emitter = createGlobalEmitter<SDKEventMap>()
-
-    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<typeof globalSDK.createClient>[0]) {
-        return globalSDK.createClient(opts)
-      },
-    }
+    return globalSDK.createDirSyncContext(props.directory)
   },
 })

+ 4 - 54
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<string, Promise<void>>, key: string, task: () => Promise<void>) {
-  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<T extends { id: string }>(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<string, Message[] | undefined>
   part: Record<string, Part[] | undefined>
@@ -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()
 
-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<string, Part[] | undefined>) => {
-    if (!(input.messageID in part)) return part
-    const next = { ...part }
-    delete next[input.messageID]
-    return next
-  })
+  return globalSync.createDirSyncContext(sdk.directory)
 }
-
-export const { use: useSync, provider: SyncProvider } = createSimpleContext({
-  name: "Sync",
-  init: () => {
-    const globalSync = useGlobalSync()
-    const sdk = useSDK()
-
-    return globalSync.createDirSyncContext(sdk.directory)
-  },
-})

+ 3 - 5
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 (
     <Show when={resolved()} keyed>
       {(resolved) => (
-        <SDKProvider directory={() => resolved}>
-          <SyncProvider>
-            <DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
-          </SyncProvider>
+        <SDKProvider directory={resolved}>
+          <DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
         </SDKProvider>
       )}
     </Show>