From cfe338a0e590f1d3c823dfc41cf7be93c2dfb7a3 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 15 Jul 2026 16:18:23 -0700 Subject: [PATCH] [eric] chat: optimistic user bubble on the first message of a draft (was 3 round-trips of nothing) --- frontend/src/shared/state/agentsSlice.ts | 49 ++++++++++++++++-------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts index 504f8acd..478c9386 100644 --- a/frontend/src/shared/state/agentsSlice.ts +++ b/frontend/src/shared/state/agentsSlice.ts @@ -346,25 +346,42 @@ export const fetchSession = createAsyncThunk( export const launchAndSendFirstMessage = createAsyncThunk( 'agents/launchAndSendFirstMessage', - async ({ draftId, config, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds, selectedAppIds, selectedSettingIds }: LaunchAndSendPayload) => { - const launchRes = await fetch(`${AGENTS_API}/launch`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(config), - }); - const launchData = await launchRes.json(); - const session = launchData.session as AgentSession; + async ({ draftId, config, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds, selectedAppIds, selectedSettingIds }: LaunchAndSendPayload, { dispatch }) => { + // Optimistic bubble on the DRAFT before the three round-trips (launch/message/refetch): without it the first message of every fresh chat rendered nothing until the network came back. The fulfilled rekey swaps in the server session, which carries the real turn by then. + const clientMessageId = _genOptimisticId(); + dispatch(addOptimisticMessage({ + sessionId: draftId, + clientMessageId, + prompt, + contextPaths, + forcedTools, + attachedSkills: attachedSkills?.map((s) => ({ id: s.id, name: s.name })), + images: images?.map((img) => ({ data: img.data, media_type: img.media_type })), + hidden: false, + })); + try { + const launchRes = await fetch(`${AGENTS_API}/launch`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config), + }); + const launchData = await launchRes.json(); + const session = launchData.session as AgentSession; - await fetch(`${AGENTS_API}/sessions/${session.id}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, selected_browser_ids: selectedBrowserIds, selected_app_output_ids: selectedAppIds, selected_setting_ids: selectedSettingIds }), - }); + await fetch(`${AGENTS_API}/sessions/${session.id}/message`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, selected_browser_ids: selectedBrowserIds, selected_app_output_ids: selectedAppIds, selected_setting_ids: selectedSettingIds, client_message_id: clientMessageId }), + }); - const refreshRes = await fetch(`${AGENTS_API}/sessions/${session.id}`); - const updatedSession = await refreshRes.json() as AgentSession; + const refreshRes = await fetch(`${AGENTS_API}/sessions/${session.id}`); + const updatedSession = await refreshRes.json() as AgentSession; - return { draftId, session: updatedSession }; + return { draftId, session: updatedSession }; + } catch (err) { + dispatch(markOptimisticFailed({ sessionId: draftId, clientMessageId })); + throw err; + } } );