@@ -29,6 +29,10 @@ class Gemini extends StatefulWidget {
29
29
}
30
30
31
31
class GeminiState extends State <Gemini > {
32
+ // Set to `true` to show a "Thinking..." message immediately.
33
+ // Set to `false` to wait for the first chunk before showing the message.
34
+ final bool _isThinkingModel = true ;
35
+
32
36
final _uuid = const Uuid ();
33
37
final _crossCache = CrossCache ();
34
38
final _scrollController = ScrollController ();
@@ -285,7 +289,10 @@ class GeminiState extends State<Gemini> {
285
289
final streamId = _uuid.v4 ();
286
290
_currentStreamId = streamId;
287
291
TextStreamMessage ? streamMessage;
288
- var isFirstChunk = true ;
292
+ Timer ? thinkingTimer;
293
+
294
+ // A flag to ensure the message is created only once.
295
+ var messageInserted = false ;
289
296
290
297
// Store scroll state per stream ID
291
298
_reachedTargetScroll[streamId] = false ;
@@ -295,6 +302,43 @@ class GeminiState extends State<Gemini> {
295
302
_isStreaming = true ;
296
303
});
297
304
305
+ // Helper to create and insert the message, ensuring it only happens once.
306
+ Future <void > createAndInsertMessage () async {
307
+ if (messageInserted || ! mounted) return ;
308
+ messageInserted = true ;
309
+
310
+ // If the timer is still active, we beat it. Cancel it.
311
+ thinkingTimer? .cancel ();
312
+
313
+ streamMessage = TextStreamMessage (
314
+ id: streamId,
315
+ authorId: _agent.id,
316
+ createdAt: DateTime .now ().toUtc (),
317
+ streamId: streamId,
318
+ );
319
+ await _chatController.insertMessage (streamMessage! );
320
+ _streamManager.startStream (streamId, streamMessage! );
321
+ }
322
+
323
+ if (_isThinkingModel) {
324
+ // For thinking models, schedule the message insertion after a delay.
325
+ thinkingTimer = Timer (const Duration (milliseconds: 300 ), () async {
326
+ await createAndInsertMessage ();
327
+ // When timer fires, message is inserted, scroll to the bottom.
328
+ // This is needed because we use shouldScrollToEndWhenAtBottom: false,
329
+ // due to custom scroll logic below, so we must also scroll to the
330
+ // thinking label manually.
331
+ WidgetsBinding .instance.addPostFrameCallback ((_) async {
332
+ if (! _scrollController.hasClients || ! mounted) return ;
333
+ await _scrollController.animateTo (
334
+ _scrollController.position.maxScrollExtent,
335
+ duration: const Duration (milliseconds: 250 ),
336
+ curve: Curves .linearToEaseOut,
337
+ );
338
+ });
339
+ });
340
+ }
341
+
298
342
try {
299
343
final response = _chatSession.sendMessageStream (content);
300
344
@@ -305,18 +349,11 @@ class GeminiState extends State<Gemini> {
305
349
final textChunk = chunk.text! ;
306
350
if (textChunk.isEmpty) return ; // Skip empty chunks
307
351
308
- if (isFirstChunk) {
309
- isFirstChunk = false ;
310
-
311
- // Create and insert the message ON the first chunk
312
- streamMessage = TextStreamMessage (
313
- id: streamId,
314
- authorId: _agent.id,
315
- createdAt: DateTime .now ().toUtc (),
316
- streamId: streamId,
317
- );
318
- await _chatController.insertMessage (streamMessage! );
319
- _streamManager.startStream (streamId, streamMessage! );
352
+ // On the first valid chunk, ensure the message is inserted.
353
+ // This handles both non-thinking models and thinking models where
354
+ // the response arrives before the timer.
355
+ if (! messageInserted) {
356
+ await createAndInsertMessage ();
320
357
}
321
358
322
359
// Ensure stream message exists before adding chunk
@@ -369,6 +406,7 @@ class GeminiState extends State<Gemini> {
369
406
}
370
407
},
371
408
onDone: () async {
409
+ thinkingTimer? .cancel ();
372
410
// Stream completed successfully (only if message was created)
373
411
if (streamMessage != null ) {
374
412
await _streamManager.completeStream (streamId);
@@ -388,10 +426,12 @@ class GeminiState extends State<Gemini> {
388
426
_reachedTargetScroll.remove (streamId);
389
427
},
390
428
onError: (error) async {
429
+ thinkingTimer? .cancel ();
391
430
_handleStreamError (streamId, error, streamMessage);
392
431
},
393
432
);
394
433
} catch (error) {
434
+ thinkingTimer? .cancel ();
395
435
// Catch other potential errors during stream processing
396
436
_handleStreamError (streamId, error, streamMessage);
397
437
}
0 commit comments