feat(apps): update UI screens and shared components
- Update home screen with new composer and interactions - Update settings screens with new profile flow - Update calendar share dialog - Update contacts screen - Add new shared widgets: confirm_sheet, phone_prefix_selector - Add new formatters: phone_display_formatter - Update tests for modified components
This commit is contained in:
@@ -46,6 +46,8 @@ class AgUiService {
|
||||
EventCallback onEvent;
|
||||
final Map<String, String> _lastEventIdByThread = {};
|
||||
int _activeStreamToken = 0;
|
||||
StreamSubscription<String>? _activeSseSubscription;
|
||||
Completer<void>? _activeSseDoneCompleter;
|
||||
|
||||
String? _threadId;
|
||||
bool _hasMoreHistory = false;
|
||||
@@ -58,6 +60,7 @@ class AgUiService {
|
||||
String content, {
|
||||
List<XFile>? images,
|
||||
}) async {
|
||||
await _cancelActiveSseSubscription();
|
||||
final streamToken = ++_activeStreamToken;
|
||||
final runInputPayload = await _buildRunInput(
|
||||
content: content,
|
||||
@@ -149,6 +152,21 @@ class AgUiService {
|
||||
|
||||
Future<void> cancelCurrentRun() async {
|
||||
_activeStreamToken += 1;
|
||||
await _cancelActiveSseSubscription();
|
||||
}
|
||||
|
||||
Future<void> _cancelActiveSseSubscription() async {
|
||||
final doneCompleter = _activeSseDoneCompleter;
|
||||
if (doneCompleter != null && !doneCompleter.isCompleted) {
|
||||
doneCompleter.complete();
|
||||
}
|
||||
_activeSseDoneCompleter = null;
|
||||
final subscription = _activeSseSubscription;
|
||||
_activeSseSubscription = null;
|
||||
if (subscription == null) {
|
||||
return;
|
||||
}
|
||||
await subscription.cancel();
|
||||
}
|
||||
|
||||
Future<void> _streamEventsFromApi(
|
||||
@@ -170,80 +188,129 @@ class AgUiService {
|
||||
String? eventId;
|
||||
var hasBoundExpectedRun = false;
|
||||
final dataBuffer = StringBuffer();
|
||||
await for (final line in sseLines) {
|
||||
if (streamToken != _activeStreamToken) {
|
||||
break;
|
||||
final done = Completer<void>();
|
||||
late final StreamSubscription<String> subscription;
|
||||
|
||||
void stopStream({Object? error, StackTrace? stackTrace}) {
|
||||
if (!done.isCompleted) {
|
||||
if (error == null) {
|
||||
done.complete();
|
||||
} else {
|
||||
done.completeError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
if (line.isEmpty) {
|
||||
if (dataBuffer.isNotEmpty) {
|
||||
final raw = dataBuffer.toString();
|
||||
dataBuffer.clear();
|
||||
Map<String, dynamic>? decoded;
|
||||
String? eventRunId;
|
||||
String? eventThreadId;
|
||||
try {
|
||||
final parsed = jsonDecode(raw);
|
||||
if (parsed is Map<String, dynamic>) {
|
||||
decoded = parsed;
|
||||
final runId = parsed['runId'];
|
||||
final thread = parsed['threadId'];
|
||||
eventRunId = runId is String ? runId : null;
|
||||
eventThreadId = thread is String ? thread : null;
|
||||
unawaited(subscription.cancel());
|
||||
}
|
||||
|
||||
final isRunStarted = eventType == AgUiEventTypeWire.runStarted;
|
||||
final isTargetRun = eventRunId == expectedRunId;
|
||||
if (isRunStarted && isTargetRun) {
|
||||
hasBoundExpectedRun = true;
|
||||
subscription = sseLines.listen(
|
||||
(line) {
|
||||
try {
|
||||
if (streamToken != _activeStreamToken) {
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
if (line.isEmpty) {
|
||||
if (dataBuffer.isNotEmpty) {
|
||||
final raw = dataBuffer.toString();
|
||||
dataBuffer.clear();
|
||||
String? eventRunId;
|
||||
String? eventThreadId;
|
||||
Map<String, dynamic>? parsedMap;
|
||||
try {
|
||||
final parsed = jsonDecode(raw);
|
||||
if (parsed is Map<String, dynamic>) {
|
||||
parsedMap = parsed;
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore malformed SSE payload and keep stream alive.
|
||||
}
|
||||
if (parsedMap != null) {
|
||||
final runId = parsedMap['runId'];
|
||||
final thread = parsedMap['threadId'];
|
||||
eventRunId = runId is String ? runId : null;
|
||||
eventThreadId = thread is String ? thread : null;
|
||||
|
||||
final isRunStarted = eventType == AgUiEventTypeWire.runStarted;
|
||||
final isTargetRun = eventRunId == expectedRunId;
|
||||
if (isRunStarted && isTargetRun) {
|
||||
hasBoundExpectedRun = true;
|
||||
}
|
||||
|
||||
final isThreadMatched =
|
||||
eventThreadId == null || eventThreadId == threadId;
|
||||
final shouldDispatch =
|
||||
isTargetRun || (hasBoundExpectedRun && isThreadMatched);
|
||||
if (shouldDispatch) {
|
||||
final event = AgUiEvent.fromJson(parsedMap);
|
||||
onEvent(event);
|
||||
}
|
||||
}
|
||||
final currentEventId = eventId;
|
||||
if (currentEventId != null && currentEventId.isNotEmpty) {
|
||||
_lastEventIdByThread[threadId] = currentEventId;
|
||||
}
|
||||
final isTerminalEvent =
|
||||
eventType == AgUiEventTypeWire.runFinished ||
|
||||
eventType == AgUiEventTypeWire.runError;
|
||||
final isTargetRun = eventRunId == expectedRunId;
|
||||
final isThreadMatched =
|
||||
eventThreadId == null || eventThreadId == threadId;
|
||||
final shouldDispatch =
|
||||
isTargetRun || (hasBoundExpectedRun && isThreadMatched);
|
||||
if (shouldDispatch) {
|
||||
final event = AgUiEvent.fromJson(parsed);
|
||||
onEvent(event);
|
||||
if (isTerminalEvent &&
|
||||
(isTargetRun || (hasBoundExpectedRun && isThreadMatched))) {
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore malformed SSE payload and keep stream alive.
|
||||
eventType = null;
|
||||
eventId = null;
|
||||
return;
|
||||
}
|
||||
final currentEventId = eventId;
|
||||
if (currentEventId != null && currentEventId.isNotEmpty) {
|
||||
_lastEventIdByThread[threadId] = currentEventId;
|
||||
if (line.startsWith(':')) {
|
||||
return;
|
||||
}
|
||||
final isTerminalEvent =
|
||||
eventType == AgUiEventTypeWire.runFinished ||
|
||||
eventType == AgUiEventTypeWire.runError;
|
||||
final isTargetRun = eventRunId == expectedRunId;
|
||||
final isThreadMatched =
|
||||
eventThreadId == null || eventThreadId == threadId;
|
||||
if (isTerminalEvent &&
|
||||
(isTargetRun || (hasBoundExpectedRun && isThreadMatched))) {
|
||||
break;
|
||||
if (line.startsWith('id:')) {
|
||||
eventId = line.substring(3).trim();
|
||||
return;
|
||||
}
|
||||
if (line.startsWith('event:')) {
|
||||
eventType = line.substring(6).trim();
|
||||
return;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
final fragment = line.substring(5).trim();
|
||||
if (dataBuffer.isNotEmpty) {
|
||||
dataBuffer.write('\n');
|
||||
}
|
||||
dataBuffer.write(fragment);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
stopStream(error: error, stackTrace: stackTrace);
|
||||
}
|
||||
eventType = null;
|
||||
eventId = null;
|
||||
continue;
|
||||
},
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
stopStream(error: error, stackTrace: stackTrace);
|
||||
},
|
||||
onDone: () {
|
||||
stopStream();
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
if (streamToken != _activeStreamToken) {
|
||||
await subscription.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
_activeSseSubscription = subscription;
|
||||
_activeSseDoneCompleter = done;
|
||||
try {
|
||||
await done.future;
|
||||
} finally {
|
||||
if (identical(_activeSseSubscription, subscription)) {
|
||||
_activeSseSubscription = null;
|
||||
}
|
||||
if (line.startsWith(':')) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('id:')) {
|
||||
eventId = line.substring(3).trim();
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('event:')) {
|
||||
eventType = line.substring(6).trim();
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
final fragment = line.substring(5).trim();
|
||||
if (dataBuffer.isNotEmpty) {
|
||||
dataBuffer.write('\n');
|
||||
}
|
||||
dataBuffer.write(fragment);
|
||||
if (identical(_activeSseDoneCompleter, done)) {
|
||||
_activeSseDoneCompleter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user