Matthias Nott
2026-03-22 69c37c43074ad20ab9c7a5b7f4464863c4d298d4
fix: image flicker, screenshot indicator, cross-session message storage

- Cache decoded image bytes to prevent flicker on widget rebuild
- gaplessPlayback prevents blank flash during Image.memory rebuild
- Screenshot button shows snackbar feedback
- Cross-session messages stored to disk immediately (not just toast)
- Messages available when user switches to that session
2 files modified
changed files
lib/screens/chat_screen.dart patch | view | blame | history
lib/widgets/message_bubble.dart patch | view | blame | history
lib/screens/chat_screen.dart
....@@ -13,6 +13,7 @@
1313 import '../models/server_config.dart';
1414 import '../providers/providers.dart';
1515 import '../services/audio_service.dart';
16
+import '../services/message_store.dart';
1617 import '../services/websocket_service.dart';
1718 import '../theme/app_theme.dart';
1819 import '../widgets/command_bar.dart';
....@@ -264,6 +265,8 @@
264265
265266 final activeId = ref.read(activeSessionIdProvider);
266267 if (sessionId != null && sessionId != activeId) {
268
+ // Store message for the other session so it's there when user switches
269
+ _storeForSession(sessionId, message);
267270 _incrementUnread(sessionId);
268271 final sessions = ref.read(sessionsProvider);
269272 final session = sessions.firstWhere(
....@@ -304,6 +307,7 @@
304307
305308 final activeId = ref.read(activeSessionIdProvider);
306309 if (sessionId != null && sessionId != activeId) {
310
+ _storeForSession(sessionId, message);
307311 _incrementUnread(sessionId);
308312 final sessions = ref.read(sessionsProvider);
309313 final session = sessions.firstWhere(
....@@ -366,6 +370,13 @@
366370 ref.read(messagesProvider.notifier).addMessage(message);
367371 ref.read(isTypingProvider.notifier).state = false;
368372 _scrollToBottom();
373
+ }
374
+
375
+ /// Store a message for a non-active session so it persists when the user switches to it.
376
+ void _storeForSession(String sessionId, Message message) {
377
+ MessageStore.loadAll(sessionId).then((existing) {
378
+ MessageStore.save(sessionId, [...existing, message]);
379
+ });
369380 }
370381
371382 void _incrementUnread(String sessionId) {
....@@ -525,6 +536,15 @@
525536 void _requestScreenshot() {
526537 _screenshotForChat = true;
527538 _sendCommand('screenshot');
539
+ if (mounted) {
540
+ ScaffoldMessenger.of(context).showSnackBar(
541
+ const SnackBar(
542
+ content: Text('Capturing screenshot...'),
543
+ duration: Duration(seconds: 2),
544
+ behavior: SnackBarBehavior.floating,
545
+ ),
546
+ );
547
+ }
528548 }
529549
530550 void _navigateToTerminal() {
lib/widgets/message_bubble.dart
....@@ -1,5 +1,6 @@
11 import 'dart:convert';
22 import 'dart:math';
3
+import 'dart:typed_data';
34
45 import 'package:flutter/material.dart';
56 import 'package:flutter/services.dart';
....@@ -8,6 +9,9 @@
89 import '../models/message.dart';
910 import '../theme/app_theme.dart';
1011 import 'image_viewer.dart';
12
+
13
+// Cache decoded image bytes to prevent flicker on widget rebuild
14
+final Map<String, Uint8List> _imageCache = {};
1115
1216 /// Chat message bubble with support for text, voice, and image types.
1317 class MessageBubble extends StatelessWidget {
....@@ -208,11 +212,13 @@
208212 return const Text('Image unavailable');
209213 }
210214
211
- final bytes = base64Decode(
212
- message.imageBase64!.contains(',')
213
- ? message.imageBase64!.split(',').last
214
- : message.imageBase64!,
215
- );
215
+ // Cache decoded bytes to prevent flicker on rebuild
216
+ final bytes = _imageCache.putIfAbsent(message.id, () {
217
+ final raw = message.imageBase64!;
218
+ return Uint8List.fromList(base64Decode(
219
+ raw.contains(',') ? raw.split(',').last : raw,
220
+ ));
221
+ });
216222
217223 return Column(
218224 crossAxisAlignment: CrossAxisAlignment.start,
....@@ -232,6 +238,7 @@
232238 width: 260,
233239 height: 180,
234240 fit: BoxFit.cover,
241
+ gaplessPlayback: true,
235242 errorBuilder: (_, e, st) => const SizedBox(
236243 width: 260,
237244 height: 60,