tui: simplify optimistic prompt reconciliation

This commit is contained in:
Kit Langton 2026-05-08 14:12:55 -04:00
commit ebaad55c8c
2 changed files with 30 additions and 27 deletions

View file

@ -1187,25 +1187,18 @@ export function Prompt(props: PromptProps) {
}, },
...nonTextParts.map(assign), ...nonTextParts.map(assign),
] ]
sync.session.addOptimisticPrompt({ const request = {
sessionID, sessionID,
messageID, messageID,
agent: agent.name, agent: agent.name,
model: selectedModel, model: selectedModel,
variant, variant,
parts, parts,
}) }
sync.session.addOptimisticPrompt(request)
sdk.client.session sdk.client.session
.prompt({ .prompt(request)
sessionID, .catch(() => sync.session.removeOptimisticPrompt(request.sessionID, request.messageID))
...selectedModel,
messageID,
agent: agent.name,
model: selectedModel,
variant,
parts,
})
.catch(() => sync.session.removeOptimisticPrompt(sessionID, messageID))
if (editorParts.length > 0) editor.markSelectionSent() if (editorParts.length > 0) editor.markSelectionSent()
} }
history.append({ history.append({

View file

@ -119,6 +119,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const kv = useKV() const kv = useKV()
const fullSyncedSessions = new Set<string>() const fullSyncedSessions = new Set<string>()
const optimisticMessages = new Set<string>()
function sessionListQuery(): { scope?: "project"; path?: string } { function sessionListQuery(): { scope?: "project"; path?: string } {
if (!kv.get("session_directory_filter_enabled", true)) return { scope: "project" } if (!kv.get("session_directory_filter_enabled", true)) return { scope: "project" }
@ -257,6 +258,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
} }
case "message.updated": { case "message.updated": {
optimisticMessages.delete(event.properties.info.id)
const messages = store.message[event.properties.info.sessionID] const messages = store.message[event.properties.info.sessionID]
if (!messages) { if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info]) setStore("message", event.properties.info.sessionID, [event.properties.info])
@ -296,6 +298,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break break
} }
case "message.removed": { case "message.removed": {
optimisticMessages.delete(event.properties.messageID)
const messages = store.message[event.properties.sessionID] const messages = store.message[event.properties.sessionID]
const result = Binary.search(messages, event.properties.messageID, (m) => m.id) const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) { if (result.found) {
@ -532,6 +535,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
variant?: string variant?: string
parts: OptimisticPromptPart[] parts: OptimisticPromptPart[]
}) { }) {
optimisticMessages.add(input.messageID)
const messages = store.message[input.sessionID] const messages = store.message[input.sessionID]
const match = messages ? Binary.search(messages, input.messageID, (m) => m.id) : undefined const match = messages ? Binary.search(messages, input.messageID, (m) => m.id) : undefined
const info: Message = { const info: Message = {
@ -552,14 +556,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: input.messageID, messageID: input.messageID,
} }
if (withIDs.type !== "text") return withIDs if (withIDs.type === "file") {
return { return {
...withIDs, ...withIDs,
metadata: { url: "",
...withIDs.metadata, }
optimistic: true,
},
} }
return withIDs
}) })
batch(() => { batch(() => {
@ -570,7 +573,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
"message", "message",
input.sessionID, input.sessionID,
produce((draft) => { produce((draft) => {
draft.splice(match?.index ?? draft.length, 0, info) Binary.insert(draft, info, (message) => message.id)
}), }),
) )
} }
@ -578,12 +581,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}) })
}, },
removeOptimisticPrompt(sessionID: string, messageID: string) { 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] const messages = store.message[sessionID]
if (!messages) return const match = messages ? Binary.search(messages, messageID, (m) => m.id) : undefined
const match = Binary.search(messages, messageID, (m) => m.id)
batch(() => { batch(() => {
if (match.found) { if (match?.found) {
setStore( setStore(
"message", "message",
sessionID, sessionID,
@ -611,12 +613,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore( setStore(
produce((draft) => { produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id) 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[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!) if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? [] draft.todo[sessionID] = todo.data ?? []
const infos: (typeof draft.message)[string] = [] const infos: (typeof draft.message)[string] = fetched.map((message) => message.info)
for (const message of messages.data ?? []) { for (const message of optimistic) {
infos.push(message.info) Binary.insert(infos, message, (item) => item.id)
}
for (const message of fetched) {
optimisticMessages.delete(message.info.id)
draft.part[message.info.id] = message.parts draft.part[message.info.id] = message.parts
} }
draft.message[sessionID] = infos draft.message[sessionID] = infos