From 2f9a0e97aeb40c19466aa4a251f6f063dfeffd5e Mon Sep 17 00:00:00 2001 From: HARSH048 Date: Sun, 19 Apr 2026 22:00:34 +0530 Subject: [PATCH] fix(pearl): correct SSE error event format and fix infinite loading on missing API key Three related bugs fixed: 1. Wrong SSE error event shape in backend routes - bubble-flows.ts and ai.ts were sending { type: 'error', error: '...' } - StreamingEvent schema requires { type: 'error', data: { error, recoverable } } - Frontend handler reads event.data.error, so errors silently fell back to 'An error occurred' or were swallowed entirely 2. Missing error event on coffee agent failure (planning phase) - When GOOGLE_API_KEY is absent, coffee.ts returns early with success:false - The route only sent stream_complete with the error buried in coffeeResult - No proper error SSE event was emitted, so the frontend never received it 3. UI stuck in infinite loading state when API key is missing - generationAbortController was never cleared after the stream ended - isGenerating stayed true forever since setGenerationCompleted was never called - Added safety net in startGenerationStream to always clear isGenerating after the stream loop exits without a generation_complete event - error event is now the single source of truth; stream_complete is a no-op --- .../src/hooks/usePearlChatStore.ts | 1 - .../bubble-studio/src/hooks/usePearlStream.ts | 5 +++ apps/bubblelab-api/src/routes/ai.ts | 10 +++-- apps/bubblelab-api/src/routes/bubble-flows.ts | 41 ++++++++++++++----- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/bubble-studio/src/hooks/usePearlChatStore.ts b/apps/bubble-studio/src/hooks/usePearlChatStore.ts index bee08d5a..7b1c93fc 100644 --- a/apps/bubble-studio/src/hooks/usePearlChatStore.ts +++ b/apps/bubble-studio/src/hooks/usePearlChatStore.ts @@ -425,7 +425,6 @@ export function handleStreamingEvent( case 'coffee_complete': case 'heartbeat': case 'stream_complete': - // Ignore these events - they're control signals, not display events break; } } diff --git a/apps/bubble-studio/src/hooks/usePearlStream.ts b/apps/bubble-studio/src/hooks/usePearlStream.ts index b2887be3..17c126ce 100644 --- a/apps/bubble-studio/src/hooks/usePearlStream.ts +++ b/apps/bubble-studio/src/hooks/usePearlStream.ts @@ -221,6 +221,11 @@ async function startGenerationStream( `[startGenerationStream] Stream completed successfully for flow ${flowId}` ); pearlStore.getState().setIsCoffeeLoading(false); + // Safety net: if no generation_complete/error event was received, clear the + // generating state so the UI doesn't stay stuck in a loading loop. + if (!pearlStore.getState().generationCompleted) { + pearlStore.getState().cancelGenerationStream(); + } return; } catch (error) { if (abortController.signal.aborted) { diff --git a/apps/bubblelab-api/src/routes/ai.ts b/apps/bubblelab-api/src/routes/ai.ts index 5f6c1d74..f9c11ff7 100644 --- a/apps/bubblelab-api/src/routes/ai.ts +++ b/apps/bubblelab-api/src/routes/ai.ts @@ -148,9 +148,13 @@ app.openapi(pearlRoute, async (c) => { await stream.writeSSE({ data: JSON.stringify({ type: 'error', - error: - error instanceof Error ? error.message : 'Unknown streaming error', - recoverable: false, + data: { + error: + error instanceof Error + ? error.message + : 'Unknown streaming error', + recoverable: false, + }, }), event: 'error', }); diff --git a/apps/bubblelab-api/src/routes/bubble-flows.ts b/apps/bubblelab-api/src/routes/bubble-flows.ts index 9fb0c117..ca482f3a 100644 --- a/apps/bubblelab-api/src/routes/bubble-flows.ts +++ b/apps/bubblelab-api/src/routes/bubble-flows.ts @@ -508,11 +508,13 @@ app.openapi(executeBubbleFlowStreamRoute, async (c) => { await stream.writeSSE({ data: JSON.stringify({ type: 'error', - error: - error instanceof Error - ? error.message - : 'Unknown streaming error', - recoverable: false, + data: { + error: + error instanceof Error + ? error.message + : 'Unknown streaming error', + recoverable: false, + }, }), event: 'error', }); @@ -1416,6 +1418,23 @@ app.openapi(generateBubbleFlowCodeRoute, async (c) => { // Clear heartbeat and send stream completion clearInterval(heartbeatInterval); + // Emit a proper error event when coffee fails (e.g. missing API key) + // before stream_complete so the frontend error handler picks it up. + if (!coffeeResult.success) { + await stream.writeSSE({ + data: JSON.stringify({ + type: 'error', + data: { + error: + coffeeResult.error || + 'Flow generation failed. Please check your API key configuration.', + recoverable: false, + }, + }), + event: 'error', + }); + } + await stream.writeSSE({ data: JSON.stringify({ type: 'stream_complete', @@ -1690,11 +1709,13 @@ app.openapi(generateBubbleFlowCodeRoute, async (c) => { await stream.writeSSE({ data: JSON.stringify({ type: 'error', - error: - error instanceof Error - ? error.message - : 'Unknown streaming error', - recoverable: false, + data: { + error: + error instanceof Error + ? error.message + : 'Unknown streaming error', + recoverable: false, + }, }), event: 'error', });