chore: update markdown dependency and refactor streaming handling

- Added `markdown` dependency version `^7.2.1` in `pubspec.yaml`.
- Updated `pubspec.lock` to reflect the direct dependency change.
- Refactored `streaming_helper.dart` to utilize `StreamingResponseController` for better stream management.
- Enhanced `ChatMessagesNotifier` to handle message streams with improved formatting and error handling.
- Updated `StreamingMarkdownWidget` to streamline markdown rendering and support new configurations.
This commit is contained in:
cogwheel0
2025-09-30 20:49:02 +05:30
parent d3b64716b9
commit 7debb7a055
10 changed files with 451 additions and 301 deletions

View File

@@ -14,18 +14,19 @@ import '../../shared/widgets/themed_dialogs.dart';
import '../../shared/theme/theme_extensions.dart';
import '../utils/debug_logger.dart';
import '../utils/openwebui_source_parser.dart';
import 'streaming_response_controller.dart';
// Keep local verbosity toggle for socket logs
const bool kSocketVerboseLogging = false;
class ActiveSocketStream {
ActiveSocketStream({
required this.streamSubscription,
required this.controller,
required this.socketSubscriptions,
required this.disposeWatchdog,
});
final StreamSubscription<String> streamSubscription;
final StreamingResponseController controller;
final List<VoidCallback> socketSubscriptions;
final VoidCallback disposeWatchdog;
}
@@ -1026,8 +1027,9 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
socketSubscriptions.add(channelSub.dispose);
}
final subscription = persistentController.stream.listen(
(chunk) {
final controller = StreamingResponseController(
stream: persistentController.stream,
onChunk: (chunk) {
var effectiveChunk = chunk;
if (webSearchEnabled && !isSearching) {
if (chunk.contains('[SEARCHING]') ||
@@ -1061,7 +1063,7 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
updateImagesFromCurrentContent();
}
},
onDone: () async {
onComplete: () {
// Unregister from persistent service
persistentService.unregisterStream(streamId);
@@ -1073,7 +1075,7 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
Future.microtask(refreshConversationSnapshot);
}
},
onError: (error) async {
onError: (error, stackTrace) async {
DebugLogger.error(
'Stream error occurred',
scope: 'streaming/helper',
@@ -1090,11 +1092,12 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
} catch (_) {}
// Check if this is a recoverable error (network issues, etc.)
final errorText = error.toString();
final isRecoverable =
error is! FormatException &&
error.toString().contains('SocketException') ||
error.toString().contains('TimeoutException') ||
error.toString().contains('HandshakeException');
(error is! FormatException &&
errorText.contains('SocketException')) ||
errorText.contains('TimeoutException') ||
errorText.contains('HandshakeException');
if (isRecoverable && socketService != null) {
// Try to recover via socket connection if available
@@ -1118,7 +1121,7 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
);
return ActiveSocketStream(
streamSubscription: subscription,
controller: controller,
socketSubscriptions: socketSubscriptions,
disposeWatchdog: () => socketWatchdog?.stop(),
);

View File

@@ -0,0 +1,95 @@
import 'dart:async';
import '../utils/debug_logger.dart';
/// Signature for callbacks that receive streaming text updates.
typedef StreamingChunkCallback = void Function(String chunk);
/// Signature for callbacks invoked when a streaming session finishes.
typedef StreamingCompletionCallback = void Function();
/// Signature for callbacks invoked when a streaming session encounters an
/// error.
typedef StreamingErrorCallback =
void Function(Object error, StackTrace stackTrace);
/// A lightweight controller that manages the lifecycle of a streamed response.
///
/// This wraps a [StreamSubscription], normalises error handling, and exposes
/// a unified cancel method so UI layers can stop streaming without having to
/// know the underlying transport (SSE, polling, etc.).
class StreamingResponseController {
StreamingResponseController({
required Stream<String> stream,
required StreamingChunkCallback onChunk,
required StreamingCompletionCallback onComplete,
required StreamingErrorCallback onError,
bool cancelOnError = true,
}) : _onChunk = onChunk,
_onComplete = onComplete,
_onError = onError {
_subscription = stream.listen(
_handleChunk,
cancelOnError: cancelOnError,
onDone: _handleCompleted,
onError: _handleError,
);
}
final StreamingChunkCallback _onChunk;
final StreamingCompletionCallback _onComplete;
final StreamingErrorCallback _onError;
StreamSubscription<String>? _subscription;
bool _isCancelled = false;
/// Whether the underlying stream subscription is still active.
bool get isActive => _subscription != null && !_isCancelled;
void _handleChunk(String chunk) {
if (_isCancelled) {
return;
}
try {
_onChunk(chunk);
} catch (err, stackTrace) {
DebugLogger.error(
'streaming-chunk-handler-failed',
scope: 'streaming/controller',
error: err,
);
_handleError(err, stackTrace);
}
}
void _handleCompleted() {
if (_isCancelled) {
return;
}
_subscription = null;
try {
_onComplete();
} catch (err, stackTrace) {
_handleError(err, stackTrace);
}
}
void _handleError(Object error, StackTrace stackTrace) {
if (_isCancelled) {
return;
}
_subscription = null;
_onError(error, stackTrace);
}
/// Cancels the underlying stream subscription.
Future<void> cancel() async {
if (_isCancelled) {
return;
}
_isCancelled = true;
final subscription = _subscription;
_subscription = null;
await subscription?.cancel();
}
}