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:
qzl
2026-04-23 12:12:41 +08:00
parent 91077a933d
commit 19e273a9e6
48 changed files with 1578 additions and 811 deletions
@@ -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;