From e1c224ddb79084e8f2685ea088ffcae666126432 Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti <63326129+spashii@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:11:32 +0200 Subject: [PATCH] feat(chat): show the host's message instantly on send The message previously only appeared after the run persisted and streamed it back, which read as a dropped message. Echo it optimistically the moment they hit send, and drop the echo once the persisted user message arrives (or on submit error). Co-Authored-By: Claude Fable 5 --- .../src/components/chat/AgenticChatPanel.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/echo/frontend/src/components/chat/AgenticChatPanel.tsx b/echo/frontend/src/components/chat/AgenticChatPanel.tsx index f8a9c0de..f7c0bfbc 100644 --- a/echo/frontend/src/components/chat/AgenticChatPanel.tsx +++ b/echo/frontend/src/components/chat/AgenticChatPanel.tsx @@ -506,6 +506,12 @@ export const AgenticChatPanel = ({ const [afterSeq, setAfterSeq] = useState(0); const [events, setEvents] = useState([]); const [input, setInput] = useState(""); + // Optimistic echo of the host's message so it appears the instant they + // hit send, before the run persists and streams it back. + const [pendingUserMessage, setPendingUserMessage] = useState<{ + content: string; + timestamp: string; + } | null>(null); const [templateKey, setTemplateKey] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [isStopping, setIsStopping] = useState(false); @@ -598,6 +604,18 @@ export const AgenticChatPanel = ({ return nodes; }, [timeline]); + // Drop the optimistic echo once the persisted user message arrives. + useEffect(() => { + if (!pendingUserMessage) return; + const landed = timeline.some( + (item) => + item.kind === "message" && + item.role === "user" && + item.content === pendingUserMessage.content, + ); + if (landed) setPendingUserMessage(null); + }, [timeline, pendingUserMessage]); + // Free tier: max 3 user turns per chat. The 4th routes to upgrade. const { workspace } = useWorkspace(); const { freeTier } = useWorkspaceUsage(workspace?.id); @@ -933,6 +951,10 @@ export const AgenticChatPanel = ({ setError(null); setIsSubmitting(true); setInput(""); + setPendingUserMessage({ + content: message, + timestamp: new Date().toISOString(), + }); try { let targetRunId = runId; @@ -967,6 +989,7 @@ export const AgenticChatPanel = ({ } } } catch (submitError) { + setPendingUserMessage(null); // Backend safety net: free-tier turn cap returns 402. if (isFreeTierLimitError(submitError) === "chat_turns") { upgradeHandlers.open(); @@ -1148,6 +1171,27 @@ export const AgenticChatPanel = ({ /> ); })} + + {pendingUserMessage && + !timeline.some( + (item) => + item.kind === "message" && + item.role === "user" && + item.content === pendingUserMessage.content, + ) && ( +
+ +
+ )}