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

perf(app): don't remount directory layout

Adam 4 недель назад
Родитель
Сommit
4afb46f571

+ 2 - 2
packages/app/src/components/dialog-select-file.tsx

@@ -32,8 +32,8 @@ export function DialogSelectFile() {
   const dialog = useDialog()
   const params = useParams()
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
-  const tabs = createMemo(() => layout.tabs(sessionKey()))
-  const view = createMemo(() => layout.view(sessionKey()))
+  const tabs = createMemo(() => layout.tabs(sessionKey))
+  const view = createMemo(() => layout.view(sessionKey))
   const state = { cleanup: undefined as (() => void) | void, committed: false }
   const [grouped, setGrouped] = createSignal(false)
   const common = [

+ 2 - 2
packages/app/src/components/prompt-input.tsx

@@ -167,8 +167,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   }
 
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
-  const tabs = createMemo(() => layout.tabs(sessionKey()))
-  const view = createMemo(() => layout.view(sessionKey()))
+  const tabs = createMemo(() => layout.tabs(sessionKey))
+  const view = createMemo(() => layout.view(sessionKey))
 
   const recent = createMemo(() => {
     const all = tabs().all()

+ 2 - 2
packages/app/src/components/session-context-usage.tsx

@@ -21,8 +21,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
 
   const variant = createMemo(() => props.variant ?? "button")
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
-  const tabs = createMemo(() => layout.tabs(sessionKey()))
-  const view = createMemo(() => layout.view(sessionKey()))
+  const tabs = createMemo(() => layout.tabs(sessionKey))
+  const view = createMemo(() => layout.view(sessionKey))
   const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
 
   const cost = createMemo(() => {

+ 1 - 1
packages/app/src/components/session/session-header.tsx

@@ -50,7 +50,7 @@ export function SessionHeader() {
   const showShare = createMemo(() => shareEnabled() && !!currentSession())
   const showReview = createMemo(() => !!currentSession())
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
-  const view = createMemo(() => layout.view(sessionKey()))
+  const view = createMemo(() => layout.view(sessionKey))
 
   const [state, setState] = createStore({
     share: false,

+ 18 - 4
packages/app/src/context/file.tsx

@@ -189,6 +189,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
     const params = useParams()
     const language = useLanguage()
 
+    const scope = createMemo(() => sdk.directory)
+
     const directory = createMemo(() => sync.data.path.directory)
 
     function normalize(input: string) {
@@ -234,6 +236,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
       file: {},
     })
 
+    createEffect(() => {
+      scope()
+      inflight.clear()
+      setStore("file", {})
+    })
+
     const viewCache = new Map<string, ViewCacheEntry>()
 
     const disposeViews = () => {
@@ -284,12 +292,16 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
       const path = normalize(input)
       if (!path) return Promise.resolve()
 
+      const directory = scope()
+      const key = `${directory}\n${path}`
+      const client = sdk.client
+
       ensure(path)
 
       const current = store.file[path]
       if (!options?.force && current?.loaded) return Promise.resolve()
 
-      const pending = inflight.get(path)
+      const pending = inflight.get(key)
       if (pending) return pending
 
       setStore(
@@ -301,9 +313,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
         }),
       )
 
-      const promise = sdk.client.file
+      const promise = client.file
         .read({ path })
         .then((x) => {
+          if (scope() !== directory) return
           setStore(
             "file",
             path,
@@ -315,6 +328,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
           )
         })
         .catch((e) => {
+          if (scope() !== directory) return
           setStore(
             "file",
             path,
@@ -330,10 +344,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
           })
         })
         .finally(() => {
-          inflight.delete(path)
+          inflight.delete(key)
         })
 
-      inflight.set(path, promise)
+      inflight.set(key, promise)
       return promise
     }
 

+ 72 - 39
packages/app/src/context/layout.tsx

@@ -1,5 +1,5 @@
 import { createStore, produce } from "solid-js/store"
-import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js"
+import { batch, createEffect, createMemo, on, onCleanup, onMount, type Accessor } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSync } from "./global-sync"
 import { useGlobalSDK } from "./global-sdk"
@@ -432,10 +432,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("mobileSidebar", "opened", (x) => !x)
         },
       },
-      view(sessionKey: string) {
-        touch(sessionKey)
-        scroll.seed(sessionKey)
-        const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
+      view(sessionKey: string | Accessor<string>) {
+        const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
+
+        touch(key())
+        scroll.seed(key())
+
+        createEffect(
+          on(
+            key,
+            (value) => {
+              touch(value)
+              scroll.seed(value)
+            },
+            { defer: true },
+          ),
+        )
+
+        const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
         const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
         const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
 
@@ -465,10 +479,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
 
         return {
           scroll(tab: string) {
-            return scroll.scroll(sessionKey, tab)
+            return scroll.scroll(key(), tab)
           },
           setScroll(tab: string, pos: SessionScroll) {
-            scroll.setScroll(sessionKey, tab, pos)
+            scroll.setScroll(key(), tab, pos)
           },
           terminal: {
             opened: terminalOpened,
@@ -497,9 +511,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           review: {
             open: createMemo(() => s().reviewOpen),
             setOpen(open: string[]) {
-              const current = store.sessionView[sessionKey]
+              const session = key()
+              const current = store.sessionView[session]
               if (!current) {
-                setStore("sessionView", sessionKey, {
+                setStore("sessionView", session, {
                   scroll: {},
                   reviewOpen: open,
                 })
@@ -507,93 +522,111 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
               }
 
               if (same(current.reviewOpen, open)) return
-              setStore("sessionView", sessionKey, "reviewOpen", open)
+              setStore("sessionView", session, "reviewOpen", open)
             },
           },
         }
       },
-      tabs(sessionKey: string) {
-        touch(sessionKey)
-        const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
+      tabs(sessionKey: string | Accessor<string>) {
+        const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
+
+        touch(key())
+
+        createEffect(
+          on(
+            key,
+            (value) => {
+              touch(value)
+            },
+            { defer: true },
+          ),
+        )
+
+        const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
         return {
           tabs,
           active: createMemo(() => tabs().active),
           all: createMemo(() => tabs().all),
           setActive(tab: string | undefined) {
-            if (!store.sessionTabs[sessionKey]) {
-              setStore("sessionTabs", sessionKey, { all: [], active: tab })
+            const session = key()
+            if (!store.sessionTabs[session]) {
+              setStore("sessionTabs", session, { all: [], active: tab })
             } else {
-              setStore("sessionTabs", sessionKey, "active", tab)
+              setStore("sessionTabs", session, "active", tab)
             }
           },
           setAll(all: string[]) {
-            if (!store.sessionTabs[sessionKey]) {
-              setStore("sessionTabs", sessionKey, { all, active: undefined })
+            const session = key()
+            if (!store.sessionTabs[session]) {
+              setStore("sessionTabs", session, { all, active: undefined })
             } else {
-              setStore("sessionTabs", sessionKey, "all", all)
+              setStore("sessionTabs", session, "all", all)
             }
           },
           async open(tab: string) {
-            const current = store.sessionTabs[sessionKey] ?? { all: [] }
+            const session = key()
+            const current = store.sessionTabs[session] ?? { all: [] }
 
             if (tab === "review") {
-              if (!store.sessionTabs[sessionKey]) {
-                setStore("sessionTabs", sessionKey, { all: [], active: tab })
+              if (!store.sessionTabs[session]) {
+                setStore("sessionTabs", session, { all: [], active: tab })
                 return
               }
-              setStore("sessionTabs", sessionKey, "active", tab)
+              setStore("sessionTabs", session, "active", tab)
               return
             }
 
             if (tab === "context") {
               const all = [tab, ...current.all.filter((x) => x !== tab)]
-              if (!store.sessionTabs[sessionKey]) {
-                setStore("sessionTabs", sessionKey, { all, active: tab })
+              if (!store.sessionTabs[session]) {
+                setStore("sessionTabs", session, { all, active: tab })
                 return
               }
-              setStore("sessionTabs", sessionKey, "all", all)
-              setStore("sessionTabs", sessionKey, "active", tab)
+              setStore("sessionTabs", session, "all", all)
+              setStore("sessionTabs", session, "active", tab)
               return
             }
 
             if (!current.all.includes(tab)) {
-              if (!store.sessionTabs[sessionKey]) {
-                setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
+              if (!store.sessionTabs[session]) {
+                setStore("sessionTabs", session, { all: [tab], active: tab })
                 return
               }
-              setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
-              setStore("sessionTabs", sessionKey, "active", tab)
+              setStore("sessionTabs", session, "all", [...current.all, tab])
+              setStore("sessionTabs", session, "active", tab)
               return
             }
 
-            if (!store.sessionTabs[sessionKey]) {
-              setStore("sessionTabs", sessionKey, { all: current.all, active: tab })
+            if (!store.sessionTabs[session]) {
+              setStore("sessionTabs", session, { all: current.all, active: tab })
               return
             }
-            setStore("sessionTabs", sessionKey, "active", tab)
+            setStore("sessionTabs", session, "active", tab)
           },
           close(tab: string) {
-            const current = store.sessionTabs[sessionKey]
+            const session = key()
+            const current = store.sessionTabs[session]
             if (!current) return
 
             const all = current.all.filter((x) => x !== tab)
             batch(() => {
-              setStore("sessionTabs", sessionKey, "all", all)
+              setStore("sessionTabs", session, "all", all)
               if (current.active !== tab) return
 
               const index = current.all.findIndex((f) => f === tab)
               const next = all[index - 1] ?? all[0]
-              setStore("sessionTabs", sessionKey, "active", next)
+              setStore("sessionTabs", session, "active", next)
             })
           },
           move(tab: string, to: number) {
-            const current = store.sessionTabs[sessionKey]
+            const session = key()
+            const current = store.sessionTabs[session]
             if (!current) return
             const index = current.all.findIndex((f) => f === tab)
             if (index === -1) return
             setStore(
               "sessionTabs",
-              sessionKey,
+              session,
               "all",
               produce((opened) => {
                 opened.splice(to, 0, opened.splice(index, 1)[0])

+ 16 - 3
packages/app/src/context/local.tsx

@@ -1,5 +1,5 @@
 import { createStore, produce, reconcile } from "solid-js/store"
-import { batch, createMemo, onCleanup } from "solid-js"
+import { batch, createEffect, createMemo, onCleanup } from "solid-js"
 import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
 import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
 import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -338,6 +338,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         node: {}, //  Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
       })
 
+      const scope = createMemo(() => sdk.directory)
+      createEffect(() => {
+        scope()
+        setStore("node", {})
+      })
+
       // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
       // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
 
@@ -394,10 +400,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
 
       const load = async (path: string) => {
+        const directory = scope()
+        const client = sdk.client
         const relativePath = relative(path)
-        await sdk.client.file
+        await client.file
           .read({ path: relativePath })
           .then((x) => {
+            if (scope() !== directory) return
             if (!store.node[relativePath]) return
             setStore(
               "node",
@@ -409,6 +418,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
             )
           })
           .catch((e) => {
+            if (scope() !== directory) return
             showToast({
               variant: "error",
               title: language.t("toast.file.loadFailed.title"),
@@ -453,9 +463,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       }
 
       const list = async (path: string) => {
-        return sdk.client.file
+        const directory = scope()
+        const client = sdk.client
+        return client.file
           .list({ path: path + "/" })
           .then((x) => {
+            if (scope() !== directory) return
             setStore(
               "node",
               produce((draft) => {

+ 28 - 11
packages/app/src/context/sdk.tsx

@@ -1,7 +1,7 @@
 import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createGlobalEmitter } from "@solid-primitives/event-bus"
-import { onCleanup } from "solid-js"
+import { createEffect, createMemo, onCleanup } from "solid-js"
 import { useGlobalSDK } from "./global-sdk"
 import { usePlatform } from "./platform"
 
@@ -10,22 +10,39 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
   init: (props: { directory: string }) => {
     const platform = usePlatform()
     const globalSDK = useGlobalSDK()
-    const sdk = createOpencodeClient({
-      baseUrl: globalSDK.url,
-      fetch: platform.fetch,
-      directory: props.directory,
-      throwOnError: true,
-    })
+
+    const directory = createMemo(() => props.directory)
+    const client = createMemo(() =>
+      createOpencodeClient({
+        baseUrl: globalSDK.url,
+        fetch: platform.fetch,
+        directory: directory(),
+        throwOnError: true,
+      }),
+    )
 
     const emitter = createGlobalEmitter<{
       [key in Event["type"]]: Extract<Event, { type: key }>
     }>()
 
-    const unsub = globalSDK.event.on(props.directory, (event) => {
-      emitter.emit(event.type, event)
+    createEffect(() => {
+      const unsub = globalSDK.event.on(directory(), (event) => {
+        emitter.emit(event.type, event)
+      })
+      onCleanup(unsub)
     })
-    onCleanup(unsub)
 
-    return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
+    return {
+      get directory() {
+        return directory()
+      },
+      get client() {
+        return client()
+      },
+      event: emitter,
+      get url() {
+        return globalSDK.url
+      },
+    }
   },
 })

+ 113 - 47
packages/app/src/context/sync.tsx

@@ -7,13 +7,20 @@ import { useGlobalSync } from "./global-sync"
 import { useSDK } from "./sdk"
 import type { Message, Part } from "@opencode-ai/sdk/v2/client"
 
+const keyFor = (directory: string, id: string) => `${directory}\n${id}`
+
 export const { use: useSync, provider: SyncProvider } = createSimpleContext({
   name: "Sync",
   init: () => {
     const globalSync = useGlobalSync()
     const sdk = useSDK()
-    const [store, setStore] = globalSync.child(sdk.directory)
-    const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
+
+    type Child = ReturnType<(typeof globalSync)["child"]>
+    type Store = Child[0]
+    type Setter = Child[1]
+
+    const current = createMemo(() => globalSync.child(sdk.directory))
+    const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
     const chunk = 400
     const inflight = new Map<string, Promise<void>>()
     const inflightDiff = new Map<string, Promise<void>>()
@@ -25,6 +32,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     })
 
     const getSession = (sessionID: string) => {
+      const store = current()[0]
       const match = Binary.search(store.session, sessionID, (s) => s.id)
       if (match.found) return store.session[match.index]
       return undefined
@@ -35,22 +43,30 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       return Math.ceil(count / chunk) * chunk
     }
 
-    const hydrateMessages = (sessionID: string) => {
-      if (meta.limit[sessionID] !== undefined) return
+    const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
+      const key = keyFor(directory, sessionID)
+      if (meta.limit[key] !== undefined) return
 
       const messages = store.message[sessionID]
       if (!messages) return
 
       const limit = limitFor(messages.length)
-      setMeta("limit", sessionID, limit)
-      setMeta("complete", sessionID, messages.length < limit)
+      setMeta("limit", key, limit)
+      setMeta("complete", key, messages.length < limit)
     }
 
-    const loadMessages = async (sessionID: string, limit: number) => {
-      if (meta.loading[sessionID]) return
+    const loadMessages = async (input: {
+      directory: string
+      client: typeof sdk.client
+      setStore: Setter
+      sessionID: string
+      limit: number
+    }) => {
+      const key = keyFor(input.directory, input.sessionID)
+      if (meta.loading[key]) return
 
-      setMeta("loading", sessionID, true)
-      await retry(() => sdk.client.session.messages({ sessionID, limit }))
+      setMeta("loading", key, true)
+      await retry(() => input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }))
         .then((messages) => {
           const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
           const next = items
@@ -60,10 +76,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             .sort((a, b) => a.id.localeCompare(b.id))
 
           batch(() => {
-            setStore("message", sessionID, reconcile(next, { key: "id" }))
+            input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
 
             for (const message of items) {
-              setStore(
+              input.setStore(
                 "part",
                 message.info.id,
                 reconcile(
@@ -76,25 +92,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
               )
             }
 
-            setMeta("limit", sessionID, limit)
-            setMeta("complete", sessionID, next.length < limit)
+            setMeta("limit", key, input.limit)
+            setMeta("complete", key, next.length < input.limit)
           })
         })
         .finally(() => {
-          setMeta("loading", sessionID, false)
+          setMeta("loading", key, false)
         })
     }
 
+    const set: (...args: Parameters<Setter>) => ReturnType<Setter> = (...args) => {
+      return current()[1](...args)
+    }
+
     return {
-      data: store,
-      set: setStore,
+      get data() {
+        return current()[0]
+      },
+      set,
       get status() {
-        return store.status
+        return current()[0].status
       },
       get ready() {
-        return store.status !== "loading"
+        return current()[0].status !== "loading"
       },
       get project() {
+        const store = current()[0]
         const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
         if (match.found) return globalSync.data.project[match.index]
         return undefined
@@ -116,7 +139,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             agent: input.agent,
             model: input.model,
           }
-          setStore(
+          current()[1](
             produce((draft) => {
               const messages = draft.message[input.sessionID]
               if (!messages) {
@@ -133,20 +156,28 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
           )
         },
         async sync(sessionID: string) {
-          const hasSession = getSession(sessionID) !== undefined
-          hydrateMessages(sessionID)
+          const directory = sdk.directory
+          const client = sdk.client
+          const [store, setStore] = globalSync.child(directory)
+          const hasSession = (() => {
+            const match = Binary.search(store.session, sessionID, (s) => s.id)
+            return match.found
+          })()
+
+          hydrateMessages(directory, store, sessionID)
 
           const hasMessages = store.message[sessionID] !== undefined
           if (hasSession && hasMessages) return
 
-          const pending = inflight.get(sessionID)
+          const key = keyFor(directory, sessionID)
+          const pending = inflight.get(key)
           if (pending) return pending
 
-          const limit = meta.limit[sessionID] ?? chunk
+          const limit = meta.limit[key] ?? chunk
 
           const sessionReq = hasSession
             ? Promise.resolve()
-            : retry(() => sdk.client.session.get({ sessionID })).then((session) => {
+            : retry(() => client.session.get({ sessionID })).then((session) => {
                 const data = session.data
                 if (!data) return
                 setStore(
@@ -162,72 +193,104 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
                 )
               })
 
-          const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit)
+          const messagesReq = hasMessages
+            ? Promise.resolve()
+            : loadMessages({
+                directory,
+                client,
+                setStore,
+                sessionID,
+                limit,
+              })
 
           const promise = Promise.all([sessionReq, messagesReq])
             .then(() => {})
             .finally(() => {
-              inflight.delete(sessionID)
+              inflight.delete(key)
             })
 
-          inflight.set(sessionID, promise)
+          inflight.set(key, promise)
           return promise
         },
         async diff(sessionID: string) {
+          const directory = sdk.directory
+          const client = sdk.client
+          const [store, setStore] = globalSync.child(directory)
           if (store.session_diff[sessionID] !== undefined) return
 
-          const pending = inflightDiff.get(sessionID)
+          const key = keyFor(directory, sessionID)
+          const pending = inflightDiff.get(key)
           if (pending) return pending
 
-          const promise = retry(() => sdk.client.session.diff({ sessionID }))
+          const promise = retry(() => client.session.diff({ sessionID }))
             .then((diff) => {
               setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
             })
             .finally(() => {
-              inflightDiff.delete(sessionID)
+              inflightDiff.delete(key)
             })
 
-          inflightDiff.set(sessionID, promise)
+          inflightDiff.set(key, promise)
           return promise
         },
         async todo(sessionID: string) {
+          const directory = sdk.directory
+          const client = sdk.client
+          const [store, setStore] = globalSync.child(directory)
           if (store.todo[sessionID] !== undefined) return
 
-          const pending = inflightTodo.get(sessionID)
+          const key = keyFor(directory, sessionID)
+          const pending = inflightTodo.get(key)
           if (pending) return pending
 
-          const promise = retry(() => sdk.client.session.todo({ sessionID }))
+          const promise = retry(() => client.session.todo({ sessionID }))
             .then((todo) => {
               setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
             })
             .finally(() => {
-              inflightTodo.delete(sessionID)
+              inflightTodo.delete(key)
             })
 
-          inflightTodo.set(sessionID, promise)
+          inflightTodo.set(key, promise)
           return promise
         },
         history: {
           more(sessionID: string) {
+            const store = current()[0]
+            const key = keyFor(sdk.directory, sessionID)
             if (store.message[sessionID] === undefined) return false
-            if (meta.limit[sessionID] === undefined) return false
-            if (meta.complete[sessionID]) return false
+            if (meta.limit[key] === undefined) return false
+            if (meta.complete[key]) return false
             return true
           },
           loading(sessionID: string) {
-            return meta.loading[sessionID] ?? false
+            const key = keyFor(sdk.directory, sessionID)
+            return meta.loading[key] ?? false
           },
           async loadMore(sessionID: string, count = chunk) {
-            if (meta.loading[sessionID]) return
-            if (meta.complete[sessionID]) return
+            const directory = sdk.directory
+            const client = sdk.client
+            const [, setStore] = globalSync.child(directory)
+            const key = keyFor(directory, sessionID)
+            if (meta.loading[key]) return
+            if (meta.complete[key]) return
 
-            const current = meta.limit[sessionID] ?? chunk
-            await loadMessages(sessionID, current + count)
+            const currentLimit = meta.limit[key] ?? chunk
+            await loadMessages({
+              directory,
+              client,
+              setStore,
+              sessionID,
+              limit: currentLimit + count,
+            })
           },
         },
         fetch: async (count = 10) => {
+          const directory = sdk.directory
+          const client = sdk.client
+          const [store, setStore] = globalSync.child(directory)
           setStore("limit", (x) => x + count)
-          await sdk.client.session.list().then((x) => {
+          await client.session.list().then((x) => {
             const sessions = (x.data ?? [])
               .filter((s) => !!s?.id)
               .slice()
@@ -236,9 +299,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             setStore("session", reconcile(sessions, { key: "id" }))
           })
         },
-        more: createMemo(() => store.session.length >= store.limit),
+        more: createMemo(() => current()[0].session.length >= current()[0].limit),
         archive: async (sessionID: string) => {
-          await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
+          const directory = sdk.directory
+          const client = sdk.client
+          const [, setStore] = globalSync.child(directory)
+          await client.session.update({ sessionID, time: { archived: Date.now() } })
           setStore(
             produce((draft) => {
               const match = Binary.search(draft.session, sessionID, (s) => s.id)
@@ -249,7 +315,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       },
       absolute,
       get directory() {
-        return store.path.directory
+        return current()[0].path.directory
       },
     }
   },

+ 1 - 1
packages/app/src/pages/directory-layout.tsx

@@ -16,7 +16,7 @@ export default function Layout(props: ParentProps) {
     return base64Decode(params.dir!)
   })
   return (
-    <Show when={params.dir} keyed>
+    <Show when={params.dir}>
       <SDKProvider directory={directory()}>
         <SyncProvider>
           {iife(() => {

+ 2 - 2
packages/app/src/pages/session.tsx

@@ -199,8 +199,8 @@ export default function Page() {
   const permission = usePermission()
   const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined)
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
-  const tabs = createMemo(() => layout.tabs(sessionKey()))
-  const view = createMemo(() => layout.view(sessionKey()))
+  const tabs = createMemo(() => layout.tabs(sessionKey))
+  const view = createMemo(() => layout.view(sessionKey))
 
   if (import.meta.env.DEV) {
     createEffect(