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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user