fix(ui): stabilize prompt async optimistic messages
Reconcile optimistic user messages by replacing the oldest synthetic pending message when the server-backed message arrives. Stop sending prompt part ids and rely on message-level replacement so v1.2.25 validation passes without duplicating optimistic content.
This commit is contained in:
@@ -5,7 +5,7 @@ import { getQuestionCallId, getQuestionMessageId } from "../../types/question"
|
||||
import type { Message, MessageInfo, ClientPart } from "../../types/message"
|
||||
import type { Session } from "../../types/session"
|
||||
import { messageStoreBus } from "./bus"
|
||||
import type { MessageStatus, SessionRevertState } from "./types"
|
||||
import type { MessageStatus, ReplaceMessageIdOptions, SessionRevertState } from "./types"
|
||||
|
||||
interface SessionMetadata {
|
||||
id: string
|
||||
@@ -121,10 +121,10 @@ export function applyPartDeltaV2(
|
||||
})
|
||||
}
|
||||
|
||||
export function replaceMessageIdV2(instanceId: string, oldId: string, newId: string): void {
|
||||
export function replaceMessageIdV2(instanceId: string, oldId: string, newId: string, options?: Omit<ReplaceMessageIdOptions, "oldId" | "newId">): void {
|
||||
if (!oldId || !newId || oldId === newId) return
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
store.replaceMessageId({ oldId, newId })
|
||||
store.replaceMessageId({ oldId, newId, ...(options ?? {}) })
|
||||
}
|
||||
|
||||
function extractPermissionMessageId(permission: PermissionRequestLike): string | undefined {
|
||||
|
||||
@@ -586,10 +586,10 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
bufferPendingPart({ messageId: input.messageId, part: input.part, receivedAt: Date.now() })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const partId = ensurePartId(input.messageId, input.part, message.partIds.length)
|
||||
const cloned = clonePart(input.part)
|
||||
|
||||
|
||||
setState(
|
||||
"messages",
|
||||
input.messageId,
|
||||
@@ -792,6 +792,8 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
id: options.newId,
|
||||
isEphemeral: false,
|
||||
updatedAt: Date.now(),
|
||||
partIds: options.clearParts ? [] : existing.partIds,
|
||||
parts: options.clearParts ? {} : existing.parts,
|
||||
}
|
||||
|
||||
setState("messages", options.newId, cloned)
|
||||
|
||||
@@ -152,6 +152,7 @@ export interface PartUpdateInput {
|
||||
export interface ReplaceMessageIdOptions {
|
||||
oldId: string
|
||||
newId: string
|
||||
clearParts?: boolean
|
||||
}
|
||||
|
||||
export interface ScrollCacheKey {
|
||||
|
||||
@@ -94,7 +94,7 @@ async function sendMessage(
|
||||
}
|
||||
|
||||
const messageId = createId("msg")
|
||||
const textPartId = createId("part")
|
||||
const textPartId = createId("prt")
|
||||
|
||||
const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments)
|
||||
|
||||
@@ -110,7 +110,6 @@ async function sendMessage(
|
||||
|
||||
const requestParts: any[] = [
|
||||
{
|
||||
id: textPartId,
|
||||
type: "text" as const,
|
||||
text: resolvedPrompt,
|
||||
},
|
||||
@@ -120,9 +119,8 @@ async function sendMessage(
|
||||
for (const att of attachments) {
|
||||
const source = att.source
|
||||
if (source.type === "file") {
|
||||
const partId = createId("part")
|
||||
const partId = createId("prt")
|
||||
requestParts.push({
|
||||
id: partId,
|
||||
type: "file" as const,
|
||||
url: att.url,
|
||||
mime: source.mime,
|
||||
@@ -148,9 +146,8 @@ async function sendMessage(
|
||||
continue
|
||||
}
|
||||
|
||||
const partId = createId("part")
|
||||
const partId = createId("prt")
|
||||
requestParts.push({
|
||||
id: partId,
|
||||
type: "text" as const,
|
||||
text: value,
|
||||
})
|
||||
@@ -184,7 +181,6 @@ async function sendMessage(
|
||||
})
|
||||
|
||||
const requestBody = {
|
||||
messageID: messageId,
|
||||
parts: requestParts,
|
||||
...(session.agent && { agent: session.agent }),
|
||||
...(session.model.providerId &&
|
||||
|
||||
@@ -240,19 +240,22 @@ function resolveMessageRole(info?: MessageInfo | null): MessageRole {
|
||||
return info?.role === "user" ? "user" : "assistant"
|
||||
}
|
||||
|
||||
function findPendingMessageId(
|
||||
function findPendingSyntheticMessageId(
|
||||
store: InstanceMessageStore,
|
||||
sessionId: string,
|
||||
role: MessageRole,
|
||||
): string | undefined {
|
||||
const messageIds = store.getSessionMessageIds(sessionId)
|
||||
const lastId = messageIds[messageIds.length - 1]
|
||||
if (!lastId) return undefined
|
||||
const record = store.getMessage(lastId)
|
||||
if (!record) return undefined
|
||||
if (record.sessionId !== sessionId) return undefined
|
||||
if (record.role !== role) return undefined
|
||||
return record.status === "sending" ? record.id : undefined
|
||||
for (const messageId of messageIds) {
|
||||
const record = store.getMessage(messageId)
|
||||
if (!record) continue
|
||||
if (record.sessionId !== sessionId) continue
|
||||
if (record.role !== role) continue
|
||||
if (record.status !== "sending") continue
|
||||
if (!record.isEphemeral) continue
|
||||
return record.id
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | MessagePartUpdatedEvent): void {
|
||||
@@ -282,9 +285,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
||||
|
||||
let record = store.getMessage(messageId)
|
||||
if (!record) {
|
||||
const pendingId = findPendingMessageId(store, sessionId, role)
|
||||
const pendingId = findPendingSyntheticMessageId(store, sessionId, role)
|
||||
if (pendingId && pendingId !== messageId) {
|
||||
replaceMessageIdV2(instanceId, pendingId, messageId)
|
||||
replaceMessageIdV2(instanceId, pendingId, messageId, { clearParts: role === "user" })
|
||||
record = store.getMessage(messageId)
|
||||
}
|
||||
}
|
||||
@@ -345,9 +348,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
||||
|
||||
let record = store.getMessage(messageId)
|
||||
if (!record) {
|
||||
const pendingId = findPendingMessageId(store, sessionId, role)
|
||||
const pendingId = findPendingSyntheticMessageId(store, sessionId, role)
|
||||
if (pendingId && pendingId !== messageId) {
|
||||
replaceMessageIdV2(instanceId, pendingId, messageId)
|
||||
replaceMessageIdV2(instanceId, pendingId, messageId, { clearParts: role === "user" })
|
||||
record = store.getMessage(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user