sendMessage method
- ChatController controller,
- String text, {
- List<
Attachment> ? attachments,
Drives controller's manual streaming API directly, so tool calls
surface as ToolCalls instead of being dropped.
Mirrors what ChatController.send does for plain text: adds a user message, opens a streaming assistant message, and finalizes it on completion or failure.
Note: ChatController.stop cancels its own internal subscription, but
this method drives the controller with a plain await for rather than
through that subscription, so stop() cannot cancel the underlying HTTP
request directly. It still stops new tokens from reaching the UI —
each loop iteration checks whether this message is still the
controller's active stream and returns early once stop() has cleared
it — but the request keeps draining server-side until it completes; its
remaining output is simply discarded.
Implementation
Future<void> sendMessage(
ChatController controller,
String text, {
List<Attachment>? attachments,
}) async {
final userMessage = ChatMessage.user(text, attachments: attachments);
controller.addMessage(userMessage);
// Snapshot before beginAssistantMessage() adds the empty streaming
// placeholder -- that placeholder must not be sent as a conversation turn.
final conversation = controller.messages;
final assistantId = controller.beginAssistantMessage();
try {
await for (final event in streamEvents(conversation)) {
if (controller.streamingMessageId != assistantId) return;
switch (event) {
case TextDelta(text: final chunk):
controller.appendToken(assistantId, chunk);
case ToolCallEvent(:final call):
controller.upsertToolCall(assistantId, call);
}
}
if (controller.streamingMessageId == assistantId) {
controller.completeMessage(assistantId);
}
} catch (error) {
if (controller.streamingMessageId == assistantId) {
controller.failMessage(assistantId, error.toString());
}
}
}