fix: show submitted user message immediately in web chat (#1478)

* fix: render submitted user message immediately in web chat

Insert the optimistic prompt into local pendingPrompts state on submit so
the user's message appears right away instead of only after the agent
responds and thread.messages refetches.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: keep queued user prompts in submission order

Offset each pending prompt's insertAt by the number of prompts already
pending so multiple follow-ups queued before the backend echoes them
don't collide at the same splice index and render out of order. Applies
to both the local state and persisted sessionStorage paths.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: roll back optimistic message on send failure, single insertAt source

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-10 09:31:56 -07:00 • committed by GitHub
parent 722279e277
commit 16eb15bab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 27 deletions

View file

@ -1,13 +1,14 @@
import { useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useQueryClient } from "@tanstack/react-query"
import type { PendingPrompt } from "@/lib/agents/pendingPrompts"
import type { AgentThread, Message } from "@/lib/agents/types"
import type { AgentThread, ImageChunk, Message } from "@/lib/agents/types"
import type { ModelSelection } from "@/lib/agents/useModelOptions"
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
import { MessageView } from "@/components/agents/ported"
import { agentThreadKeys, useSendAgentMessage } from "@/lib/agents/queries"
import {
addPendingPrompt,
dropPendingPrompts,
getPendingPrompts,
} from "@/lib/agents/pendingPrompts"
@ -93,6 +94,46 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
)
}, [queryClient, thread])
const handleSubmit = useCallback(
(content: string, images: Array<ImageChunk>) => {
const entry: PendingPrompt = {
prompt: content,
insertAt: thread.messages.length + pendingPrompts.length,
images,
}
addPendingPrompt(thread.id, entry.prompt, entry.insertAt, entry.images)
setPendingPrompts((prev) => [...prev, entry])
sendMessage.mutate(
{
content,
images,
model_id: activeSelection?.modelId ?? null,
effort: activeSelection?.effort ?? null,
},
{
onError: () => {
setPendingPrompts(
dropPendingPrompts(
thread.id,
(p) =>
p.insertAt === entry.insertAt &&
p.prompt === entry.prompt &&
pendingImageKey(p) === pendingImageKey(entry)
)
)
},
}
)
},
[
sendMessage,
thread.id,
thread.messages.length,
pendingPrompts.length,
activeSelection,
]
)
const displayMessages = useMemo<Array<Message>>(() => {
if (pendingPrompts.length === 0) return thread.messages
const baseTimestamp = new Date().toISOString()
@ -136,14 +177,7 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
compact
busy={isStreaming}
disabled={sendMessage.isPending}
onSubmit={(content, images) =>
sendMessage.mutate({
content,
images,
model_id: activeSelection?.modelId ?? null,
effort: activeSelection?.effort ?? null,
})
}
onSubmit={handleSubmit}
models={models}
selection={activeSelection}
onSelectionChange={setSelection}
@ -162,14 +196,7 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
compact
busy={isStreaming}
disabled={sendMessage.isPending}
onSubmit={(content, images) =>
sendMessage.mutate({
content,
images,
model_id: activeSelection?.modelId ?? null,
effort: activeSelection?.effort ?? null,
})
}
onSubmit={handleSubmit}
models={models}
selection={activeSelection}
onSelectionChange={setSelection}

View file

@ -144,15 +144,6 @@ export function useSendAgentMessage(threadId: string) {
model_id: vars.model_id,
effort: vars.effort,
}),
onMutate: (vars) => {
const cached = queryClient.getQueryData<{ messages?: Array<unknown> }>(
agentThreadKeys.detail(threadId)
)
const insertAt = Array.isArray(cached?.messages)
? cached.messages.length
: 0
addPendingPrompt(threadId, vars.content, insertAt, vars.images)
},
onSuccess: (thread) => {
queryClient.setQueryData(
agentThreadKeys.detail(threadId),