refactor(markdown): remove deprecated stream formatter and enhance preprocessor

This commit is contained in:
cogwheel
2025-12-22 14:07:04 +05:30
parent 653162cb76
commit 5fd68f86fe
12 changed files with 347 additions and 505 deletions

View File

@@ -16,7 +16,6 @@ import '../../../core/services/streaming_helper.dart';
import '../../../core/services/streaming_response_controller.dart';
import '../../../core/services/worker_manager.dart';
import '../../../core/utils/debug_logger.dart';
import '../../../core/utils/markdown_stream_formatter.dart';
import '../../../core/utils/tool_calls_parser.dart';
import '../models/chat_context_attachment.dart';
import '../providers/context_attachments_provider.dart';
@@ -109,9 +108,6 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
bool _taskStatusCheckInFlight = false;
bool _observedRemoteTask = false;
MarkdownStreamFormatter? _markdownFormatter;
String? _activeStreamingMessageId;
bool _initialized = false;
@override
@@ -180,7 +176,6 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
// Cancel any existing message stream when switching conversations
_cancelMessageStream();
_clearStreamingFormatter(); // Explicitly clear formatter on conversation switch
_stopRemoteTaskMonitor();
if (next != null) {
@@ -222,16 +217,10 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
if (controller != null && controller.isActive) {
unawaited(controller.cancel());
}
_clearStreamingFormatter();
cancelSocketSubscriptions();
_stopRemoteTaskMonitor();
}
void _clearStreamingFormatter() {
_markdownFormatter = null;
_activeStreamingMessageId = null;
}
/// Checks if streaming cleanup is needed when adopting server messages.
/// Must be called BEFORE updating state, as it compares current local state
/// with incoming server state.
@@ -397,39 +386,6 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
}
}
void _ensureFormatterForMessage(ChatMessage message) {
// If we're switching to a different message, clear the old formatter first
if (_markdownFormatter != null && _activeStreamingMessageId != message.id) {
DebugLogger.log(
'Clearing formatter for message switch: $_activeStreamingMessageId -> ${message.id}',
scope: 'chat/providers',
);
_clearStreamingFormatter();
}
// If formatter already exists for this message, reuse it
if (_markdownFormatter != null && _activeStreamingMessageId == message.id) {
return;
}
// Create new formatter
final formatter = MarkdownStreamFormatter();
// Only seed with existing content if this is a resume scenario
// For new messages (empty content), start fresh to avoid duplication
final seed = _stripStreamingPlaceholders(message.content);
if (seed.isNotEmpty && message.content.isNotEmpty) {
DebugLogger.log(
'Seeding formatter with existing content (${seed.length} chars) for message ${message.id}',
scope: 'chat/providers',
);
formatter.seed(seed);
}
_markdownFormatter = formatter;
_activeStreamingMessageId = message.id;
}
String _stripStreamingPlaceholders(String content) {
var result = content;
const ti = '[TYPING_INDICATOR]';
@@ -443,15 +399,6 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
return result;
}
String _finalizeFormatter(String messageId, String fallback) {
if (_markdownFormatter != null && _activeStreamingMessageId == messageId) {
final output = _markdownFormatter!.finalize();
_clearStreamingFormatter();
return output;
}
return fallback;
}
void _touchStreamingActivity() {
_lastStreamingActivity = DateTime.now();
if (_hasStreamingAssistant) {
@@ -728,16 +675,11 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
}
void appendToLastMessage(String content) {
if (state.isEmpty) {
return;
}
if (state.isEmpty) return;
final lastMessage = state.last;
if (lastMessage.role != 'assistant') {
return;
}
if (lastMessage.role != 'assistant') return;
if (!lastMessage.isStreaming) {
// Ignore late chunks when streaming already finished
DebugLogger.log(
'Ignoring late chunk for finished message: ${lastMessage.id}',
scope: 'chat/providers',
@@ -745,52 +687,21 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
return;
}
_ensureFormatterForMessage(lastMessage);
// Defensive check: ensure the formatter is for the correct message
// This prevents cross-message pollution when messages change rapidly
if (_activeStreamingMessageId != lastMessage.id) {
DebugLogger.warning(
'Formatter message ID mismatch: active=$_activeStreamingMessageId, last=${lastMessage.id}. Resetting formatter.',
);
_clearStreamingFormatter();
_ensureFormatterForMessage(lastMessage);
}
final formatter = _markdownFormatter!;
final preview = formatter.ingest(content);
// Append content directly - the widget's normalize() handles incomplete markdown
state = [
...state.sublist(0, state.length - 1),
lastMessage.copyWith(content: preview),
lastMessage.copyWith(content: lastMessage.content + content),
];
_touchStreamingActivity();
}
void replaceLastMessageContent(String content) {
if (state.isEmpty) {
return;
}
if (state.isEmpty) return;
final lastMessage = state.last;
if (lastMessage.role != 'assistant') {
return;
}
_ensureFormatterForMessage(lastMessage);
// Defensive check: ensure the formatter is for the correct message
if (_activeStreamingMessageId != lastMessage.id) {
DebugLogger.warning(
'Formatter message ID mismatch in replace: active=$_activeStreamingMessageId, last=${lastMessage.id}. Resetting formatter.',
);
_clearStreamingFormatter();
_ensureFormatterForMessage(lastMessage);
}
final formatter = _markdownFormatter!;
final sanitized = formatter.replace(_stripStreamingPlaceholders(content));
if (lastMessage.role != 'assistant') return;
final sanitized = _stripStreamingPlaceholders(content);
state = [
...state.sublist(0, state.length - 1),
lastMessage.copyWith(content: sanitized),
@@ -804,8 +715,7 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
final lastMessage = state.last;
if (lastMessage.role != 'assistant' || !lastMessage.isStreaming) return;
final finalized = _finalizeFormatter(lastMessage.id, lastMessage.content);
final cleaned = _stripStreamingPlaceholders(finalized);
final cleaned = _stripStreamingPlaceholders(lastMessage.content);
var updatedLast = lastMessage.copyWith(
isStreaming: false,
@@ -1005,11 +915,7 @@ Future<void> restoreDefaultModel(dynamic ref) async {
try {
await ref.read(defaultModelProvider.future);
} catch (e) {
DebugLogger.error(
'restore-default-failed',
scope: 'chat/model',
error: e,
);
DebugLogger.error('restore-default-failed', scope: 'chat/model', error: e);
}
}

View File

@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/services/settings_service.dart';
import '../../../core/providers/app_providers.dart';
import '../../../core/utils/markdown_to_text.dart';
import '../../../shared/widgets/markdown/markdown_preprocessor.dart';
import '../services/text_to_speech_service.dart';
enum TtsPlaybackStatus { idle, initializing, loading, speaking, paused, error }
@@ -218,7 +218,7 @@ class TextToSpeechController extends Notifier<TextToSpeechState> {
}
// Prepare sentence split for highlighting
final cleanText = MarkdownToText.convert(text);
final cleanText = ConduitMarkdownPreprocessor.toPlainText(text);
final sentences = _service.splitTextForSpeech(cleanText);
final offsets = _computeOffsets(cleanText, sentences);