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', });