refactor: fix lints
This commit is contained in:
@@ -209,7 +209,9 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
|
||||
|
||||
final activeServer = ref.watch(activeServerProvider);
|
||||
final token = ref.watch(authTokenProvider3);
|
||||
final transportMode = ref.watch(appSettingsProvider).socketTransportMode; // 'auto' or 'ws'
|
||||
final transportMode = ref
|
||||
.watch(appSettingsProvider)
|
||||
.socketTransportMode; // 'auto' or 'ws'
|
||||
|
||||
return activeServer.maybeWhen(
|
||||
data: (server) {
|
||||
@@ -223,7 +225,9 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
|
||||
// ignore unawaited_futures
|
||||
s.connect();
|
||||
ref.onDispose(() {
|
||||
try { s.dispose(); } catch (_) {}
|
||||
try {
|
||||
s.dispose();
|
||||
} catch (_) {}
|
||||
});
|
||||
return s;
|
||||
},
|
||||
@@ -373,7 +377,8 @@ final defaultModelAutoSelectionProvider = Provider<void>((ref) {
|
||||
}
|
||||
|
||||
// Fallback: keep current selection or pick first available
|
||||
selected ??= ref.read(selectedModelProvider) ??
|
||||
selected ??=
|
||||
ref.read(selectedModelProvider) ??
|
||||
(models.isNotEmpty ? models.first : null);
|
||||
|
||||
if (selected != null) {
|
||||
@@ -481,11 +486,11 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
||||
conversationMap[conversation.id] = conversation.copyWith(
|
||||
folderId: folderIdToUse,
|
||||
);
|
||||
final _idPreview = conversation.id.length > 8
|
||||
final idPreview = conversation.id.length > 8
|
||||
? conversation.id.substring(0, 8)
|
||||
: conversation.id;
|
||||
foundation.debugPrint(
|
||||
'DEBUG: Updated conversation $_idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})',
|
||||
'DEBUG: Updated conversation $idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})',
|
||||
);
|
||||
} else {
|
||||
conversationMap[conversation.id] = conversation;
|
||||
@@ -547,11 +552,11 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
||||
// Use map to prevent duplicates - this will overwrite if ID already exists
|
||||
conversationMap[toAdd.id] = toAdd;
|
||||
existingIds.add(toAdd.id);
|
||||
final _idPreview = toAdd.id.length > 8
|
||||
final idPreview = toAdd.id.length > 8
|
||||
? toAdd.id.substring(0, 8)
|
||||
: toAdd.id;
|
||||
foundation.debugPrint(
|
||||
'DEBUG: Added missing conversation from folder fetch: $_idPreview -> folder ${folder.id}',
|
||||
'DEBUG: Added missing conversation from folder fetch: $idPreview -> folder ${folder.id}',
|
||||
);
|
||||
} else {
|
||||
// Create a minimal placeholder if not returned by folder API
|
||||
@@ -566,11 +571,11 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
||||
// Use map to prevent duplicates
|
||||
conversationMap[convId] = placeholder;
|
||||
existingIds.add(convId);
|
||||
final _idPreview = convId.length > 8
|
||||
final idPreview = convId.length > 8
|
||||
? convId.substring(0, 8)
|
||||
: convId;
|
||||
foundation.debugPrint(
|
||||
'DEBUG: Added placeholder conversation for missing ID: $_idPreview -> folder ${folder.id}',
|
||||
'DEBUG: Added placeholder conversation for missing ID: $idPreview -> folder ${folder.id}',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -694,16 +699,18 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
|
||||
if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) {
|
||||
try {
|
||||
// Exact ID match only
|
||||
selectedModel =
|
||||
models.firstWhere((model) => model.id == userDefaultModelId);
|
||||
selectedModel = models.firstWhere(
|
||||
(model) => model.id == userDefaultModelId,
|
||||
);
|
||||
foundation.debugPrint(
|
||||
'DEBUG: Found user default model by ID: ${selectedModel.name}',
|
||||
);
|
||||
} catch (e) {
|
||||
// Attempt a one-time migration if the stored value was a model name
|
||||
// from older versions. Only migrate on exact, unique name match.
|
||||
final nameMatches =
|
||||
models.where((m) => m.name == userDefaultModelId).toList();
|
||||
final nameMatches = models
|
||||
.where((m) => m.name == userDefaultModelId)
|
||||
.toList();
|
||||
if (nameMatches.length == 1) {
|
||||
selectedModel = nameMatches.first;
|
||||
foundation.debugPrint(
|
||||
@@ -719,7 +726,8 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
|
||||
'DEBUG: User default model "$userDefaultModelId" not found by ID and '
|
||||
'no unique name match. Ignoring.',
|
||||
);
|
||||
selectedModel = null; // Will fall back to server default or first model
|
||||
selectedModel =
|
||||
null; // Will fall back to server default or first model
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -732,14 +740,17 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
|
||||
if (defaultModelId != null && defaultModelId.isNotEmpty) {
|
||||
// Find the model that matches the default model ID (ID only)
|
||||
try {
|
||||
selectedModel =
|
||||
models.firstWhere((model) => model.id == defaultModelId);
|
||||
selectedModel = models.firstWhere(
|
||||
(model) => model.id == defaultModelId,
|
||||
);
|
||||
foundation.debugPrint(
|
||||
'DEBUG: Found server default model by ID: ${selectedModel.name}',
|
||||
);
|
||||
} catch (e) {
|
||||
// If server returned a name instead of ID, attempt exact name match.
|
||||
final byName = models.where((m) => m.name == defaultModelId).toList();
|
||||
final byName = models
|
||||
.where((m) => m.name == defaultModelId)
|
||||
.toList();
|
||||
if (byName.length == 1) {
|
||||
selectedModel = byName.first;
|
||||
foundation.debugPrint(
|
||||
|
||||
@@ -392,11 +392,11 @@ class ApiService {
|
||||
debugPrint(
|
||||
'🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}',
|
||||
);
|
||||
final _sampleStr = chatData.toString();
|
||||
final _preview = _sampleStr.length > 200
|
||||
? _sampleStr.substring(0, 200)
|
||||
: _sampleStr;
|
||||
debugPrint('🔍 DEBUG: Sample chat data: $_preview...');
|
||||
final samplePreviewSource = chatData.toString();
|
||||
final preview = samplePreviewSource.length > 200
|
||||
? samplePreviewSource.substring(0, 200)
|
||||
: samplePreviewSource;
|
||||
debugPrint('🔍 DEBUG: Sample chat data: $preview...');
|
||||
}
|
||||
|
||||
final conversation = _parseOpenWebUIChat(chatData);
|
||||
@@ -475,8 +475,8 @@ class ApiService {
|
||||
|
||||
// Debug logging for folder assignment
|
||||
if (folderId != null) {
|
||||
final _idPreview = id.length > 8 ? id.substring(0, 8) : id;
|
||||
debugPrint('🔍 DEBUG: Conversation $_idPreview has folderId: $folderId');
|
||||
final idPreview = id.length > 8 ? id.substring(0, 8) : id;
|
||||
debugPrint('🔍 DEBUG: Conversation $idPreview has folderId: $folderId');
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
@@ -3357,11 +3357,11 @@ class ApiService {
|
||||
} else if (response.data is Map) {
|
||||
DebugLogger.log(' Object keys: ${(response.data as Map).keys}');
|
||||
}
|
||||
final _dataStr = response.data.toString();
|
||||
final _dataPreview = _dataStr.length > 200
|
||||
? _dataStr.substring(0, 200)
|
||||
: _dataStr;
|
||||
DebugLogger.log(' Sample data: $_dataPreview...');
|
||||
final dataSampleSource = response.data.toString();
|
||||
final dataPreview = dataSampleSource.length > 200
|
||||
? dataSampleSource.substring(0, 200)
|
||||
: dataSampleSource;
|
||||
DebugLogger.log(' Sample data: $dataPreview...');
|
||||
} catch (e) {
|
||||
debugPrint('❌ $endpoint - Error: $e');
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
// Message update callbacks
|
||||
required void Function(String) appendToLastMessage,
|
||||
required void Function(String) replaceLastMessageContent,
|
||||
required void Function(ChatMessage Function(ChatMessage)) updateLastMessageWith,
|
||||
required void Function(ChatMessage Function(ChatMessage))
|
||||
updateLastMessageWith,
|
||||
required void Function() finishStreaming,
|
||||
required List<ChatMessage> Function() getMessages,
|
||||
}) {
|
||||
@@ -71,7 +72,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
bool suppressSocketContent = suppressSocketContentInitially;
|
||||
bool usingDynamicChannel = usingDynamicChannelInitially;
|
||||
|
||||
void _updateImagesFromCurrentContent() {
|
||||
void updateImagesFromCurrentContent() {
|
||||
try {
|
||||
final msgs = getMessages();
|
||||
if (msgs.isEmpty || msgs.last.role != 'assistant') return;
|
||||
@@ -236,13 +237,13 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
for (final call in tc) {
|
||||
if (call is Map<String, dynamic>) {
|
||||
final fn = call['function'];
|
||||
final name =
|
||||
(fn is Map && fn['name'] is String)
|
||||
? fn['name'] as String
|
||||
: null;
|
||||
final name = (fn is Map && fn['name'] is String)
|
||||
? fn['name'] as String
|
||||
: null;
|
||||
if (name is String && name.isNotEmpty) {
|
||||
final msgs = getMessages();
|
||||
final exists = (msgs.isNotEmpty) &&
|
||||
final exists =
|
||||
(msgs.isNotEmpty) &&
|
||||
RegExp(
|
||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
||||
RegExp.escape(name) +
|
||||
@@ -262,20 +263,20 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
final content = delta['content']?.toString() ?? '';
|
||||
if (content.isNotEmpty) {
|
||||
appendToLastMessage(content);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
if (s.isNotEmpty) {
|
||||
appendToLastMessage(s);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (s.isNotEmpty) {
|
||||
appendToLastMessage(s);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
} else if (line is Map) {
|
||||
@@ -320,10 +321,12 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
: null;
|
||||
if (name is String && name.isNotEmpty) {
|
||||
final msgs = getMessages();
|
||||
final exists = (msgs.isNotEmpty) &&
|
||||
final exists =
|
||||
(msgs.isNotEmpty) &&
|
||||
RegExp(
|
||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
||||
RegExp.escape(name) + r'\"',
|
||||
RegExp.escape(name) +
|
||||
r'\"',
|
||||
multiLine: true,
|
||||
).hasMatch(msgs.last.content);
|
||||
if (!exists) {
|
||||
@@ -353,10 +356,12 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
: null;
|
||||
if (name is String && name.isNotEmpty) {
|
||||
final msgs = getMessages();
|
||||
final exists = (msgs.isNotEmpty) &&
|
||||
final exists =
|
||||
(msgs.isNotEmpty) &&
|
||||
RegExp(
|
||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
||||
RegExp.escape(name) + r'\"',
|
||||
RegExp.escape(name) +
|
||||
r'\"',
|
||||
multiLine: true,
|
||||
).hasMatch(msgs.last.content);
|
||||
if (!exists) {
|
||||
@@ -372,7 +377,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
final content = delta['content']?.toString() ?? '';
|
||||
if (content.isNotEmpty) {
|
||||
appendToLastMessage(content);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,7 +414,9 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
final list = chatObj['messages'];
|
||||
if (list is List) {
|
||||
final target = list.firstWhere(
|
||||
(m) => (m is Map && (m['id']?.toString() == assistantMessageId)),
|
||||
(m) =>
|
||||
(m is Map &&
|
||||
(m['id']?.toString() == assistantMessageId)),
|
||||
orElse: () => null,
|
||||
);
|
||||
if (target != null) {
|
||||
@@ -431,7 +438,8 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
final history = chatObj['history'];
|
||||
if (history is Map && history['messages'] is Map) {
|
||||
final Map<String, dynamic> messagesMap =
|
||||
(history['messages'] as Map).cast<String, dynamic>();
|
||||
(history['messages'] as Map)
|
||||
.cast<String, dynamic>();
|
||||
final msg = messagesMap[assistantMessageId];
|
||||
if (msg is Map) {
|
||||
final rawContent = msg['content'];
|
||||
@@ -454,7 +462,8 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
replaceLastMessageContent(content);
|
||||
}
|
||||
}
|
||||
} catch (_) {} finally {
|
||||
} catch (_) {
|
||||
} finally {
|
||||
finishStreaming();
|
||||
}
|
||||
});
|
||||
@@ -483,21 +492,23 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
}
|
||||
if (content.isNotEmpty) {
|
||||
// Replace current assistant message with a readable error
|
||||
replaceLastMessageContent('⚠️ ' + content);
|
||||
replaceLastMessageContent('⚠️ $content');
|
||||
}
|
||||
} catch (_) {}
|
||||
// Ensure UI exits streaming state
|
||||
finishStreaming();
|
||||
} else if ((type == 'chat:message:delta' || type == 'message') && payload != null) {
|
||||
} else if ((type == 'chat:message:delta' || type == 'message') &&
|
||||
payload != null) {
|
||||
// Incremental message content over socket; respect suppression on SSE-driven flows
|
||||
if (!suppressSocketContent) {
|
||||
final content = payload['content']?.toString() ?? '';
|
||||
if (content.isNotEmpty) {
|
||||
appendToLastMessage(content);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
} else if ((type == 'chat:message' || type == 'replace') && payload != null) {
|
||||
} else if ((type == 'chat:message' || type == 'replace') &&
|
||||
payload != null) {
|
||||
// Full message replacement over socket; respect suppression on SSE-driven flows
|
||||
if (!suppressSocketContent) {
|
||||
final content = payload['content']?.toString() ?? '';
|
||||
@@ -600,10 +611,9 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
} else if (type == 'event:status' && payload != null) {
|
||||
final status = payload['status']?.toString() ?? '';
|
||||
if (status.isNotEmpty) {
|
||||
updateLastMessageWith((m) => m.copyWith(metadata: {
|
||||
...?m.metadata,
|
||||
'status': status,
|
||||
}));
|
||||
updateLastMessageWith(
|
||||
(m) => m.copyWith(metadata: {...?m.metadata, 'status': status}),
|
||||
);
|
||||
}
|
||||
} else if (type == 'event:tool' && payload != null) {
|
||||
// Accept files from both 'result' and 'files'
|
||||
@@ -624,7 +634,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
final content = payload['content']?.toString() ?? '';
|
||||
if (content.isNotEmpty) {
|
||||
appendToLastMessage(content);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -640,7 +650,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
final content = payload['content']?.toString() ?? '';
|
||||
if (content.isNotEmpty) {
|
||||
appendToLastMessage(content);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -656,7 +666,9 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
} catch (_) {}
|
||||
try {
|
||||
final msgs = getMessages();
|
||||
if (msgs.isNotEmpty && msgs.last.role == 'assistant' && msgs.last.isStreaming) {
|
||||
if (msgs.isNotEmpty &&
|
||||
msgs.last.role == 'assistant' &&
|
||||
msgs.last.isStreaming) {
|
||||
finishStreaming();
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -681,17 +693,21 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
||||
}
|
||||
}
|
||||
|
||||
if (isSearching && (chunk.contains('[/SEARCHING]') || chunk.contains('Search complete'))) {
|
||||
if (isSearching &&
|
||||
(chunk.contains('[/SEARCHING]') ||
|
||||
chunk.contains('Search complete'))) {
|
||||
isSearching = false;
|
||||
updateLastMessageWith(
|
||||
(message) => message.copyWith(metadata: {'webSearchActive': false}),
|
||||
);
|
||||
effectiveChunk = effectiveChunk.replaceAll('[SEARCHING]', '').replaceAll('[/SEARCHING]', '');
|
||||
effectiveChunk = effectiveChunk
|
||||
.replaceAll('[SEARCHING]', '')
|
||||
.replaceAll('[/SEARCHING]', '');
|
||||
}
|
||||
|
||||
if (effectiveChunk.trim().isNotEmpty) {
|
||||
appendToLastMessage(effectiveChunk);
|
||||
_updateImagesFromCurrentContent();
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
},
|
||||
onDone: () async {
|
||||
|
||||
@@ -49,7 +49,9 @@ class ReasoningParser {
|
||||
if (openingIdx >= 0 && !content.contains('</details>')) {
|
||||
final after = content.substring(openingIdx);
|
||||
// Try to extract optional summary
|
||||
final summaryMatch = RegExp(r'<summary>([^<]*)<\/summary>').firstMatch(after);
|
||||
final summaryMatch = RegExp(
|
||||
r'<summary>([^<]*)<\/summary>',
|
||||
).firstMatch(after);
|
||||
final summary = (summaryMatch?.group(1) ?? '').trim();
|
||||
final reasoning = after
|
||||
.replaceAll(RegExp(r'^<details[^>]*>'), '')
|
||||
@@ -80,7 +82,11 @@ class ReasoningParser {
|
||||
for (final pair in tagPairs) {
|
||||
final start = RegExp.escape(pair[0]);
|
||||
final end = RegExp.escape(pair[1]);
|
||||
final tagRegex = RegExp('($start)([\s\S]*?)($end)', multiLine: true, dotAll: true);
|
||||
final tagRegex = RegExp(
|
||||
'($start)(.*?)($end)',
|
||||
multiLine: true,
|
||||
dotAll: true,
|
||||
);
|
||||
final match = tagRegex.firstMatch(content);
|
||||
if (match != null) {
|
||||
final reasoning = (match.group(2) ?? '').trim();
|
||||
@@ -144,7 +150,8 @@ class ReasoningParser {
|
||||
if (nextDetails == -1 && nextRawStart == -1) {
|
||||
nextIdx = -1;
|
||||
kind = 'none';
|
||||
} else if (nextDetails != -1 && (nextRawStart == -1 || nextDetails < nextRawStart)) {
|
||||
} else if (nextDetails != -1 &&
|
||||
(nextRawStart == -1 || nextDetails < nextRawStart)) {
|
||||
nextIdx = nextDetails;
|
||||
kind = 'details';
|
||||
} else {
|
||||
@@ -219,7 +226,9 @@ class ReasoningParser {
|
||||
if (depth != 0) {
|
||||
// Unclosed; treat as streaming partial
|
||||
final after = content.substring(openEnd + 1);
|
||||
final summaryMatch = RegExp(r'<summary>([^<]*)<\/summary>').firstMatch(after);
|
||||
final summaryMatch = RegExp(
|
||||
r'<summary>([^<]*)<\/summary>',
|
||||
).firstMatch(after);
|
||||
final summary = (summaryMatch?.group(1) ?? '').trim();
|
||||
final reasoning = after
|
||||
.replaceAll(RegExp(r'^\s*<summary>[\s\S]*?<\/summary>'), '')
|
||||
@@ -238,8 +247,13 @@ class ReasoningParser {
|
||||
break;
|
||||
} else {
|
||||
// Closed block: extract inner content
|
||||
final inner = content.substring(openEnd + 1, i - 10); // without </details>
|
||||
final sumMatch = RegExp(r'<summary>([^<]*)<\/summary>').firstMatch(inner);
|
||||
final inner = content.substring(
|
||||
openEnd + 1,
|
||||
i - 10,
|
||||
); // without </details>
|
||||
final sumMatch = RegExp(
|
||||
r'<summary>([^<]*)<\/summary>',
|
||||
).firstMatch(inner);
|
||||
final summary = (sumMatch?.group(1) ?? '').trim();
|
||||
final reasoning = inner
|
||||
.replaceAll(RegExp(r'<summary>[\s\S]*?<\/summary>'), '')
|
||||
|
||||
@@ -47,6 +47,7 @@ class ToolCallsParser {
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('&', '&');
|
||||
}
|
||||
|
||||
/// Represents a mixed stream of text and tool-call entries in original order
|
||||
/// as they appeared in the content.
|
||||
static List<ToolCallsSegment>? segments(String content) {
|
||||
@@ -97,7 +98,9 @@ class ToolCallsParser {
|
||||
i = nextOpen + 8; // '<details'
|
||||
} else {
|
||||
depth--;
|
||||
i = (nextClose != -1) ? nextClose + 10 : content.length; // '</details>'
|
||||
i = (nextClose != -1)
|
||||
? nextClose + 10
|
||||
: content.length; // '</details>'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,17 +108,16 @@ class ToolCallsParser {
|
||||
|
||||
if (isToolCalls) {
|
||||
// Decode attributes for tool call tile
|
||||
dynamic _decode(String? s) {
|
||||
if (s == null || s.isEmpty) return null;
|
||||
dynamic decodeAttribute(String? source) {
|
||||
if (source == null || source.isEmpty) return null;
|
||||
try {
|
||||
final unescaped = _unescapeHtml(s);
|
||||
final unescaped = _unescapeHtml(source);
|
||||
return json.decode(unescaped);
|
||||
} catch (_) {
|
||||
// If JSON decode fails, return unescaped string for display
|
||||
try {
|
||||
return _unescapeHtml(s);
|
||||
return _unescapeHtml(source);
|
||||
} catch (_) {
|
||||
return s;
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,9 +125,9 @@ class ToolCallsParser {
|
||||
final id = (attrs['id'] ?? '');
|
||||
final name = (attrs['name'] ?? 'tool');
|
||||
final done = (attrs['done'] == 'true');
|
||||
final args = _decode(attrs['arguments']);
|
||||
final result = _decode(attrs['result']);
|
||||
final files = _decode(attrs['files']);
|
||||
final args = decodeAttribute(attrs['arguments']);
|
||||
final result = decodeAttribute(attrs['result']);
|
||||
final files = decodeAttribute(attrs['files']);
|
||||
|
||||
segs.add(
|
||||
ToolCallsSegment.entry(
|
||||
@@ -207,7 +209,9 @@ class ToolCallsParser {
|
||||
if (parsed == null) return content;
|
||||
final buf = StringBuffer();
|
||||
for (final c in parsed.toolCalls) {
|
||||
buf.writeln(c.done ? 'Tool Executed: ${c.name}' : 'Running tool: ${c.name}…');
|
||||
buf.writeln(
|
||||
c.done ? 'Tool Executed: ${c.name}' : 'Running tool: ${c.name}…',
|
||||
);
|
||||
final args = _prettyMaybe(c.arguments, max: 400);
|
||||
final res = _prettyMaybe(c.result, max: 800);
|
||||
if (args.isNotEmpty) {
|
||||
@@ -239,8 +243,8 @@ class ToolCallsParser {
|
||||
|
||||
/// Sanitize assistant/user content before sending to the API, mirroring
|
||||
/// the web client's `processDetails` behavior:
|
||||
/// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks
|
||||
/// - Replace <details type="tool_calls" ...>...</details> blocks with the
|
||||
/// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks
|
||||
/// - Replace <details type="tool_calls" ...>...</details> blocks with the
|
||||
/// JSON-serialized `result` attribute (as a quoted string) when available;
|
||||
/// otherwise replace with an empty string.
|
||||
static String sanitizeForApi(String content) {
|
||||
@@ -251,7 +255,7 @@ class ToolCallsParser {
|
||||
for (final t in removeTypes) {
|
||||
content = content.replaceAll(
|
||||
RegExp(
|
||||
'<details\\s+type=\"${t}\"[^>]*>[\\s\\S]*?<\\/details>',
|
||||
'<details\\s+type="$t"[^>]*>[\\s\\S]*?</details>',
|
||||
multiLine: true,
|
||||
dotAll: true,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user