diff --git a/lib/providers/chat_provider.dart b/lib/providers/chat_provider.dart index 4b4e687..50fa882 100644 --- a/lib/providers/chat_provider.dart +++ b/lib/providers/chat_provider.dart @@ -100,24 +100,38 @@ class MessagesNotifier extends FamilyAsyncNotifier, int> { // ── Step 3: Poll the API until we have a completed assistant response. // - // If SSE delivered content the server is done; one reload suffices. - // If SSE delivered nothing, the LLM may still be generating — we retry - // with short delays so the response appears as soon as it's ready. - // A completed message is one whose status is not 'generating'. + // We always retry up to 20 times regardless of whether SSE streamed + // content. SSE ending does NOT mean the server has finished writing to + // the DB — there can be a brief gap between the stream closing and the + // row being marked complete. + // + // To avoid clobbering SSE-streamed text with a stale empty DB row, we + // only replace state when the polled data is complete OR has content. try { - final maxAttempts = streamedContent ? 1 : 20; - for (var attempt = 0; attempt < maxAttempts; attempt++) { + for (var attempt = 0; attempt < 20; attempt++) { if (attempt > 0) await Future.delayed(const Duration(seconds: 2)); final (conv, fresh) = await repo.getMessages(convId); - state = AsyncData(fresh); ref.read(conversationsProvider.notifier).patchConversation(conv); + final done = fresh.any( (m) => m.role == MessageRole.assistant && m.status != 'generating', ); + final polledHasContent = fresh.any( + (m) => m.role == MessageRole.assistant && m.content.isNotEmpty, + ); + + // Update UI state when: + // • SSE never delivered anything (polling is the only data source), or + // • the polled response is complete or has content to show. + if (!streamedContent || done || polledHasContent) { + state = AsyncData(fresh); + } + if (done) break; } } catch (_) { - // If every reload fails, at least clear the generating placeholder. + // Polling failed entirely — clear the generating placeholder so the UI + // doesn't spin forever. final msgs = state.valueOrNull; if (msgs != null && msgs.isNotEmpty && msgs.last.status == 'generating') { state = AsyncData([