send method
- String text, {
- SendOptions? options,
- List<
TextAttachment> attachments = const [],
Sends text and streams the reply into messages.
options and attachments are per-turn, mirroring
ChatGptSession.send/ChatGptClient.sendWithRotation directly.
Precedence between options and this controller's own model /
webSearch: options, when supplied, is used exactly as given —
it is never merged with the controller's settings. If options is
omitted (the default, and the only form that existed before this
parameter was added), this turn uses currentOptions — built fresh
from the controller's current model and webSearch at the moment
send is called. There is no field-by-field fallback in either
direction: a caller who passes SendOptions(model: 'gpt-5-6') gets
webSearch: null for that turn (SendOptions' own default) even if
this.webSearch is true — not the controller's value quietly
filled in. This keeps the rule predictable from the call site alone:
pass options and it is the whole story for that turn; omit it and
the controller's current settings are the whole story.
Do not build a per-turn options override from a bare SendOptions(...)
literal — that drops model back to SendOptions' own 'auto'
default, invisibly, because verbatim precedence means nothing fills
it back in. Extend currentOptions instead:
controller.send(text, options: controller.currentOptions.copyWith(canvas: true)).
See currentOptions's doc comment for why this matters.
Implementation
Future<void> send(
String text, {
SendOptions? options,
List<TextAttachment> attachments = const [],
}) async {
if (_isStreaming || text.trim().isEmpty) return;
_lastPrompt = text;
_error = null;
_isStreaming = true;
_isWritingReply = true;
notifyListeners();
final started = DateTime.now();
void log(String what) {
final ms = DateTime.now().difference(started).inMilliseconds;
onLog?.call('[chatgpt_free] +${ms}ms $what');
}
log('turn started');
var firstDelta = true;
final completer = Completer<void>();
_pendingSend = completer;
_subscription = _client
.sendWithRotation(
_session,
text,
options: options ?? currentOptions,
attachments: attachments,
)
.listen(
(event) {
if (event is TextDelta && firstDelta) {
firstDelta = false;
log('first text');
}
if (event is ReplyCompleted) {
_isWritingReply = false;
log('reply complete — indicator off, stream still open');
}
if (event is TurnCompleted) {
log('turn closed (model: ${event.actualModel})');
}
if (event is ConversationTitled) {
log('titled "${event.title}"');
}
if (event is ModelDowngraded) {
_downgradeNotice =
'Requested ${event.requested}, answered by ${event.actual}.';
}
_messages = _session.history;
notifyListeners();
},
onError: (Object e) {
_error = e is ChatGptException ? e : TransportException('$e');
_isStreaming = false;
_messages = _session.history;
notifyListeners();
_completeSend();
},
onDone: () {
_isStreaming = false;
_messages = _session.history;
notifyListeners();
_completeSend();
},
cancelOnError: true,
);
return completer.future;
}