diff options
| author | Adam <[email protected]> | 2026-03-13 07:38:03 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-13 07:38:03 -0500 |
| commit | 1a3735b6197a192be10a2576ed21edee93f9da21 (patch) | |
| tree | bc55b2d672b378b8838411718fc1e9522d041121 /packages/app/src/context/sync.tsx | |
| parent | d4ae13f2a0e7748dd8f3a94ec21ee05050ec2cf7 (diff) | |
| download | opencode-1a3735b6197a192be10a2576ed21edee93f9da21.tar.gz opencode-1a3735b6197a192be10a2576ed21edee93f9da21.zip | |
fix(app): better optimistic prompt submit (#17337)
Diffstat (limited to 'packages/app/src/context/sync.tsx')
| -rw-r--r-- | packages/app/src/context/sync.tsx | 103 |
1 files changed, 102 insertions, 1 deletions
diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 9dc6623a7..0f2008723 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -54,6 +54,67 @@ type OptimisticRemoveInput = { messageID: string } +type OptimisticItem = { + message: Message + parts: Part[] +} + +type MessagePage = { + session: Message[] + part: { id: string; part: Part[] }[] + cursor?: string + complete: boolean +} + +const hasParts = (parts: Part[] | undefined, want: Part[]) => { + if (!parts) return want.length === 0 + return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found) +} + +const mergeParts = (parts: Part[] | undefined, want: Part[]) => { + if (!parts) return sortParts(want) + const next = [...parts] + let changed = false + for (const part of want) { + const result = Binary.search(next, part.id, (item) => item.id) + if (result.found) continue + next.splice(result.index, 0, part) + changed = true + } + if (!changed) return parts + return next +} + +export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) { + if (items.length === 0) return { ...page, confirmed: [] as string[] } + + const session = [...page.session] + const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)])) + const confirmed: string[] = [] + + for (const item of items) { + const result = Binary.search(session, item.message.id, (message) => message.id) + const found = result.found + if (!found) session.splice(result.index, 0, item.message) + + const current = part.get(item.message.id) + if (found && hasParts(current, item.parts)) { + confirmed.push(item.message.id) + continue + } + + part.set(item.message.id, mergeParts(current, item.parts)) + } + + return { + cursor: page.cursor, + complete: page.complete, + session, + part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })), + confirmed, + } +} + export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) { const messages = draft.message[input.sessionID] if (messages) { @@ -121,6 +182,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const inflight = new Map<string, Promise<void>>() const inflightDiff = new Map<string, Promise<void>>() const inflightTodo = new Map<string, Promise<void>>() + const optimistic = new Map<string, Map<string, OptimisticItem>>() const maxDirs = 30 const seen = new Map<string, Set<string>>() const [meta, setMeta] = createStore({ @@ -137,6 +199,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return undefined } + const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => { + const key = keyFor(directory, sessionID) + const list = optimistic.get(key) + if (list) { + list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) }) + return + } + optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]])) + } + + const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => { + const key = keyFor(directory, sessionID) + if (!messageID) { + optimistic.delete(key) + return + } + + const list = optimistic.get(key) + if (!list) return + list.delete(messageID) + if (list.size === 0) optimistic.delete(key) + } + + const getOptimistic = (directory: string, sessionID: string) => [ + ...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []), + ] + const seenFor = (directory: string) => { const existing = seen.get(directory) if (existing) { @@ -159,6 +248,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const clearMeta = (directory: string, sessionIDs: string[]) => { if (sessionIDs.length === 0) return + for (const sessionID of sessionIDs) { + clearOptimistic(directory, sessionID) + } setMeta( produce((draft) => { for (const sessionID of sessionIDs) { @@ -232,8 +324,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ setMeta("loading", key, true) await fetchMessages(input) - .then((next) => { + .then((page) => { if (!tracked(input.directory, input.sessionID)) return + const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID)) + for (const messageID of next.confirmed) { + clearOptimistic(input.directory, input.sessionID, messageID) + } const [store] = globalSync.child(input.directory, { bootstrap: false }) const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : [] const message = input.mode === "prepend" ? merge(cached, next.session) : next.session @@ -290,11 +386,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ get: getSession, optimistic: { add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) { + const directory = input.directory ?? sdk.directory const [, setStore] = target(input.directory) + setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts }) setOptimisticAdd(setStore as (...args: unknown[]) => void, input) }, remove(input: { directory?: string; sessionID: string; messageID: string }) { + const directory = input.directory ?? sdk.directory const [, setStore] = target(input.directory) + clearOptimistic(directory, input.sessionID, input.messageID) setOptimisticRemove(setStore as (...args: unknown[]) => void, input) }, }, @@ -316,6 +416,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ variant: input.variant, } const [, setStore] = target() + setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts }) setOptimisticAdd(setStore as (...args: unknown[]) => void, { sessionID: input.sessionID, message, |
