diff --git a/site/src/pages/AgentsPage/AgentChatInput.tsx b/site/src/pages/AgentsPage/AgentChatInput.tsx index b07e99f677..81b74d60ce 100644 --- a/site/src/pages/AgentsPage/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/AgentChatInput.tsx @@ -324,25 +324,35 @@ export const AgentChatInput = memo( isEditingHistoryMessage && editingHistoryMessageID !== null ? editingHistoryMessageID : undefined; + // Capture the raw input before clearing so we can restore + // it if the request fails. + const capturedInput = input; + + // Clear the input and editing state immediately so the + // user can start typing their next message without waiting + // for the network round-trip. + setInput(""); + onInputChange?.(""); + if (queueEditID !== null) { + setEditingQueuedMessageID(null); + setDraftBeforeQueueEdit(null); + } + if (isEditingHistoryMessage) { + setIsEditingHistoryMessage(false); + setEditingHistoryMessageID(null); + setDraftBeforeHistoryEdit(null); + onEditCleared?.(); + } + try { - await onSend(input, editedMessageID); + await onSend(capturedInput, editedMessageID); if (queueEditID !== null && onDeleteQueuedMessage) { await onDeleteQueuedMessage(queueEditID); } - setInput(""); - onInputChange?.(""); - if (queueEditID !== null) { - setEditingQueuedMessageID(null); - setDraftBeforeQueueEdit(null); - } - if (isEditingHistoryMessage) { - setIsEditingHistoryMessage(false); - setEditingHistoryMessageID(null); - setDraftBeforeHistoryEdit(null); - onEditCleared?.(); - } } catch { - // Keep input on failure so the user can retry. + // Restore the input so the user can retry. + setInput(capturedInput); + onInputChange?.(capturedInput); } finally { // Re-focus the textarea so the user can keep typing. textareaRef.current?.focus(); diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx index 116ed2ae17..3dc0bad79a 100644 --- a/site/src/pages/AgentsPage/AgentDetail.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.tsx @@ -614,7 +614,31 @@ const AgentDetail: FC = () => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTop = 0; } - await sendMutation.mutateAsync(request); + + // Inject an optimistic user message so the bubble appears in + // the timeline immediately, without waiting for the server. + const previousMessages = getOrderedMessagesFromStore(store); + const previousChatStatus = store.getSnapshot().chatStatus; + const optimisticMessage: TypesGen.ChatMessage = { + id: -Date.now(), + chat_id: agentId, + created_at: new Date().toISOString(), + role: "user", + content: toOptimisticMessageParts(content), + }; + store.upsertDurableMessage(optimisticMessage); + store.clearStreamState(); + store.setChatStatus("pending"); + + try { + await sendMutation.mutateAsync(request); + } catch (error) { + // Roll back the optimistic message so the timeline + // returns to its previous state. + store.replaceMessages(previousMessages); + store.setChatStatus(previousChatStatus); + throw error; + } if (typeof window !== "undefined") { if (selectedModelConfigID) { localStorage.setItem(