Enable command queueing and fix message duplication issues

- Remove sending() state blocking to allow queueing multiple messages
- Add optimistic message creation with temp IDs for immediate UI feedback
- Implement QUEUED badge display matching TUI behavior (accent color, bold)
- Compute queued state: user message ID > last assistant message ID
- Clear prompt input immediately before async send for better UX
- Fix duplicate messages by properly finding and replacing temp messages
- Fix duplicate parts by clearing optimistic parts when real message arrives
- Fix state mutation bugs in message.part.updated handler (immutable updates)
- Fix state mutation bugs in message.updated handler (immutable updates)

Matches TUI implementation for consistent user experience across clients.
This commit is contained in:
Shantur Rathore
2025-10-24 16:38:42 +01:00
parent e3bc947195
commit 7be4248e20
5 changed files with 149 additions and 73 deletions

View File

@@ -487,34 +487,40 @@ function handleMessageUpdate(instanceId: string, event: any): void {
const part = event.properties?.part
if (!part) return
const session = instanceSessions.get(part.sessionID)
if (!session) return
let message = session.messages.find((m) => m.id === part.messageID)
if (!message) {
message = {
id: part.messageID,
sessionId: part.sessionID,
type: "assistant",
parts: [part],
timestamp: Date.now(),
status: "streaming",
}
session.messages.push(message)
} else {
const partIndex = message.parts.findIndex((p: any) => p.id === part.id)
if (partIndex === -1) {
message.parts.push(part)
} else {
message.parts[partIndex] = part
}
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
instanceSessions.set(part.sessionID, { ...session })
const session = instanceSessions.get(part.sessionID)
if (!session) return prev
const messages = [...session.messages]
const messageIndex = messages.findIndex((m) => m.id === part.messageID)
if (messageIndex === -1) {
messages.push({
id: part.messageID,
sessionId: part.sessionID,
type: "assistant",
parts: [part],
timestamp: Date.now(),
status: "streaming",
})
} else {
const message = messages[messageIndex]
const parts = [...message.parts]
const partIndex = parts.findIndex((p: any) => p.id === part.id)
if (partIndex === -1) {
parts.push(part)
} else {
parts[partIndex] = part
}
messages[messageIndex] = { ...message, parts }
}
instanceSessions.set(part.sessionID, { ...session, messages })
next.set(instanceId, instanceSessions)
return next
})
@@ -522,36 +528,51 @@ function handleMessageUpdate(instanceId: string, event: any): void {
const info = event.properties?.info
if (!info) return
const session = instanceSessions.get(info.sessionID)
if (!session) return
let message = session.messages.find((m) => m.id === info.id)
if (!message) {
message = {
id: info.id,
sessionId: info.sessionID,
type: info.role === "user" ? "user" : "assistant",
parts: [],
timestamp: info.time?.created || Date.now(),
status: "complete",
}
session.messages.push(message)
} else {
// Update existing message - replace temp message with real one
message.id = info.id
message.status = "complete"
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
const updatedSession = instanceSessions.get(info.sessionID)
if (updatedSession) {
const messagesInfo = new Map(updatedSession.messagesInfo)
messagesInfo.set(info.id, info)
instanceSessions.set(info.sessionID, { ...updatedSession, messagesInfo })
const session = instanceSessions.get(info.sessionID)
if (!session) return prev
const messages = [...session.messages]
const messageIndex = messages.findIndex((m) => m.id === info.id)
const tempMessageIndex = messages.findIndex(
(m) =>
m.id.startsWith("temp-") &&
m.type === (info.role === "user" ? "user" : "assistant") &&
m.status === "sending",
)
if (messageIndex > -1) {
messages[messageIndex] = {
...messages[messageIndex],
status: "complete",
}
} else if (tempMessageIndex > -1) {
messages[tempMessageIndex] = {
id: info.id,
sessionId: info.sessionID,
type: info.role === "user" ? "user" : "assistant",
parts: [],
timestamp: info.time?.created || Date.now(),
status: "complete",
}
} else {
messages.push({
id: info.id,
sessionId: info.sessionID,
type: info.role === "user" ? "user" : "assistant",
parts: [],
timestamp: info.time?.created || Date.now(),
status: "complete",
})
}
const messagesInfo = new Map(session.messagesInfo)
messagesInfo.set(info.id, info)
instanceSessions.set(info.sessionID, { ...session, messages, messagesInfo })
next.set(instanceId, instanceSessions)
return next
})
@@ -639,6 +660,36 @@ async function sendMessage(
throw new Error("Session not found")
}
const tempMessageId = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`
const textParts: any[] = []
textParts.push({
type: "text" as const,
text: prompt,
id: `${tempMessageId}-text`,
})
const optimisticMessage: Message = {
id: tempMessageId,
sessionId,
type: "user",
parts: textParts,
timestamp: Date.now(),
status: "sending",
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
const session = instanceSessions.get(sessionId)
if (session) {
const messages = [...session.messages, optimisticMessage]
instanceSessions.set(sessionId, { ...session, messages })
next.set(instanceId, instanceSessions)
}
return next
})
const parts: any[] = [
{
type: "text" as const,