diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index dabeb34d7b..f84ad1dee8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1179,25 +1179,18 @@ export function Prompt(props: PromptProps) { }, ...nonTextParts.map(assign), ] - sync.session.addOptimisticPrompt({ + const request = { sessionID, messageID, agent: agent.name, model: selectedModel, variant, parts, - }) + } + sync.session.addOptimisticPrompt(request) sdk.client.session - .prompt({ - sessionID, - ...selectedModel, - messageID, - agent: agent.name, - model: selectedModel, - variant, - parts, - }) - .catch(() => sync.session.removeOptimisticPrompt(sessionID, messageID)) + .prompt(request) + .catch(() => sync.session.removeOptimisticPrompt(request.sessionID, request.messageID)) if (editorParts.length > 0) editor.markSelectionSent() } history.append({ diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index d6738b10ce..cc87e24353 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -118,6 +118,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const kv = useKV() const fullSyncedSessions = new Set() + const optimisticMessages = new Set() let syncedWorkspace = project.workspace.current() function sessionListQuery(): { scope?: "project"; path?: string } { @@ -257,6 +258,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } case "message.updated": { + optimisticMessages.delete(event.properties.info.id) const messages = store.message[event.properties.info.sessionID] if (!messages) { setStore("message", event.properties.info.sessionID, [event.properties.info]) @@ -296,6 +298,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ break } case "message.removed": { + optimisticMessages.delete(event.properties.messageID) const messages = store.message[event.properties.sessionID] const result = Binary.search(messages, event.properties.messageID, (m) => m.id) if (result.found) { @@ -526,6 +529,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ variant?: string parts: OptimisticPromptPart[] }) { + optimisticMessages.add(input.messageID) const messages = store.message[input.sessionID] const match = messages ? Binary.search(messages, input.messageID, (m) => m.id) : undefined const info: Message = { @@ -546,14 +550,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sessionID: input.sessionID, messageID: input.messageID, } - if (withIDs.type !== "text") return withIDs - return { - ...withIDs, - metadata: { - ...withIDs.metadata, - optimistic: true, - }, + if (withIDs.type === "file") { + return { + ...withIDs, + url: "", + } } + return withIDs }) batch(() => { @@ -564,7 +567,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ "message", input.sessionID, produce((draft) => { - draft.splice(match?.index ?? draft.length, 0, info) + Binary.insert(draft, info, (message) => message.id) }), ) } @@ -572,12 +575,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }) }, removeOptimisticPrompt(sessionID: string, messageID: string) { - if (!store.part[messageID]?.some((part) => part.type === "text" && part.metadata?.optimistic === true)) return + if (!optimisticMessages.delete(messageID)) return const messages = store.message[sessionID] - if (!messages) return - const match = Binary.search(messages, messageID, (m) => m.id) + const match = messages ? Binary.search(messages, messageID, (m) => m.id) : undefined batch(() => { - if (match.found) { + if (match?.found) { setStore( "message", sessionID, @@ -605,11 +607,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ setStore( produce((draft) => { const match = Binary.search(draft.session, sessionID, (s) => s.id) + const fetched = messages.data! + const fetchedIDs = new Set(fetched.map((message) => message.info.id)) + const optimistic = (draft.message[sessionID] ?? []).filter( + (message) => optimisticMessages.has(message.id) && !fetchedIDs.has(message.id), + ) if (match.found) draft.session[match.index] = session.data! if (!match.found) draft.session.splice(match.index, 0, session.data!) draft.todo[sessionID] = todo.data ?? [] - draft.message[sessionID] = messages.data!.map((x) => x.info) - for (const message of messages.data!) { + draft.message[sessionID] = fetched.map((x) => x.info) + for (const message of optimistic) { + Binary.insert(draft.message[sessionID], message, (item) => item.id) + } + for (const message of fetched) { + optimisticMessages.delete(message.info.id) draft.part[message.info.id] = message.parts } draft.session_diff[sessionID] = diff.data ?? []