From 16eb15bab3e9ad1eb44bd9e09db4ff912e471015 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Wed, 10 Jun 2026 09:31:56 -0700 Subject: [PATCH] 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] * 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] * fix: roll back optimistic message on send failure, single insertAt source --------- Co-authored-by: open-swe[bot] --- ui/src/components/agents/AgentThreadView.tsx | 63 ++++++++++++++------ ui/src/lib/agents/queries.ts | 9 --- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/ui/src/components/agents/AgentThreadView.tsx b/ui/src/components/agents/AgentThreadView.tsx index 5cb7290c..296bb7b1 100644 --- a/ui/src/components/agents/AgentThreadView.tsx +++ b/ui/src/components/agents/AgentThreadView.tsx @@ -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) => { + 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>(() => { 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} diff --git a/ui/src/lib/agents/queries.ts b/ui/src/lib/agents/queries.ts index 032bdf6f..9e872d52 100644 --- a/ui/src/lib/agents/queries.ts +++ b/ui/src/lib/agents/queries.ts @@ -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 }>( - 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),