refactor: clean CLI taxonomy — canonical subcommands, merged memory.update, no aliases
- 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
This commit is contained in:
@@ -10,11 +10,7 @@ String agUiEventLabel(AgUiEventType type) {
|
||||
AgUiEventType.stepStarted => l10n.agUiEventStepStarted,
|
||||
AgUiEventType.stepFinished => l10n.agUiEventStepFinished,
|
||||
AgUiEventType.textMessageEnd => l10n.agUiEventTextMessageEnd,
|
||||
AgUiEventType.toolCallStart => l10n.agUiEventToolCallStart,
|
||||
AgUiEventType.toolCallArgs => l10n.agUiEventToolCallArgs,
|
||||
AgUiEventType.toolCallEnd => l10n.agUiEventToolCallEnd,
|
||||
AgUiEventType.toolCallResult => l10n.agUiEventToolCallResult,
|
||||
AgUiEventType.toolCallError => l10n.agUiEventToolCallError,
|
||||
AgUiEventType.unknown => l10n.agUiEventUnknown,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,7 +257,17 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
|
||||
bool _shouldRefreshCalendarForTool(ToolCallResultEvent event) {
|
||||
final name = event.toolName.trim().toLowerCase();
|
||||
final status = event.status.trim().toLowerCase();
|
||||
if (name != 'calendar_write') {
|
||||
if (name != 'project_cli') {
|
||||
return false;
|
||||
}
|
||||
final args = event.toolCallArgs;
|
||||
if (args == null) {
|
||||
return false;
|
||||
}
|
||||
final command = (args['command'] as String?)?.trim().toLowerCase();
|
||||
final subcommand = (args['subcommand'] as String?)?.trim().toLowerCase();
|
||||
const mutationSubcommands = {'create', 'update', 'delete'};
|
||||
if (command != 'calendar' || !mutationSubcommands.contains(subcommand)) {
|
||||
return false;
|
||||
}
|
||||
return status == 'success' || status == 'partial';
|
||||
|
||||
@@ -24,24 +24,13 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
case AgUiEventType.runFinished:
|
||||
_trackChatCompleted();
|
||||
_clearRunMetrics();
|
||||
emit(
|
||||
_resetRunState().copyWith(items: _removeToolCallItems(state.items)),
|
||||
);
|
||||
emit(_resetRunState());
|
||||
case AgUiEventType.runError:
|
||||
final errorEvent = event as RunErrorEvent;
|
||||
_clearRunMetrics();
|
||||
final isCanceledByUser = errorEvent.code == 'RUN_CANCELED';
|
||||
emit(
|
||||
_resetRunState(
|
||||
error: isCanceledByUser ? null : errorEvent.message,
|
||||
).copyWith(
|
||||
items: _markActiveToolCallsFailed(
|
||||
state.items,
|
||||
reason: isCanceledByUser
|
||||
? L10n.current.chatRunCanceled
|
||||
: L10n.current.chatRunFailed,
|
||||
),
|
||||
),
|
||||
_resetRunState(error: isCanceledByUser ? null : errorEvent.message),
|
||||
);
|
||||
case AgUiEventType.stepStarted:
|
||||
_handleStepStarted(event as StepStartedEvent);
|
||||
@@ -49,12 +38,6 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
_handleStepFinished(event as StepFinishedEvent);
|
||||
case AgUiEventType.textMessageEnd:
|
||||
_handleTextMessageEnd(event as TextMessageEndEvent);
|
||||
case AgUiEventType.toolCallStart:
|
||||
_handleToolCallStart(event as ToolCallStartEvent);
|
||||
case AgUiEventType.toolCallArgs:
|
||||
_handleToolCallArgs(event as ToolCallArgsEvent);
|
||||
case AgUiEventType.toolCallEnd:
|
||||
_handleToolCallEnd(event as ToolCallEndEvent);
|
||||
case AgUiEventType.toolCallResult:
|
||||
_handleToolCallResult(event as ToolCallResultEvent);
|
||||
case AgUiEventType.unknown:
|
||||
@@ -84,12 +67,13 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
state.items,
|
||||
event.messageId,
|
||||
event.answer,
|
||||
event.suggestedActions,
|
||||
timestamp,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: _removeToolCallItems(items),
|
||||
items: items,
|
||||
currentMessageId: null,
|
||||
isWaitingFirstToken: false,
|
||||
isStreaming: false,
|
||||
@@ -101,6 +85,7 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
List<ChatListItem> items,
|
||||
String messageId,
|
||||
String content,
|
||||
List<String> suggestedActions,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final result = List<ChatListItem>.from(items);
|
||||
@@ -110,7 +95,11 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
|
||||
if (index >= 0) {
|
||||
final existing = result[index] as TextMessageItem;
|
||||
result[index] = existing.copyWith(content: content, isStreaming: false);
|
||||
result[index] = existing.copyWith(
|
||||
content: content,
|
||||
isStreaming: false,
|
||||
suggestedActions: suggestedActions,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -121,73 +110,18 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
timestamp: timestamp,
|
||||
sender: MessageSender.ai,
|
||||
isStreaming: false,
|
||||
suggestedActions: suggestedActions,
|
||||
),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _handleToolCallStart(ToolCallStartEvent event) {
|
||||
final exists = state.items.any(
|
||||
(item) => item is ToolCallItem && item.id == event.toolCallId,
|
||||
);
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: [
|
||||
...state.items,
|
||||
ToolCallItem(
|
||||
id: event.toolCallId,
|
||||
callId: event.toolCallId,
|
||||
toolName: event.toolCallName,
|
||||
args: const {},
|
||||
status: ToolCallStatus.pending,
|
||||
timestamp: DateTime.now(),
|
||||
sender: MessageSender.ai,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleToolCallArgs(ToolCallArgsEvent event) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: state.items.map((item) {
|
||||
if (item is ToolCallItem && item.id == event.toolCallId) {
|
||||
return item.copyWith(args: event.args);
|
||||
}
|
||||
return item;
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleToolCallEnd(ToolCallEndEvent event) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: state.items.map((item) {
|
||||
if (item is ToolCallItem && item.id == event.toolCallId) {
|
||||
return item.copyWith(status: ToolCallStatus.executing);
|
||||
}
|
||||
return item;
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleToolCallResult(ToolCallResultEvent event) {
|
||||
if (_shouldRefreshCalendarForTool(event)) {
|
||||
unawaited(_refreshCalendarAfterToolMutation());
|
||||
}
|
||||
final timestamp = DateTime.now();
|
||||
final items = state.items.map((item) {
|
||||
if (item is ToolCallItem && item.id == event.toolCallId) {
|
||||
return item.copyWith(status: ToolCallStatus.completed);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
final items = List<ChatListItem>.from(state.items);
|
||||
|
||||
final uiSchema = event.uiSchema;
|
||||
if (uiSchema != null) {
|
||||
@@ -219,30 +153,12 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
items.add(uiItem);
|
||||
}
|
||||
|
||||
List<ChatListItem> _removeToolCallItems(List<ChatListItem> items) {
|
||||
return items.where((item) => item is! ToolCallItem).toList();
|
||||
}
|
||||
|
||||
List<ChatListItem> _markActiveToolCallsFailed(
|
||||
List<ChatListItem> items, {
|
||||
required String reason,
|
||||
}) {
|
||||
return items.map((item) {
|
||||
if (item is! ToolCallItem ||
|
||||
item.status == ToolCallStatus.error ||
|
||||
item.status == ToolCallStatus.completed) {
|
||||
return item;
|
||||
}
|
||||
return item.copyWith(status: ToolCallStatus.error, errorMessage: reason);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
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' || normalizedRole == 'tools';
|
||||
final isTool = normalizedRole == 'tool';
|
||||
final sender = isUser ? MessageSender.user : MessageSender.ai;
|
||||
final attachments = msg.attachments
|
||||
.map(
|
||||
@@ -262,11 +178,12 @@ extension _ChatBlocEvents on ChatBloc {
|
||||
sender: sender,
|
||||
isLocalEcho: false,
|
||||
attachments: attachments,
|
||||
suggestedActions: msg.suggestedActions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isTool && msg.uiSchema != null) {
|
||||
if (isTool && msg.uiSchema != null) {
|
||||
converted.add(
|
||||
ToolResultItem(
|
||||
id: '${msg.id}-ui',
|
||||
|
||||
@@ -375,7 +375,15 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: _itemSpacing,
|
||||
),
|
||||
child: HomeChatItemRenderer.build(context, item),
|
||||
child: HomeChatItemRenderer.build(
|
||||
context,
|
||||
item,
|
||||
onSuggestedActionTap: (suggestion) =>
|
||||
_sendMessage(
|
||||
context,
|
||||
overrideContent: suggestion,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:social_app/core/chat/chat_list_item.dart';
|
||||
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../core/utils/tool_name_localizer.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/ui_schema/ui_schema_renderer.dart';
|
||||
|
||||
@@ -21,18 +19,28 @@ const _toolResultWidthFactor = 0.88;
|
||||
const _iconSize = AppSpacing.xxl;
|
||||
|
||||
class HomeChatItemRenderer {
|
||||
static Widget build(BuildContext context, ChatListItem item) {
|
||||
static Widget build(
|
||||
BuildContext context,
|
||||
ChatListItem item, {
|
||||
ValueChanged<String>? onSuggestedActionTap,
|
||||
}) {
|
||||
switch (item.type) {
|
||||
case ChatItemType.message:
|
||||
return _buildMessageItem(context, item as TextMessageItem);
|
||||
case ChatItemType.toolCall:
|
||||
return _buildToolCallItem(context, item as ToolCallItem);
|
||||
return _buildMessageItem(
|
||||
context,
|
||||
item as TextMessageItem,
|
||||
onSuggestedActionTap: onSuggestedActionTap,
|
||||
);
|
||||
case ChatItemType.toolResult:
|
||||
return _buildToolResultItem(context, item as ToolResultItem);
|
||||
}
|
||||
}
|
||||
|
||||
static Widget _buildMessageItem(BuildContext context, TextMessageItem item) {
|
||||
static Widget _buildMessageItem(
|
||||
BuildContext context,
|
||||
TextMessageItem item, {
|
||||
ValueChanged<String>? onSuggestedActionTap,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isUser = item.sender == MessageSender.user;
|
||||
final maxMessageWidth =
|
||||
@@ -41,6 +49,11 @@ class HomeChatItemRenderer {
|
||||
item.attachments,
|
||||
);
|
||||
final hasRenderableAttachments = imageAttachments.isNotEmpty;
|
||||
final suggestedActionTap = onSuggestedActionTap;
|
||||
final shouldRenderSuggestions =
|
||||
!isUser &&
|
||||
item.suggestedActions.isNotEmpty &&
|
||||
suggestedActionTap != null;
|
||||
return Column(
|
||||
crossAxisAlignment: isUser
|
||||
? CrossAxisAlignment.end
|
||||
@@ -98,6 +111,29 @@ class HomeChatItemRenderer {
|
||||
imageAttachments: imageAttachments,
|
||||
),
|
||||
),
|
||||
if (shouldRenderSuggestions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.xs),
|
||||
child: Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.xs,
|
||||
children: item.suggestedActions
|
||||
.map(
|
||||
(action) => GestureDetector(
|
||||
onTap: () => suggestedActionTap(action),
|
||||
child: Text(
|
||||
action,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -221,83 +257,6 @@ class HomeChatItemRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildToolCallItem(BuildContext context, ToolCallItem item) {
|
||||
final l10n = context.l10n;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final (statusText, statusColor, statusIcon) = switch (item.status) {
|
||||
ToolCallStatus.pending => (
|
||||
l10n.homeToolPreparing,
|
||||
colorScheme.onSurfaceVariant,
|
||||
LucideIcons.clock,
|
||||
),
|
||||
ToolCallStatus.executing => (
|
||||
l10n.homeToolExecuting,
|
||||
colorScheme.primary,
|
||||
LucideIcons.loader,
|
||||
),
|
||||
ToolCallStatus.error => (
|
||||
item.errorMessage ?? l10n.homeToolExecutionFailed,
|
||||
colorScheme.error,
|
||||
LucideIcons.alertCircle,
|
||||
),
|
||||
ToolCallStatus.completed => (
|
||||
l10n.homeToolCompleted,
|
||||
colorScheme.tertiary,
|
||||
LucideIcons.checkCircle,
|
||||
),
|
||||
};
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Icon(statusIcon, size: 14, color: statusColor),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
localizeToolName(item.toolName),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildToolResultItem(
|
||||
BuildContext context,
|
||||
ToolResultItem item,
|
||||
|
||||
@@ -45,7 +45,7 @@ class _JobDetailScreenState extends State<JobDetailScreen> {
|
||||
String _contextSource = 'latest_chat';
|
||||
String _contextWindowMode = 'day';
|
||||
int _contextWindowCount = 2;
|
||||
final Set<String> _selectedTools = <String>{'memory.write', 'memory.forget'};
|
||||
final Set<String> _selectedTools = <String>{'memory.update'};
|
||||
|
||||
ColorScheme get _colorScheme => Theme.of(context).colorScheme;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user