fix: stabilize chat run lifecycle rendering

This commit is contained in:
qzl
2026-03-17 15:58:29 +08:00
parent 3bf7640000
commit cf56b358ad
5 changed files with 673 additions and 75 deletions
@@ -118,10 +118,16 @@ class ChatBloc extends Cubit<ChatState> {
),
);
case AgUiEventType.runFinished:
emit(_resetRunState());
emit(
_resetRunState().copyWith(items: _removeToolCallItems(state.items)),
);
case AgUiEventType.runError:
final errorEvent = event as RunErrorEvent;
emit(_resetRunState(error: errorEvent.message));
emit(
_resetRunState(
error: errorEvent.message,
).copyWith(items: _markActiveToolCallsFailed(state.items)),
);
case AgUiEventType.stepStarted:
_handleStepStarted(event as StepStartedEvent);
case AgUiEventType.stepFinished:
@@ -167,9 +173,11 @@ class ChatBloc extends Cubit<ChatState> {
_upsertUiSchema(items, event.messageId, uiSchema, timestamp);
}
final withoutToolCalls = _removeToolCallItems(items);
emit(
state.copyWith(
items: items,
items: withoutToolCalls,
currentMessageId: null,
isWaitingFirstToken: false,
isStreaming: false,
@@ -264,13 +272,38 @@ class ChatBloc extends Cubit<ChatState> {
}
void _handleToolCallResult(ToolCallResultEvent event) {
final items = state.items.where((item) {
return !(item is ToolCallItem && item.id == event.toolCallId);
final items = state.items.map((item) {
if (item is ToolCallItem && item.id == event.toolCallId) {
return item.copyWith(status: ToolCallStatus.completed);
}
return item;
}).toList();
emit(state.copyWith(items: items));
}
List<ChatListItem> _removeToolCallItems(List<ChatListItem> items) {
return items.where((item) => item is! ToolCallItem).toList();
}
List<ChatListItem> _markActiveToolCallsFailed(List<ChatListItem> items) {
return items.map((item) {
if (item is! ToolCallItem) {
return item;
}
if (item.status == ToolCallStatus.error) {
return item;
}
if (item.status == ToolCallStatus.completed) {
return item;
}
return item.copyWith(
status: ToolCallStatus.error,
errorMessage: '本次运行已失败',
);
}).toList();
}
void _handleToolCallError(ToolCallErrorEvent event) {
final items = state.items.map((item) {
if (item is ToolCallItem && item.id == event.toolCallId) {
@@ -341,16 +374,18 @@ class ChatBloc extends Cubit<ChatState> {
}
Future<void> sendMessage(String content, {List<XFile>? images}) async {
final messageId = 'user-${DateTime.now().millisecondsSinceEpoch}';
final attachments = (images ?? const <XFile>[])
.map(
(image) => <String, dynamic>{
'path': image.path,
'mimeType': 'image/*',
'mimeType': image.mimeType ?? 'image/jpeg',
'uploading': true,
},
)
.toList();
final userMessage = TextMessageItem(
id: 'user-${DateTime.now().millisecondsSinceEpoch}',
id: messageId,
content: content,
timestamp: DateTime.now(),
sender: MessageSender.user,
@@ -367,8 +402,13 @@ class ChatBloc extends Cubit<ChatState> {
),
);
try {
await _service.sendMessage(content, images: images);
final sendResult = await _service.sendMessage(content, images: images);
_syncUploadedAttachments(
messageId: messageId,
uploadedAttachments: sendResult.uploadedAttachments,
);
} catch (error) {
_markAttachmentUploadDone(messageId);
emit(
state.copyWith(
isSending: false,
@@ -381,6 +421,63 @@ class ChatBloc extends Cubit<ChatState> {
}
}
void _syncUploadedAttachments({
required String messageId,
required List<UploadedAttachment> uploadedAttachments,
}) {
if (uploadedAttachments.isEmpty) {
_markAttachmentUploadDone(messageId);
return;
}
final items = state.items.map((item) {
if (item is! TextMessageItem || item.id != messageId) {
return item;
}
final synced = item.attachments.map((attachment) {
final localPath = attachment['path'];
if (localPath is! String || localPath.isEmpty) {
return <String, dynamic>{...attachment, 'uploading': false};
}
UploadedAttachment? matched;
for (final candidate in uploadedAttachments) {
if (candidate.localPath == localPath) {
matched = candidate;
break;
}
}
if (matched == null) {
return <String, dynamic>{...attachment, 'uploading': false};
}
return <String, dynamic>{
...attachment,
'url': matched.url,
'mimeType': matched.mimeType,
'uploading': false,
};
}).toList();
return item.copyWith(attachments: synced);
}).toList();
emit(state.copyWith(items: items));
}
void _markAttachmentUploadDone(String messageId) {
final items = state.items.map((item) {
if (item is! TextMessageItem || item.id != messageId) {
return item;
}
final done = item.attachments
.map(
(attachment) => <String, dynamic>{
...attachment,
'uploading': false,
},
)
.toList();
return item.copyWith(attachments: done);
}).toList();
emit(state.copyWith(items: items));
}
Future<void> loadHistory() async {
if (state.isLoadingHistory) return;
emit(state.copyWith(isLoadingHistory: true));