diff options
| author | Adam <[email protected]> | 2026-01-07 08:41:16 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-08 17:48:15 -0600 |
| commit | 374275eeb691006fa8f422d6267aa694ab38a992 (patch) | |
| tree | 9159db2dabb0ba2e10e82b7efb3cd4fbabf024d0 /packages/app/src/context | |
| parent | faa848cfb13df4e435dc236fe895032ea6f26dde (diff) | |
| download | opencode-374275eeb691006fa8f422d6267aa694ab38a992.tar.gz opencode-374275eeb691006fa8f422d6267aa694ab38a992.zip | |
feat(app): chunk message loading, lazy load diffs
Diffstat (limited to 'packages/app/src/context')
| -rw-r--r-- | packages/app/src/context/sync.tsx | 200 |
1 files changed, 144 insertions, 56 deletions
diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index a237871f9..7138c4ab4 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,5 +1,5 @@ import { batch, createMemo } from "solid-js" -import { produce, reconcile } from "solid-js/store" +import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" import { createSimpleContext } from "@opencode-ai/ui/context" @@ -14,6 +14,60 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() const [store, setStore] = globalSync.child(sdk.directory) const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") + const chunk = 200 + const inflight = new Map<string, Promise<void>>() + const inflightDiff = new Map<string, Promise<void>>() + const inflightTodo = new Map<string, Promise<void>>() + const [meta, setMeta] = createStore({ + limit: {} as Record<string, number>, + complete: {} as Record<string, boolean>, + loading: {} as Record<string, boolean>, + }) + + const getSession = (sessionID: string) => { + const match = Binary.search(store.session, sessionID, (s) => s.id) + if (match.found) return store.session[match.index] + return undefined + } + + const loadMessages = async (sessionID: string, limit: number) => { + if (meta.loading[sessionID]) return + + setMeta("loading", sessionID, true) + await retry(() => sdk.client.session.messages({ sessionID, limit })) + .then((messages) => { + const items = (messages.data ?? []).filter((x) => !!x?.info?.id) + const next = items + .map((x) => x.info) + .filter((m) => !!m?.id) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)) + + batch(() => { + setStore("message", sessionID, reconcile(next, { key: "id" })) + + for (const message of items) { + setStore( + "part", + message.info.id, + reconcile( + message.parts + .filter((p) => !!p?.id) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)), + { key: "id" }, + ), + ) + } + + setMeta("limit", sessionID, limit) + setMeta("complete", sessionID, next.length < limit) + }) + }) + .finally(() => { + setMeta("loading", sessionID, false) + }) + } return { data: store, @@ -30,11 +84,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return undefined }, session: { - get(sessionID: string) { - const match = Binary.search(store.session, sessionID, (s) => s.id) - if (match.found) return store.session[match.index] - return undefined - }, + get: getSession, addOptimisticMessage(input: { sessionID: string messageID: string @@ -66,58 +116,96 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }), ) }, - async sync(sessionID: string, _isRetry = false) { - const [session, messages, todo, diff] = await Promise.all([ - retry(() => sdk.client.session.get({ sessionID })), - retry(() => sdk.client.session.messages({ sessionID, limit: 1000 })), - retry(() => sdk.client.session.todo({ sessionID })), - retry(() => sdk.client.session.diff({ sessionID })), - ]) + async sync(sessionID: string) { + const hasSession = getSession(sessionID) !== undefined + const hasMessages = store.message[sessionID] !== undefined && meta.limit[sessionID] !== undefined + if (hasSession && hasMessages) return - batch(() => { - setStore( - "session", - produce((draft) => { - const match = Binary.search(draft, sessionID, (s) => s.id) - if (match.found) { - draft[match.index] = session.data! - return - } - draft.splice(match.index, 0, session.data!) - }), - ) - - setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" })) - setStore( - "message", - sessionID, - reconcile( - (messages.data ?? []) - .map((x) => x.info) - .filter((m) => !!m?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) - - for (const message of messages.data ?? []) { - if (!message?.info?.id) continue - setStore( - "part", - message.info.id, - reconcile( - message.parts - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) - } + const pending = inflight.get(sessionID) + if (pending) return pending - setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" })) - }) + const limit = meta.limit[sessionID] ?? chunk + + const sessionReq = hasSession + ? Promise.resolve() + : retry(() => sdk.client.session.get({ sessionID })).then((session) => { + const data = session.data + if (!data) return + setStore( + "session", + produce((draft) => { + const match = Binary.search(draft, sessionID, (s) => s.id) + if (match.found) { + draft[match.index] = data + return + } + draft.splice(match.index, 0, data) + }), + ) + }) + + const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit) + + const promise = Promise.all([sessionReq, messagesReq]) + .then(() => {}) + .finally(() => { + inflight.delete(sessionID) + }) + + inflight.set(sessionID, promise) + return promise + }, + async diff(sessionID: string) { + if (store.session_diff[sessionID] !== undefined) return + + const pending = inflightDiff.get(sessionID) + if (pending) return pending + + const promise = retry(() => sdk.client.session.diff({ sessionID })) + .then((diff) => { + setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" })) + }) + .finally(() => { + inflightDiff.delete(sessionID) + }) + + inflightDiff.set(sessionID, promise) + return promise + }, + async todo(sessionID: string) { + if (store.todo[sessionID] !== undefined) return + + const pending = inflightTodo.get(sessionID) + if (pending) return pending + + const promise = retry(() => sdk.client.session.todo({ sessionID })) + .then((todo) => { + setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" })) + }) + .finally(() => { + inflightTodo.delete(sessionID) + }) + + inflightTodo.set(sessionID, promise) + return promise + }, + history: { + more(sessionID: string) { + if (store.message[sessionID] === undefined) return false + if (meta.limit[sessionID] === undefined) return false + if (meta.complete[sessionID]) return false + return true + }, + loading(sessionID: string) { + return meta.loading[sessionID] ?? false + }, + async loadMore(sessionID: string, count = chunk) { + if (meta.loading[sessionID]) return + if (meta.complete[sessionID]) return + + const current = meta.limit[sessionID] ?? chunk + await loadMessages(sessionID, current + count) + }, }, fetch: async (count = 10) => { setStore("limit", (x) => x + count) |
