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
+2 -3
View File
@@ -52,10 +52,9 @@ android {
buildTypes {
release {
if (!keystorePropertiesFile.exists()) {
throw GradleException("Missing apps/android/key.properties for release signing")
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
+23 -56
View File
@@ -5,9 +5,6 @@ class AgUiEventTypeWire {
static const stepStarted = 'STEP_STARTED';
static const stepFinished = 'STEP_FINISHED';
static const textMessageEnd = 'TEXT_MESSAGE_END';
static const toolCallStart = 'TOOL_CALL_START';
static const toolCallArgs = 'TOOL_CALL_ARGS';
static const toolCallEnd = 'TOOL_CALL_END';
static const toolCallResult = 'TOOL_CALL_RESULT';
}
@@ -18,9 +15,6 @@ enum AgUiEventType {
stepStarted,
stepFinished,
textMessageEnd,
toolCallStart,
toolCallArgs,
toolCallEnd,
toolCallResult,
unknown,
}
@@ -32,9 +26,6 @@ const _wireToTypeMap = {
AgUiEventTypeWire.stepStarted: AgUiEventType.stepStarted,
AgUiEventTypeWire.stepFinished: AgUiEventType.stepFinished,
AgUiEventTypeWire.textMessageEnd: AgUiEventType.textMessageEnd,
AgUiEventTypeWire.toolCallStart: AgUiEventType.toolCallStart,
AgUiEventTypeWire.toolCallArgs: AgUiEventType.toolCallArgs,
AgUiEventTypeWire.toolCallEnd: AgUiEventType.toolCallEnd,
AgUiEventTypeWire.toolCallResult: AgUiEventType.toolCallResult,
};
@@ -45,9 +36,6 @@ const _typeToWireMap = {
AgUiEventType.stepStarted: AgUiEventTypeWire.stepStarted,
AgUiEventType.stepFinished: AgUiEventTypeWire.stepFinished,
AgUiEventType.textMessageEnd: AgUiEventTypeWire.textMessageEnd,
AgUiEventType.toolCallStart: AgUiEventTypeWire.toolCallStart,
AgUiEventType.toolCallArgs: AgUiEventTypeWire.toolCallArgs,
AgUiEventType.toolCallEnd: AgUiEventTypeWire.toolCallEnd,
AgUiEventType.toolCallResult: AgUiEventTypeWire.toolCallResult,
AgUiEventType.unknown: '',
};
@@ -74,9 +62,6 @@ abstract class AgUiEvent {
AgUiEventType.stepStarted => StepStartedEvent.fromJson(json),
AgUiEventType.stepFinished => StepFinishedEvent.fromJson(json),
AgUiEventType.textMessageEnd => TextMessageEndEvent.fromJson(json),
AgUiEventType.toolCallStart => ToolCallStartEvent.fromJson(json),
AgUiEventType.toolCallArgs => ToolCallArgsEvent.fromJson(json),
AgUiEventType.toolCallEnd => ToolCallEndEvent.fromJson(json),
AgUiEventType.toolCallResult => ToolCallResultEvent.fromJson(json),
AgUiEventType.unknown => UnknownAgUiEvent(rawJson: json),
};
@@ -157,12 +142,14 @@ class TextMessageEndEvent extends AgUiEvent {
required this.answer,
required this.role,
required this.status,
this.suggestedActions = const <String>[],
}) : super(type: AgUiEventType.textMessageEnd);
final String messageId;
final String answer;
final String role;
final String status;
final List<String> suggestedActions;
factory TextMessageEndEvent.fromJson(Map<String, dynamic> json) =>
TextMessageEndEvent(
@@ -170,53 +157,17 @@ class TextMessageEndEvent extends AgUiEvent {
answer: _asString(json['answer']),
role: _asString(json['role'], fallback: 'assistant'),
status: _asString(json['status'], fallback: 'success'),
suggestedActions: _asStringList(json['suggested_actions']),
);
}
class ToolCallStartEvent extends AgUiEvent {
ToolCallStartEvent({required this.toolCallId, required this.toolCallName})
: super(type: AgUiEventType.toolCallStart);
final String toolCallId;
final String toolCallName;
factory ToolCallStartEvent.fromJson(Map<String, dynamic> json) =>
ToolCallStartEvent(
toolCallId: _asString(json['toolCallId']),
toolCallName: _asString(json['toolCallName']),
);
}
class ToolCallArgsEvent extends AgUiEvent {
ToolCallArgsEvent({required this.toolCallId, required this.args})
: super(type: AgUiEventType.toolCallArgs);
final String toolCallId;
final Map<String, dynamic> args;
factory ToolCallArgsEvent.fromJson(Map<String, dynamic> json) =>
ToolCallArgsEvent(
toolCallId: _asString(json['toolCallId']),
args: _asMap(json['args']) ?? const {},
);
}
class ToolCallEndEvent extends AgUiEvent {
ToolCallEndEvent({required this.toolCallId})
: super(type: AgUiEventType.toolCallEnd);
final String toolCallId;
factory ToolCallEndEvent.fromJson(Map<String, dynamic> json) =>
ToolCallEndEvent(toolCallId: _asString(json['toolCallId']));
}
class ToolCallResultEvent extends AgUiEvent {
ToolCallResultEvent({
required this.messageId,
required this.toolCallId,
required this.toolName,
required this.resultSummary,
this.toolCallArgs,
this.result,
required this.status,
required this.uiSchema,
}) : super(type: AgUiEventType.toolCallResult);
@@ -224,7 +175,8 @@ class ToolCallResultEvent extends AgUiEvent {
final String messageId;
final String toolCallId;
final String toolName;
final String resultSummary;
final Map<String, dynamic>? toolCallArgs;
final Object? result;
final String status;
final Map<String, dynamic>? uiSchema;
@@ -233,7 +185,8 @@ class ToolCallResultEvent extends AgUiEvent {
messageId: _asString(json['messageId']),
toolCallId: _asString(json['tool_call_id']),
toolName: _asString(json['tool_name']),
resultSummary: _asString(json['result']),
toolCallArgs: _asMap(json['tool_call_args']),
result: json['result'],
status: _asString(json['status'], fallback: 'success'),
uiSchema: _asMap(json['ui_schema']),
);
@@ -280,6 +233,7 @@ class HistoryMessage {
required this.content,
required this.timestamp,
this.attachments = const <HistoryAttachment>[],
this.suggestedActions = const <String>[],
this.uiSchema,
});
@@ -289,6 +243,7 @@ class HistoryMessage {
final String content;
final DateTime timestamp;
final List<HistoryAttachment> attachments;
final List<String> suggestedActions;
final Map<String, dynamic>? uiSchema;
factory HistoryMessage.fromJson(Map<String, dynamic> json) => HistoryMessage(
@@ -298,6 +253,7 @@ class HistoryMessage {
content: _asString(json['content']),
timestamp: _parseTimestamp(_asString(json['timestamp'])),
attachments: _parseHistoryAttachments(json['attachments']),
suggestedActions: _asStringList(json['suggestedActions']),
uiSchema: _asMap(json['ui_schema']),
);
}
@@ -374,3 +330,14 @@ List<HistoryAttachment> _parseHistoryAttachments(Object? value) {
)
.toList();
}
List<String> _asStringList(Object? value) {
if (value is! List) {
return const <String>[];
}
return value
.whereType<String>()
.map((item) => item.trim())
.where((item) => item.isNotEmpty)
.toList();
}
@@ -91,6 +91,7 @@ class ChatHistoryRepository extends CachedRepository<HistorySnapshot> {
},
)
.toList(growable: false),
'suggestedActions': message.suggestedActions,
'ui_schema': message.uiSchema,
};
}
+5 -51
View File
@@ -1,9 +1,7 @@
enum ChatItemType { message, toolCall, toolResult }
enum ChatItemType { message, toolResult }
enum MessageSender { user, ai }
enum ToolCallStatus { pending, executing, completed, error }
abstract class ChatListItem {
String get id;
DateTime get timestamp;
@@ -22,6 +20,7 @@ class TextMessageItem extends ChatListItem {
final bool isStreaming;
final bool isLocalEcho;
final List<Map<String, dynamic>> attachments;
final List<String> suggestedActions;
TextMessageItem({
required this.id,
@@ -31,6 +30,7 @@ class TextMessageItem extends ChatListItem {
this.isStreaming = false,
this.isLocalEcho = false,
this.attachments = const [],
this.suggestedActions = const [],
});
@override
@@ -44,6 +44,7 @@ class TextMessageItem extends ChatListItem {
bool? isStreaming,
bool? isLocalEcho,
List<Map<String, dynamic>>? attachments,
List<String>? suggestedActions,
}) => TextMessageItem(
id: id ?? this.id,
content: content ?? this.content,
@@ -52,54 +53,7 @@ class TextMessageItem extends ChatListItem {
isStreaming: isStreaming ?? this.isStreaming,
isLocalEcho: isLocalEcho ?? this.isLocalEcho,
attachments: attachments ?? this.attachments,
);
}
class ToolCallItem extends ChatListItem {
@override
final String id;
final String callId;
final String toolName;
final Map<String, dynamic> args;
final ToolCallStatus status;
final String? errorMessage;
@override
final DateTime timestamp;
@override
final MessageSender sender;
ToolCallItem({
required this.id,
required this.callId,
required this.toolName,
required this.args,
required this.status,
this.errorMessage,
required this.timestamp,
required this.sender,
});
@override
ChatItemType get type => ChatItemType.toolCall;
ToolCallItem copyWith({
String? id,
String? callId,
String? toolName,
Map<String, dynamic>? args,
ToolCallStatus? status,
String? errorMessage,
DateTime? timestamp,
MessageSender? sender,
}) => ToolCallItem(
id: id ?? this.id,
callId: callId ?? this.callId,
toolName: toolName ?? this.toolName,
args: args ?? this.args,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
timestamp: timestamp ?? this.timestamp,
sender: sender ?? this.sender,
suggestedActions: suggestedActions ?? this.suggestedActions,
);
}
+13 -20
View File
@@ -1,21 +1,13 @@
import '../l10n/l10n.dart';
const Map<String, String> _toolNameAliases = {
'calendar_read': 'calendar.read',
'calendar_write': 'calendar.write',
'calendar_share': 'calendar.share',
'user_lookup': 'user.lookup',
'memory_write': 'memory.write',
'memory_forget': 'memory.forget',
};
const List<String> automationToolOptions = [
'calendar.create',
'calendar.read',
'calendar.write',
'calendar.update',
'calendar.delete',
'calendar.share',
'user.lookup',
'memory.write',
'memory.forget',
'contacts.read',
'memory.update',
];
String localizeToolName(String rawName) {
@@ -23,20 +15,21 @@ String localizeToolName(String rawName) {
if (normalized.isEmpty) {
return rawName;
}
final canonical = _toolNameAliases[normalized] ?? normalized;
switch (canonical) {
switch (normalized) {
case 'calendar.create':
return L10n.current.toolCalendarWrite;
case 'calendar.read':
return L10n.current.toolCalendarRead;
case 'calendar.write':
case 'calendar.update':
return L10n.current.toolCalendarWrite;
case 'calendar.delete':
return L10n.current.toolCalendarWrite;
case 'calendar.share':
return L10n.current.toolCalendarShare;
case 'user.lookup':
case 'contacts.read':
return L10n.current.toolUserLookup;
case 'memory.write':
case 'memory.update':
return L10n.current.toolMemoryWrite;
case 'memory.forget':
return L10n.current.toolMemoryForget;
default:
return rawName;
}
@@ -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;
+14
View File
@@ -20,4 +20,18 @@ void main() {
expect(message.timestamp.isUtc, isFalse);
expect(message.timestamp, expected);
});
test('history message parses suggested actions', () {
final raw = <String, dynamic>{
'id': 'm2',
'seq': 2,
'role': 'assistant',
'content': 'done',
'suggestedActions': const ['查看日程', '创建会议'],
'timestamp': '2026-03-29T16:06:27.870001+00:00',
};
final message = HistoryMessage.fromJson(raw);
expect(message.suggestedActions, ['查看日程', '创建会议']);
});
}
@@ -234,7 +234,7 @@ void main() {
});
test(
'tool calendar_write success triggers calendar refresh callback',
'tool calendar_create success triggers calendar refresh callback',
() async {
final service = _FakeAgUiService();
var refreshCalls = 0;
@@ -250,8 +250,9 @@ void main() {
ToolCallResultEvent(
messageId: 'msg-1',
toolCallId: 'call-1',
toolName: 'calendar_write',
resultSummary: 'ok',
toolName: 'project_cli',
toolCallArgs: const {'command': 'calendar', 'subcommand': 'create'},
result: const {'ok': true},
status: 'success',
uiSchema: null,
),