a3fe0b4b61
The mobile app was receiving SSE status events but showing the peek text only while the assistant bubble was empty — as soon as the first token arrived, the status line was replaced by streaming content and any tool calls fired mid-turn left no trace. Tool call SSE events were also being dropped by the parser, and Message.fromJson never read the persisted tool_calls array, so chips never rendered after reload either. Parse tool_call SSE frames into a new ChatToolCall event, carry tool calls on Message, and update the chat and briefing streaming loops to append chips to the in-flight assistant message as they arrive. Rework ChatMessageBubble to show a chip row + rolling peek status line above any streamed text, matching the web ToolCallCard/status indicator behaviour across chat, briefing, and briefing history surfaces. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
68 lines
2.0 KiB
Dart
68 lines
2.0 KiB
Dart
enum MessageRole { user, assistant }
|
|
|
|
class Message {
|
|
final int? id;
|
|
final int conversationId;
|
|
final MessageRole role;
|
|
final String content;
|
|
final String status; // "complete" | "generating"
|
|
final DateTime? createdAt;
|
|
final Map<String, dynamic>? metadata;
|
|
// Tool invocations attached to this message. Each entry matches the shape
|
|
// persisted by the backend (`function`, `arguments`, `result`, `status`) so
|
|
// the UI can render the same chips whether they arrive live over SSE or
|
|
// from a reload.
|
|
final List<Map<String, dynamic>>? toolCalls;
|
|
|
|
const Message({
|
|
this.id,
|
|
required this.conversationId,
|
|
required this.role,
|
|
required this.content,
|
|
this.status = 'complete',
|
|
this.createdAt,
|
|
this.metadata,
|
|
this.toolCalls,
|
|
});
|
|
|
|
factory Message.fromJson(Map<String, dynamic> json) {
|
|
final rawCalls = json['tool_calls'];
|
|
List<Map<String, dynamic>>? parsedCalls;
|
|
if (rawCalls is List) {
|
|
parsedCalls = [
|
|
for (final tc in rawCalls)
|
|
if (tc is Map<String, dynamic>) tc,
|
|
];
|
|
if (parsedCalls.isEmpty) parsedCalls = null;
|
|
}
|
|
return Message(
|
|
id: json['id'] as int?,
|
|
conversationId: json['conversation_id'] as int,
|
|
role: json['role'] == 'user' ? MessageRole.user : MessageRole.assistant,
|
|
content: json['content'] as String,
|
|
status: json['status'] as String? ?? 'complete',
|
|
createdAt: json['created_at'] != null
|
|
? DateTime.parse(json['created_at'] as String)
|
|
: null,
|
|
metadata: json['metadata'] as Map<String, dynamic>?,
|
|
toolCalls: parsedCalls,
|
|
);
|
|
}
|
|
|
|
Message copyWith({
|
|
String? content,
|
|
String? status,
|
|
List<Map<String, dynamic>>? toolCalls,
|
|
}) =>
|
|
Message(
|
|
id: id,
|
|
conversationId: conversationId,
|
|
role: role,
|
|
content: content ?? this.content,
|
|
status: status ?? this.status,
|
|
createdAt: createdAt,
|
|
metadata: metadata,
|
|
toolCalls: toolCalls ?? this.toolCalls,
|
|
);
|
|
}
|