Fix chat response never appearing after send
Root cause: when SSE delivered any chunk, maxAttempts was set to 1 with no delay. That single immediate poll hit the server before it finished writing to the DB, received status='generating' with empty content, then overwrote the SSE-streamed text with the stale empty row and exited. isStreaming went false and the UI was stuck showing '...'. Two fixes: 1. Always poll up to 20 times regardless of SSE success. SSE closing does not mean the server has committed the final row to the DB. 2. When SSE delivered content, only replace state with polled data if the polled assistant message is complete or has content — this prevents clobbering streamed text with a stale empty DB row while the server is still writing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,24 +100,38 @@ class MessagesNotifier extends FamilyAsyncNotifier<List<Message>, 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([
|
||||
|
||||
Reference in New Issue
Block a user