19e273a9e6
- calendar: split write → create/read/update/delete/share - contacts: rename lookup → read - memory: merge write+forget → update (unified action field in operations) - Remove all alias/normalization logic from adapter and handlers - Update tool_postprocessor ui_hints builders to canonical keys - Remove frontend legacy TOOL_CALL_START/ARGS/END events and ToolCallItem - Update SKILL.md files and protocol docs - Update tests and settings screens
216 lines
5.8 KiB
Dart
216 lines
5.8 KiB
Dart
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
|
|
|
part of 'chat_bloc.dart';
|
|
|
|
extension _ChatBlocEvents on ChatBloc {
|
|
void _handleEvent(AgUiEvent event) {
|
|
switch (event.type) {
|
|
case AgUiEventType.runStarted:
|
|
final runStartedEvent = event as RunStartedEvent;
|
|
_recordRunStarted(
|
|
runId: runStartedEvent.runId,
|
|
threadId: runStartedEvent.threadId,
|
|
);
|
|
emit(
|
|
state.copyWith(
|
|
isSending: false,
|
|
isWaitingFirstToken: true,
|
|
isCancelling: false,
|
|
error: null,
|
|
currentStage: null,
|
|
hasSeenStep: false,
|
|
),
|
|
);
|
|
case AgUiEventType.runFinished:
|
|
_trackChatCompleted();
|
|
_clearRunMetrics();
|
|
emit(_resetRunState());
|
|
case AgUiEventType.runError:
|
|
final errorEvent = event as RunErrorEvent;
|
|
_clearRunMetrics();
|
|
final isCanceledByUser = errorEvent.code == 'RUN_CANCELED';
|
|
emit(
|
|
_resetRunState(error: isCanceledByUser ? null : errorEvent.message),
|
|
);
|
|
case AgUiEventType.stepStarted:
|
|
_handleStepStarted(event as StepStartedEvent);
|
|
case AgUiEventType.stepFinished:
|
|
_handleStepFinished(event as StepFinishedEvent);
|
|
case AgUiEventType.textMessageEnd:
|
|
_handleTextMessageEnd(event as TextMessageEndEvent);
|
|
case AgUiEventType.toolCallResult:
|
|
_handleToolCallResult(event as ToolCallResultEvent);
|
|
case AgUiEventType.unknown:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void _handleStepStarted(StepStartedEvent event) {
|
|
emit(
|
|
state.copyWith(
|
|
currentStage: stageFromStepName(event.stepName),
|
|
hasSeenStep: true,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleStepFinished(StepFinishedEvent event) {
|
|
if (state.currentStage == stageFromStepName(event.stepName)) {
|
|
emit(state.copyWith(currentStage: null));
|
|
}
|
|
}
|
|
|
|
void _handleTextMessageEnd(TextMessageEndEvent event) {
|
|
_recordRunFirstResponse();
|
|
final timestamp = DateTime.now();
|
|
final items = _updateOrAddMessage(
|
|
state.items,
|
|
event.messageId,
|
|
event.answer,
|
|
event.suggestedActions,
|
|
timestamp,
|
|
);
|
|
|
|
emit(
|
|
state.copyWith(
|
|
items: items,
|
|
currentMessageId: null,
|
|
isWaitingFirstToken: false,
|
|
isStreaming: false,
|
|
),
|
|
);
|
|
}
|
|
|
|
List<ChatListItem> _updateOrAddMessage(
|
|
List<ChatListItem> items,
|
|
String messageId,
|
|
String content,
|
|
List<String> suggestedActions,
|
|
DateTime timestamp,
|
|
) {
|
|
final result = List<ChatListItem>.from(items);
|
|
final index = result.indexWhere(
|
|
(item) => item.id == messageId && item is TextMessageItem,
|
|
);
|
|
|
|
if (index >= 0) {
|
|
final existing = result[index] as TextMessageItem;
|
|
result[index] = existing.copyWith(
|
|
content: content,
|
|
isStreaming: false,
|
|
suggestedActions: suggestedActions,
|
|
);
|
|
return result;
|
|
}
|
|
|
|
result.add(
|
|
TextMessageItem(
|
|
id: messageId,
|
|
content: content,
|
|
timestamp: timestamp,
|
|
sender: MessageSender.ai,
|
|
isStreaming: false,
|
|
suggestedActions: suggestedActions,
|
|
),
|
|
);
|
|
return result;
|
|
}
|
|
|
|
void _handleToolCallResult(ToolCallResultEvent event) {
|
|
if (_shouldRefreshCalendarForTool(event)) {
|
|
unawaited(_refreshCalendarAfterToolMutation());
|
|
}
|
|
final timestamp = DateTime.now();
|
|
final items = List<ChatListItem>.from(state.items);
|
|
|
|
final uiSchema = event.uiSchema;
|
|
if (uiSchema != null) {
|
|
_upsertToolResultUi(items, event.toolCallId, uiSchema, timestamp);
|
|
}
|
|
|
|
emit(state.copyWith(items: items));
|
|
}
|
|
|
|
void _upsertToolResultUi(
|
|
List<ChatListItem> items,
|
|
String toolCallId,
|
|
Map<String, dynamic> uiSchema,
|
|
DateTime timestamp,
|
|
) {
|
|
final uiItemId = '$toolCallId-ui';
|
|
final uiItem = ToolResultItem(
|
|
id: uiItemId,
|
|
callId: toolCallId,
|
|
uiSchema: uiSchema,
|
|
timestamp: timestamp,
|
|
sender: MessageSender.ai,
|
|
);
|
|
final existingIndex = items.indexWhere((item) => item.id == uiItemId);
|
|
if (existingIndex >= 0) {
|
|
items[existingIndex] = uiItem;
|
|
return;
|
|
}
|
|
items.add(uiItem);
|
|
}
|
|
|
|
List<ChatListItem> _convertHistoryMessages(List<HistoryMessage> messages) {
|
|
final converted = <ChatListItem>[];
|
|
for (final msg in messages) {
|
|
final normalizedRole = msg.role.toLowerCase();
|
|
final isUser = normalizedRole == 'user';
|
|
final isTool = normalizedRole == 'tool';
|
|
final sender = isUser ? MessageSender.user : MessageSender.ai;
|
|
final attachments = msg.attachments
|
|
.map(
|
|
(attachment) => <String, dynamic>{
|
|
'url': attachment.url,
|
|
'mimeType': attachment.mimeType,
|
|
},
|
|
)
|
|
.toList();
|
|
|
|
if (!isTool && (msg.content.isNotEmpty || isUser)) {
|
|
converted.add(
|
|
TextMessageItem(
|
|
id: msg.id,
|
|
content: msg.content,
|
|
timestamp: msg.timestamp,
|
|
sender: sender,
|
|
isLocalEcho: false,
|
|
attachments: attachments,
|
|
suggestedActions: msg.suggestedActions,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (isTool && msg.uiSchema != null) {
|
|
converted.add(
|
|
ToolResultItem(
|
|
id: '${msg.id}-ui',
|
|
callId: msg.id,
|
|
uiSchema: msg.uiSchema!,
|
|
timestamp: msg.timestamp,
|
|
sender: MessageSender.ai,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return converted;
|
|
}
|
|
|
|
DateTime? _extractDateFromItems(List<ChatListItem> items) {
|
|
if (items.isEmpty) {
|
|
return null;
|
|
}
|
|
return items
|
|
.map(
|
|
(item) => DateTime(
|
|
item.timestamp.year,
|
|
item.timestamp.month,
|
|
item.timestamp.day,
|
|
),
|
|
)
|
|
.reduce((a, b) => a.isBefore(b) ? a : b);
|
|
}
|
|
}
|