From 98393e6ddc0271d9b7066e9e8c796f8ac6cb0dd1 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 17:18:43 +0300 Subject: [PATCH 01/13] feat: proto update with DeleteSession, SubagentService, NotificationService - Update proto submodule to latest (DeleteSession RPC, command registry group/displayName/sessionId fields, SubagentService, NotificationService) - Regenerate all Dart protobuf code - Implement deleteSession() in SessionsNotifier with cache cleanup - Add ConfirmDeleteDialog widget with destructive styling - Wire delete flow in SessionsScreen with confirmation - Add deleteSession tests - Set default model in StartConversation to prevent relay "default" fallback - Update command palette for new proto fields - Add notification service plan doc Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-02-17-notification-service.md | 233 ++ .../notifiers/commands_providers.dart | 15 +- .../notifiers/conversation_notifier.dart | 43 +- .../conversation/widgets/command_palette.dart | 9 +- .../sessions/notifiers/sessions_notifier.dart | 20 + .../widgets/confirm_delete_dialog.dart | 42 + lib/features/sessions/widgets/widgets.dart | 3 + lib/generated/betcode/v1/agent.pb.dart | 109 + lib/generated/betcode/v1/agent.pbgrpc.dart | 31 + lib/generated/betcode/v1/agent.pbjson.dart | 25 + lib/generated/betcode/v1/notification.pb.dart | 271 ++ .../betcode/v1/notification.pbenum.dart | 40 + .../betcode/v1/notification.pbgrpc.dart | 108 + .../betcode/v1/notification.pbjson.dart | 94 + lib/generated/betcode/v1/subagent.pb.dart | 2730 +++++++++++++++++ lib/generated/betcode/v1/subagent.pbenum.dart | 81 + lib/generated/betcode/v1/subagent.pbgrpc.dart | 298 ++ lib/generated/betcode/v1/subagent.pbjson.dart | 785 +++++ proto | 2 +- .../notifiers/commands_notifier_test.dart | 36 +- .../widgets/command_palette_test.dart | 5 +- .../notifiers/sessions_notifier_test.dart | 43 + 22 files changed, 4972 insertions(+), 51 deletions(-) create mode 100644 docs/plans/2026-02-17-notification-service.md create mode 100644 lib/features/sessions/widgets/confirm_delete_dialog.dart create mode 100644 lib/features/sessions/widgets/widgets.dart create mode 100644 lib/generated/betcode/v1/notification.pb.dart create mode 100644 lib/generated/betcode/v1/notification.pbenum.dart create mode 100644 lib/generated/betcode/v1/notification.pbgrpc.dart create mode 100644 lib/generated/betcode/v1/notification.pbjson.dart create mode 100644 lib/generated/betcode/v1/subagent.pb.dart create mode 100644 lib/generated/betcode/v1/subagent.pbenum.dart create mode 100644 lib/generated/betcode/v1/subagent.pbgrpc.dart create mode 100644 lib/generated/betcode/v1/subagent.pbjson.dart diff --git a/docs/plans/2026-02-17-notification-service.md b/docs/plans/2026-02-17-notification-service.md new file mode 100644 index 0000000..72af8cf --- /dev/null +++ b/docs/plans/2026-02-17-notification-service.md @@ -0,0 +1,233 @@ +# Notification Service — Push Notification Device Registration + +**Date**: 2026-02-17 +**Status**: Draft + +## Overview + +Push notification device registration for mobile clients. The app registers FCM (Android) / APNs (iOS) device tokens with the betcode-daemon via the `NotificationService` gRPC API so the daemon can push permission requests, task completions, and session status changes to idle devices. + +## Proto API + +Defined in `proto/betcode/v1/notification.proto`: + +```protobuf +service NotificationService { + rpc RegisterDevice(RegisterDeviceRequest) returns (RegisterDeviceResponse); + rpc UnregisterDevice(UnregisterDeviceRequest) returns (UnregisterDeviceResponse); +} + +message RegisterDeviceRequest { + string device_token = 1; // FCM/APNs token + DevicePlatform platform = 2; // ANDROID or IOS + string user_id = 3; // Authenticated user ID +} + +enum DevicePlatform { + DEVICE_PLATFORM_UNSPECIFIED = 0; + DEVICE_PLATFORM_ANDROID = 1; + DEVICE_PLATFORM_IOS = 2; +} + +message RegisterDeviceResponse { bool success = 1; } +message UnregisterDeviceRequest { string device_token = 1; } +message UnregisterDeviceResponse { bool success = 1; } +``` + +## Dependencies + +### New packages + +| Package | Purpose | +|---------|---------| +| `firebase_messaging` | FCM token acquisition, push receipt, token refresh stream | +| `firebase_core` | Firebase initialization (required by `firebase_messaging`) | + +### Platform setup + +- **Android**: Add `google-services.json` to `android/app/`, apply `com.google.gms:google-services` plugin in `android/app/build.gradle`. +- **iOS**: Add APNs capability in Xcode, upload APNs auth key to Firebase console. `firebase_messaging` handles the APNs-to-FCM token bridging automatically. + +## Architecture + +### File layout + +``` +lib/features/notifications/ + notification_notifier.dart # NotificationNotifier (registration state machine) + notification_providers.dart # Riverpod providers + notifications.dart # Barrel export +``` + +### Providers + +Add to `lib/core/grpc/service_providers.dart`: + +```dart +/// Provides the [NotificationServiceClient] for device registration. +final notificationServiceProvider = Provider((ref) { + final manager = ref.watch(grpcClientManagerProvider); + return NotificationServiceClient( + manager.channel, + interceptors: manager.interceptors, + ); +}); +``` + +This follows the exact pattern used by all other service providers (e.g. `agentServiceProvider`, `machineServiceProvider`). + +### NotificationNotifier + +A `Notifier` managing the device registration lifecycle: + +```dart +@freezed +sealed class NotificationState with _$NotificationState { + const factory NotificationState.unregistered() = _Unregistered; + const factory NotificationState.registering() = _Registering; + const factory NotificationState.registered({required String deviceToken}) = _Registered; + const factory NotificationState.error(String message) = _Error; +} +``` + +Key methods: + +- `register(String userId)` -- Requests FCM token, sends `RegisterDevice` RPC, stores token in secure storage, transitions to `registered`. +- `unregister()` -- Reads stored token, sends `UnregisterDevice` RPC, clears stored token, transitions to `unregistered`. +- `_onTokenRefresh(String newToken)` -- Re-registers with the new token (FCM tokens can rotate at any time). + +### Secure storage + +Add a `_keyDeviceToken` constant to `SecureStorageService` with matching `readDeviceToken()` / `writeDeviceToken()` / `deleteDeviceToken()` methods, following the existing pattern for `_keyAccessToken`. + +## Registration Flow + +### On login (`setTokens()` in `auth_notifier.dart`) + +``` +setTokens() called + --> AuthState transitions to authenticated + --> Riverpod listener on authNotifierProvider detects authenticated state + --> NotificationNotifier.register(userId) fires: + 1. FirebaseMessaging.instance.getToken() --> deviceToken + 2. notificationServiceClient.registerDevice( + RegisterDeviceRequest( + deviceToken: deviceToken, + platform: _detectPlatform(), + userId: userId, + ) + ) + 3. secureStorage.writeDeviceToken(deviceToken) + 4. State --> NotificationState.registered(deviceToken: deviceToken) +``` + +The registration is triggered by a Riverpod listener (not by modifying `setTokens()` directly), keeping `AuthNotifier` free of notification concerns. + +### On logout (`logout()` in `auth_notifier.dart`) + +``` +logout() called + --> AuthState transitions to unauthenticated + --> Riverpod listener detects unauthenticated state + --> NotificationNotifier.unregister() fires: + 1. Read stored device token from secure storage + 2. notificationServiceClient.unregisterDevice( + UnregisterDeviceRequest(deviceToken: storedToken) + ) + 3. secureStorage.deleteDeviceToken() + 4. State --> NotificationState.unregistered() +``` + +If the `UnregisterDevice` RPC fails (e.g. already offline), the token is still cleared locally. The daemon will eventually expire stale tokens server-side. + +## Token Refresh + +FCM tokens can rotate at any time (app update, cache clear, etc.). The notifier subscribes to the refresh stream: + +```dart +FirebaseMessaging.instance.onTokenRefresh.listen((newToken) { + // Re-register with the daemon using the new token + _onTokenRefresh(newToken); +}); +``` + +`_onTokenRefresh` sends a new `RegisterDevice` RPC with the updated token. The daemon is expected to handle this idempotently (upsert by user_id + platform). + +## Platform Detection + +Map `dart:io` `Platform` to the proto enum: + +```dart +DevicePlatform _detectPlatform() { + if (Platform.isAndroid) return DevicePlatform.DEVICE_PLATFORM_ANDROID; + if (Platform.isIOS) return DevicePlatform.DEVICE_PLATFORM_IOS; + return DevicePlatform.DEVICE_PLATFORM_UNSPECIFIED; +} +``` + +Desktop platforms (macOS) return `UNSPECIFIED` and skip registration entirely. + +## Existing Infrastructure + +### NotificationCache drift table (`database.dart:146-155`) + +```dart +class NotificationCache extends Table { + TextColumn get notificationId => text()(); + IntColumn get receivedAt => integer()(); + + @override + Set get primaryKey => {notificationId}; +} +``` + +Already included in the `@DriftDatabase` tables list. Used for **incoming** notification deduplication -- when a push arrives, insert `notificationId`; if it already exists, drop the duplicate. This is separate from the registration flow but part of the same feature area. + +### SecureStorageService (`secure_storage.dart`) + +Will be extended with `readDeviceToken()`, `writeDeviceToken()`, and `deleteDeviceToken()` methods using a new `_keyDeviceToken = 'device_token'` constant. Follows the existing pattern for access tokens and relay config. + +## Error Handling + +- **Registration failure**: Transition to `NotificationState.error(message)`. Retry on next app foreground or next auth state change. +- **Unregistration failure**: Log and proceed with local cleanup. Daemon handles stale token expiry. +- **FCM unavailable**: On platforms where `FirebaseMessaging` is not available (desktop), skip registration silently. +- **No permission**: On iOS, `FirebaseMessaging.instance.requestPermission()` must be called before `getToken()`. If the user denies, state stays `unregistered`. + +## Testing Strategy + +### Unit tests (`test/features/notifications/`) + +1. **Registration flow**: Mock `NotificationServiceClient` and `FirebaseMessaging`. Verify `RegisterDevice` RPC is called with correct token, platform, and userId. Verify state transitions: `unregistered -> registering -> registered`. + +2. **Unregistration flow**: Verify `UnregisterDevice` RPC is called with stored token. Verify secure storage is cleared. Verify state transitions: `registered -> unregistered`. + +3. **Token refresh**: Simulate `onTokenRefresh` stream emission. Verify re-registration RPC with new token. + +4. **Error handling**: Simulate gRPC errors. Verify state transitions to `error`. Verify unregistration proceeds despite RPC failure. + +5. **Auth state listener**: Verify registration triggers on `authenticated` state, unregistration triggers on `unauthenticated` state. + +### Mocking approach + +```dart +class MockNotificationServiceClient extends Mock + implements NotificationServiceClient {} + +class MockFirebaseMessaging extends Mock implements FirebaseMessaging {} +``` + +Use `FakeResponseFuture` from the shared test helpers (see deduplication plan Task 8) to mock gRPC responses. + +## Implementation Order + +1. Generate Dart code from `notification.proto` (`protoc --dart_out=grpc:lib/generated/`) +2. Add `notificationServiceProvider` to `service_providers.dart` +3. Add `readDeviceToken` / `writeDeviceToken` / `deleteDeviceToken` to `SecureStorageService` +4. Create `lib/features/notifications/notification_notifier.dart` with state and logic +5. Create `lib/features/notifications/notification_providers.dart` with Riverpod providers +6. Wire auth state listener in notification providers (listen to `authNotifierProvider`) +7. Add `firebase_messaging` and `firebase_core` to `pubspec.yaml` +8. Platform setup (Android `google-services.json`, iOS APNs capability) +9. Write unit tests +10. Integration test on physical device diff --git a/lib/features/commands/notifiers/commands_providers.dart b/lib/features/commands/notifiers/commands_providers.dart index d4c3b2d..de3e84c 100644 --- a/lib/features/commands/notifiers/commands_providers.dart +++ b/lib/features/commands/notifiers/commands_providers.dart @@ -9,11 +9,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// global commands. Use `ref.watch(commandsProvider(sessionId))` in /// widgets to reactively rebuild on loading / data / error transitions. // ignore: specify_nonobvious_property_types, the family provider type is not publicly exported -final commandsProvider = AsyncNotifierProvider.family< - CommandsNotifier, - List, - String? ->((sessionId) { - final notifier = CommandsNotifier()..sessionId = sessionId; - return notifier; -}); +final commandsProvider = + AsyncNotifierProvider.family, String?>( + (sessionId) { + final notifier = CommandsNotifier()..sessionId = sessionId; + return notifier; + }, + ); diff --git a/lib/features/conversation/notifiers/conversation_notifier.dart b/lib/features/conversation/notifiers/conversation_notifier.dart index 35ce7a0..b43d4bb 100644 --- a/lib/features/conversation/notifiers/conversation_notifier.dart +++ b/lib/features/conversation/notifiers/conversation_notifier.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math' show min; +import 'package:betcode_app/core/grpc/app_exceptions.dart'; import 'package:betcode_app/core/grpc/service_providers.dart'; import 'package:betcode_app/core/lifecycle/lifecycle.dart'; import 'package:betcode_app/features/conversation/models/conversation_state.dart'; @@ -29,6 +30,9 @@ class ConversationNotifier extends AsyncNotifier Duration(seconds: 30), ]; + /// Default model used for new sessions when no model is explicitly chosen. + static const _defaultModel = 'claude-sonnet-4'; + StreamController? _requestController; StreamSubscription? _eventSubscription; int _reconnectAttempt = 0; @@ -112,6 +116,7 @@ class ConversationNotifier extends AsyncNotifier start: pb.StartConversation( sessionId: sessionId ?? '', workingDirectory: workingDirectory, + model: _defaultModel, ), ), ); @@ -135,7 +140,10 @@ class ConversationNotifier extends AsyncNotifier await _loadHistory(sessionId!); } } on Exception catch (e) { - state = AsyncData(ConversationState.error(e.toString())); + final message = e is AppException + ? e.message + : 'Failed to start conversation: $e'; + state = AsyncData(ConversationState.error(message)); } } @@ -165,8 +173,13 @@ class ConversationNotifier extends AsyncNotifier final seq = current is ConversationActive ? current.lastSequence : 0; debugPrint('[Conversation] History loaded, lastSequence=$seq'); } on GrpcError catch (e) { - // Non-fatal: continue with empty history if replay fails. debugPrint('[Conversation] History load failed: $e'); + final current = state.value; + if (current is ConversationActive) { + state = AsyncData( + current.copyWith(errorMessage: "Couldn't load message history."), + ); + } } } @@ -291,9 +304,10 @@ class ConversationNotifier extends AsyncNotifier // Don't retry fatal errors. if (_isFatalError(error)) { _isReconnecting = false; - state = AsyncData( - ConversationState.error('Stream error: $error'), - ); + final message = error is AppException + ? error.message + : 'Stream error: $error'; + state = AsyncData(ConversationState.error(message)); unawaited(_requestController?.close()); _requestController = null; return; @@ -311,21 +325,19 @@ class ConversationNotifier extends AsyncNotifier _attemptReconnection(current); } else { _isReconnecting = false; - state = AsyncData( - ConversationState.error('Stream error: $error'), - ); + final message = error is AppException + ? error.message + : 'Stream error: $error'; + state = AsyncData(ConversationState.error(message)); unawaited(_requestController?.close()); _requestController = null; } } bool _isFatalError(Object error) { - if (error is GrpcError) { - return error.code == StatusCode.unauthenticated || - error.code == StatusCode.permissionDenied || - error.code == StatusCode.notFound; - } - return false; + return error is AuthExpiredError || + error is PermissionDeniedError || + error is SessionNotFoundError; } void _attemptReconnection(ConversationActive active) { @@ -373,8 +385,7 @@ class ConversationNotifier extends AsyncNotifier // used resumeSession (server-streaming, read-only) which left // _requestController null — silently dropping all user messages. _requestController = StreamController(); - final responseStream = - _client.converse(_requestController!.stream); + final responseStream = _client.converse(_requestController!.stream); _eventSubscription = responseStream.listen( _onReconnectEvent, diff --git a/lib/features/conversation/widgets/command_palette.dart b/lib/features/conversation/widgets/command_palette.dart index 9c22fbe..0cf3d12 100644 --- a/lib/features/conversation/widgets/command_palette.dart +++ b/lib/features/conversation/widgets/command_palette.dart @@ -27,13 +27,14 @@ class CommandPalette extends ConsumerWidget { final lower = query.toLowerCase(); // Local static commands filtered by query. - final localCommands = InputCommand.filter(query) - .map(CommandItem.fromLocal) - .toList(); + final localCommands = InputCommand.filter( + query, + ).map(CommandItem.fromLocal).toList(); // Dynamic daemon commands filtered by query. final daemonCommandsAsync = ref.watch(commandsProvider(sessionId)); - final daemonCommands = daemonCommandsAsync.value + final daemonCommands = + daemonCommandsAsync.value ?.where( (e) => e.name.toLowerCase().contains(lower) || diff --git a/lib/features/sessions/notifiers/sessions_notifier.dart b/lib/features/sessions/notifiers/sessions_notifier.dart index 2348632..97b4a7f 100644 --- a/lib/features/sessions/notifiers/sessions_notifier.dart +++ b/lib/features/sessions/notifiers/sessions_notifier.dart @@ -76,6 +76,26 @@ class SessionsNotifier extends AsyncNotifier> { await refresh(); } + /// Permanently deletes a session and all its messages. + /// + /// Throws on gRPC/timeout errors so callers can display feedback. + Future deleteSession(String sessionId) async { + final client = ref.read(agentServiceProvider); + await client + .deleteSession(DeleteSessionRequest(sessionId: sessionId)) + .timeout(_mutationTimeout); + await _removeFromCache(sessionId); + await refresh(); + } + + /// Removes a deleted session from the local drift cache. + Future _removeFromCache(String sessionId) async { + final db = ref.read(appDatabaseProvider); + await db.batch((batch) { + batch.deleteWhere(db.cachedSessions, (t) => t.id.equals(sessionId)); + }); + } + /// Upserts each [SessionSummary] into the local [CachedSessions] table so /// the data is available when offline. Future _cacheToDb(Iterable sessions) async { diff --git a/lib/features/sessions/widgets/confirm_delete_dialog.dart b/lib/features/sessions/widgets/confirm_delete_dialog.dart new file mode 100644 index 0000000..11196ea --- /dev/null +++ b/lib/features/sessions/widgets/confirm_delete_dialog.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +/// A confirmation dialog shown before permanently deleting a session. +/// +/// Returns `true` on confirm, `null` on cancel. +class ConfirmDeleteDialog extends StatelessWidget { + const ConfirmDeleteDialog({super.key}); + + /// Convenience method to show the dialog and return the result. + static Future show(BuildContext context) { + return showDialog( + context: context, + builder: (_) => const ConfirmDeleteDialog(), + ); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AlertDialog( + title: const Text('Delete Session'), + content: const Text( + 'This will permanently delete the session and all its messages. ' + 'This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + style: FilledButton.styleFrom( + backgroundColor: colorScheme.error, + foregroundColor: colorScheme.onError, + ), + child: const Text('Delete'), + ), + ], + ); + } +} diff --git a/lib/features/sessions/widgets/widgets.dart b/lib/features/sessions/widgets/widgets.dart new file mode 100644 index 0000000..10c0244 --- /dev/null +++ b/lib/features/sessions/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'confirm_delete_dialog.dart'; +export 'rename_session_dialog.dart'; +export 'session_card.dart'; diff --git a/lib/generated/betcode/v1/agent.pb.dart b/lib/generated/betcode/v1/agent.pb.dart index 897f8cd..b8640a3 100644 --- a/lib/generated/betcode/v1/agent.pb.dart +++ b/lib/generated/betcode/v1/agent.pb.dart @@ -3415,6 +3415,115 @@ class RenameSessionResponse extends $pb.GeneratedMessage { static RenameSessionResponse? _defaultInstance; } +class DeleteSessionRequest extends $pb.GeneratedMessage { + factory DeleteSessionRequest({ + $core.String? sessionId, + }) { + final result = create(); + if (sessionId != null) result.sessionId = sessionId; + return result; + } + + DeleteSessionRequest._(); + + factory DeleteSessionRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeleteSessionRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeleteSessionRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'sessionId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeleteSessionRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeleteSessionRequest copyWith(void Function(DeleteSessionRequest) updates) => + super.copyWith((message) => updates(message as DeleteSessionRequest)) + as DeleteSessionRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeleteSessionRequest create() => DeleteSessionRequest._(); + @$core.override + DeleteSessionRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static DeleteSessionRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeleteSessionRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get sessionId => $_getSZ(0); + @$pb.TagNumber(1) + set sessionId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSessionId() => $_has(0); + @$pb.TagNumber(1) + void clearSessionId() => $_clearField(1); +} + +class DeleteSessionResponse extends $pb.GeneratedMessage { + factory DeleteSessionResponse({ + $core.bool? deleted, + }) { + final result = create(); + if (deleted != null) result.deleted = deleted; + return result; + } + + DeleteSessionResponse._(); + + factory DeleteSessionResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeleteSessionResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeleteSessionResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'deleted') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeleteSessionResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeleteSessionResponse copyWith( + void Function(DeleteSessionResponse) updates) => + super.copyWith((message) => updates(message as DeleteSessionResponse)) + as DeleteSessionResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeleteSessionResponse create() => DeleteSessionResponse._(); + @$core.override + DeleteSessionResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static DeleteSessionResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeleteSessionResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get deleted => $_getBF(0); + @$pb.TagNumber(1) + set deleted($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasDeleted() => $_has(0); + @$pb.TagNumber(1) + void clearDeleted() => $_clearField(1); +} + const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = diff --git a/lib/generated/betcode/v1/agent.pbgrpc.dart b/lib/generated/betcode/v1/agent.pbgrpc.dart index d8fcb4b..92ec3f1 100644 --- a/lib/generated/betcode/v1/agent.pbgrpc.dart +++ b/lib/generated/betcode/v1/agent.pbgrpc.dart @@ -124,6 +124,14 @@ class AgentServiceClient extends $grpc.Client { return $createUnaryCall(_$renameSession, request, options: options); } + /// Delete a session and all its messages. + $grpc.ResponseFuture<$0.DeleteSessionResponse> deleteSession( + $0.DeleteSessionRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$deleteSession, request, options: options); + } + // method descriptors static final _$converse = $grpc.ClientMethod<$0.AgentRequest, $0.AgentEvent>( @@ -180,6 +188,11 @@ class AgentServiceClient extends $grpc.Client { '/betcode.v1.AgentService/RenameSession', ($0.RenameSessionRequest value) => value.writeToBuffer(), $0.RenameSessionResponse.fromBuffer); + static final _$deleteSession = + $grpc.ClientMethod<$0.DeleteSessionRequest, $0.DeleteSessionResponse>( + '/betcode.v1.AgentService/DeleteSession', + ($0.DeleteSessionRequest value) => value.writeToBuffer(), + $0.DeleteSessionResponse.fromBuffer); } @$pb.GrpcServiceName('betcode.v1.AgentService') @@ -279,6 +292,15 @@ abstract class AgentServiceBase extends $grpc.Service { ($core.List<$core.int> value) => $0.RenameSessionRequest.fromBuffer(value), ($0.RenameSessionResponse value) => value.writeToBuffer())); + $addMethod( + $grpc.ServiceMethod<$0.DeleteSessionRequest, $0.DeleteSessionResponse>( + 'DeleteSession', + deleteSession_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.DeleteSessionRequest.fromBuffer(value), + ($0.DeleteSessionResponse value) => value.writeToBuffer())); } $async.Stream<$0.AgentEvent> converse( @@ -371,4 +393,13 @@ abstract class AgentServiceBase extends $grpc.Service { $async.Future<$0.RenameSessionResponse> renameSession( $grpc.ServiceCall call, $0.RenameSessionRequest request); + + $async.Future<$0.DeleteSessionResponse> deleteSession_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.DeleteSessionRequest> $request) async { + return deleteSession($call, await $request); + } + + $async.Future<$0.DeleteSessionResponse> deleteSession( + $grpc.ServiceCall call, $0.DeleteSessionRequest request); } diff --git a/lib/generated/betcode/v1/agent.pbjson.dart b/lib/generated/betcode/v1/agent.pbjson.dart index 55d27a0..d79d49b 100644 --- a/lib/generated/betcode/v1/agent.pbjson.dart +++ b/lib/generated/betcode/v1/agent.pbjson.dart @@ -1042,3 +1042,28 @@ const RenameSessionResponse$json = { /// Descriptor for `RenameSessionResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List renameSessionResponseDescriptor = $convert.base64Decode('ChVSZW5hbWVTZXNzaW9uUmVzcG9uc2U='); + +@$core.Deprecated('Use deleteSessionRequestDescriptor instead') +const DeleteSessionRequest$json = { + '1': 'DeleteSessionRequest', + '2': [ + {'1': 'session_id', '3': 1, '4': 1, '5': 9, '10': 'sessionId'}, + ], +}; + +/// Descriptor for `DeleteSessionRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deleteSessionRequestDescriptor = $convert.base64Decode( + 'ChREZWxldGVTZXNzaW9uUmVxdWVzdBIdCgpzZXNzaW9uX2lkGAEgASgJUglzZXNzaW9uSWQ='); + +@$core.Deprecated('Use deleteSessionResponseDescriptor instead') +const DeleteSessionResponse$json = { + '1': 'DeleteSessionResponse', + '2': [ + {'1': 'deleted', '3': 1, '4': 1, '5': 8, '10': 'deleted'}, + ], +}; + +/// Descriptor for `DeleteSessionResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deleteSessionResponseDescriptor = + $convert.base64Decode( + 'ChVEZWxldGVTZXNzaW9uUmVzcG9uc2USGAoHZGVsZXRlZBgBIAEoCFIHZGVsZXRlZA=='); diff --git a/lib/generated/betcode/v1/notification.pb.dart b/lib/generated/betcode/v1/notification.pb.dart new file mode 100644 index 0000000..e4515e6 --- /dev/null +++ b/lib/generated/betcode/v1/notification.pb.dart @@ -0,0 +1,271 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/notification.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'notification.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'notification.pbenum.dart'; + +class RegisterDeviceRequest extends $pb.GeneratedMessage { + factory RegisterDeviceRequest({ + $core.String? deviceToken, + DevicePlatform? platform, + $core.String? userId, + }) { + final result = create(); + if (deviceToken != null) result.deviceToken = deviceToken; + if (platform != null) result.platform = platform; + if (userId != null) result.userId = userId; + return result; + } + + RegisterDeviceRequest._(); + + factory RegisterDeviceRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RegisterDeviceRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RegisterDeviceRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'deviceToken') + ..aE(2, _omitFieldNames ? '' : 'platform', + enumValues: DevicePlatform.values) + ..aOS(3, _omitFieldNames ? '' : 'userId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RegisterDeviceRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RegisterDeviceRequest copyWith( + void Function(RegisterDeviceRequest) updates) => + super.copyWith((message) => updates(message as RegisterDeviceRequest)) + as RegisterDeviceRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RegisterDeviceRequest create() => RegisterDeviceRequest._(); + @$core.override + RegisterDeviceRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static RegisterDeviceRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RegisterDeviceRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get deviceToken => $_getSZ(0); + @$pb.TagNumber(1) + set deviceToken($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasDeviceToken() => $_has(0); + @$pb.TagNumber(1) + void clearDeviceToken() => $_clearField(1); + + @$pb.TagNumber(2) + DevicePlatform get platform => $_getN(1); + @$pb.TagNumber(2) + set platform(DevicePlatform value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasPlatform() => $_has(1); + @$pb.TagNumber(2) + void clearPlatform() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get userId => $_getSZ(2); + @$pb.TagNumber(3) + set userId($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasUserId() => $_has(2); + @$pb.TagNumber(3) + void clearUserId() => $_clearField(3); +} + +class RegisterDeviceResponse extends $pb.GeneratedMessage { + factory RegisterDeviceResponse({ + $core.bool? success, + }) { + final result = create(); + if (success != null) result.success = success; + return result; + } + + RegisterDeviceResponse._(); + + factory RegisterDeviceResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RegisterDeviceResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RegisterDeviceResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'success') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RegisterDeviceResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RegisterDeviceResponse copyWith( + void Function(RegisterDeviceResponse) updates) => + super.copyWith((message) => updates(message as RegisterDeviceResponse)) + as RegisterDeviceResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RegisterDeviceResponse create() => RegisterDeviceResponse._(); + @$core.override + RegisterDeviceResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static RegisterDeviceResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RegisterDeviceResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get success => $_getBF(0); + @$pb.TagNumber(1) + set success($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasSuccess() => $_has(0); + @$pb.TagNumber(1) + void clearSuccess() => $_clearField(1); +} + +class UnregisterDeviceRequest extends $pb.GeneratedMessage { + factory UnregisterDeviceRequest({ + $core.String? deviceToken, + }) { + final result = create(); + if (deviceToken != null) result.deviceToken = deviceToken; + return result; + } + + UnregisterDeviceRequest._(); + + factory UnregisterDeviceRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory UnregisterDeviceRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'UnregisterDeviceRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'deviceToken') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + UnregisterDeviceRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + UnregisterDeviceRequest copyWith( + void Function(UnregisterDeviceRequest) updates) => + super.copyWith((message) => updates(message as UnregisterDeviceRequest)) + as UnregisterDeviceRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UnregisterDeviceRequest create() => UnregisterDeviceRequest._(); + @$core.override + UnregisterDeviceRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static UnregisterDeviceRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static UnregisterDeviceRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get deviceToken => $_getSZ(0); + @$pb.TagNumber(1) + set deviceToken($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasDeviceToken() => $_has(0); + @$pb.TagNumber(1) + void clearDeviceToken() => $_clearField(1); +} + +class UnregisterDeviceResponse extends $pb.GeneratedMessage { + factory UnregisterDeviceResponse({ + $core.bool? success, + }) { + final result = create(); + if (success != null) result.success = success; + return result; + } + + UnregisterDeviceResponse._(); + + factory UnregisterDeviceResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory UnregisterDeviceResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'UnregisterDeviceResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'success') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + UnregisterDeviceResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + UnregisterDeviceResponse copyWith( + void Function(UnregisterDeviceResponse) updates) => + super.copyWith((message) => updates(message as UnregisterDeviceResponse)) + as UnregisterDeviceResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UnregisterDeviceResponse create() => UnregisterDeviceResponse._(); + @$core.override + UnregisterDeviceResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static UnregisterDeviceResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static UnregisterDeviceResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get success => $_getBF(0); + @$pb.TagNumber(1) + set success($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasSuccess() => $_has(0); + @$pb.TagNumber(1) + void clearSuccess() => $_clearField(1); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/generated/betcode/v1/notification.pbenum.dart b/lib/generated/betcode/v1/notification.pbenum.dart new file mode 100644 index 0000000..6b45ec7 --- /dev/null +++ b/lib/generated/betcode/v1/notification.pbenum.dart @@ -0,0 +1,40 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/notification.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class DevicePlatform extends $pb.ProtobufEnum { + static const DevicePlatform DEVICE_PLATFORM_UNSPECIFIED = + DevicePlatform._(0, _omitEnumNames ? '' : 'DEVICE_PLATFORM_UNSPECIFIED'); + static const DevicePlatform DEVICE_PLATFORM_ANDROID = + DevicePlatform._(1, _omitEnumNames ? '' : 'DEVICE_PLATFORM_ANDROID'); + static const DevicePlatform DEVICE_PLATFORM_IOS = + DevicePlatform._(2, _omitEnumNames ? '' : 'DEVICE_PLATFORM_IOS'); + + static const $core.List values = [ + DEVICE_PLATFORM_UNSPECIFIED, + DEVICE_PLATFORM_ANDROID, + DEVICE_PLATFORM_IOS, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static DevicePlatform? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const DevicePlatform._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/generated/betcode/v1/notification.pbgrpc.dart b/lib/generated/betcode/v1/notification.pbgrpc.dart new file mode 100644 index 0000000..3c8e31b --- /dev/null +++ b/lib/generated/betcode/v1/notification.pbgrpc.dart @@ -0,0 +1,108 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/notification.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'notification.pb.dart' as $0; + +export 'notification.pb.dart'; + +/// NotificationService manages device registrations for push notifications. +@$pb.GrpcServiceName('betcode.v1.NotificationService') +class NotificationServiceClient extends $grpc.Client { + /// The hostname for this service. + static const $core.String defaultHost = ''; + + /// OAuth scopes needed for the client. + static const $core.List<$core.String> oauthScopes = [ + '', + ]; + + NotificationServiceClient(super.channel, {super.options, super.interceptors}); + + /// Register a device token for push notifications. + $grpc.ResponseFuture<$0.RegisterDeviceResponse> registerDevice( + $0.RegisterDeviceRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$registerDevice, request, options: options); + } + + /// Unregister a device token to stop receiving push notifications. + $grpc.ResponseFuture<$0.UnregisterDeviceResponse> unregisterDevice( + $0.UnregisterDeviceRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$unregisterDevice, request, options: options); + } + + // method descriptors + + static final _$registerDevice = + $grpc.ClientMethod<$0.RegisterDeviceRequest, $0.RegisterDeviceResponse>( + '/betcode.v1.NotificationService/RegisterDevice', + ($0.RegisterDeviceRequest value) => value.writeToBuffer(), + $0.RegisterDeviceResponse.fromBuffer); + static final _$unregisterDevice = $grpc.ClientMethod< + $0.UnregisterDeviceRequest, $0.UnregisterDeviceResponse>( + '/betcode.v1.NotificationService/UnregisterDevice', + ($0.UnregisterDeviceRequest value) => value.writeToBuffer(), + $0.UnregisterDeviceResponse.fromBuffer); +} + +@$pb.GrpcServiceName('betcode.v1.NotificationService') +abstract class NotificationServiceBase extends $grpc.Service { + $core.String get $name => 'betcode.v1.NotificationService'; + + NotificationServiceBase() { + $addMethod($grpc.ServiceMethod<$0.RegisterDeviceRequest, + $0.RegisterDeviceResponse>( + 'RegisterDevice', + registerDevice_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.RegisterDeviceRequest.fromBuffer(value), + ($0.RegisterDeviceResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.UnregisterDeviceRequest, + $0.UnregisterDeviceResponse>( + 'UnregisterDevice', + unregisterDevice_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.UnregisterDeviceRequest.fromBuffer(value), + ($0.UnregisterDeviceResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.RegisterDeviceResponse> registerDevice_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.RegisterDeviceRequest> $request) async { + return registerDevice($call, await $request); + } + + $async.Future<$0.RegisterDeviceResponse> registerDevice( + $grpc.ServiceCall call, $0.RegisterDeviceRequest request); + + $async.Future<$0.UnregisterDeviceResponse> unregisterDevice_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.UnregisterDeviceRequest> $request) async { + return unregisterDevice($call, await $request); + } + + $async.Future<$0.UnregisterDeviceResponse> unregisterDevice( + $grpc.ServiceCall call, $0.UnregisterDeviceRequest request); +} diff --git a/lib/generated/betcode/v1/notification.pbjson.dart b/lib/generated/betcode/v1/notification.pbjson.dart new file mode 100644 index 0000000..d447d37 --- /dev/null +++ b/lib/generated/betcode/v1/notification.pbjson.dart @@ -0,0 +1,94 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/notification.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports +// ignore_for_file: unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use devicePlatformDescriptor instead') +const DevicePlatform$json = { + '1': 'DevicePlatform', + '2': [ + {'1': 'DEVICE_PLATFORM_UNSPECIFIED', '2': 0}, + {'1': 'DEVICE_PLATFORM_ANDROID', '2': 1}, + {'1': 'DEVICE_PLATFORM_IOS', '2': 2}, + ], +}; + +/// Descriptor for `DevicePlatform`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List devicePlatformDescriptor = $convert.base64Decode( + 'Cg5EZXZpY2VQbGF0Zm9ybRIfChtERVZJQ0VfUExBVEZPUk1fVU5TUEVDSUZJRUQQABIbChdERV' + 'ZJQ0VfUExBVEZPUk1fQU5EUk9JRBABEhcKE0RFVklDRV9QTEFURk9STV9JT1MQAg=='); + +@$core.Deprecated('Use registerDeviceRequestDescriptor instead') +const RegisterDeviceRequest$json = { + '1': 'RegisterDeviceRequest', + '2': [ + {'1': 'device_token', '3': 1, '4': 1, '5': 9, '10': 'deviceToken'}, + { + '1': 'platform', + '3': 2, + '4': 1, + '5': 14, + '6': '.betcode.v1.DevicePlatform', + '10': 'platform' + }, + {'1': 'user_id', '3': 3, '4': 1, '5': 9, '10': 'userId'}, + ], +}; + +/// Descriptor for `RegisterDeviceRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List registerDeviceRequestDescriptor = $convert.base64Decode( + 'ChVSZWdpc3RlckRldmljZVJlcXVlc3QSIQoMZGV2aWNlX3Rva2VuGAEgASgJUgtkZXZpY2VUb2' + 'tlbhI2CghwbGF0Zm9ybRgCIAEoDjIaLmJldGNvZGUudjEuRGV2aWNlUGxhdGZvcm1SCHBsYXRm' + 'b3JtEhcKB3VzZXJfaWQYAyABKAlSBnVzZXJJZA=='); + +@$core.Deprecated('Use registerDeviceResponseDescriptor instead') +const RegisterDeviceResponse$json = { + '1': 'RegisterDeviceResponse', + '2': [ + {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, + ], +}; + +/// Descriptor for `RegisterDeviceResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List registerDeviceResponseDescriptor = + $convert.base64Decode( + 'ChZSZWdpc3RlckRldmljZVJlc3BvbnNlEhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3M='); + +@$core.Deprecated('Use unregisterDeviceRequestDescriptor instead') +const UnregisterDeviceRequest$json = { + '1': 'UnregisterDeviceRequest', + '2': [ + {'1': 'device_token', '3': 1, '4': 1, '5': 9, '10': 'deviceToken'}, + ], +}; + +/// Descriptor for `UnregisterDeviceRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List unregisterDeviceRequestDescriptor = + $convert.base64Decode( + 'ChdVbnJlZ2lzdGVyRGV2aWNlUmVxdWVzdBIhCgxkZXZpY2VfdG9rZW4YASABKAlSC2RldmljZV' + 'Rva2Vu'); + +@$core.Deprecated('Use unregisterDeviceResponseDescriptor instead') +const UnregisterDeviceResponse$json = { + '1': 'UnregisterDeviceResponse', + '2': [ + {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, + ], +}; + +/// Descriptor for `UnregisterDeviceResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List unregisterDeviceResponseDescriptor = + $convert.base64Decode( + 'ChhVbnJlZ2lzdGVyRGV2aWNlUmVzcG9uc2USGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2Vzcw=='); diff --git a/lib/generated/betcode/v1/subagent.pb.dart b/lib/generated/betcode/v1/subagent.pb.dart new file mode 100644 index 0000000..b33d925 --- /dev/null +++ b/lib/generated/betcode/v1/subagent.pb.dart @@ -0,0 +1,2730 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/subagent.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; +import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart' + as $1; + +import 'subagent.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'subagent.pbenum.dart'; + +/// SpawnSubagentRequest spawns a new Claude subprocess as a subagent. +class SpawnSubagentRequest extends $pb.GeneratedMessage { + factory SpawnSubagentRequest({ + $core.String? parentSessionId, + $core.String? prompt, + $core.String? model, + $core.String? workingDirectory, + $core.Iterable<$core.String>? allowedTools, + $core.int? maxTurns, + $core.Iterable<$core.MapEntry<$core.String, $core.String>>? env, + $core.String? name, + $core.bool? autoApprove, + }) { + final result = create(); + if (parentSessionId != null) result.parentSessionId = parentSessionId; + if (prompt != null) result.prompt = prompt; + if (model != null) result.model = model; + if (workingDirectory != null) result.workingDirectory = workingDirectory; + if (allowedTools != null) result.allowedTools.addAll(allowedTools); + if (maxTurns != null) result.maxTurns = maxTurns; + if (env != null) result.env.addEntries(env); + if (name != null) result.name = name; + if (autoApprove != null) result.autoApprove = autoApprove; + return result; + } + + SpawnSubagentRequest._(); + + factory SpawnSubagentRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SpawnSubagentRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SpawnSubagentRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'parentSessionId') + ..aOS(2, _omitFieldNames ? '' : 'prompt') + ..aOS(3, _omitFieldNames ? '' : 'model') + ..aOS(4, _omitFieldNames ? '' : 'workingDirectory') + ..pPS(5, _omitFieldNames ? '' : 'allowedTools') + ..aI(6, _omitFieldNames ? '' : 'maxTurns') + ..m<$core.String, $core.String>(7, _omitFieldNames ? '' : 'env', + entryClassName: 'SpawnSubagentRequest.EnvEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('betcode.v1')) + ..aOS(8, _omitFieldNames ? '' : 'name') + ..aOB(9, _omitFieldNames ? '' : 'autoApprove') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SpawnSubagentRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SpawnSubagentRequest copyWith(void Function(SpawnSubagentRequest) updates) => + super.copyWith((message) => updates(message as SpawnSubagentRequest)) + as SpawnSubagentRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpawnSubagentRequest create() => SpawnSubagentRequest._(); + @$core.override + SpawnSubagentRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SpawnSubagentRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SpawnSubagentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get parentSessionId => $_getSZ(0); + @$pb.TagNumber(1) + set parentSessionId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasParentSessionId() => $_has(0); + @$pb.TagNumber(1) + void clearParentSessionId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get prompt => $_getSZ(1); + @$pb.TagNumber(2) + set prompt($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasPrompt() => $_has(1); + @$pb.TagNumber(2) + void clearPrompt() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get model => $_getSZ(2); + @$pb.TagNumber(3) + set model($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasModel() => $_has(2); + @$pb.TagNumber(3) + void clearModel() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get workingDirectory => $_getSZ(3); + @$pb.TagNumber(4) + set workingDirectory($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasWorkingDirectory() => $_has(3); + @$pb.TagNumber(4) + void clearWorkingDirectory() => $_clearField(4); + + @$pb.TagNumber(5) + $pb.PbList<$core.String> get allowedTools => $_getList(4); + + @$pb.TagNumber(6) + $core.int get maxTurns => $_getIZ(5); + @$pb.TagNumber(6) + set maxTurns($core.int value) => $_setSignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasMaxTurns() => $_has(5); + @$pb.TagNumber(6) + void clearMaxTurns() => $_clearField(6); + + @$pb.TagNumber(7) + $pb.PbMap<$core.String, $core.String> get env => $_getMap(6); + + @$pb.TagNumber(8) + $core.String get name => $_getSZ(7); + @$pb.TagNumber(8) + set name($core.String value) => $_setString(7, value); + @$pb.TagNumber(8) + $core.bool hasName() => $_has(7); + @$pb.TagNumber(8) + void clearName() => $_clearField(8); + + @$pb.TagNumber(9) + $core.bool get autoApprove => $_getBF(8); + @$pb.TagNumber(9) + set autoApprove($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasAutoApprove() => $_has(8); + @$pb.TagNumber(9) + void clearAutoApprove() => $_clearField(9); +} + +/// SpawnSubagentResponse returns the IDs of the spawned subagent. +class SpawnSubagentResponse extends $pb.GeneratedMessage { + factory SpawnSubagentResponse({ + $core.String? subagentId, + $core.String? sessionId, + }) { + final result = create(); + if (subagentId != null) result.subagentId = subagentId; + if (sessionId != null) result.sessionId = sessionId; + return result; + } + + SpawnSubagentResponse._(); + + factory SpawnSubagentResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SpawnSubagentResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SpawnSubagentResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'subagentId') + ..aOS(2, _omitFieldNames ? '' : 'sessionId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SpawnSubagentResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SpawnSubagentResponse copyWith( + void Function(SpawnSubagentResponse) updates) => + super.copyWith((message) => updates(message as SpawnSubagentResponse)) + as SpawnSubagentResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpawnSubagentResponse create() => SpawnSubagentResponse._(); + @$core.override + SpawnSubagentResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SpawnSubagentResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SpawnSubagentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get subagentId => $_getSZ(0); + @$pb.TagNumber(1) + set subagentId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSubagentId() => $_has(0); + @$pb.TagNumber(1) + void clearSubagentId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get sessionId => $_getSZ(1); + @$pb.TagNumber(2) + set sessionId($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasSessionId() => $_has(1); + @$pb.TagNumber(2) + void clearSessionId() => $_clearField(2); +} + +/// WatchSubagentRequest subscribes to a subagent's event stream. +class WatchSubagentRequest extends $pb.GeneratedMessage { + factory WatchSubagentRequest({ + $core.String? subagentId, + $fixnum.Int64? fromSequence, + }) { + final result = create(); + if (subagentId != null) result.subagentId = subagentId; + if (fromSequence != null) result.fromSequence = fromSequence; + return result; + } + + WatchSubagentRequest._(); + + factory WatchSubagentRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WatchSubagentRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WatchSubagentRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'subagentId') + ..a<$fixnum.Int64>( + 2, _omitFieldNames ? '' : 'fromSequence', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WatchSubagentRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WatchSubagentRequest copyWith(void Function(WatchSubagentRequest) updates) => + super.copyWith((message) => updates(message as WatchSubagentRequest)) + as WatchSubagentRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WatchSubagentRequest create() => WatchSubagentRequest._(); + @$core.override + WatchSubagentRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WatchSubagentRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WatchSubagentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get subagentId => $_getSZ(0); + @$pb.TagNumber(1) + set subagentId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSubagentId() => $_has(0); + @$pb.TagNumber(1) + void clearSubagentId() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get fromSequence => $_getI64(1); + @$pb.TagNumber(2) + set fromSequence($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasFromSequence() => $_has(1); + @$pb.TagNumber(2) + void clearFromSequence() => $_clearField(2); +} + +/// SendToSubagentRequest sends input to a running subagent. +class SendToSubagentRequest extends $pb.GeneratedMessage { + factory SendToSubagentRequest({ + $core.String? subagentId, + $core.String? content, + }) { + final result = create(); + if (subagentId != null) result.subagentId = subagentId; + if (content != null) result.content = content; + return result; + } + + SendToSubagentRequest._(); + + factory SendToSubagentRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SendToSubagentRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SendToSubagentRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'subagentId') + ..aOS(2, _omitFieldNames ? '' : 'content') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SendToSubagentRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SendToSubagentRequest copyWith( + void Function(SendToSubagentRequest) updates) => + super.copyWith((message) => updates(message as SendToSubagentRequest)) + as SendToSubagentRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SendToSubagentRequest create() => SendToSubagentRequest._(); + @$core.override + SendToSubagentRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SendToSubagentRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SendToSubagentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get subagentId => $_getSZ(0); + @$pb.TagNumber(1) + set subagentId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSubagentId() => $_has(0); + @$pb.TagNumber(1) + void clearSubagentId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get content => $_getSZ(1); + @$pb.TagNumber(2) + set content($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasContent() => $_has(1); + @$pb.TagNumber(2) + void clearContent() => $_clearField(2); +} + +/// SendToSubagentResponse acknowledges input delivery. +class SendToSubagentResponse extends $pb.GeneratedMessage { + factory SendToSubagentResponse({ + $core.bool? delivered, + }) { + final result = create(); + if (delivered != null) result.delivered = delivered; + return result; + } + + SendToSubagentResponse._(); + + factory SendToSubagentResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SendToSubagentResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SendToSubagentResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'delivered') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SendToSubagentResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SendToSubagentResponse copyWith( + void Function(SendToSubagentResponse) updates) => + super.copyWith((message) => updates(message as SendToSubagentResponse)) + as SendToSubagentResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SendToSubagentResponse create() => SendToSubagentResponse._(); + @$core.override + SendToSubagentResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SendToSubagentResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SendToSubagentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get delivered => $_getBF(0); + @$pb.TagNumber(1) + set delivered($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasDelivered() => $_has(0); + @$pb.TagNumber(1) + void clearDelivered() => $_clearField(1); +} + +/// CancelSubagentRequest cancels a running subagent. +class CancelSubagentRequest extends $pb.GeneratedMessage { + factory CancelSubagentRequest({ + $core.String? subagentId, + $core.String? reason, + $core.bool? force, + $core.bool? cleanupWorktree, + }) { + final result = create(); + if (subagentId != null) result.subagentId = subagentId; + if (reason != null) result.reason = reason; + if (force != null) result.force = force; + if (cleanupWorktree != null) result.cleanupWorktree = cleanupWorktree; + return result; + } + + CancelSubagentRequest._(); + + factory CancelSubagentRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory CancelSubagentRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'CancelSubagentRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'subagentId') + ..aOS(2, _omitFieldNames ? '' : 'reason') + ..aOB(3, _omitFieldNames ? '' : 'force') + ..aOB(4, _omitFieldNames ? '' : 'cleanupWorktree') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CancelSubagentRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CancelSubagentRequest copyWith( + void Function(CancelSubagentRequest) updates) => + super.copyWith((message) => updates(message as CancelSubagentRequest)) + as CancelSubagentRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CancelSubagentRequest create() => CancelSubagentRequest._(); + @$core.override + CancelSubagentRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static CancelSubagentRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static CancelSubagentRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get subagentId => $_getSZ(0); + @$pb.TagNumber(1) + set subagentId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSubagentId() => $_has(0); + @$pb.TagNumber(1) + void clearSubagentId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get reason => $_getSZ(1); + @$pb.TagNumber(2) + set reason($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasReason() => $_has(1); + @$pb.TagNumber(2) + void clearReason() => $_clearField(2); + + @$pb.TagNumber(3) + $core.bool get force => $_getBF(2); + @$pb.TagNumber(3) + set force($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasForce() => $_has(2); + @$pb.TagNumber(3) + void clearForce() => $_clearField(3); + + @$pb.TagNumber(4) + $core.bool get cleanupWorktree => $_getBF(3); + @$pb.TagNumber(4) + set cleanupWorktree($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasCleanupWorktree() => $_has(3); + @$pb.TagNumber(4) + void clearCleanupWorktree() => $_clearField(4); +} + +/// CancelSubagentResponse returns the result of cancellation. +class CancelSubagentResponse extends $pb.GeneratedMessage { + factory CancelSubagentResponse({ + $core.bool? cancelled, + $core.String? finalStatus, + $core.int? toolCallsExecuted, + $core.int? toolCallsAutoApproved, + }) { + final result = create(); + if (cancelled != null) result.cancelled = cancelled; + if (finalStatus != null) result.finalStatus = finalStatus; + if (toolCallsExecuted != null) result.toolCallsExecuted = toolCallsExecuted; + if (toolCallsAutoApproved != null) + result.toolCallsAutoApproved = toolCallsAutoApproved; + return result; + } + + CancelSubagentResponse._(); + + factory CancelSubagentResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory CancelSubagentResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'CancelSubagentResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'cancelled') + ..aOS(2, _omitFieldNames ? '' : 'finalStatus') + ..aI(3, _omitFieldNames ? '' : 'toolCallsExecuted') + ..aI(4, _omitFieldNames ? '' : 'toolCallsAutoApproved') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CancelSubagentResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CancelSubagentResponse copyWith( + void Function(CancelSubagentResponse) updates) => + super.copyWith((message) => updates(message as CancelSubagentResponse)) + as CancelSubagentResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CancelSubagentResponse create() => CancelSubagentResponse._(); + @$core.override + CancelSubagentResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static CancelSubagentResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static CancelSubagentResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get cancelled => $_getBF(0); + @$pb.TagNumber(1) + set cancelled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasCancelled() => $_has(0); + @$pb.TagNumber(1) + void clearCancelled() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get finalStatus => $_getSZ(1); + @$pb.TagNumber(2) + set finalStatus($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasFinalStatus() => $_has(1); + @$pb.TagNumber(2) + void clearFinalStatus() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get toolCallsExecuted => $_getIZ(2); + @$pb.TagNumber(3) + set toolCallsExecuted($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasToolCallsExecuted() => $_has(2); + @$pb.TagNumber(3) + void clearToolCallsExecuted() => $_clearField(3); + + @$pb.TagNumber(4) + $core.int get toolCallsAutoApproved => $_getIZ(3); + @$pb.TagNumber(4) + set toolCallsAutoApproved($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasToolCallsAutoApproved() => $_has(3); + @$pb.TagNumber(4) + void clearToolCallsAutoApproved() => $_clearField(4); +} + +/// ListSubagentsRequest lists subagents under a parent session. +class ListSubagentsRequest extends $pb.GeneratedMessage { + factory ListSubagentsRequest({ + $core.String? parentSessionId, + $core.String? statusFilter, + }) { + final result = create(); + if (parentSessionId != null) result.parentSessionId = parentSessionId; + if (statusFilter != null) result.statusFilter = statusFilter; + return result; + } + + ListSubagentsRequest._(); + + factory ListSubagentsRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ListSubagentsRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ListSubagentsRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'parentSessionId') + ..aOS(2, _omitFieldNames ? '' : 'statusFilter') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ListSubagentsRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ListSubagentsRequest copyWith(void Function(ListSubagentsRequest) updates) => + super.copyWith((message) => updates(message as ListSubagentsRequest)) + as ListSubagentsRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ListSubagentsRequest create() => ListSubagentsRequest._(); + @$core.override + ListSubagentsRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ListSubagentsRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ListSubagentsRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get parentSessionId => $_getSZ(0); + @$pb.TagNumber(1) + set parentSessionId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasParentSessionId() => $_has(0); + @$pb.TagNumber(1) + void clearParentSessionId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get statusFilter => $_getSZ(1); + @$pb.TagNumber(2) + set statusFilter($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasStatusFilter() => $_has(1); + @$pb.TagNumber(2) + void clearStatusFilter() => $_clearField(2); +} + +/// ListSubagentsResponse returns matching subagents. +class ListSubagentsResponse extends $pb.GeneratedMessage { + factory ListSubagentsResponse({ + $core.Iterable? subagents, + }) { + final result = create(); + if (subagents != null) result.subagents.addAll(subagents); + return result; + } + + ListSubagentsResponse._(); + + factory ListSubagentsResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ListSubagentsResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ListSubagentsResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..pPM(1, _omitFieldNames ? '' : 'subagents', + subBuilder: SubagentInfo.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ListSubagentsResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ListSubagentsResponse copyWith( + void Function(ListSubagentsResponse) updates) => + super.copyWith((message) => updates(message as ListSubagentsResponse)) + as ListSubagentsResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ListSubagentsResponse create() => ListSubagentsResponse._(); + @$core.override + ListSubagentsResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ListSubagentsResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ListSubagentsResponse? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList get subagents => $_getList(0); +} + +/// SubagentInfo describes a subagent's current state. +class SubagentInfo extends $pb.GeneratedMessage { + factory SubagentInfo({ + $core.String? id, + $core.String? parentSessionId, + $core.String? sessionId, + $core.String? name, + $core.String? prompt, + $core.String? model, + $core.String? workingDirectory, + SubagentStatus? status, + $core.bool? autoApprove, + $core.int? maxTurns, + $core.Iterable<$core.String>? allowedTools, + $core.String? resultSummary, + $1.Timestamp? createdAt, + $1.Timestamp? completedAt, + }) { + final result = create(); + if (id != null) result.id = id; + if (parentSessionId != null) result.parentSessionId = parentSessionId; + if (sessionId != null) result.sessionId = sessionId; + if (name != null) result.name = name; + if (prompt != null) result.prompt = prompt; + if (model != null) result.model = model; + if (workingDirectory != null) result.workingDirectory = workingDirectory; + if (status != null) result.status = status; + if (autoApprove != null) result.autoApprove = autoApprove; + if (maxTurns != null) result.maxTurns = maxTurns; + if (allowedTools != null) result.allowedTools.addAll(allowedTools); + if (resultSummary != null) result.resultSummary = resultSummary; + if (createdAt != null) result.createdAt = createdAt; + if (completedAt != null) result.completedAt = completedAt; + return result; + } + + SubagentInfo._(); + + factory SubagentInfo.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentInfo.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentInfo', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'id') + ..aOS(2, _omitFieldNames ? '' : 'parentSessionId') + ..aOS(3, _omitFieldNames ? '' : 'sessionId') + ..aOS(4, _omitFieldNames ? '' : 'name') + ..aOS(5, _omitFieldNames ? '' : 'prompt') + ..aOS(6, _omitFieldNames ? '' : 'model') + ..aOS(7, _omitFieldNames ? '' : 'workingDirectory') + ..aE(8, _omitFieldNames ? '' : 'status', + enumValues: SubagentStatus.values) + ..aOB(9, _omitFieldNames ? '' : 'autoApprove') + ..aI(10, _omitFieldNames ? '' : 'maxTurns') + ..pPS(11, _omitFieldNames ? '' : 'allowedTools') + ..aOS(12, _omitFieldNames ? '' : 'resultSummary') + ..aOM<$1.Timestamp>(13, _omitFieldNames ? '' : 'createdAt', + subBuilder: $1.Timestamp.create) + ..aOM<$1.Timestamp>(14, _omitFieldNames ? '' : 'completedAt', + subBuilder: $1.Timestamp.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentInfo clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentInfo copyWith(void Function(SubagentInfo) updates) => + super.copyWith((message) => updates(message as SubagentInfo)) + as SubagentInfo; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentInfo create() => SubagentInfo._(); + @$core.override + SubagentInfo createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get id => $_getSZ(0); + @$pb.TagNumber(1) + set id($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get parentSessionId => $_getSZ(1); + @$pb.TagNumber(2) + set parentSessionId($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasParentSessionId() => $_has(1); + @$pb.TagNumber(2) + void clearParentSessionId() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get sessionId => $_getSZ(2); + @$pb.TagNumber(3) + set sessionId($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasSessionId() => $_has(2); + @$pb.TagNumber(3) + void clearSessionId() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get name => $_getSZ(3); + @$pb.TagNumber(4) + set name($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasName() => $_has(3); + @$pb.TagNumber(4) + void clearName() => $_clearField(4); + + @$pb.TagNumber(5) + $core.String get prompt => $_getSZ(4); + @$pb.TagNumber(5) + set prompt($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasPrompt() => $_has(4); + @$pb.TagNumber(5) + void clearPrompt() => $_clearField(5); + + @$pb.TagNumber(6) + $core.String get model => $_getSZ(5); + @$pb.TagNumber(6) + set model($core.String value) => $_setString(5, value); + @$pb.TagNumber(6) + $core.bool hasModel() => $_has(5); + @$pb.TagNumber(6) + void clearModel() => $_clearField(6); + + @$pb.TagNumber(7) + $core.String get workingDirectory => $_getSZ(6); + @$pb.TagNumber(7) + set workingDirectory($core.String value) => $_setString(6, value); + @$pb.TagNumber(7) + $core.bool hasWorkingDirectory() => $_has(6); + @$pb.TagNumber(7) + void clearWorkingDirectory() => $_clearField(7); + + @$pb.TagNumber(8) + SubagentStatus get status => $_getN(7); + @$pb.TagNumber(8) + set status(SubagentStatus value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasStatus() => $_has(7); + @$pb.TagNumber(8) + void clearStatus() => $_clearField(8); + + @$pb.TagNumber(9) + $core.bool get autoApprove => $_getBF(8); + @$pb.TagNumber(9) + set autoApprove($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasAutoApprove() => $_has(8); + @$pb.TagNumber(9) + void clearAutoApprove() => $_clearField(9); + + @$pb.TagNumber(10) + $core.int get maxTurns => $_getIZ(9); + @$pb.TagNumber(10) + set maxTurns($core.int value) => $_setSignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasMaxTurns() => $_has(9); + @$pb.TagNumber(10) + void clearMaxTurns() => $_clearField(10); + + @$pb.TagNumber(11) + $pb.PbList<$core.String> get allowedTools => $_getList(10); + + @$pb.TagNumber(12) + $core.String get resultSummary => $_getSZ(11); + @$pb.TagNumber(12) + set resultSummary($core.String value) => $_setString(11, value); + @$pb.TagNumber(12) + $core.bool hasResultSummary() => $_has(11); + @$pb.TagNumber(12) + void clearResultSummary() => $_clearField(12); + + @$pb.TagNumber(13) + $1.Timestamp get createdAt => $_getN(12); + @$pb.TagNumber(13) + set createdAt($1.Timestamp value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasCreatedAt() => $_has(12); + @$pb.TagNumber(13) + void clearCreatedAt() => $_clearField(13); + @$pb.TagNumber(13) + $1.Timestamp ensureCreatedAt() => $_ensure(12); + + @$pb.TagNumber(14) + $1.Timestamp get completedAt => $_getN(13); + @$pb.TagNumber(14) + set completedAt($1.Timestamp value) => $_setField(14, value); + @$pb.TagNumber(14) + $core.bool hasCompletedAt() => $_has(13); + @$pb.TagNumber(14) + void clearCompletedAt() => $_clearField(14); + @$pb.TagNumber(14) + $1.Timestamp ensureCompletedAt() => $_ensure(13); +} + +enum SubagentEvent_Event { + started, + output, + toolUse, + permissionRequest, + completed, + failed, + cancelled, + notSet +} + +/// SubagentEvent wraps all subagent-to-client event types. +class SubagentEvent extends $pb.GeneratedMessage { + factory SubagentEvent({ + $core.String? subagentId, + $1.Timestamp? timestamp, + SubagentStarted? started, + SubagentOutput? output, + SubagentToolUse? toolUse, + SubagentPermissionRequest? permissionRequest, + SubagentCompleted? completed, + SubagentFailed? failed, + SubagentCancelled? cancelled, + }) { + final result = create(); + if (subagentId != null) result.subagentId = subagentId; + if (timestamp != null) result.timestamp = timestamp; + if (started != null) result.started = started; + if (output != null) result.output = output; + if (toolUse != null) result.toolUse = toolUse; + if (permissionRequest != null) result.permissionRequest = permissionRequest; + if (completed != null) result.completed = completed; + if (failed != null) result.failed = failed; + if (cancelled != null) result.cancelled = cancelled; + return result; + } + + SubagentEvent._(); + + factory SubagentEvent.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentEvent.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, SubagentEvent_Event> + _SubagentEvent_EventByTag = { + 3: SubagentEvent_Event.started, + 4: SubagentEvent_Event.output, + 5: SubagentEvent_Event.toolUse, + 6: SubagentEvent_Event.permissionRequest, + 7: SubagentEvent_Event.completed, + 8: SubagentEvent_Event.failed, + 9: SubagentEvent_Event.cancelled, + 0: SubagentEvent_Event.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentEvent', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..oo(0, [3, 4, 5, 6, 7, 8, 9]) + ..aOS(1, _omitFieldNames ? '' : 'subagentId') + ..aOM<$1.Timestamp>(2, _omitFieldNames ? '' : 'timestamp', + subBuilder: $1.Timestamp.create) + ..aOM(3, _omitFieldNames ? '' : 'started', + subBuilder: SubagentStarted.create) + ..aOM(4, _omitFieldNames ? '' : 'output', + subBuilder: SubagentOutput.create) + ..aOM(5, _omitFieldNames ? '' : 'toolUse', + subBuilder: SubagentToolUse.create) + ..aOM( + 6, _omitFieldNames ? '' : 'permissionRequest', + subBuilder: SubagentPermissionRequest.create) + ..aOM(7, _omitFieldNames ? '' : 'completed', + subBuilder: SubagentCompleted.create) + ..aOM(8, _omitFieldNames ? '' : 'failed', + subBuilder: SubagentFailed.create) + ..aOM(9, _omitFieldNames ? '' : 'cancelled', + subBuilder: SubagentCancelled.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentEvent clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentEvent copyWith(void Function(SubagentEvent) updates) => + super.copyWith((message) => updates(message as SubagentEvent)) + as SubagentEvent; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentEvent create() => SubagentEvent._(); + @$core.override + SubagentEvent createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentEvent getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentEvent? _defaultInstance; + + @$pb.TagNumber(3) + @$pb.TagNumber(4) + @$pb.TagNumber(5) + @$pb.TagNumber(6) + @$pb.TagNumber(7) + @$pb.TagNumber(8) + @$pb.TagNumber(9) + SubagentEvent_Event whichEvent() => + _SubagentEvent_EventByTag[$_whichOneof(0)]!; + @$pb.TagNumber(3) + @$pb.TagNumber(4) + @$pb.TagNumber(5) + @$pb.TagNumber(6) + @$pb.TagNumber(7) + @$pb.TagNumber(8) + @$pb.TagNumber(9) + void clearEvent() => $_clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.String get subagentId => $_getSZ(0); + @$pb.TagNumber(1) + set subagentId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSubagentId() => $_has(0); + @$pb.TagNumber(1) + void clearSubagentId() => $_clearField(1); + + @$pb.TagNumber(2) + $1.Timestamp get timestamp => $_getN(1); + @$pb.TagNumber(2) + set timestamp($1.Timestamp value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasTimestamp() => $_has(1); + @$pb.TagNumber(2) + void clearTimestamp() => $_clearField(2); + @$pb.TagNumber(2) + $1.Timestamp ensureTimestamp() => $_ensure(1); + + @$pb.TagNumber(3) + SubagentStarted get started => $_getN(2); + @$pb.TagNumber(3) + set started(SubagentStarted value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasStarted() => $_has(2); + @$pb.TagNumber(3) + void clearStarted() => $_clearField(3); + @$pb.TagNumber(3) + SubagentStarted ensureStarted() => $_ensure(2); + + @$pb.TagNumber(4) + SubagentOutput get output => $_getN(3); + @$pb.TagNumber(4) + set output(SubagentOutput value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasOutput() => $_has(3); + @$pb.TagNumber(4) + void clearOutput() => $_clearField(4); + @$pb.TagNumber(4) + SubagentOutput ensureOutput() => $_ensure(3); + + @$pb.TagNumber(5) + SubagentToolUse get toolUse => $_getN(4); + @$pb.TagNumber(5) + set toolUse(SubagentToolUse value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasToolUse() => $_has(4); + @$pb.TagNumber(5) + void clearToolUse() => $_clearField(5); + @$pb.TagNumber(5) + SubagentToolUse ensureToolUse() => $_ensure(4); + + @$pb.TagNumber(6) + SubagentPermissionRequest get permissionRequest => $_getN(5); + @$pb.TagNumber(6) + set permissionRequest(SubagentPermissionRequest value) => + $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasPermissionRequest() => $_has(5); + @$pb.TagNumber(6) + void clearPermissionRequest() => $_clearField(6); + @$pb.TagNumber(6) + SubagentPermissionRequest ensurePermissionRequest() => $_ensure(5); + + @$pb.TagNumber(7) + SubagentCompleted get completed => $_getN(6); + @$pb.TagNumber(7) + set completed(SubagentCompleted value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasCompleted() => $_has(6); + @$pb.TagNumber(7) + void clearCompleted() => $_clearField(7); + @$pb.TagNumber(7) + SubagentCompleted ensureCompleted() => $_ensure(6); + + @$pb.TagNumber(8) + SubagentFailed get failed => $_getN(7); + @$pb.TagNumber(8) + set failed(SubagentFailed value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasFailed() => $_has(7); + @$pb.TagNumber(8) + void clearFailed() => $_clearField(8); + @$pb.TagNumber(8) + SubagentFailed ensureFailed() => $_ensure(7); + + @$pb.TagNumber(9) + SubagentCancelled get cancelled => $_getN(8); + @$pb.TagNumber(9) + set cancelled(SubagentCancelled value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasCancelled() => $_has(8); + @$pb.TagNumber(9) + void clearCancelled() => $_clearField(9); + @$pb.TagNumber(9) + SubagentCancelled ensureCancelled() => $_ensure(8); +} + +/// SubagentStarted indicates the subagent process has started. +class SubagentStarted extends $pb.GeneratedMessage { + factory SubagentStarted({ + $core.String? sessionId, + $core.String? model, + }) { + final result = create(); + if (sessionId != null) result.sessionId = sessionId; + if (model != null) result.model = model; + return result; + } + + SubagentStarted._(); + + factory SubagentStarted.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentStarted.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentStarted', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'sessionId') + ..aOS(2, _omitFieldNames ? '' : 'model') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentStarted clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentStarted copyWith(void Function(SubagentStarted) updates) => + super.copyWith((message) => updates(message as SubagentStarted)) + as SubagentStarted; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentStarted create() => SubagentStarted._(); + @$core.override + SubagentStarted createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentStarted getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentStarted? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get sessionId => $_getSZ(0); + @$pb.TagNumber(1) + set sessionId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSessionId() => $_has(0); + @$pb.TagNumber(1) + void clearSessionId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get model => $_getSZ(1); + @$pb.TagNumber(2) + set model($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasModel() => $_has(1); + @$pb.TagNumber(2) + void clearModel() => $_clearField(2); +} + +/// SubagentOutput streams incremental text output from the subagent. +class SubagentOutput extends $pb.GeneratedMessage { + factory SubagentOutput({ + $core.String? text, + $core.bool? isComplete, + }) { + final result = create(); + if (text != null) result.text = text; + if (isComplete != null) result.isComplete = isComplete; + return result; + } + + SubagentOutput._(); + + factory SubagentOutput.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentOutput.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentOutput', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'text') + ..aOB(2, _omitFieldNames ? '' : 'isComplete') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentOutput clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentOutput copyWith(void Function(SubagentOutput) updates) => + super.copyWith((message) => updates(message as SubagentOutput)) + as SubagentOutput; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentOutput create() => SubagentOutput._(); + @$core.override + SubagentOutput createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentOutput getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentOutput? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get text => $_getSZ(0); + @$pb.TagNumber(1) + set text($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasText() => $_has(0); + @$pb.TagNumber(1) + void clearText() => $_clearField(1); + + @$pb.TagNumber(2) + $core.bool get isComplete => $_getBF(1); + @$pb.TagNumber(2) + set isComplete($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasIsComplete() => $_has(1); + @$pb.TagNumber(2) + void clearIsComplete() => $_clearField(2); +} + +/// SubagentToolUse indicates the subagent is invoking a tool. +class SubagentToolUse extends $pb.GeneratedMessage { + factory SubagentToolUse({ + $core.String? toolId, + $core.String? toolName, + $core.String? description, + }) { + final result = create(); + if (toolId != null) result.toolId = toolId; + if (toolName != null) result.toolName = toolName; + if (description != null) result.description = description; + return result; + } + + SubagentToolUse._(); + + factory SubagentToolUse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentToolUse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentToolUse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'toolId') + ..aOS(2, _omitFieldNames ? '' : 'toolName') + ..aOS(3, _omitFieldNames ? '' : 'description') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentToolUse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentToolUse copyWith(void Function(SubagentToolUse) updates) => + super.copyWith((message) => updates(message as SubagentToolUse)) + as SubagentToolUse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentToolUse create() => SubagentToolUse._(); + @$core.override + SubagentToolUse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentToolUse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentToolUse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get toolId => $_getSZ(0); + @$pb.TagNumber(1) + set toolId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasToolId() => $_has(0); + @$pb.TagNumber(1) + void clearToolId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get toolName => $_getSZ(1); + @$pb.TagNumber(2) + set toolName($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasToolName() => $_has(1); + @$pb.TagNumber(2) + void clearToolName() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get description => $_getSZ(2); + @$pb.TagNumber(3) + set description($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasDescription() => $_has(2); + @$pb.TagNumber(3) + void clearDescription() => $_clearField(3); +} + +/// SubagentPermissionRequest forwards a permission request from the subagent. +class SubagentPermissionRequest extends $pb.GeneratedMessage { + factory SubagentPermissionRequest({ + $core.String? requestId, + $core.String? toolName, + $core.String? description, + }) { + final result = create(); + if (requestId != null) result.requestId = requestId; + if (toolName != null) result.toolName = toolName; + if (description != null) result.description = description; + return result; + } + + SubagentPermissionRequest._(); + + factory SubagentPermissionRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentPermissionRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentPermissionRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'requestId') + ..aOS(2, _omitFieldNames ? '' : 'toolName') + ..aOS(3, _omitFieldNames ? '' : 'description') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentPermissionRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentPermissionRequest copyWith( + void Function(SubagentPermissionRequest) updates) => + super.copyWith((message) => updates(message as SubagentPermissionRequest)) + as SubagentPermissionRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentPermissionRequest create() => SubagentPermissionRequest._(); + @$core.override + SubagentPermissionRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentPermissionRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentPermissionRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get requestId => $_getSZ(0); + @$pb.TagNumber(1) + set requestId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasRequestId() => $_has(0); + @$pb.TagNumber(1) + void clearRequestId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get toolName => $_getSZ(1); + @$pb.TagNumber(2) + set toolName($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasToolName() => $_has(1); + @$pb.TagNumber(2) + void clearToolName() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get description => $_getSZ(2); + @$pb.TagNumber(3) + set description($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasDescription() => $_has(2); + @$pb.TagNumber(3) + void clearDescription() => $_clearField(3); +} + +/// SubagentCompleted indicates the subagent finished successfully. +class SubagentCompleted extends $pb.GeneratedMessage { + factory SubagentCompleted({ + $core.int? exitCode, + $core.String? resultSummary, + }) { + final result = create(); + if (exitCode != null) result.exitCode = exitCode; + if (resultSummary != null) result.resultSummary = resultSummary; + return result; + } + + SubagentCompleted._(); + + factory SubagentCompleted.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentCompleted.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentCompleted', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aI(1, _omitFieldNames ? '' : 'exitCode') + ..aOS(2, _omitFieldNames ? '' : 'resultSummary') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentCompleted clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentCompleted copyWith(void Function(SubagentCompleted) updates) => + super.copyWith((message) => updates(message as SubagentCompleted)) + as SubagentCompleted; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentCompleted create() => SubagentCompleted._(); + @$core.override + SubagentCompleted createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentCompleted getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentCompleted? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get exitCode => $_getIZ(0); + @$pb.TagNumber(1) + set exitCode($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasExitCode() => $_has(0); + @$pb.TagNumber(1) + void clearExitCode() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get resultSummary => $_getSZ(1); + @$pb.TagNumber(2) + set resultSummary($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasResultSummary() => $_has(1); + @$pb.TagNumber(2) + void clearResultSummary() => $_clearField(2); +} + +/// SubagentFailed indicates the subagent exited with an error. +class SubagentFailed extends $pb.GeneratedMessage { + factory SubagentFailed({ + $core.int? exitCode, + $core.String? errorMessage, + }) { + final result = create(); + if (exitCode != null) result.exitCode = exitCode; + if (errorMessage != null) result.errorMessage = errorMessage; + return result; + } + + SubagentFailed._(); + + factory SubagentFailed.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentFailed.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentFailed', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aI(1, _omitFieldNames ? '' : 'exitCode') + ..aOS(2, _omitFieldNames ? '' : 'errorMessage') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentFailed clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentFailed copyWith(void Function(SubagentFailed) updates) => + super.copyWith((message) => updates(message as SubagentFailed)) + as SubagentFailed; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentFailed create() => SubagentFailed._(); + @$core.override + SubagentFailed createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentFailed getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentFailed? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get exitCode => $_getIZ(0); + @$pb.TagNumber(1) + set exitCode($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasExitCode() => $_has(0); + @$pb.TagNumber(1) + void clearExitCode() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get errorMessage => $_getSZ(1); + @$pb.TagNumber(2) + set errorMessage($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasErrorMessage() => $_has(1); + @$pb.TagNumber(2) + void clearErrorMessage() => $_clearField(2); +} + +/// SubagentCancelled indicates the subagent was cancelled. +class SubagentCancelled extends $pb.GeneratedMessage { + factory SubagentCancelled({ + $core.String? reason, + }) { + final result = create(); + if (reason != null) result.reason = reason; + return result; + } + + SubagentCancelled._(); + + factory SubagentCancelled.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SubagentCancelled.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SubagentCancelled', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'reason') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentCancelled clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SubagentCancelled copyWith(void Function(SubagentCancelled) updates) => + super.copyWith((message) => updates(message as SubagentCancelled)) + as SubagentCancelled; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SubagentCancelled create() => SubagentCancelled._(); + @$core.override + SubagentCancelled createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static SubagentCancelled getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SubagentCancelled? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get reason => $_getSZ(0); + @$pb.TagNumber(1) + set reason($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasReason() => $_has(0); + @$pb.TagNumber(1) + void clearReason() => $_clearField(1); +} + +/// RevokeAutoApproveRequest revokes auto-approve on a running subagent. +class RevokeAutoApproveRequest extends $pb.GeneratedMessage { + factory RevokeAutoApproveRequest({ + $core.String? subagentId, + $core.String? reason, + $core.bool? terminateIfPending, + }) { + final result = create(); + if (subagentId != null) result.subagentId = subagentId; + if (reason != null) result.reason = reason; + if (terminateIfPending != null) + result.terminateIfPending = terminateIfPending; + return result; + } + + RevokeAutoApproveRequest._(); + + factory RevokeAutoApproveRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RevokeAutoApproveRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RevokeAutoApproveRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'subagentId') + ..aOS(2, _omitFieldNames ? '' : 'reason') + ..aOB(3, _omitFieldNames ? '' : 'terminateIfPending') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RevokeAutoApproveRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RevokeAutoApproveRequest copyWith( + void Function(RevokeAutoApproveRequest) updates) => + super.copyWith((message) => updates(message as RevokeAutoApproveRequest)) + as RevokeAutoApproveRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RevokeAutoApproveRequest create() => RevokeAutoApproveRequest._(); + @$core.override + RevokeAutoApproveRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static RevokeAutoApproveRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RevokeAutoApproveRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get subagentId => $_getSZ(0); + @$pb.TagNumber(1) + set subagentId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasSubagentId() => $_has(0); + @$pb.TagNumber(1) + void clearSubagentId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get reason => $_getSZ(1); + @$pb.TagNumber(2) + set reason($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasReason() => $_has(1); + @$pb.TagNumber(2) + void clearReason() => $_clearField(2); + + @$pb.TagNumber(3) + $core.bool get terminateIfPending => $_getBF(2); + @$pb.TagNumber(3) + set terminateIfPending($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasTerminateIfPending() => $_has(2); + @$pb.TagNumber(3) + void clearTerminateIfPending() => $_clearField(3); +} + +/// RevokeAutoApproveResponse returns the result of revocation. +class RevokeAutoApproveResponse extends $pb.GeneratedMessage { + factory RevokeAutoApproveResponse({ + $core.bool? revoked, + $core.int? pendingToolCalls, + $core.String? subagentStatus, + }) { + final result = create(); + if (revoked != null) result.revoked = revoked; + if (pendingToolCalls != null) result.pendingToolCalls = pendingToolCalls; + if (subagentStatus != null) result.subagentStatus = subagentStatus; + return result; + } + + RevokeAutoApproveResponse._(); + + factory RevokeAutoApproveResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RevokeAutoApproveResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RevokeAutoApproveResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'revoked') + ..aI(2, _omitFieldNames ? '' : 'pendingToolCalls') + ..aOS(3, _omitFieldNames ? '' : 'subagentStatus') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RevokeAutoApproveResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RevokeAutoApproveResponse copyWith( + void Function(RevokeAutoApproveResponse) updates) => + super.copyWith((message) => updates(message as RevokeAutoApproveResponse)) + as RevokeAutoApproveResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RevokeAutoApproveResponse create() => RevokeAutoApproveResponse._(); + @$core.override + RevokeAutoApproveResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static RevokeAutoApproveResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RevokeAutoApproveResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get revoked => $_getBF(0); + @$pb.TagNumber(1) + set revoked($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasRevoked() => $_has(0); + @$pb.TagNumber(1) + void clearRevoked() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get pendingToolCalls => $_getIZ(1); + @$pb.TagNumber(2) + set pendingToolCalls($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasPendingToolCalls() => $_has(1); + @$pb.TagNumber(2) + void clearPendingToolCalls() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get subagentStatus => $_getSZ(2); + @$pb.TagNumber(3) + set subagentStatus($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasSubagentStatus() => $_has(2); + @$pb.TagNumber(3) + void clearSubagentStatus() => $_clearField(3); +} + +/// CreateOrchestrationRequest creates a multi-step orchestration plan. +class CreateOrchestrationRequest extends $pb.GeneratedMessage { + factory CreateOrchestrationRequest({ + $core.String? parentSessionId, + $core.Iterable? steps, + OrchestrationStrategy? strategy, + }) { + final result = create(); + if (parentSessionId != null) result.parentSessionId = parentSessionId; + if (steps != null) result.steps.addAll(steps); + if (strategy != null) result.strategy = strategy; + return result; + } + + CreateOrchestrationRequest._(); + + factory CreateOrchestrationRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory CreateOrchestrationRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'CreateOrchestrationRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'parentSessionId') + ..pPM(2, _omitFieldNames ? '' : 'steps', + subBuilder: OrchestrationStep.create) + ..aE(3, _omitFieldNames ? '' : 'strategy', + enumValues: OrchestrationStrategy.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CreateOrchestrationRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CreateOrchestrationRequest copyWith( + void Function(CreateOrchestrationRequest) updates) => + super.copyWith( + (message) => updates(message as CreateOrchestrationRequest)) + as CreateOrchestrationRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateOrchestrationRequest create() => CreateOrchestrationRequest._(); + @$core.override + CreateOrchestrationRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static CreateOrchestrationRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static CreateOrchestrationRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get parentSessionId => $_getSZ(0); + @$pb.TagNumber(1) + set parentSessionId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasParentSessionId() => $_has(0); + @$pb.TagNumber(1) + void clearParentSessionId() => $_clearField(1); + + @$pb.TagNumber(2) + $pb.PbList get steps => $_getList(1); + + @$pb.TagNumber(3) + OrchestrationStrategy get strategy => $_getN(2); + @$pb.TagNumber(3) + set strategy(OrchestrationStrategy value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasStrategy() => $_has(2); + @$pb.TagNumber(3) + void clearStrategy() => $_clearField(3); +} + +/// CreateOrchestrationResponse returns the orchestration ID. +class CreateOrchestrationResponse extends $pb.GeneratedMessage { + factory CreateOrchestrationResponse({ + $core.String? orchestrationId, + $core.int? totalSteps, + }) { + final result = create(); + if (orchestrationId != null) result.orchestrationId = orchestrationId; + if (totalSteps != null) result.totalSteps = totalSteps; + return result; + } + + CreateOrchestrationResponse._(); + + factory CreateOrchestrationResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory CreateOrchestrationResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'CreateOrchestrationResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'orchestrationId') + ..aI(2, _omitFieldNames ? '' : 'totalSteps') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CreateOrchestrationResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CreateOrchestrationResponse copyWith( + void Function(CreateOrchestrationResponse) updates) => + super.copyWith( + (message) => updates(message as CreateOrchestrationResponse)) + as CreateOrchestrationResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CreateOrchestrationResponse create() => + CreateOrchestrationResponse._(); + @$core.override + CreateOrchestrationResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static CreateOrchestrationResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static CreateOrchestrationResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get orchestrationId => $_getSZ(0); + @$pb.TagNumber(1) + set orchestrationId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasOrchestrationId() => $_has(0); + @$pb.TagNumber(1) + void clearOrchestrationId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get totalSteps => $_getIZ(1); + @$pb.TagNumber(2) + set totalSteps($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasTotalSteps() => $_has(1); + @$pb.TagNumber(2) + void clearTotalSteps() => $_clearField(2); +} + +/// OrchestrationStep defines a single step in an orchestration plan. +class OrchestrationStep extends $pb.GeneratedMessage { + factory OrchestrationStep({ + $core.String? id, + $core.String? name, + $core.String? prompt, + $core.String? model, + $core.String? workingDirectory, + $core.Iterable<$core.String>? allowedTools, + $core.Iterable<$core.String>? dependsOn, + $core.int? maxTurns, + $core.bool? autoApprove, + }) { + final result = create(); + if (id != null) result.id = id; + if (name != null) result.name = name; + if (prompt != null) result.prompt = prompt; + if (model != null) result.model = model; + if (workingDirectory != null) result.workingDirectory = workingDirectory; + if (allowedTools != null) result.allowedTools.addAll(allowedTools); + if (dependsOn != null) result.dependsOn.addAll(dependsOn); + if (maxTurns != null) result.maxTurns = maxTurns; + if (autoApprove != null) result.autoApprove = autoApprove; + return result; + } + + OrchestrationStep._(); + + factory OrchestrationStep.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory OrchestrationStep.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'OrchestrationStep', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'id') + ..aOS(2, _omitFieldNames ? '' : 'name') + ..aOS(3, _omitFieldNames ? '' : 'prompt') + ..aOS(4, _omitFieldNames ? '' : 'model') + ..aOS(5, _omitFieldNames ? '' : 'workingDirectory') + ..pPS(6, _omitFieldNames ? '' : 'allowedTools') + ..pPS(7, _omitFieldNames ? '' : 'dependsOn') + ..aI(8, _omitFieldNames ? '' : 'maxTurns') + ..aOB(9, _omitFieldNames ? '' : 'autoApprove') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationStep clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationStep copyWith(void Function(OrchestrationStep) updates) => + super.copyWith((message) => updates(message as OrchestrationStep)) + as OrchestrationStep; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static OrchestrationStep create() => OrchestrationStep._(); + @$core.override + OrchestrationStep createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static OrchestrationStep getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static OrchestrationStep? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get id => $_getSZ(0); + @$pb.TagNumber(1) + set id($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get prompt => $_getSZ(2); + @$pb.TagNumber(3) + set prompt($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasPrompt() => $_has(2); + @$pb.TagNumber(3) + void clearPrompt() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get model => $_getSZ(3); + @$pb.TagNumber(4) + set model($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasModel() => $_has(3); + @$pb.TagNumber(4) + void clearModel() => $_clearField(4); + + @$pb.TagNumber(5) + $core.String get workingDirectory => $_getSZ(4); + @$pb.TagNumber(5) + set workingDirectory($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasWorkingDirectory() => $_has(4); + @$pb.TagNumber(5) + void clearWorkingDirectory() => $_clearField(5); + + @$pb.TagNumber(6) + $pb.PbList<$core.String> get allowedTools => $_getList(5); + + @$pb.TagNumber(7) + $pb.PbList<$core.String> get dependsOn => $_getList(6); + + @$pb.TagNumber(8) + $core.int get maxTurns => $_getIZ(7); + @$pb.TagNumber(8) + set maxTurns($core.int value) => $_setSignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasMaxTurns() => $_has(7); + @$pb.TagNumber(8) + void clearMaxTurns() => $_clearField(8); + + @$pb.TagNumber(9) + $core.bool get autoApprove => $_getBF(8); + @$pb.TagNumber(9) + set autoApprove($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasAutoApprove() => $_has(8); + @$pb.TagNumber(9) + void clearAutoApprove() => $_clearField(9); +} + +/// WatchOrchestrationRequest subscribes to orchestration progress events. +class WatchOrchestrationRequest extends $pb.GeneratedMessage { + factory WatchOrchestrationRequest({ + $core.String? orchestrationId, + }) { + final result = create(); + if (orchestrationId != null) result.orchestrationId = orchestrationId; + return result; + } + + WatchOrchestrationRequest._(); + + factory WatchOrchestrationRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WatchOrchestrationRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WatchOrchestrationRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'orchestrationId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WatchOrchestrationRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WatchOrchestrationRequest copyWith( + void Function(WatchOrchestrationRequest) updates) => + super.copyWith((message) => updates(message as WatchOrchestrationRequest)) + as WatchOrchestrationRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WatchOrchestrationRequest create() => WatchOrchestrationRequest._(); + @$core.override + WatchOrchestrationRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WatchOrchestrationRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WatchOrchestrationRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get orchestrationId => $_getSZ(0); + @$pb.TagNumber(1) + set orchestrationId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasOrchestrationId() => $_has(0); + @$pb.TagNumber(1) + void clearOrchestrationId() => $_clearField(1); +} + +enum OrchestrationEvent_Event { + stepStarted, + stepCompleted, + stepFailed, + completed, + failed, + notSet +} + +/// OrchestrationEvent wraps orchestration progress events. +class OrchestrationEvent extends $pb.GeneratedMessage { + factory OrchestrationEvent({ + $core.String? orchestrationId, + $1.Timestamp? timestamp, + StepStarted? stepStarted, + StepCompleted? stepCompleted, + StepFailed? stepFailed, + OrchestrationCompleted? completed, + OrchestrationFailed? failed, + }) { + final result = create(); + if (orchestrationId != null) result.orchestrationId = orchestrationId; + if (timestamp != null) result.timestamp = timestamp; + if (stepStarted != null) result.stepStarted = stepStarted; + if (stepCompleted != null) result.stepCompleted = stepCompleted; + if (stepFailed != null) result.stepFailed = stepFailed; + if (completed != null) result.completed = completed; + if (failed != null) result.failed = failed; + return result; + } + + OrchestrationEvent._(); + + factory OrchestrationEvent.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory OrchestrationEvent.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, OrchestrationEvent_Event> + _OrchestrationEvent_EventByTag = { + 3: OrchestrationEvent_Event.stepStarted, + 4: OrchestrationEvent_Event.stepCompleted, + 5: OrchestrationEvent_Event.stepFailed, + 6: OrchestrationEvent_Event.completed, + 7: OrchestrationEvent_Event.failed, + 0: OrchestrationEvent_Event.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'OrchestrationEvent', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..oo(0, [3, 4, 5, 6, 7]) + ..aOS(1, _omitFieldNames ? '' : 'orchestrationId') + ..aOM<$1.Timestamp>(2, _omitFieldNames ? '' : 'timestamp', + subBuilder: $1.Timestamp.create) + ..aOM(3, _omitFieldNames ? '' : 'stepStarted', + subBuilder: StepStarted.create) + ..aOM(4, _omitFieldNames ? '' : 'stepCompleted', + subBuilder: StepCompleted.create) + ..aOM(5, _omitFieldNames ? '' : 'stepFailed', + subBuilder: StepFailed.create) + ..aOM(6, _omitFieldNames ? '' : 'completed', + subBuilder: OrchestrationCompleted.create) + ..aOM(7, _omitFieldNames ? '' : 'failed', + subBuilder: OrchestrationFailed.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationEvent clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationEvent copyWith(void Function(OrchestrationEvent) updates) => + super.copyWith((message) => updates(message as OrchestrationEvent)) + as OrchestrationEvent; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static OrchestrationEvent create() => OrchestrationEvent._(); + @$core.override + OrchestrationEvent createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static OrchestrationEvent getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static OrchestrationEvent? _defaultInstance; + + @$pb.TagNumber(3) + @$pb.TagNumber(4) + @$pb.TagNumber(5) + @$pb.TagNumber(6) + @$pb.TagNumber(7) + OrchestrationEvent_Event whichEvent() => + _OrchestrationEvent_EventByTag[$_whichOneof(0)]!; + @$pb.TagNumber(3) + @$pb.TagNumber(4) + @$pb.TagNumber(5) + @$pb.TagNumber(6) + @$pb.TagNumber(7) + void clearEvent() => $_clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.String get orchestrationId => $_getSZ(0); + @$pb.TagNumber(1) + set orchestrationId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasOrchestrationId() => $_has(0); + @$pb.TagNumber(1) + void clearOrchestrationId() => $_clearField(1); + + @$pb.TagNumber(2) + $1.Timestamp get timestamp => $_getN(1); + @$pb.TagNumber(2) + set timestamp($1.Timestamp value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasTimestamp() => $_has(1); + @$pb.TagNumber(2) + void clearTimestamp() => $_clearField(2); + @$pb.TagNumber(2) + $1.Timestamp ensureTimestamp() => $_ensure(1); + + @$pb.TagNumber(3) + StepStarted get stepStarted => $_getN(2); + @$pb.TagNumber(3) + set stepStarted(StepStarted value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasStepStarted() => $_has(2); + @$pb.TagNumber(3) + void clearStepStarted() => $_clearField(3); + @$pb.TagNumber(3) + StepStarted ensureStepStarted() => $_ensure(2); + + @$pb.TagNumber(4) + StepCompleted get stepCompleted => $_getN(3); + @$pb.TagNumber(4) + set stepCompleted(StepCompleted value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasStepCompleted() => $_has(3); + @$pb.TagNumber(4) + void clearStepCompleted() => $_clearField(4); + @$pb.TagNumber(4) + StepCompleted ensureStepCompleted() => $_ensure(3); + + @$pb.TagNumber(5) + StepFailed get stepFailed => $_getN(4); + @$pb.TagNumber(5) + set stepFailed(StepFailed value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasStepFailed() => $_has(4); + @$pb.TagNumber(5) + void clearStepFailed() => $_clearField(5); + @$pb.TagNumber(5) + StepFailed ensureStepFailed() => $_ensure(4); + + @$pb.TagNumber(6) + OrchestrationCompleted get completed => $_getN(5); + @$pb.TagNumber(6) + set completed(OrchestrationCompleted value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasCompleted() => $_has(5); + @$pb.TagNumber(6) + void clearCompleted() => $_clearField(6); + @$pb.TagNumber(6) + OrchestrationCompleted ensureCompleted() => $_ensure(5); + + @$pb.TagNumber(7) + OrchestrationFailed get failed => $_getN(6); + @$pb.TagNumber(7) + set failed(OrchestrationFailed value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasFailed() => $_has(6); + @$pb.TagNumber(7) + void clearFailed() => $_clearField(7); + @$pb.TagNumber(7) + OrchestrationFailed ensureFailed() => $_ensure(6); +} + +/// StepStarted indicates an orchestration step has begun. +class StepStarted extends $pb.GeneratedMessage { + factory StepStarted({ + $core.String? stepId, + $core.String? subagentId, + $core.String? name, + }) { + final result = create(); + if (stepId != null) result.stepId = stepId; + if (subagentId != null) result.subagentId = subagentId; + if (name != null) result.name = name; + return result; + } + + StepStarted._(); + + factory StepStarted.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StepStarted.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StepStarted', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'stepId') + ..aOS(2, _omitFieldNames ? '' : 'subagentId') + ..aOS(3, _omitFieldNames ? '' : 'name') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StepStarted clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StepStarted copyWith(void Function(StepStarted) updates) => + super.copyWith((message) => updates(message as StepStarted)) + as StepStarted; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StepStarted create() => StepStarted._(); + @$core.override + StepStarted createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static StepStarted getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StepStarted? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get stepId => $_getSZ(0); + @$pb.TagNumber(1) + set stepId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasStepId() => $_has(0); + @$pb.TagNumber(1) + void clearStepId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get subagentId => $_getSZ(1); + @$pb.TagNumber(2) + set subagentId($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasSubagentId() => $_has(1); + @$pb.TagNumber(2) + void clearSubagentId() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get name => $_getSZ(2); + @$pb.TagNumber(3) + set name($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasName() => $_has(2); + @$pb.TagNumber(3) + void clearName() => $_clearField(3); +} + +/// StepCompleted indicates a step finished successfully. +class StepCompleted extends $pb.GeneratedMessage { + factory StepCompleted({ + $core.String? stepId, + $core.String? resultSummary, + $core.int? completedCount, + $core.int? totalCount, + }) { + final result = create(); + if (stepId != null) result.stepId = stepId; + if (resultSummary != null) result.resultSummary = resultSummary; + if (completedCount != null) result.completedCount = completedCount; + if (totalCount != null) result.totalCount = totalCount; + return result; + } + + StepCompleted._(); + + factory StepCompleted.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StepCompleted.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StepCompleted', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'stepId') + ..aOS(2, _omitFieldNames ? '' : 'resultSummary') + ..aI(3, _omitFieldNames ? '' : 'completedCount') + ..aI(4, _omitFieldNames ? '' : 'totalCount') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StepCompleted clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StepCompleted copyWith(void Function(StepCompleted) updates) => + super.copyWith((message) => updates(message as StepCompleted)) + as StepCompleted; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StepCompleted create() => StepCompleted._(); + @$core.override + StepCompleted createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static StepCompleted getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StepCompleted? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get stepId => $_getSZ(0); + @$pb.TagNumber(1) + set stepId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasStepId() => $_has(0); + @$pb.TagNumber(1) + void clearStepId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get resultSummary => $_getSZ(1); + @$pb.TagNumber(2) + set resultSummary($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasResultSummary() => $_has(1); + @$pb.TagNumber(2) + void clearResultSummary() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get completedCount => $_getIZ(2); + @$pb.TagNumber(3) + set completedCount($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasCompletedCount() => $_has(2); + @$pb.TagNumber(3) + void clearCompletedCount() => $_clearField(3); + + @$pb.TagNumber(4) + $core.int get totalCount => $_getIZ(3); + @$pb.TagNumber(4) + set totalCount($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasTotalCount() => $_has(3); + @$pb.TagNumber(4) + void clearTotalCount() => $_clearField(4); +} + +/// StepFailed indicates a step failed. +class StepFailed extends $pb.GeneratedMessage { + factory StepFailed({ + $core.String? stepId, + $core.String? errorMessage, + $core.Iterable<$core.String>? blockedSteps, + }) { + final result = create(); + if (stepId != null) result.stepId = stepId; + if (errorMessage != null) result.errorMessage = errorMessage; + if (blockedSteps != null) result.blockedSteps.addAll(blockedSteps); + return result; + } + + StepFailed._(); + + factory StepFailed.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StepFailed.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StepFailed', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'stepId') + ..aOS(2, _omitFieldNames ? '' : 'errorMessage') + ..pPS(3, _omitFieldNames ? '' : 'blockedSteps') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StepFailed clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StepFailed copyWith(void Function(StepFailed) updates) => + super.copyWith((message) => updates(message as StepFailed)) as StepFailed; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StepFailed create() => StepFailed._(); + @$core.override + StepFailed createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static StepFailed getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StepFailed? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get stepId => $_getSZ(0); + @$pb.TagNumber(1) + set stepId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasStepId() => $_has(0); + @$pb.TagNumber(1) + void clearStepId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get errorMessage => $_getSZ(1); + @$pb.TagNumber(2) + set errorMessage($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasErrorMessage() => $_has(1); + @$pb.TagNumber(2) + void clearErrorMessage() => $_clearField(2); + + @$pb.TagNumber(3) + $pb.PbList<$core.String> get blockedSteps => $_getList(2); +} + +/// OrchestrationCompleted indicates all steps finished. +class OrchestrationCompleted extends $pb.GeneratedMessage { + factory OrchestrationCompleted({ + $core.int? totalSteps, + $core.int? succeeded, + $core.int? failed, + }) { + final result = create(); + if (totalSteps != null) result.totalSteps = totalSteps; + if (succeeded != null) result.succeeded = succeeded; + if (failed != null) result.failed = failed; + return result; + } + + OrchestrationCompleted._(); + + factory OrchestrationCompleted.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory OrchestrationCompleted.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'OrchestrationCompleted', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aI(1, _omitFieldNames ? '' : 'totalSteps') + ..aI(2, _omitFieldNames ? '' : 'succeeded') + ..aI(3, _omitFieldNames ? '' : 'failed') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationCompleted clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationCompleted copyWith( + void Function(OrchestrationCompleted) updates) => + super.copyWith((message) => updates(message as OrchestrationCompleted)) + as OrchestrationCompleted; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static OrchestrationCompleted create() => OrchestrationCompleted._(); + @$core.override + OrchestrationCompleted createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static OrchestrationCompleted getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static OrchestrationCompleted? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get totalSteps => $_getIZ(0); + @$pb.TagNumber(1) + set totalSteps($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasTotalSteps() => $_has(0); + @$pb.TagNumber(1) + void clearTotalSteps() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get succeeded => $_getIZ(1); + @$pb.TagNumber(2) + set succeeded($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasSucceeded() => $_has(1); + @$pb.TagNumber(2) + void clearSucceeded() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get failed => $_getIZ(2); + @$pb.TagNumber(3) + set failed($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasFailed() => $_has(2); + @$pb.TagNumber(3) + void clearFailed() => $_clearField(3); +} + +/// OrchestrationFailed indicates the orchestration could not complete. +class OrchestrationFailed extends $pb.GeneratedMessage { + factory OrchestrationFailed({ + $core.String? errorMessage, + $core.int? completedSteps, + $core.int? failedSteps, + }) { + final result = create(); + if (errorMessage != null) result.errorMessage = errorMessage; + if (completedSteps != null) result.completedSteps = completedSteps; + if (failedSteps != null) result.failedSteps = failedSteps; + return result; + } + + OrchestrationFailed._(); + + factory OrchestrationFailed.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory OrchestrationFailed.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'OrchestrationFailed', + package: const $pb.PackageName(_omitMessageNames ? '' : 'betcode.v1'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'errorMessage') + ..aI(2, _omitFieldNames ? '' : 'completedSteps') + ..aI(3, _omitFieldNames ? '' : 'failedSteps') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationFailed clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OrchestrationFailed copyWith(void Function(OrchestrationFailed) updates) => + super.copyWith((message) => updates(message as OrchestrationFailed)) + as OrchestrationFailed; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static OrchestrationFailed create() => OrchestrationFailed._(); + @$core.override + OrchestrationFailed createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static OrchestrationFailed getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static OrchestrationFailed? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get errorMessage => $_getSZ(0); + @$pb.TagNumber(1) + set errorMessage($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasErrorMessage() => $_has(0); + @$pb.TagNumber(1) + void clearErrorMessage() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get completedSteps => $_getIZ(1); + @$pb.TagNumber(2) + set completedSteps($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasCompletedSteps() => $_has(1); + @$pb.TagNumber(2) + void clearCompletedSteps() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get failedSteps => $_getIZ(2); + @$pb.TagNumber(3) + set failedSteps($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasFailedSteps() => $_has(2); + @$pb.TagNumber(3) + void clearFailedSteps() => $_clearField(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/generated/betcode/v1/subagent.pbenum.dart b/lib/generated/betcode/v1/subagent.pbenum.dart new file mode 100644 index 0000000..a3c88bf --- /dev/null +++ b/lib/generated/betcode/v1/subagent.pbenum.dart @@ -0,0 +1,81 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/subagent.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// SubagentStatus represents a subagent's lifecycle state. +class SubagentStatus extends $pb.ProtobufEnum { + static const SubagentStatus SUBAGENT_STATUS_UNSPECIFIED = + SubagentStatus._(0, _omitEnumNames ? '' : 'SUBAGENT_STATUS_UNSPECIFIED'); + static const SubagentStatus SUBAGENT_STATUS_PENDING = + SubagentStatus._(1, _omitEnumNames ? '' : 'SUBAGENT_STATUS_PENDING'); + static const SubagentStatus SUBAGENT_STATUS_RUNNING = + SubagentStatus._(2, _omitEnumNames ? '' : 'SUBAGENT_STATUS_RUNNING'); + static const SubagentStatus SUBAGENT_STATUS_COMPLETED = + SubagentStatus._(3, _omitEnumNames ? '' : 'SUBAGENT_STATUS_COMPLETED'); + static const SubagentStatus SUBAGENT_STATUS_FAILED = + SubagentStatus._(4, _omitEnumNames ? '' : 'SUBAGENT_STATUS_FAILED'); + static const SubagentStatus SUBAGENT_STATUS_CANCELLED = + SubagentStatus._(5, _omitEnumNames ? '' : 'SUBAGENT_STATUS_CANCELLED'); + + static const $core.List values = [ + SUBAGENT_STATUS_UNSPECIFIED, + SUBAGENT_STATUS_PENDING, + SUBAGENT_STATUS_RUNNING, + SUBAGENT_STATUS_COMPLETED, + SUBAGENT_STATUS_FAILED, + SUBAGENT_STATUS_CANCELLED, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 5); + static SubagentStatus? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const SubagentStatus._(super.value, super.name); +} + +/// OrchestrationStrategy defines how orchestration steps are executed. +class OrchestrationStrategy extends $pb.ProtobufEnum { + static const OrchestrationStrategy ORCHESTRATION_STRATEGY_UNSPECIFIED = + OrchestrationStrategy._( + 0, _omitEnumNames ? '' : 'ORCHESTRATION_STRATEGY_UNSPECIFIED'); + static const OrchestrationStrategy ORCHESTRATION_STRATEGY_PARALLEL = + OrchestrationStrategy._( + 1, _omitEnumNames ? '' : 'ORCHESTRATION_STRATEGY_PARALLEL'); + static const OrchestrationStrategy ORCHESTRATION_STRATEGY_SEQUENTIAL = + OrchestrationStrategy._( + 2, _omitEnumNames ? '' : 'ORCHESTRATION_STRATEGY_SEQUENTIAL'); + static const OrchestrationStrategy ORCHESTRATION_STRATEGY_DAG = + OrchestrationStrategy._( + 3, _omitEnumNames ? '' : 'ORCHESTRATION_STRATEGY_DAG'); + + static const $core.List values = + [ + ORCHESTRATION_STRATEGY_UNSPECIFIED, + ORCHESTRATION_STRATEGY_PARALLEL, + ORCHESTRATION_STRATEGY_SEQUENTIAL, + ORCHESTRATION_STRATEGY_DAG, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 3); + static OrchestrationStrategy? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const OrchestrationStrategy._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/generated/betcode/v1/subagent.pbgrpc.dart b/lib/generated/betcode/v1/subagent.pbgrpc.dart new file mode 100644 index 0000000..45c65f1 --- /dev/null +++ b/lib/generated/betcode/v1/subagent.pbgrpc.dart @@ -0,0 +1,298 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/subagent.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'subagent.pb.dart' as $0; + +export 'subagent.pb.dart'; + +/// SubagentService provides daemon-orchestrated subagent management. +/// Unlike Level 1 (Claude-internal Task tool), Level 2 subagents are +/// independent Claude Code subprocesses managed by the daemon. +@$pb.GrpcServiceName('betcode.v1.SubagentService') +class SubagentServiceClient extends $grpc.Client { + /// The hostname for this service. + static const $core.String defaultHost = ''; + + /// OAuth scopes needed for the client. + static const $core.List<$core.String> oauthScopes = [ + '', + ]; + + SubagentServiceClient(super.channel, {super.options, super.interceptors}); + + /// Spawn a new subagent subprocess under a parent session. + $grpc.ResponseFuture<$0.SpawnSubagentResponse> spawnSubagent( + $0.SpawnSubagentRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$spawnSubagent, request, options: options); + } + + /// Watch a subagent's event stream. + $grpc.ResponseStream<$0.SubagentEvent> watchSubagent( + $0.WatchSubagentRequest request, { + $grpc.CallOptions? options, + }) { + return $createStreamingCall( + _$watchSubagent, $async.Stream.fromIterable([request]), + options: options); + } + + /// Send input to a running subagent. + $grpc.ResponseFuture<$0.SendToSubagentResponse> sendToSubagent( + $0.SendToSubagentRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$sendToSubagent, request, options: options); + } + + /// Cancel a running subagent. + $grpc.ResponseFuture<$0.CancelSubagentResponse> cancelSubagent( + $0.CancelSubagentRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$cancelSubagent, request, options: options); + } + + /// List subagents for a parent session. + $grpc.ResponseFuture<$0.ListSubagentsResponse> listSubagents( + $0.ListSubagentsRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$listSubagents, request, options: options); + } + + /// Create a multi-step orchestration (parallel, sequential, or DAG). + $grpc.ResponseFuture<$0.CreateOrchestrationResponse> createOrchestration( + $0.CreateOrchestrationRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$createOrchestration, request, options: options); + } + + /// Watch orchestration progress events. + $grpc.ResponseStream<$0.OrchestrationEvent> watchOrchestration( + $0.WatchOrchestrationRequest request, { + $grpc.CallOptions? options, + }) { + return $createStreamingCall( + _$watchOrchestration, $async.Stream.fromIterable([request]), + options: options); + } + + /// Revoke auto-approve permissions on a running subagent. + $grpc.ResponseFuture<$0.RevokeAutoApproveResponse> revokeAutoApprove( + $0.RevokeAutoApproveRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$revokeAutoApprove, request, options: options); + } + + // method descriptors + + static final _$spawnSubagent = + $grpc.ClientMethod<$0.SpawnSubagentRequest, $0.SpawnSubagentResponse>( + '/betcode.v1.SubagentService/SpawnSubagent', + ($0.SpawnSubagentRequest value) => value.writeToBuffer(), + $0.SpawnSubagentResponse.fromBuffer); + static final _$watchSubagent = + $grpc.ClientMethod<$0.WatchSubagentRequest, $0.SubagentEvent>( + '/betcode.v1.SubagentService/WatchSubagent', + ($0.WatchSubagentRequest value) => value.writeToBuffer(), + $0.SubagentEvent.fromBuffer); + static final _$sendToSubagent = + $grpc.ClientMethod<$0.SendToSubagentRequest, $0.SendToSubagentResponse>( + '/betcode.v1.SubagentService/SendToSubagent', + ($0.SendToSubagentRequest value) => value.writeToBuffer(), + $0.SendToSubagentResponse.fromBuffer); + static final _$cancelSubagent = + $grpc.ClientMethod<$0.CancelSubagentRequest, $0.CancelSubagentResponse>( + '/betcode.v1.SubagentService/CancelSubagent', + ($0.CancelSubagentRequest value) => value.writeToBuffer(), + $0.CancelSubagentResponse.fromBuffer); + static final _$listSubagents = + $grpc.ClientMethod<$0.ListSubagentsRequest, $0.ListSubagentsResponse>( + '/betcode.v1.SubagentService/ListSubagents', + ($0.ListSubagentsRequest value) => value.writeToBuffer(), + $0.ListSubagentsResponse.fromBuffer); + static final _$createOrchestration = $grpc.ClientMethod< + $0.CreateOrchestrationRequest, $0.CreateOrchestrationResponse>( + '/betcode.v1.SubagentService/CreateOrchestration', + ($0.CreateOrchestrationRequest value) => value.writeToBuffer(), + $0.CreateOrchestrationResponse.fromBuffer); + static final _$watchOrchestration = + $grpc.ClientMethod<$0.WatchOrchestrationRequest, $0.OrchestrationEvent>( + '/betcode.v1.SubagentService/WatchOrchestration', + ($0.WatchOrchestrationRequest value) => value.writeToBuffer(), + $0.OrchestrationEvent.fromBuffer); + static final _$revokeAutoApprove = $grpc.ClientMethod< + $0.RevokeAutoApproveRequest, $0.RevokeAutoApproveResponse>( + '/betcode.v1.SubagentService/RevokeAutoApprove', + ($0.RevokeAutoApproveRequest value) => value.writeToBuffer(), + $0.RevokeAutoApproveResponse.fromBuffer); +} + +@$pb.GrpcServiceName('betcode.v1.SubagentService') +abstract class SubagentServiceBase extends $grpc.Service { + $core.String get $name => 'betcode.v1.SubagentService'; + + SubagentServiceBase() { + $addMethod( + $grpc.ServiceMethod<$0.SpawnSubagentRequest, $0.SpawnSubagentResponse>( + 'SpawnSubagent', + spawnSubagent_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.SpawnSubagentRequest.fromBuffer(value), + ($0.SpawnSubagentResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.WatchSubagentRequest, $0.SubagentEvent>( + 'WatchSubagent', + watchSubagent_Pre, + false, + true, + ($core.List<$core.int> value) => + $0.WatchSubagentRequest.fromBuffer(value), + ($0.SubagentEvent value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.SendToSubagentRequest, + $0.SendToSubagentResponse>( + 'SendToSubagent', + sendToSubagent_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.SendToSubagentRequest.fromBuffer(value), + ($0.SendToSubagentResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.CancelSubagentRequest, + $0.CancelSubagentResponse>( + 'CancelSubagent', + cancelSubagent_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.CancelSubagentRequest.fromBuffer(value), + ($0.CancelSubagentResponse value) => value.writeToBuffer())); + $addMethod( + $grpc.ServiceMethod<$0.ListSubagentsRequest, $0.ListSubagentsResponse>( + 'ListSubagents', + listSubagents_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.ListSubagentsRequest.fromBuffer(value), + ($0.ListSubagentsResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.CreateOrchestrationRequest, + $0.CreateOrchestrationResponse>( + 'CreateOrchestration', + createOrchestration_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.CreateOrchestrationRequest.fromBuffer(value), + ($0.CreateOrchestrationResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.WatchOrchestrationRequest, + $0.OrchestrationEvent>( + 'WatchOrchestration', + watchOrchestration_Pre, + false, + true, + ($core.List<$core.int> value) => + $0.WatchOrchestrationRequest.fromBuffer(value), + ($0.OrchestrationEvent value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.RevokeAutoApproveRequest, + $0.RevokeAutoApproveResponse>( + 'RevokeAutoApprove', + revokeAutoApprove_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.RevokeAutoApproveRequest.fromBuffer(value), + ($0.RevokeAutoApproveResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.SpawnSubagentResponse> spawnSubagent_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.SpawnSubagentRequest> $request) async { + return spawnSubagent($call, await $request); + } + + $async.Future<$0.SpawnSubagentResponse> spawnSubagent( + $grpc.ServiceCall call, $0.SpawnSubagentRequest request); + + $async.Stream<$0.SubagentEvent> watchSubagent_Pre($grpc.ServiceCall $call, + $async.Future<$0.WatchSubagentRequest> $request) async* { + yield* watchSubagent($call, await $request); + } + + $async.Stream<$0.SubagentEvent> watchSubagent( + $grpc.ServiceCall call, $0.WatchSubagentRequest request); + + $async.Future<$0.SendToSubagentResponse> sendToSubagent_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.SendToSubagentRequest> $request) async { + return sendToSubagent($call, await $request); + } + + $async.Future<$0.SendToSubagentResponse> sendToSubagent( + $grpc.ServiceCall call, $0.SendToSubagentRequest request); + + $async.Future<$0.CancelSubagentResponse> cancelSubagent_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.CancelSubagentRequest> $request) async { + return cancelSubagent($call, await $request); + } + + $async.Future<$0.CancelSubagentResponse> cancelSubagent( + $grpc.ServiceCall call, $0.CancelSubagentRequest request); + + $async.Future<$0.ListSubagentsResponse> listSubagents_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.ListSubagentsRequest> $request) async { + return listSubagents($call, await $request); + } + + $async.Future<$0.ListSubagentsResponse> listSubagents( + $grpc.ServiceCall call, $0.ListSubagentsRequest request); + + $async.Future<$0.CreateOrchestrationResponse> createOrchestration_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.CreateOrchestrationRequest> $request) async { + return createOrchestration($call, await $request); + } + + $async.Future<$0.CreateOrchestrationResponse> createOrchestration( + $grpc.ServiceCall call, $0.CreateOrchestrationRequest request); + + $async.Stream<$0.OrchestrationEvent> watchOrchestration_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.WatchOrchestrationRequest> $request) async* { + yield* watchOrchestration($call, await $request); + } + + $async.Stream<$0.OrchestrationEvent> watchOrchestration( + $grpc.ServiceCall call, $0.WatchOrchestrationRequest request); + + $async.Future<$0.RevokeAutoApproveResponse> revokeAutoApprove_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.RevokeAutoApproveRequest> $request) async { + return revokeAutoApprove($call, await $request); + } + + $async.Future<$0.RevokeAutoApproveResponse> revokeAutoApprove( + $grpc.ServiceCall call, $0.RevokeAutoApproveRequest request); +} diff --git a/lib/generated/betcode/v1/subagent.pbjson.dart b/lib/generated/betcode/v1/subagent.pbjson.dart new file mode 100644 index 0000000..d80554e --- /dev/null +++ b/lib/generated/betcode/v1/subagent.pbjson.dart @@ -0,0 +1,785 @@ +// This is a generated file - do not edit. +// +// Generated from betcode/v1/subagent.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_relative_imports +// ignore_for_file: unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use subagentStatusDescriptor instead') +const SubagentStatus$json = { + '1': 'SubagentStatus', + '2': [ + {'1': 'SUBAGENT_STATUS_UNSPECIFIED', '2': 0}, + {'1': 'SUBAGENT_STATUS_PENDING', '2': 1}, + {'1': 'SUBAGENT_STATUS_RUNNING', '2': 2}, + {'1': 'SUBAGENT_STATUS_COMPLETED', '2': 3}, + {'1': 'SUBAGENT_STATUS_FAILED', '2': 4}, + {'1': 'SUBAGENT_STATUS_CANCELLED', '2': 5}, + ], +}; + +/// Descriptor for `SubagentStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List subagentStatusDescriptor = $convert.base64Decode( + 'Cg5TdWJhZ2VudFN0YXR1cxIfChtTVUJBR0VOVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIbChdTVU' + 'JBR0VOVF9TVEFUVVNfUEVORElORxABEhsKF1NVQkFHRU5UX1NUQVRVU19SVU5OSU5HEAISHQoZ' + 'U1VCQUdFTlRfU1RBVFVTX0NPTVBMRVRFRBADEhoKFlNVQkFHRU5UX1NUQVRVU19GQUlMRUQQBB' + 'IdChlTVUJBR0VOVF9TVEFUVVNfQ0FOQ0VMTEVEEAU='); + +@$core.Deprecated('Use orchestrationStrategyDescriptor instead') +const OrchestrationStrategy$json = { + '1': 'OrchestrationStrategy', + '2': [ + {'1': 'ORCHESTRATION_STRATEGY_UNSPECIFIED', '2': 0}, + {'1': 'ORCHESTRATION_STRATEGY_PARALLEL', '2': 1}, + {'1': 'ORCHESTRATION_STRATEGY_SEQUENTIAL', '2': 2}, + {'1': 'ORCHESTRATION_STRATEGY_DAG', '2': 3}, + ], +}; + +/// Descriptor for `OrchestrationStrategy`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List orchestrationStrategyDescriptor = $convert.base64Decode( + 'ChVPcmNoZXN0cmF0aW9uU3RyYXRlZ3kSJgoiT1JDSEVTVFJBVElPTl9TVFJBVEVHWV9VTlNQRU' + 'NJRklFRBAAEiMKH09SQ0hFU1RSQVRJT05fU1RSQVRFR1lfUEFSQUxMRUwQARIlCiFPUkNIRVNU' + 'UkFUSU9OX1NUUkFURUdZX1NFUVVFTlRJQUwQAhIeChpPUkNIRVNUUkFUSU9OX1NUUkFURUdZX0' + 'RBRxAD'); + +@$core.Deprecated('Use spawnSubagentRequestDescriptor instead') +const SpawnSubagentRequest$json = { + '1': 'SpawnSubagentRequest', + '2': [ + {'1': 'parent_session_id', '3': 1, '4': 1, '5': 9, '10': 'parentSessionId'}, + {'1': 'prompt', '3': 2, '4': 1, '5': 9, '10': 'prompt'}, + {'1': 'model', '3': 3, '4': 1, '5': 9, '10': 'model'}, + { + '1': 'working_directory', + '3': 4, + '4': 1, + '5': 9, + '10': 'workingDirectory' + }, + {'1': 'allowed_tools', '3': 5, '4': 3, '5': 9, '10': 'allowedTools'}, + {'1': 'max_turns', '3': 6, '4': 1, '5': 5, '10': 'maxTurns'}, + { + '1': 'env', + '3': 7, + '4': 3, + '5': 11, + '6': '.betcode.v1.SpawnSubagentRequest.EnvEntry', + '10': 'env' + }, + {'1': 'name', '3': 8, '4': 1, '5': 9, '10': 'name'}, + {'1': 'auto_approve', '3': 9, '4': 1, '5': 8, '10': 'autoApprove'}, + ], + '3': [SpawnSubagentRequest_EnvEntry$json], +}; + +@$core.Deprecated('Use spawnSubagentRequestDescriptor instead') +const SpawnSubagentRequest_EnvEntry$json = { + '1': 'EnvEntry', + '2': [ + {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': {'7': true}, +}; + +/// Descriptor for `SpawnSubagentRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List spawnSubagentRequestDescriptor = $convert.base64Decode( + 'ChRTcGF3blN1YmFnZW50UmVxdWVzdBIqChFwYXJlbnRfc2Vzc2lvbl9pZBgBIAEoCVIPcGFyZW' + '50U2Vzc2lvbklkEhYKBnByb21wdBgCIAEoCVIGcHJvbXB0EhQKBW1vZGVsGAMgASgJUgVtb2Rl' + 'bBIrChF3b3JraW5nX2RpcmVjdG9yeRgEIAEoCVIQd29ya2luZ0RpcmVjdG9yeRIjCg1hbGxvd2' + 'VkX3Rvb2xzGAUgAygJUgxhbGxvd2VkVG9vbHMSGwoJbWF4X3R1cm5zGAYgASgFUghtYXhUdXJu' + 'cxI7CgNlbnYYByADKAsyKS5iZXRjb2RlLnYxLlNwYXduU3ViYWdlbnRSZXF1ZXN0LkVudkVudH' + 'J5UgNlbnYSEgoEbmFtZRgIIAEoCVIEbmFtZRIhCgxhdXRvX2FwcHJvdmUYCSABKAhSC2F1dG9B' + 'cHByb3ZlGjYKCEVudkVudHJ5EhAKA2tleRgBIAEoCVIDa2V5EhQKBXZhbHVlGAIgASgJUgV2YW' + 'x1ZToCOAE='); + +@$core.Deprecated('Use spawnSubagentResponseDescriptor instead') +const SpawnSubagentResponse$json = { + '1': 'SpawnSubagentResponse', + '2': [ + {'1': 'subagent_id', '3': 1, '4': 1, '5': 9, '10': 'subagentId'}, + {'1': 'session_id', '3': 2, '4': 1, '5': 9, '10': 'sessionId'}, + ], +}; + +/// Descriptor for `SpawnSubagentResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List spawnSubagentResponseDescriptor = $convert.base64Decode( + 'ChVTcGF3blN1YmFnZW50UmVzcG9uc2USHwoLc3ViYWdlbnRfaWQYASABKAlSCnN1YmFnZW50SW' + 'QSHQoKc2Vzc2lvbl9pZBgCIAEoCVIJc2Vzc2lvbklk'); + +@$core.Deprecated('Use watchSubagentRequestDescriptor instead') +const WatchSubagentRequest$json = { + '1': 'WatchSubagentRequest', + '2': [ + {'1': 'subagent_id', '3': 1, '4': 1, '5': 9, '10': 'subagentId'}, + {'1': 'from_sequence', '3': 2, '4': 1, '5': 4, '10': 'fromSequence'}, + ], +}; + +/// Descriptor for `WatchSubagentRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List watchSubagentRequestDescriptor = $convert.base64Decode( + 'ChRXYXRjaFN1YmFnZW50UmVxdWVzdBIfCgtzdWJhZ2VudF9pZBgBIAEoCVIKc3ViYWdlbnRJZB' + 'IjCg1mcm9tX3NlcXVlbmNlGAIgASgEUgxmcm9tU2VxdWVuY2U='); + +@$core.Deprecated('Use sendToSubagentRequestDescriptor instead') +const SendToSubagentRequest$json = { + '1': 'SendToSubagentRequest', + '2': [ + {'1': 'subagent_id', '3': 1, '4': 1, '5': 9, '10': 'subagentId'}, + {'1': 'content', '3': 2, '4': 1, '5': 9, '10': 'content'}, + ], +}; + +/// Descriptor for `SendToSubagentRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sendToSubagentRequestDescriptor = $convert.base64Decode( + 'ChVTZW5kVG9TdWJhZ2VudFJlcXVlc3QSHwoLc3ViYWdlbnRfaWQYASABKAlSCnN1YmFnZW50SW' + 'QSGAoHY29udGVudBgCIAEoCVIHY29udGVudA=='); + +@$core.Deprecated('Use sendToSubagentResponseDescriptor instead') +const SendToSubagentResponse$json = { + '1': 'SendToSubagentResponse', + '2': [ + {'1': 'delivered', '3': 1, '4': 1, '5': 8, '10': 'delivered'}, + ], +}; + +/// Descriptor for `SendToSubagentResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sendToSubagentResponseDescriptor = + $convert.base64Decode( + 'ChZTZW5kVG9TdWJhZ2VudFJlc3BvbnNlEhwKCWRlbGl2ZXJlZBgBIAEoCFIJZGVsaXZlcmVk'); + +@$core.Deprecated('Use cancelSubagentRequestDescriptor instead') +const CancelSubagentRequest$json = { + '1': 'CancelSubagentRequest', + '2': [ + {'1': 'subagent_id', '3': 1, '4': 1, '5': 9, '10': 'subagentId'}, + {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, + {'1': 'force', '3': 3, '4': 1, '5': 8, '10': 'force'}, + {'1': 'cleanup_worktree', '3': 4, '4': 1, '5': 8, '10': 'cleanupWorktree'}, + ], +}; + +/// Descriptor for `CancelSubagentRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List cancelSubagentRequestDescriptor = $convert.base64Decode( + 'ChVDYW5jZWxTdWJhZ2VudFJlcXVlc3QSHwoLc3ViYWdlbnRfaWQYASABKAlSCnN1YmFnZW50SW' + 'QSFgoGcmVhc29uGAIgASgJUgZyZWFzb24SFAoFZm9yY2UYAyABKAhSBWZvcmNlEikKEGNsZWFu' + 'dXBfd29ya3RyZWUYBCABKAhSD2NsZWFudXBXb3JrdHJlZQ=='); + +@$core.Deprecated('Use cancelSubagentResponseDescriptor instead') +const CancelSubagentResponse$json = { + '1': 'CancelSubagentResponse', + '2': [ + {'1': 'cancelled', '3': 1, '4': 1, '5': 8, '10': 'cancelled'}, + {'1': 'final_status', '3': 2, '4': 1, '5': 9, '10': 'finalStatus'}, + { + '1': 'tool_calls_executed', + '3': 3, + '4': 1, + '5': 5, + '10': 'toolCallsExecuted' + }, + { + '1': 'tool_calls_auto_approved', + '3': 4, + '4': 1, + '5': 5, + '10': 'toolCallsAutoApproved' + }, + ], +}; + +/// Descriptor for `CancelSubagentResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List cancelSubagentResponseDescriptor = $convert.base64Decode( + 'ChZDYW5jZWxTdWJhZ2VudFJlc3BvbnNlEhwKCWNhbmNlbGxlZBgBIAEoCFIJY2FuY2VsbGVkEi' + 'EKDGZpbmFsX3N0YXR1cxgCIAEoCVILZmluYWxTdGF0dXMSLgoTdG9vbF9jYWxsc19leGVjdXRl' + 'ZBgDIAEoBVIRdG9vbENhbGxzRXhlY3V0ZWQSNwoYdG9vbF9jYWxsc19hdXRvX2FwcHJvdmVkGA' + 'QgASgFUhV0b29sQ2FsbHNBdXRvQXBwcm92ZWQ='); + +@$core.Deprecated('Use listSubagentsRequestDescriptor instead') +const ListSubagentsRequest$json = { + '1': 'ListSubagentsRequest', + '2': [ + {'1': 'parent_session_id', '3': 1, '4': 1, '5': 9, '10': 'parentSessionId'}, + {'1': 'status_filter', '3': 2, '4': 1, '5': 9, '10': 'statusFilter'}, + ], +}; + +/// Descriptor for `ListSubagentsRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List listSubagentsRequestDescriptor = $convert.base64Decode( + 'ChRMaXN0U3ViYWdlbnRzUmVxdWVzdBIqChFwYXJlbnRfc2Vzc2lvbl9pZBgBIAEoCVIPcGFyZW' + '50U2Vzc2lvbklkEiMKDXN0YXR1c19maWx0ZXIYAiABKAlSDHN0YXR1c0ZpbHRlcg=='); + +@$core.Deprecated('Use listSubagentsResponseDescriptor instead') +const ListSubagentsResponse$json = { + '1': 'ListSubagentsResponse', + '2': [ + { + '1': 'subagents', + '3': 1, + '4': 3, + '5': 11, + '6': '.betcode.v1.SubagentInfo', + '10': 'subagents' + }, + ], +}; + +/// Descriptor for `ListSubagentsResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List listSubagentsResponseDescriptor = $convert.base64Decode( + 'ChVMaXN0U3ViYWdlbnRzUmVzcG9uc2USNgoJc3ViYWdlbnRzGAEgAygLMhguYmV0Y29kZS52MS' + '5TdWJhZ2VudEluZm9SCXN1YmFnZW50cw=='); + +@$core.Deprecated('Use subagentInfoDescriptor instead') +const SubagentInfo$json = { + '1': 'SubagentInfo', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'}, + {'1': 'parent_session_id', '3': 2, '4': 1, '5': 9, '10': 'parentSessionId'}, + {'1': 'session_id', '3': 3, '4': 1, '5': 9, '10': 'sessionId'}, + {'1': 'name', '3': 4, '4': 1, '5': 9, '10': 'name'}, + {'1': 'prompt', '3': 5, '4': 1, '5': 9, '10': 'prompt'}, + {'1': 'model', '3': 6, '4': 1, '5': 9, '10': 'model'}, + { + '1': 'working_directory', + '3': 7, + '4': 1, + '5': 9, + '10': 'workingDirectory' + }, + { + '1': 'status', + '3': 8, + '4': 1, + '5': 14, + '6': '.betcode.v1.SubagentStatus', + '10': 'status' + }, + {'1': 'auto_approve', '3': 9, '4': 1, '5': 8, '10': 'autoApprove'}, + {'1': 'max_turns', '3': 10, '4': 1, '5': 5, '10': 'maxTurns'}, + {'1': 'allowed_tools', '3': 11, '4': 3, '5': 9, '10': 'allowedTools'}, + {'1': 'result_summary', '3': 12, '4': 1, '5': 9, '10': 'resultSummary'}, + { + '1': 'created_at', + '3': 13, + '4': 1, + '5': 11, + '6': '.google.protobuf.Timestamp', + '10': 'createdAt' + }, + { + '1': 'completed_at', + '3': 14, + '4': 1, + '5': 11, + '6': '.google.protobuf.Timestamp', + '10': 'completedAt' + }, + ], +}; + +/// Descriptor for `SubagentInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentInfoDescriptor = $convert.base64Decode( + 'CgxTdWJhZ2VudEluZm8SDgoCaWQYASABKAlSAmlkEioKEXBhcmVudF9zZXNzaW9uX2lkGAIgAS' + 'gJUg9wYXJlbnRTZXNzaW9uSWQSHQoKc2Vzc2lvbl9pZBgDIAEoCVIJc2Vzc2lvbklkEhIKBG5h' + 'bWUYBCABKAlSBG5hbWUSFgoGcHJvbXB0GAUgASgJUgZwcm9tcHQSFAoFbW9kZWwYBiABKAlSBW' + '1vZGVsEisKEXdvcmtpbmdfZGlyZWN0b3J5GAcgASgJUhB3b3JraW5nRGlyZWN0b3J5EjIKBnN0' + 'YXR1cxgIIAEoDjIaLmJldGNvZGUudjEuU3ViYWdlbnRTdGF0dXNSBnN0YXR1cxIhCgxhdXRvX2' + 'FwcHJvdmUYCSABKAhSC2F1dG9BcHByb3ZlEhsKCW1heF90dXJucxgKIAEoBVIIbWF4VHVybnMS' + 'IwoNYWxsb3dlZF90b29scxgLIAMoCVIMYWxsb3dlZFRvb2xzEiUKDnJlc3VsdF9zdW1tYXJ5GA' + 'wgASgJUg1yZXN1bHRTdW1tYXJ5EjkKCmNyZWF0ZWRfYXQYDSABKAsyGi5nb29nbGUucHJvdG9i' + 'dWYuVGltZXN0YW1wUgljcmVhdGVkQXQSPQoMY29tcGxldGVkX2F0GA4gASgLMhouZ29vZ2xlLn' + 'Byb3RvYnVmLlRpbWVzdGFtcFILY29tcGxldGVkQXQ='); + +@$core.Deprecated('Use subagentEventDescriptor instead') +const SubagentEvent$json = { + '1': 'SubagentEvent', + '2': [ + {'1': 'subagent_id', '3': 1, '4': 1, '5': 9, '10': 'subagentId'}, + { + '1': 'timestamp', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.protobuf.Timestamp', + '10': 'timestamp' + }, + { + '1': 'started', + '3': 3, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentStarted', + '9': 0, + '10': 'started' + }, + { + '1': 'output', + '3': 4, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentOutput', + '9': 0, + '10': 'output' + }, + { + '1': 'tool_use', + '3': 5, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentToolUse', + '9': 0, + '10': 'toolUse' + }, + { + '1': 'permission_request', + '3': 6, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentPermissionRequest', + '9': 0, + '10': 'permissionRequest' + }, + { + '1': 'completed', + '3': 7, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentCompleted', + '9': 0, + '10': 'completed' + }, + { + '1': 'failed', + '3': 8, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentFailed', + '9': 0, + '10': 'failed' + }, + { + '1': 'cancelled', + '3': 9, + '4': 1, + '5': 11, + '6': '.betcode.v1.SubagentCancelled', + '9': 0, + '10': 'cancelled' + }, + ], + '8': [ + {'1': 'event'}, + ], +}; + +/// Descriptor for `SubagentEvent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentEventDescriptor = $convert.base64Decode( + 'Cg1TdWJhZ2VudEV2ZW50Eh8KC3N1YmFnZW50X2lkGAEgASgJUgpzdWJhZ2VudElkEjgKCXRpbW' + 'VzdGFtcBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBSCXRpbWVzdGFtcBI3Cgdz' + 'dGFydGVkGAMgASgLMhsuYmV0Y29kZS52MS5TdWJhZ2VudFN0YXJ0ZWRIAFIHc3RhcnRlZBI0Cg' + 'ZvdXRwdXQYBCABKAsyGi5iZXRjb2RlLnYxLlN1YmFnZW50T3V0cHV0SABSBm91dHB1dBI4Cgh0' + 'b29sX3VzZRgFIAEoCzIbLmJldGNvZGUudjEuU3ViYWdlbnRUb29sVXNlSABSB3Rvb2xVc2USVg' + 'oScGVybWlzc2lvbl9yZXF1ZXN0GAYgASgLMiUuYmV0Y29kZS52MS5TdWJhZ2VudFBlcm1pc3Np' + 'b25SZXF1ZXN0SABSEXBlcm1pc3Npb25SZXF1ZXN0Ej0KCWNvbXBsZXRlZBgHIAEoCzIdLmJldG' + 'NvZGUudjEuU3ViYWdlbnRDb21wbGV0ZWRIAFIJY29tcGxldGVkEjQKBmZhaWxlZBgIIAEoCzIa' + 'LmJldGNvZGUudjEuU3ViYWdlbnRGYWlsZWRIAFIGZmFpbGVkEj0KCWNhbmNlbGxlZBgJIAEoCz' + 'IdLmJldGNvZGUudjEuU3ViYWdlbnRDYW5jZWxsZWRIAFIJY2FuY2VsbGVkQgcKBWV2ZW50'); + +@$core.Deprecated('Use subagentStartedDescriptor instead') +const SubagentStarted$json = { + '1': 'SubagentStarted', + '2': [ + {'1': 'session_id', '3': 1, '4': 1, '5': 9, '10': 'sessionId'}, + {'1': 'model', '3': 2, '4': 1, '5': 9, '10': 'model'}, + ], +}; + +/// Descriptor for `SubagentStarted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentStartedDescriptor = $convert.base64Decode( + 'Cg9TdWJhZ2VudFN0YXJ0ZWQSHQoKc2Vzc2lvbl9pZBgBIAEoCVIJc2Vzc2lvbklkEhQKBW1vZG' + 'VsGAIgASgJUgVtb2RlbA=='); + +@$core.Deprecated('Use subagentOutputDescriptor instead') +const SubagentOutput$json = { + '1': 'SubagentOutput', + '2': [ + {'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'}, + {'1': 'is_complete', '3': 2, '4': 1, '5': 8, '10': 'isComplete'}, + ], +}; + +/// Descriptor for `SubagentOutput`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentOutputDescriptor = $convert.base64Decode( + 'Cg5TdWJhZ2VudE91dHB1dBISCgR0ZXh0GAEgASgJUgR0ZXh0Eh8KC2lzX2NvbXBsZXRlGAIgAS' + 'gIUgppc0NvbXBsZXRl'); + +@$core.Deprecated('Use subagentToolUseDescriptor instead') +const SubagentToolUse$json = { + '1': 'SubagentToolUse', + '2': [ + {'1': 'tool_id', '3': 1, '4': 1, '5': 9, '10': 'toolId'}, + {'1': 'tool_name', '3': 2, '4': 1, '5': 9, '10': 'toolName'}, + {'1': 'description', '3': 3, '4': 1, '5': 9, '10': 'description'}, + ], +}; + +/// Descriptor for `SubagentToolUse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentToolUseDescriptor = $convert.base64Decode( + 'Cg9TdWJhZ2VudFRvb2xVc2USFwoHdG9vbF9pZBgBIAEoCVIGdG9vbElkEhsKCXRvb2xfbmFtZR' + 'gCIAEoCVIIdG9vbE5hbWUSIAoLZGVzY3JpcHRpb24YAyABKAlSC2Rlc2NyaXB0aW9u'); + +@$core.Deprecated('Use subagentPermissionRequestDescriptor instead') +const SubagentPermissionRequest$json = { + '1': 'SubagentPermissionRequest', + '2': [ + {'1': 'request_id', '3': 1, '4': 1, '5': 9, '10': 'requestId'}, + {'1': 'tool_name', '3': 2, '4': 1, '5': 9, '10': 'toolName'}, + {'1': 'description', '3': 3, '4': 1, '5': 9, '10': 'description'}, + ], +}; + +/// Descriptor for `SubagentPermissionRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentPermissionRequestDescriptor = $convert.base64Decode( + 'ChlTdWJhZ2VudFBlcm1pc3Npb25SZXF1ZXN0Eh0KCnJlcXVlc3RfaWQYASABKAlSCXJlcXVlc3' + 'RJZBIbCgl0b29sX25hbWUYAiABKAlSCHRvb2xOYW1lEiAKC2Rlc2NyaXB0aW9uGAMgASgJUgtk' + 'ZXNjcmlwdGlvbg=='); + +@$core.Deprecated('Use subagentCompletedDescriptor instead') +const SubagentCompleted$json = { + '1': 'SubagentCompleted', + '2': [ + {'1': 'exit_code', '3': 1, '4': 1, '5': 5, '10': 'exitCode'}, + {'1': 'result_summary', '3': 2, '4': 1, '5': 9, '10': 'resultSummary'}, + ], +}; + +/// Descriptor for `SubagentCompleted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentCompletedDescriptor = $convert.base64Decode( + 'ChFTdWJhZ2VudENvbXBsZXRlZBIbCglleGl0X2NvZGUYASABKAVSCGV4aXRDb2RlEiUKDnJlc3' + 'VsdF9zdW1tYXJ5GAIgASgJUg1yZXN1bHRTdW1tYXJ5'); + +@$core.Deprecated('Use subagentFailedDescriptor instead') +const SubagentFailed$json = { + '1': 'SubagentFailed', + '2': [ + {'1': 'exit_code', '3': 1, '4': 1, '5': 5, '10': 'exitCode'}, + {'1': 'error_message', '3': 2, '4': 1, '5': 9, '10': 'errorMessage'}, + ], +}; + +/// Descriptor for `SubagentFailed`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentFailedDescriptor = $convert.base64Decode( + 'Cg5TdWJhZ2VudEZhaWxlZBIbCglleGl0X2NvZGUYASABKAVSCGV4aXRDb2RlEiMKDWVycm9yX2' + '1lc3NhZ2UYAiABKAlSDGVycm9yTWVzc2FnZQ=='); + +@$core.Deprecated('Use subagentCancelledDescriptor instead') +const SubagentCancelled$json = { + '1': 'SubagentCancelled', + '2': [ + {'1': 'reason', '3': 1, '4': 1, '5': 9, '10': 'reason'}, + ], +}; + +/// Descriptor for `SubagentCancelled`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List subagentCancelledDescriptor = $convert.base64Decode( + 'ChFTdWJhZ2VudENhbmNlbGxlZBIWCgZyZWFzb24YASABKAlSBnJlYXNvbg=='); + +@$core.Deprecated('Use revokeAutoApproveRequestDescriptor instead') +const RevokeAutoApproveRequest$json = { + '1': 'RevokeAutoApproveRequest', + '2': [ + {'1': 'subagent_id', '3': 1, '4': 1, '5': 9, '10': 'subagentId'}, + {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, + { + '1': 'terminate_if_pending', + '3': 3, + '4': 1, + '5': 8, + '10': 'terminateIfPending' + }, + ], +}; + +/// Descriptor for `RevokeAutoApproveRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List revokeAutoApproveRequestDescriptor = $convert.base64Decode( + 'ChhSZXZva2VBdXRvQXBwcm92ZVJlcXVlc3QSHwoLc3ViYWdlbnRfaWQYASABKAlSCnN1YmFnZW' + '50SWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24SMAoUdGVybWluYXRlX2lmX3BlbmRpbmcYAyAB' + 'KAhSEnRlcm1pbmF0ZUlmUGVuZGluZw=='); + +@$core.Deprecated('Use revokeAutoApproveResponseDescriptor instead') +const RevokeAutoApproveResponse$json = { + '1': 'RevokeAutoApproveResponse', + '2': [ + {'1': 'revoked', '3': 1, '4': 1, '5': 8, '10': 'revoked'}, + { + '1': 'pending_tool_calls', + '3': 2, + '4': 1, + '5': 5, + '10': 'pendingToolCalls' + }, + {'1': 'subagent_status', '3': 3, '4': 1, '5': 9, '10': 'subagentStatus'}, + ], +}; + +/// Descriptor for `RevokeAutoApproveResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List revokeAutoApproveResponseDescriptor = $convert.base64Decode( + 'ChlSZXZva2VBdXRvQXBwcm92ZVJlc3BvbnNlEhgKB3Jldm9rZWQYASABKAhSB3Jldm9rZWQSLA' + 'oScGVuZGluZ190b29sX2NhbGxzGAIgASgFUhBwZW5kaW5nVG9vbENhbGxzEicKD3N1YmFnZW50' + 'X3N0YXR1cxgDIAEoCVIOc3ViYWdlbnRTdGF0dXM='); + +@$core.Deprecated('Use createOrchestrationRequestDescriptor instead') +const CreateOrchestrationRequest$json = { + '1': 'CreateOrchestrationRequest', + '2': [ + {'1': 'parent_session_id', '3': 1, '4': 1, '5': 9, '10': 'parentSessionId'}, + { + '1': 'steps', + '3': 2, + '4': 3, + '5': 11, + '6': '.betcode.v1.OrchestrationStep', + '10': 'steps' + }, + { + '1': 'strategy', + '3': 3, + '4': 1, + '5': 14, + '6': '.betcode.v1.OrchestrationStrategy', + '10': 'strategy' + }, + ], +}; + +/// Descriptor for `CreateOrchestrationRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List createOrchestrationRequestDescriptor = $convert.base64Decode( + 'ChpDcmVhdGVPcmNoZXN0cmF0aW9uUmVxdWVzdBIqChFwYXJlbnRfc2Vzc2lvbl9pZBgBIAEoCV' + 'IPcGFyZW50U2Vzc2lvbklkEjMKBXN0ZXBzGAIgAygLMh0uYmV0Y29kZS52MS5PcmNoZXN0cmF0' + 'aW9uU3RlcFIFc3RlcHMSPQoIc3RyYXRlZ3kYAyABKA4yIS5iZXRjb2RlLnYxLk9yY2hlc3RyYX' + 'Rpb25TdHJhdGVneVIIc3RyYXRlZ3k='); + +@$core.Deprecated('Use createOrchestrationResponseDescriptor instead') +const CreateOrchestrationResponse$json = { + '1': 'CreateOrchestrationResponse', + '2': [ + {'1': 'orchestration_id', '3': 1, '4': 1, '5': 9, '10': 'orchestrationId'}, + {'1': 'total_steps', '3': 2, '4': 1, '5': 5, '10': 'totalSteps'}, + ], +}; + +/// Descriptor for `CreateOrchestrationResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List createOrchestrationResponseDescriptor = + $convert.base64Decode( + 'ChtDcmVhdGVPcmNoZXN0cmF0aW9uUmVzcG9uc2USKQoQb3JjaGVzdHJhdGlvbl9pZBgBIAEoCV' + 'IPb3JjaGVzdHJhdGlvbklkEh8KC3RvdGFsX3N0ZXBzGAIgASgFUgp0b3RhbFN0ZXBz'); + +@$core.Deprecated('Use orchestrationStepDescriptor instead') +const OrchestrationStep$json = { + '1': 'OrchestrationStep', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'}, + {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + {'1': 'prompt', '3': 3, '4': 1, '5': 9, '10': 'prompt'}, + {'1': 'model', '3': 4, '4': 1, '5': 9, '10': 'model'}, + { + '1': 'working_directory', + '3': 5, + '4': 1, + '5': 9, + '10': 'workingDirectory' + }, + {'1': 'allowed_tools', '3': 6, '4': 3, '5': 9, '10': 'allowedTools'}, + {'1': 'depends_on', '3': 7, '4': 3, '5': 9, '10': 'dependsOn'}, + {'1': 'max_turns', '3': 8, '4': 1, '5': 5, '10': 'maxTurns'}, + {'1': 'auto_approve', '3': 9, '4': 1, '5': 8, '10': 'autoApprove'}, + ], +}; + +/// Descriptor for `OrchestrationStep`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List orchestrationStepDescriptor = $convert.base64Decode( + 'ChFPcmNoZXN0cmF0aW9uU3RlcBIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZR' + 'IWCgZwcm9tcHQYAyABKAlSBnByb21wdBIUCgVtb2RlbBgEIAEoCVIFbW9kZWwSKwoRd29ya2lu' + 'Z19kaXJlY3RvcnkYBSABKAlSEHdvcmtpbmdEaXJlY3RvcnkSIwoNYWxsb3dlZF90b29scxgGIA' + 'MoCVIMYWxsb3dlZFRvb2xzEh0KCmRlcGVuZHNfb24YByADKAlSCWRlcGVuZHNPbhIbCgltYXhf' + 'dHVybnMYCCABKAVSCG1heFR1cm5zEiEKDGF1dG9fYXBwcm92ZRgJIAEoCFILYXV0b0FwcHJvdm' + 'U='); + +@$core.Deprecated('Use watchOrchestrationRequestDescriptor instead') +const WatchOrchestrationRequest$json = { + '1': 'WatchOrchestrationRequest', + '2': [ + {'1': 'orchestration_id', '3': 1, '4': 1, '5': 9, '10': 'orchestrationId'}, + ], +}; + +/// Descriptor for `WatchOrchestrationRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List watchOrchestrationRequestDescriptor = + $convert.base64Decode( + 'ChlXYXRjaE9yY2hlc3RyYXRpb25SZXF1ZXN0EikKEG9yY2hlc3RyYXRpb25faWQYASABKAlSD2' + '9yY2hlc3RyYXRpb25JZA=='); + +@$core.Deprecated('Use orchestrationEventDescriptor instead') +const OrchestrationEvent$json = { + '1': 'OrchestrationEvent', + '2': [ + {'1': 'orchestration_id', '3': 1, '4': 1, '5': 9, '10': 'orchestrationId'}, + { + '1': 'timestamp', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.protobuf.Timestamp', + '10': 'timestamp' + }, + { + '1': 'step_started', + '3': 3, + '4': 1, + '5': 11, + '6': '.betcode.v1.StepStarted', + '9': 0, + '10': 'stepStarted' + }, + { + '1': 'step_completed', + '3': 4, + '4': 1, + '5': 11, + '6': '.betcode.v1.StepCompleted', + '9': 0, + '10': 'stepCompleted' + }, + { + '1': 'step_failed', + '3': 5, + '4': 1, + '5': 11, + '6': '.betcode.v1.StepFailed', + '9': 0, + '10': 'stepFailed' + }, + { + '1': 'completed', + '3': 6, + '4': 1, + '5': 11, + '6': '.betcode.v1.OrchestrationCompleted', + '9': 0, + '10': 'completed' + }, + { + '1': 'failed', + '3': 7, + '4': 1, + '5': 11, + '6': '.betcode.v1.OrchestrationFailed', + '9': 0, + '10': 'failed' + }, + ], + '8': [ + {'1': 'event'}, + ], +}; + +/// Descriptor for `OrchestrationEvent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List orchestrationEventDescriptor = $convert.base64Decode( + 'ChJPcmNoZXN0cmF0aW9uRXZlbnQSKQoQb3JjaGVzdHJhdGlvbl9pZBgBIAEoCVIPb3JjaGVzdH' + 'JhdGlvbklkEjgKCXRpbWVzdGFtcBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBS' + 'CXRpbWVzdGFtcBI8CgxzdGVwX3N0YXJ0ZWQYAyABKAsyFy5iZXRjb2RlLnYxLlN0ZXBTdGFydG' + 'VkSABSC3N0ZXBTdGFydGVkEkIKDnN0ZXBfY29tcGxldGVkGAQgASgLMhkuYmV0Y29kZS52MS5T' + 'dGVwQ29tcGxldGVkSABSDXN0ZXBDb21wbGV0ZWQSOQoLc3RlcF9mYWlsZWQYBSABKAsyFi5iZX' + 'Rjb2RlLnYxLlN0ZXBGYWlsZWRIAFIKc3RlcEZhaWxlZBJCCgljb21wbGV0ZWQYBiABKAsyIi5i' + 'ZXRjb2RlLnYxLk9yY2hlc3RyYXRpb25Db21wbGV0ZWRIAFIJY29tcGxldGVkEjkKBmZhaWxlZB' + 'gHIAEoCzIfLmJldGNvZGUudjEuT3JjaGVzdHJhdGlvbkZhaWxlZEgAUgZmYWlsZWRCBwoFZXZl' + 'bnQ='); + +@$core.Deprecated('Use stepStartedDescriptor instead') +const StepStarted$json = { + '1': 'StepStarted', + '2': [ + {'1': 'step_id', '3': 1, '4': 1, '5': 9, '10': 'stepId'}, + {'1': 'subagent_id', '3': 2, '4': 1, '5': 9, '10': 'subagentId'}, + {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'}, + ], +}; + +/// Descriptor for `StepStarted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List stepStartedDescriptor = $convert.base64Decode( + 'CgtTdGVwU3RhcnRlZBIXCgdzdGVwX2lkGAEgASgJUgZzdGVwSWQSHwoLc3ViYWdlbnRfaWQYAi' + 'ABKAlSCnN1YmFnZW50SWQSEgoEbmFtZRgDIAEoCVIEbmFtZQ=='); + +@$core.Deprecated('Use stepCompletedDescriptor instead') +const StepCompleted$json = { + '1': 'StepCompleted', + '2': [ + {'1': 'step_id', '3': 1, '4': 1, '5': 9, '10': 'stepId'}, + {'1': 'result_summary', '3': 2, '4': 1, '5': 9, '10': 'resultSummary'}, + {'1': 'completed_count', '3': 3, '4': 1, '5': 5, '10': 'completedCount'}, + {'1': 'total_count', '3': 4, '4': 1, '5': 5, '10': 'totalCount'}, + ], +}; + +/// Descriptor for `StepCompleted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List stepCompletedDescriptor = $convert.base64Decode( + 'Cg1TdGVwQ29tcGxldGVkEhcKB3N0ZXBfaWQYASABKAlSBnN0ZXBJZBIlCg5yZXN1bHRfc3VtbW' + 'FyeRgCIAEoCVINcmVzdWx0U3VtbWFyeRInCg9jb21wbGV0ZWRfY291bnQYAyABKAVSDmNvbXBs' + 'ZXRlZENvdW50Eh8KC3RvdGFsX2NvdW50GAQgASgFUgp0b3RhbENvdW50'); + +@$core.Deprecated('Use stepFailedDescriptor instead') +const StepFailed$json = { + '1': 'StepFailed', + '2': [ + {'1': 'step_id', '3': 1, '4': 1, '5': 9, '10': 'stepId'}, + {'1': 'error_message', '3': 2, '4': 1, '5': 9, '10': 'errorMessage'}, + {'1': 'blocked_steps', '3': 3, '4': 3, '5': 9, '10': 'blockedSteps'}, + ], +}; + +/// Descriptor for `StepFailed`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List stepFailedDescriptor = $convert.base64Decode( + 'CgpTdGVwRmFpbGVkEhcKB3N0ZXBfaWQYASABKAlSBnN0ZXBJZBIjCg1lcnJvcl9tZXNzYWdlGA' + 'IgASgJUgxlcnJvck1lc3NhZ2USIwoNYmxvY2tlZF9zdGVwcxgDIAMoCVIMYmxvY2tlZFN0ZXBz'); + +@$core.Deprecated('Use orchestrationCompletedDescriptor instead') +const OrchestrationCompleted$json = { + '1': 'OrchestrationCompleted', + '2': [ + {'1': 'total_steps', '3': 1, '4': 1, '5': 5, '10': 'totalSteps'}, + {'1': 'succeeded', '3': 2, '4': 1, '5': 5, '10': 'succeeded'}, + {'1': 'failed', '3': 3, '4': 1, '5': 5, '10': 'failed'}, + ], +}; + +/// Descriptor for `OrchestrationCompleted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List orchestrationCompletedDescriptor = $convert.base64Decode( + 'ChZPcmNoZXN0cmF0aW9uQ29tcGxldGVkEh8KC3RvdGFsX3N0ZXBzGAEgASgFUgp0b3RhbFN0ZX' + 'BzEhwKCXN1Y2NlZWRlZBgCIAEoBVIJc3VjY2VlZGVkEhYKBmZhaWxlZBgDIAEoBVIGZmFpbGVk'); + +@$core.Deprecated('Use orchestrationFailedDescriptor instead') +const OrchestrationFailed$json = { + '1': 'OrchestrationFailed', + '2': [ + {'1': 'error_message', '3': 1, '4': 1, '5': 9, '10': 'errorMessage'}, + {'1': 'completed_steps', '3': 2, '4': 1, '5': 5, '10': 'completedSteps'}, + {'1': 'failed_steps', '3': 3, '4': 1, '5': 5, '10': 'failedSteps'}, + ], +}; + +/// Descriptor for `OrchestrationFailed`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List orchestrationFailedDescriptor = $convert.base64Decode( + 'ChNPcmNoZXN0cmF0aW9uRmFpbGVkEiMKDWVycm9yX21lc3NhZ2UYASABKAlSDGVycm9yTWVzc2' + 'FnZRInCg9jb21wbGV0ZWRfc3RlcHMYAiABKAVSDmNvbXBsZXRlZFN0ZXBzEiEKDGZhaWxlZF9z' + 'dGVwcxgDIAEoBVILZmFpbGVkU3RlcHM='); diff --git a/proto b/proto index bba1736..0f96f86 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit bba17362027338edc582ae201e44f78d979ff550 +Subproject commit 0f96f865d26c97c2101f04287aa587989c765a99 diff --git a/test/features/commands/notifiers/commands_notifier_test.dart b/test/features/commands/notifiers/commands_notifier_test.dart index 8be5df3..023b6a4 100644 --- a/test/features/commands/notifiers/commands_notifier_test.dart +++ b/test/features/commands/notifiers/commands_notifier_test.dart @@ -176,9 +176,10 @@ void main() { ); final captured = - verify(() => mockClient.getCommandRegistry(captureAny())) - .captured - .single as GetCommandRegistryRequest; + verify( + () => mockClient.getCommandRegistry(captureAny()), + ).captured.single + as GetCommandRegistryRequest; expect(captured.sessionId, 'sess-123'); }); @@ -190,9 +191,10 @@ void main() { await container.read(commandsProvider(null).future); final captured = - verify(() => mockClient.getCommandRegistry(captureAny())) - .captured - .single as GetCommandRegistryRequest; + verify( + () => mockClient.getCommandRegistry(captureAny()), + ).captured.single + as GetCommandRegistryRequest; expect(captured.sessionId, ''); }); }); @@ -248,12 +250,10 @@ void main() { ), ]; when(() => mockClient.listAgents(any())).thenAnswer( - (_) => - FakeResponseFuture.value(ListAgentsResponse(agents: agents)), + (_) => FakeResponseFuture.value(ListAgentsResponse(agents: agents)), ); - final result = - await notifier.listAgents(query: 'test', maxResults: 10); + final result = await notifier.listAgents(query: 'test', maxResults: 10); expect(result, hasLength(2)); expect(result[0].name, 'agent-1'); @@ -272,9 +272,8 @@ void main() { await notifier.listAgents(query: 'claude', maxResults: 5); final captured = - verify(() => mockClient.listAgents(captureAny())) - .captured - .single as ListAgentsRequest; + verify(() => mockClient.listAgents(captureAny())).captured.single + as ListAgentsRequest; expect(captured.query, 'claude'); expect(captured.maxResults, 5); @@ -329,12 +328,10 @@ void main() { ), ]; when(() => mockClient.listPath(any())).thenAnswer( - (_) => - FakeResponseFuture.value(ListPathResponse(entries: entries)), + (_) => FakeResponseFuture.value(ListPathResponse(entries: entries)), ); - final result = - await notifier.listPath(query: '/home', maxResults: 10); + final result = await notifier.listPath(query: '/home', maxResults: 10); expect(result, hasLength(2)); expect(result[0].path, '/home/user/project'); @@ -355,9 +352,8 @@ void main() { await notifier.listPath(query: '/tmp', maxResults: 15); final captured = - verify(() => mockClient.listPath(captureAny())) - .captured - .single as ListPathRequest; + verify(() => mockClient.listPath(captureAny())).captured.single + as ListPathRequest; expect(captured.query, '/tmp'); expect(captured.maxResults, 15); diff --git a/test/features/conversation/widgets/command_palette_test.dart b/test/features/conversation/widgets/command_palette_test.dart index 4460ef2..d1a0856 100644 --- a/test/features/conversation/widgets/command_palette_test.dart +++ b/test/features/conversation/widgets/command_palette_test.dart @@ -203,8 +203,9 @@ void main() { expect(find.text('/exit'), findsOneWidget); }); - testWidgets('daemon command overrides local command with same name', - (t) async { + testWidgets('daemon command overrides local command with same name', ( + t, + ) async { final daemonCommands = AsyncData([ CommandEntry( name: 'exit', diff --git a/test/features/sessions/notifiers/sessions_notifier_test.dart b/test/features/sessions/notifiers/sessions_notifier_test.dart index 98c6228..85b6386 100644 --- a/test/features/sessions/notifiers/sessions_notifier_test.dart +++ b/test/features/sessions/notifiers/sessions_notifier_test.dart @@ -48,6 +48,7 @@ void main() { registerFallbackValue(ListSessionsRequest()); registerFallbackValue(RenameSessionRequest()); registerFallbackValue(CompactSessionRequest()); + registerFallbackValue(DeleteSessionRequest()); registerFallbackValue((Batch _) async {}); }); @@ -318,4 +319,46 @@ void main() { verify(() => mockClient.listSessions(any())).called(2); }); }); + + group('SessionsNotifier - deleteSession', () { + test('calls deleteSession RPC with correct session ID', () async { + // Build initial state + when(() => mockClient.listSessions(any())).thenAnswer( + (_) => FakeResponseFuture.value( + ListSessionsResponse(sessions: [makeSession('s-1')]), + ), + ); + await container.read(sessionsProvider.future); + + when( + () => mockClient.deleteSession(any()), + ).thenAnswer((_) => FakeResponseFuture.value(DeleteSessionResponse())); + + final notifier = container.read(sessionsProvider.notifier); + await notifier.deleteSession('s-1'); + + final captured = + verify(() => mockClient.deleteSession(captureAny())).captured.single + as DeleteSessionRequest; + expect(captured.sessionId, 's-1'); + }); + + test('refreshes sessions after deletion', () async { + await initNotifier( + container: container, + provider: sessionsProvider, + stubEmpty: stubSessionsEmpty, + ); + + when( + () => mockClient.deleteSession(any()), + ).thenAnswer((_) => FakeResponseFuture.value(DeleteSessionResponse())); + + final notifier = container.read(sessionsProvider.notifier); + await notifier.deleteSession('s-1'); + + // listSessions is called once for build, once for refresh after delete + verify(() => mockClient.listSessions(any())).called(2); + }); + }); } From cca65f97e4dcd9608172d66641b79ff0dd89ce61 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 17:19:04 +0300 Subject: [PATCH 02/13] feat: health check on resume and UNIMPLEMENTED tolerance - Add health check verification in GrpcLifecycleBridge.onResumed() to detect zombie channels after short backgrounds (< 5 min) - Catch UNIMPLEMENTED in healthCheckFn as defense-in-depth (proves connection is alive even if relay lacks betcode.v1.Health) - Add hasHealthCheck and healthCheck() to GrpcClientManager - Add tests for health check scenarios (failing, UNIMPLEMENTED, no check) Co-Authored-By: Claude Opus 4.6 --- lib/core/grpc/client_manager.dart | 13 +++ lib/core/grpc/grpc_providers.dart | 17 +++- lib/core/grpc/lifecycle_bridge.dart | 23 +++++- test/core/grpc/lifecycle_bridge_test.dart | 96 +++++++++++++++++++++++ 4 files changed, 143 insertions(+), 6 deletions(-) diff --git a/lib/core/grpc/client_manager.dart b/lib/core/grpc/client_manager.dart index 1b7a025..4642068 100644 --- a/lib/core/grpc/client_manager.dart +++ b/lib/core/grpc/client_manager.dart @@ -86,6 +86,19 @@ class GrpcClientManager { /// Whether a health check function has been configured. bool get hasHealthCheck => _healthCheckFn != null; + /// Runs the configured health check against the current channel. + /// + /// Throws [StateError] if no health check is configured or no channel + /// is available. Re-throws whatever the health check function throws + /// on failure (typically [GrpcError]). + Future healthCheck() async { + final fn = _healthCheckFn; + if (fn == null) { + throw StateError('No health check function configured'); + } + await fn(channel); + } + /// The host from the last [connect] call, or null if never connected. String? get host => _host; diff --git a/lib/core/grpc/grpc_providers.dart b/lib/core/grpc/grpc_providers.dart index fa9dcb4..c104fdd 100644 --- a/lib/core/grpc/grpc_providers.dart +++ b/lib/core/grpc/grpc_providers.dart @@ -34,13 +34,22 @@ final grpcClientManagerProvider = Provider((ref) { machineIdProvider: () async => ref.read(selectedMachineIdProvider), ), LoggingInterceptor(), + ErrorMappingInterceptor(), ], healthCheckFn: (channel) async { final client = HealthClient(channel); - await client.check( - HealthCheckRequest(), - options: CallOptions(timeout: const Duration(seconds: 5)), - ); + try { + await client.check( + HealthCheckRequest(), + options: CallOptions(timeout: const Duration(seconds: 5)), + ); + } on GrpcError catch (e) { + if (e.code == StatusCode.unimplemented) { + // Server responded — connection is alive, just no Health service. + return; + } + rethrow; + } }, ); diff --git a/lib/core/grpc/lifecycle_bridge.dart b/lib/core/grpc/lifecycle_bridge.dart index fbeb794..ee045bb 100644 --- a/lib/core/grpc/lifecycle_bridge.dart +++ b/lib/core/grpc/lifecycle_bridge.dart @@ -40,8 +40,9 @@ class GrpcLifecycleBridge { /// Called when the app returns to foreground (resumed). /// /// Cancels the teardown timer if it hasn't fired yet. If the channel was - /// torn down during extended background, reconnects using the stored - /// connection parameters. + /// torn down during extended background, reconnects immediately. Otherwise, + /// runs a health check to detect zombie channels (TCP connection killed by + /// the OS during sleep) and reconnects if the channel is dead. void onResumed() { _teardownTimer?.cancel(); _teardownTimer = null; @@ -50,6 +51,24 @@ class GrpcLifecycleBridge { if (_tornDown) { _tornDown = false; _reconnect(); + } else if (_manager.channelOrNull != null && _manager.hasHealthCheck) { + // Channel exists but may be a zombie — OS often kills idle TCP sockets + // while the app is backgrounded. Fire a health check to find out. + unawaited(_verifyOrReconnect()); + } + } + + /// Runs a health check against the existing channel. If it fails, tears + /// down the dead channel and reconnects. + Future _verifyOrReconnect() async { + try { + await _manager.healthCheck(); + } on Object catch (e) { + debugPrint( + '[GrpcLifecycleBridge] Health check failed after resume, ' + 'reconnecting: $e', + ); + _reconnect(); } } diff --git a/test/core/grpc/lifecycle_bridge_test.dart b/test/core/grpc/lifecycle_bridge_test.dart index 0b62b11..76cda74 100644 --- a/test/core/grpc/lifecycle_bridge_test.dart +++ b/test/core/grpc/lifecycle_bridge_test.dart @@ -3,6 +3,7 @@ import 'package:betcode_app/core/grpc/connection_state.dart'; import 'package:betcode_app/core/grpc/lifecycle_bridge.dart'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:grpc/grpc.dart'; void main() { late GrpcClientManager manager; @@ -184,5 +185,100 @@ void main() { bridge.onResumed(); expect(manager.isPaused, isFalse); }); + + test( + 'short background with failing health check triggers reconnect', + () async { + // Create a manager whose health check always fails, simulating a + // zombie channel (TCP died while the phone was asleep). + final failingManager = GrpcClientManager( + healthCheckFn: (_) async { + throw Exception('connection dead'); + }, + ); + final failingBridge = GrpcLifecycleBridge(failingManager); + addTearDown(() async { + failingBridge.dispose(); + await failingManager.dispose(); + }); + + await failingManager.connect('localhost', 50051); + expect(failingManager.status, GrpcConnectionStatus.connected); + + fakeAsync((async) { + // Short background (< 5 minutes) + failingBridge.onPaused(); + async.elapse(const Duration(minutes: 1)); + failingBridge.onResumed(); + // Flush the health check + reconnect futures + async.flushMicrotasks(); + + // Manager should have attempted to reconnect (status is connecting + // or connected depending on whether connect() to localhost succeeded) + expect( + failingManager.status, + anyOf( + GrpcConnectionStatus.connecting, + GrpcConnectionStatus.connected, + ), + ); + }); + }, + ); + + test( + 'short background with UNIMPLEMENTED health check does not reconnect', + () async { + // Simulate a relay that returns UNIMPLEMENTED for the Health service. + // The healthCheckFn (as configured in grpc_providers) catches + // UNIMPLEMENTED and returns normally — connection is alive. + final unimplManager = GrpcClientManager( + healthCheckFn: (_) async { + try { + throw const GrpcError.unimplemented(); + } on GrpcError catch (e) { + if (e.code == StatusCode.unimplemented) return; + rethrow; + } + }, + ); + final unimplBridge = GrpcLifecycleBridge(unimplManager); + addTearDown(() async { + unimplBridge.dispose(); + await unimplManager.dispose(); + }); + + await unimplManager.connect('localhost', 50051); + expect(unimplManager.status, GrpcConnectionStatus.connected); + + fakeAsync((async) { + unimplBridge.onPaused(); + async.elapse(const Duration(minutes: 1)); + unimplBridge.onResumed(); + async.flushMicrotasks(); + + // Channel should still be present — UNIMPLEMENTED means alive + expect(unimplManager.channelOrNull, isNotNull); + expect(unimplManager.status, GrpcConnectionStatus.connected); + }); + }, + ); + + test('short background with no health check skips verification', () async { + // Default manager has no health check + await manager.connect('localhost', 50051); + expect(manager.hasHealthCheck, isFalse); + + fakeAsync((async) { + bridge.onPaused(); + async.elapse(const Duration(minutes: 1)); + bridge.onResumed(); + async.flushMicrotasks(); + + // Channel should still be the same (no reconnect triggered) + expect(manager.channelOrNull, isNotNull); + expect(manager.status, GrpcConnectionStatus.connected); + }); + }); }); } From f56b02ea122184758c1d174cec0c4b00d46a3c70 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 17:19:26 +0300 Subject: [PATCH 03/13] feat: typed error handling with AppException hierarchy and connectivity banner - Add sealed AppException class with 8 typed subclasses (NetworkError, RelayUnavailableError, SessionNotFoundError, SessionInvalidError, AuthExpiredError, PermissionDeniedError, ServerError, RateLimitError) - Add mapGrpcError() function mapping gRPC status codes to AppExceptions with human-readable messages and context-sensitive session detection - Add ErrorMappingInterceptor as last in gRPC interceptor chain - Add global ConnectivityBanner widget in app.dart showing persistent network/relay status (red for offline, orange for relay unreachable) - Update ConversationNotifier to use typed exceptions for fatal error detection and human-readable error messages - Update SessionsScreen to catch typed exceptions with friendly messages and handle SessionNotFoundError on delete gracefully - Update ErrorDisplay widget to show AppException.message instead of raw toString() - Update AuthNotifier._isAuthError() to recognize typed exceptions - Add comprehensive tests (35 new test cases) - Add error handling plan doc Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-02-18-error-handling.md | 1087 +++++++++++++++++ lib/app.dart | 20 +- lib/core/auth/auth_notifier.dart | 6 + lib/core/grpc/app_exceptions.dart | 72 ++ lib/core/grpc/error_mapping.dart | 78 ++ lib/core/grpc/grpc.dart | 2 + lib/core/grpc/interceptors.dart | 150 +++ .../sessions/screens/sessions_screen.dart | 44 +- lib/shared/widgets/connectivity_banner.dart | 70 ++ lib/shared/widgets/error_display.dart | 5 +- lib/shared/widgets/widgets.dart | 1 + test/core/grpc/app_exceptions_test.dart | 77 ++ .../grpc/error_mapping_interceptor_test.dart | 158 +++ test/core/grpc/error_mapping_test.dart | 143 +++ test/core/grpc/grpc_providers_test.dart | 28 +- .../notifiers/conversation_notifier_test.dart | 33 +- .../conversation/widgets/input_bar_test.dart | 4 +- .../widgets/connectivity_banner_test.dart | 78 ++ 18 files changed, 2029 insertions(+), 27 deletions(-) create mode 100644 docs/plans/2026-02-18-error-handling.md create mode 100644 lib/core/grpc/app_exceptions.dart create mode 100644 lib/core/grpc/error_mapping.dart create mode 100644 lib/shared/widgets/connectivity_banner.dart create mode 100644 test/core/grpc/app_exceptions_test.dart create mode 100644 test/core/grpc/error_mapping_interceptor_test.dart create mode 100644 test/core/grpc/error_mapping_test.dart create mode 100644 test/shared/widgets/connectivity_banner_test.dart diff --git a/docs/plans/2026-02-18-error-handling.md b/docs/plans/2026-02-18-error-handling.md new file mode 100644 index 0000000..c376096 --- /dev/null +++ b/docs/plans/2026-02-18-error-handling.md @@ -0,0 +1,1087 @@ +# Error Handling Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace raw gRPC error display with typed exceptions, human-readable messages, and a global connectivity banner. + +**Architecture:** A new `ErrorMappingInterceptor` at the end of the gRPC interceptor chain catches `GrpcError` and rethrows as typed `AppException` subclasses. A global `ConnectivityBanner` widget wraps the app for persistent network/relay status. Per-feature code catches typed exceptions for context-appropriate UI responses. + +**Tech Stack:** Dart sealed classes, gRPC `ClientInterceptor`, Riverpod `StreamProvider`, Flutter `AnimatedSize`/`AnimatedOpacity`. + +--- + +## Task 1: Exception Hierarchy + +**Files:** +- Create: `lib/core/grpc/app_exceptions.dart` +- Modify: `lib/core/grpc/grpc.dart` (add barrel export) +- Create: `test/core/grpc/app_exceptions_test.dart` + +### Step 1: Write the failing test + +```dart +// test/core/grpc/app_exceptions_test.dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppException hierarchy', () { + test('NetworkError carries message and cause', () { + final cause = Exception('timeout'); + final error = NetworkError('No internet connection', cause: cause); + + expect(error.message, 'No internet connection'); + expect(error.cause, cause); + expect(error.toString(), contains('No internet connection')); + }); + + test('SessionNotFoundError includes sessionId', () { + final error = SessionNotFoundError( + 'Session no longer exists', + sessionId: 'abc-123', + ); + expect(error.sessionId, 'abc-123'); + }); + + test('all exception types are AppException', () { + expect(NetworkError('msg'), isA()); + expect(RelayUnavailableError('msg'), isA()); + expect( + SessionNotFoundError('msg', sessionId: 's'), + isA(), + ); + expect(SessionInvalidError('msg'), isA()); + expect(AuthExpiredError('msg'), isA()); + expect(PermissionDeniedError('msg'), isA()); + expect(ServerError('msg'), isA()); + expect(RateLimitError('msg'), isA()); + }); + + test('AppException is sealed — switch is exhaustive', () { + // This test verifies the sealed class is exhaustive at compile time. + // If a new subclass is added without updating this switch, it will + // fail to compile. + AppException error = NetworkError('test'); + final result = switch (error) { + NetworkError() => 'network', + RelayUnavailableError() => 'relay', + SessionNotFoundError() => 'session_not_found', + SessionInvalidError() => 'session_invalid', + AuthExpiredError() => 'auth', + PermissionDeniedError() => 'denied', + ServerError() => 'server', + RateLimitError() => 'rate_limit', + }; + expect(result, 'network'); + }); + }); +} +``` + +### Step 2: Run test to verify it fails + +Run: `flutter test test/core/grpc/app_exceptions_test.dart` +Expected: FAIL — cannot resolve `app_exceptions.dart` + +### Step 3: Write the exception hierarchy + +```dart +// lib/core/grpc/app_exceptions.dart + +/// Typed exception hierarchy for gRPC and network errors. +/// +/// Each subclass carries a user-facing [message] and an optional [cause] +/// (typically the original [GrpcError]) for debug logging. +/// +/// Sealed so `switch` expressions are exhaustive — the compiler enforces +/// that every UI error handler covers all cases. +sealed class AppException implements Exception { + const AppException(this.message, {this.cause}); + + /// Human-readable message safe to display in the UI. + final String message; + + /// The underlying error (typically [GrpcError]) for debug logging. + final Object? cause; + + @override + String toString() => message; +} + +/// Device has no network, DNS failure, connection refused, or timeout. +class NetworkError extends AppException { + const NetworkError(super.message, {super.cause}); +} + +/// gRPC channel to relay is down (TLS handshake failure, channel shutting +/// down, relay process not running). +class RelayUnavailableError extends AppException { + const RelayUnavailableError(super.message, {super.cause}); +} + +/// The requested session does not exist (deleted or never created). +class SessionNotFoundError extends AppException { + const SessionNotFoundError(super.message, {super.cause, required this.sessionId}); + + /// The session ID that was not found. + final String sessionId; +} + +/// Session operation failed due to invalid arguments or precondition +/// violation (e.g. empty name, session in wrong state). +class SessionInvalidError extends AppException { + const SessionInvalidError(super.message, {super.cause}); +} + +/// Authentication token expired or was revoked. Must re-login. +class AuthExpiredError extends AppException { + const AuthExpiredError(super.message, {super.cause}); +} + +/// The authenticated user does not have permission for this operation. +class PermissionDeniedError extends AppException { + const PermissionDeniedError(super.message, {super.cause}); +} + +/// Unexpected server-side error (INTERNAL, UNKNOWN, DATA_LOSS, etc.). +class ServerError extends AppException { + const ServerError(super.message, {super.cause}); +} + +/// Too many requests — back off and retry. +class RateLimitError extends AppException { + const RateLimitError(super.message, {super.cause}); +} +``` + +### Step 4: Add barrel export + +In `lib/core/grpc/grpc.dart`, add: +```dart +export 'app_exceptions.dart'; +``` + +### Step 5: Run test to verify it passes + +Run: `flutter test test/core/grpc/app_exceptions_test.dart` +Expected: PASS (all 4 tests) + +### Step 6: Commit + +```bash +git add lib/core/grpc/app_exceptions.dart lib/core/grpc/grpc.dart test/core/grpc/app_exceptions_test.dart +git commit -m "feat: add sealed AppException hierarchy for typed gRPC error handling" +``` + +--- + +## Task 2: Error Mapping Function + +**Files:** +- Create: `lib/core/grpc/error_mapping.dart` +- Modify: `lib/core/grpc/grpc.dart` (add barrel export) +- Create: `test/core/grpc/error_mapping_test.dart` + +### Step 1: Write the failing tests + +```dart +// test/core/grpc/error_mapping_test.dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:betcode_app/core/grpc/error_mapping.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:grpc/grpc.dart'; + +void main() { + group('mapGrpcError', () { + test('UNAVAILABLE with handshake error → RelayUnavailableError', () { + final grpcError = GrpcError.custom( + StatusCode.unavailable, + 'Error connecting: HandshakeException: WRONG_VERSION_NUMBER', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + expect(result.cause, grpcError); + }); + + test('UNAVAILABLE with channel shutting down → RelayUnavailableError', () { + final grpcError = GrpcError.custom( + StatusCode.unavailable, + 'Channel shutting down.', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('UNAVAILABLE generic → NetworkError', () { + final grpcError = GrpcError.custom( + StatusCode.unavailable, + 'Connection timed out', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('NOT_FOUND on session RPC → SessionNotFoundError', () { + final grpcError = const GrpcError.notFound('session not found'); + final result = mapGrpcError( + grpcError, + method: '/betcode.v1.AgentService/DeleteSession', + ); + expect(result, isA()); + }); + + test('NOT_FOUND on non-session RPC → ServerError', () { + final grpcError = const GrpcError.notFound('machine not found'); + final result = mapGrpcError( + grpcError, + method: '/betcode.v1.MachineService/ListMachines', + ); + expect(result, isA()); + }); + + test('UNAUTHENTICATED → AuthExpiredError', () { + final result = mapGrpcError(const GrpcError.unauthenticated()); + expect(result, isA()); + }); + + test('PERMISSION_DENIED → PermissionDeniedError', () { + final result = mapGrpcError( + GrpcError.custom(StatusCode.permissionDenied, 'not owner'), + ); + expect(result, isA()); + }); + + test('RESOURCE_EXHAUSTED → RateLimitError', () { + final result = mapGrpcError(const GrpcError.resourceExhausted()); + expect(result, isA()); + }); + + test('INVALID_ARGUMENT → SessionInvalidError', () { + final result = mapGrpcError( + GrpcError.custom(StatusCode.invalidArgument, 'name too long'), + ); + expect(result, isA()); + }); + + test('FAILED_PRECONDITION → SessionInvalidError', () { + final result = mapGrpcError( + GrpcError.custom(StatusCode.failedPrecondition, 'session locked'), + ); + expect(result, isA()); + }); + + test('INTERNAL → ServerError', () { + final result = mapGrpcError(const GrpcError.internal()); + expect(result, isA()); + }); + + test('DEADLINE_EXCEEDED → NetworkError', () { + final result = mapGrpcError(const GrpcError.deadlineExceeded()); + expect(result, isA()); + }); + + test('UNKNOWN → ServerError', () { + final result = mapGrpcError( + GrpcError.custom(StatusCode.unknown, 'something broke'), + ); + expect(result, isA()); + }); + + test('preserves original GrpcError as cause', () { + final grpcError = const GrpcError.internal('db crashed'); + final result = mapGrpcError(grpcError); + expect(result.cause, grpcError); + }); + }); +} +``` + +### Step 2: Run test to verify it fails + +Run: `flutter test test/core/grpc/error_mapping_test.dart` +Expected: FAIL — cannot resolve `error_mapping.dart` + +### Step 3: Write the mapping function + +```dart +// lib/core/grpc/error_mapping.dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:grpc/grpc.dart'; + +/// RPC method substrings that indicate session-related operations. +const _sessionMethods = [ + 'Session', // DeleteSession, RenameSession, CompactSession, ResumeSession + 'Converse', // The bidi conversation stream +]; + +/// Maps a [GrpcError] to the appropriate [AppException] subclass. +/// +/// The optional [method] parameter is the gRPC method path +/// (e.g. `/betcode.v1.AgentService/DeleteSession`) and is used to +/// distinguish session-specific NOT_FOUND from generic NOT_FOUND. +AppException mapGrpcError(GrpcError error, {String? method}) { + return switch (error.code) { + StatusCode.unavailable => _mapUnavailable(error), + StatusCode.deadlineExceeded => NetworkError( + 'Request timed out. Check your connection and try again.', + cause: error, + ), + StatusCode.notFound => _isSessionMethod(method) + ? SessionNotFoundError( + 'Session no longer exists.', + cause: error, + sessionId: '', + ) + : ServerError( + 'The requested resource was not found.', + cause: error, + ), + StatusCode.unauthenticated => AuthExpiredError( + 'Your session has expired. Please log in again.', + cause: error, + ), + StatusCode.permissionDenied => PermissionDeniedError( + 'You don\'t have permission for this action.', + cause: error, + ), + StatusCode.resourceExhausted => RateLimitError( + 'Too many requests. Please wait a moment and try again.', + cause: error, + ), + StatusCode.invalidArgument || StatusCode.failedPrecondition => + SessionInvalidError( + error.message ?? 'Invalid request.', + cause: error, + ), + _ => ServerError( + 'Something went wrong. Please try again.', + cause: error, + ), + }; +} + +AppException _mapUnavailable(GrpcError error) { + final msg = error.message ?? ''; + if (msg.contains('HandshakeException') || + msg.contains('WRONG_VERSION_NUMBER') || + msg.contains('Channel shutting down') || + msg.contains('TLS') || + msg.contains('CERTIFICATE')) { + return RelayUnavailableError( + 'Unable to reach the relay server.', + cause: error, + ); + } + return NetworkError( + 'Connection lost. Retrying...', + cause: error, + ); +} + +bool _isSessionMethod(String? method) { + if (method == null) return false; + return _sessionMethods.any(method.contains); +} +``` + +### Step 4: Add barrel export + +In `lib/core/grpc/grpc.dart`, add: +```dart +export 'error_mapping.dart'; +``` + +### Step 5: Run test to verify it passes + +Run: `flutter test test/core/grpc/error_mapping_test.dart` +Expected: PASS (all 14 tests) + +### Step 6: Commit + +```bash +git add lib/core/grpc/error_mapping.dart lib/core/grpc/grpc.dart test/core/grpc/error_mapping_test.dart +git commit -m "feat: add mapGrpcError function mapping status codes to typed AppExceptions" +``` + +--- + +## Task 3: Error Mapping Interceptor + +**Files:** +- Modify: `lib/core/grpc/interceptors.dart` (add `ErrorMappingInterceptor`) +- Modify: `lib/core/grpc/grpc_providers.dart:27-37` (add interceptor to chain) +- Create: `test/core/grpc/error_mapping_interceptor_test.dart` + +### Step 1: Write the failing tests + +```dart +// test/core/grpc/error_mapping_interceptor_test.dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:betcode_app/core/grpc/interceptors.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:grpc/grpc.dart'; + +import '../../core/interceptor_test_helpers.dart'; + +void main() { + group('ErrorMappingInterceptor', () { + late ErrorMappingInterceptor interceptor; + + setUp(() { + interceptor = ErrorMappingInterceptor(); + }); + + test('passes through successful unary responses unchanged', () async { + final response = interceptor.interceptUnary( + FakeClientMethod('/test/Ok'), + 'request', + CallOptions(), + (method, request, options) => MockResponseFuture.value('success'), + ); + expect(await response, 'success'); + }); + + test('maps GrpcError to AppException on unary failure', () async { + final response = interceptor.interceptUnary( + FakeClientMethod('/betcode.v1.AgentService/DeleteSession'), + 'request', + CallOptions(), + (method, request, options) => + MockResponseFuture.error(const GrpcError.notFound()), + ); + expect( + () => response, + throwsA(isA()), + ); + }); + + test('maps UNAVAILABLE to NetworkError on unary', () async { + final response = interceptor.interceptUnary( + FakeClientMethod('/test/Rpc'), + 'request', + CallOptions(), + (method, request, options) => + MockResponseFuture.error(const GrpcError.unavailable()), + ); + expect( + () => response, + throwsA(isA()), + ); + }); + + test('non-GrpcError passes through unmapped', () async { + final response = interceptor.interceptUnary( + FakeClientMethod('/test/Rpc'), + 'request', + CallOptions(), + (method, request, options) => + MockResponseFuture.error(Exception('not grpc')), + ); + expect( + () => response, + throwsA(isA()), + ); + }); + }); +} +``` + +Note: this test depends on `test/core/interceptor_test_helpers.dart` which should already contain `FakeClientMethod` and `MockResponseFuture` helpers. Check the existing file and add any missing helpers. If `MockResponseFuture.error` doesn't exist, add it following the existing pattern. + +### Step 2: Run test to verify it fails + +Run: `flutter test test/core/grpc/error_mapping_interceptor_test.dart` +Expected: FAIL — `ErrorMappingInterceptor` not found + +### Step 3: Add the interceptor class + +Append to `lib/core/grpc/interceptors.dart`: + +```dart +/// Maps [GrpcError] responses to typed [AppException] subclasses. +/// +/// Must be the **last** interceptor in the chain so it wraps errors from +/// all preceding interceptors (auth refresh, logging, etc.). +class ErrorMappingInterceptor extends ClientInterceptor { + @override + ResponseFuture interceptUnary( + ClientMethod method, + Q request, + CallOptions options, + ClientUnaryInvoker invoker, + ) { + final response = invoker(method, request, options); + return response.catchError( + (Object error) => throw mapGrpcError(error as GrpcError, method: method.path), + test: (error) => error is GrpcError, + ) as ResponseFuture; + } + + @override + ResponseStream interceptStreaming( + ClientMethod method, + Stream requests, + CallOptions options, + ClientStreamingInvoker invoker, + ) { + final stream = invoker(method, requests, options); + return stream.handleError( + (Object error) => throw mapGrpcError(error as GrpcError, method: method.path), + test: (error) => error is GrpcError, + ) as ResponseStream; + } +} +``` + +Add import at top of `interceptors.dart`: +```dart +import 'package:betcode_app/core/grpc/error_mapping.dart'; +``` + +### Step 4: Wire into interceptor chain + +In `lib/core/grpc/grpc_providers.dart:27-37`, add `ErrorMappingInterceptor()` as the **last** item in the interceptors list: + +```dart +interceptors: [ + TokenRefreshInterceptor( + authNotifier: authNotifier, + authClientFactory: () => AuthServiceClient(manager.channel), + ), + AuthInterceptor(tokenProvider: () async => authNotifier.accessToken), + MachineIdInterceptor( + machineIdProvider: () async => ref.read(selectedMachineIdProvider), + ), + LoggingInterceptor(), + ErrorMappingInterceptor(), // <-- ADD: must be last +], +``` + +### Step 5: Run test to verify it passes + +Run: `flutter test test/core/grpc/error_mapping_interceptor_test.dart` +Expected: PASS + +Also run existing interceptor tests to verify no regressions: +Run: `flutter test test/core/grpc/interceptors_test.dart test/core/grpc/token_refresh_interceptor_test.dart` +Expected: PASS + +### Step 6: Commit + +```bash +git add lib/core/grpc/interceptors.dart lib/core/grpc/grpc_providers.dart test/core/grpc/error_mapping_interceptor_test.dart +git commit -m "feat: add ErrorMappingInterceptor to convert GrpcError into typed AppExceptions" +``` + +--- + +## Task 4: Global Connectivity Banner + +**Files:** +- Create: `lib/shared/widgets/connectivity_banner.dart` +- Modify: `lib/shared/widgets/widgets.dart` (add barrel export, if exists) +- Modify: `lib/app.dart:11-22` (wrap MaterialApp.router) +- Create: `test/shared/widgets/connectivity_banner_test.dart` + +### Step 1: Write the failing test + +```dart +// test/shared/widgets/connectivity_banner_test.dart +import 'package:betcode_app/core/grpc/connection_state.dart'; +import 'package:betcode_app/core/grpc/grpc_providers.dart'; +import 'package:betcode_app/core/sync/connectivity.dart'; +import 'package:betcode_app/shared/widgets/connectivity_banner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ConnectivityBanner', () { + Widget buildApp({ + AsyncValue connectionStatus = + const AsyncData(GrpcConnectionStatus.connected), + AsyncValue networkStatus = + const AsyncData(NetworkStatus.online), + }) { + return ProviderScope( + overrides: [ + connectionStatusProvider.overrideWith((_) => connectionStatus.when( + data: (d) => Stream.value(d), + loading: () => const Stream.empty(), + error: (e, s) => Stream.error(e, s), + )), + networkStatusProvider.overrideWith((_) => networkStatus.when( + data: (d) => Stream.value(d), + loading: () => const Stream.empty(), + error: (e, s) => Stream.error(e, s), + )), + ], + child: const MaterialApp( + home: Scaffold( + body: Column( + children: [ + ConnectivityBanner(), + Expanded(child: Placeholder()), + ], + ), + ), + ), + ); + } + + testWidgets('hidden when online and connected', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect(find.text('No internet connection'), findsNothing); + expect(find.text('Relay unreachable'), findsNothing); + }); + + testWidgets('shows offline banner when network is offline', (tester) async { + await tester.pumpWidget(buildApp( + networkStatus: const AsyncData(NetworkStatus.offline), + )); + await tester.pumpAndSettle(); + expect(find.textContaining('No internet'), findsOneWidget); + }); + + testWidgets('shows relay banner when disconnected but online', + (tester) async { + await tester.pumpWidget(buildApp( + connectionStatus: + const AsyncData(GrpcConnectionStatus.reconnecting), + )); + await tester.pumpAndSettle(); + expect(find.textContaining('reconnecting'), findsOneWidget); + }); + }); +} +``` + +### Step 2: Run test to verify it fails + +Run: `flutter test test/shared/widgets/connectivity_banner_test.dart` +Expected: FAIL — cannot resolve `connectivity_banner.dart` + +### Step 3: Write the banner widget + +```dart +// lib/shared/widgets/connectivity_banner.dart +import 'package:betcode_app/core/grpc/connection_state.dart'; +import 'package:betcode_app/core/grpc/grpc_providers.dart'; +import 'package:betcode_app/core/sync/connectivity.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// A persistent banner shown at the top of the app when there are +/// connectivity issues. +/// +/// Watches [networkStatusProvider] and [connectionStatusProvider] to +/// determine what to display. Hidden (zero height) when everything is fine. +class ConnectivityBanner extends ConsumerWidget { + const ConnectivityBanner({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final networkAsync = ref.watch(networkStatusProvider); + final connectionAsync = ref.watch(connectionStatusProvider); + + final isOffline = + networkAsync.valueOrNull == NetworkStatus.offline; + final connectionStatus = connectionAsync.valueOrNull; + final isRelayDown = !isOffline && + (connectionStatus == GrpcConnectionStatus.disconnected || + connectionStatus == GrpcConnectionStatus.reconnecting); + + final String? message; + final Color? backgroundColor; + final IconData? icon; + + if (isOffline) { + message = 'No internet connection'; + backgroundColor = Theme.of(context).colorScheme.error; + icon = Icons.wifi_off; + } else if (isRelayDown) { + message = connectionStatus == GrpcConnectionStatus.reconnecting + ? 'Relay unreachable — reconnecting...' + : 'Relay unreachable'; + backgroundColor = Theme.of(context).colorScheme.tertiary; + icon = Icons.cloud_off; + } else { + message = null; + backgroundColor = null; + icon = null; + } + + return AnimatedSize( + duration: const Duration(milliseconds: 200), + child: message != null + ? MaterialBanner( + content: Row( + children: [ + Icon(icon, color: Colors.white, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + backgroundColor: backgroundColor, + actions: const [SizedBox.shrink()], + ) + : const SizedBox.shrink(), + ); + } +} +``` + +### Step 4: Wrap `MaterialApp.router` in `app.dart` + +Replace `lib/app.dart:11-22` build method: + +```dart +@override +Widget build(BuildContext context, WidgetRef ref) { + ref.watch(relayAutoReconnectProvider); + final router = ref.watch(routerProvider); + + return Column( + children: [ + const ConnectivityBanner(), + Expanded( + child: MaterialApp.router( + title: 'BetCode', + debugShowCheckedModeBanner: false, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + routerConfig: router, + ), + ), + ], + ); +} +``` + +Add import: +```dart +import 'package:betcode_app/shared/widgets/connectivity_banner.dart'; +``` + +### Step 5: Run test to verify it passes + +Run: `flutter test test/shared/widgets/connectivity_banner_test.dart` +Expected: PASS + +### Step 6: Commit + +```bash +git add lib/shared/widgets/connectivity_banner.dart lib/app.dart test/shared/widgets/connectivity_banner_test.dart +git commit -m "feat: add global ConnectivityBanner for persistent network/relay status" +``` + +--- + +## Task 5: Update Conversation Notifier Error Handling + +**Files:** +- Modify: `lib/features/conversation/notifiers/conversation_notifier.dart` + - `_isFatalError()` (lines 326-333) + - `_handleStreamError()` (lines 290-324) + - `startConversation()` (lines 141-143) +- Modify: `test/features/conversation/notifiers/conversation_notifier_test.dart` (add new test cases) + +### Step 1: Write the failing tests + +Add to `test/features/conversation/notifiers/conversation_notifier_test.dart` in an appropriate test group: + +```dart +group('typed error handling', () { + test('SessionNotFoundError transitions to error with sessionExpired', () async { + // Setup: start a conversation, then simulate stream error with NOT_FOUND + // The conversation state should transition to ConversationState.error() + // and the error message should be human-readable. + }); + + test('NetworkError triggers reconnection, not fatal', () async { + // Setup: start conversation, simulate NetworkError on stream + // Should attempt reconnection, not go to error state. + }); + + test('AuthExpiredError is fatal, no reconnect', () async { + // Setup: start conversation, simulate AuthExpiredError + // Should go directly to error state, no reconnect attempt. + }); + + test('history load failure sets errorMessage on active state', () async { + // Setup: start conversation with sessionId, mock resumeSession to throw + // Conversation should be ConversationActive with + // errorMessage = "Couldn't load message history" + }); +}); +``` + +Note: follow the existing test patterns in `conversation_notifier_test.dart` and `conversation_notifier_helpers.dart` for mocking. These are skeleton tests — fill in based on the existing mock setup. + +### Step 2: Run tests to verify they fail + +Run: `flutter test test/features/conversation/notifiers/conversation_notifier_test.dart` +Expected: New tests FAIL + +### Step 3: Update `_isFatalError` to use typed exceptions + +Replace `_isFatalError` (lines 326-333): + +```dart +bool _isFatalError(Object error) { + return error is AuthExpiredError || + error is PermissionDeniedError || + error is SessionNotFoundError; +} +``` + +Add import: +```dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +``` + +### Step 4: Update `_handleStreamError` to use human-readable messages + +In `_handleStreamError` (lines 290-324), replace the raw error string in `ConversationState.error(...)`: + +```dart +void _handleStreamError(Object error) { + debugPrint('[Conversation] Stream error: $error'); + unawaited(_eventSubscription?.cancel()); + _eventSubscription = null; + + if (_isFatalError(error)) { + _isReconnecting = false; + final message = error is AppException + ? error.message + : 'Stream error: $error'; + state = AsyncData(ConversationState.error(message)); + unawaited(_requestController?.close()); + _requestController = null; + return; + } + + // ... rest of reconnection logic stays the same, but update error messages: + // Replace 'Stream error: $error' with human-readable message from AppException +} +``` + +### Step 5: Update `_loadHistory` to set `errorMessage` + +Replace the catch block in `_loadHistory` (lines 171-174): + +```dart +} on GrpcError catch (e) { + debugPrint('[Conversation] History load failed: $e'); + final current = state.value; + if (current is ConversationActive) { + state = AsyncData( + current.copyWith( + errorMessage: "Couldn't load message history.", + ), + ); + } +} +``` + +### Step 6: Update `startConversation` catch block + +Replace the catch block in `startConversation` (lines 141-143): + +```dart +} on Exception catch (e) { + final message = e is AppException ? e.message : 'Failed to start conversation: $e'; + state = AsyncData(ConversationState.error(message)); +} +``` + +### Step 7: Run tests to verify they pass + +Run: `flutter test test/features/conversation/notifiers/conversation_notifier_test.dart` +Expected: PASS (all tests including new ones) + +### Step 8: Commit + +```bash +git add lib/features/conversation/notifiers/conversation_notifier.dart test/features/conversation/notifiers/conversation_notifier_test.dart +git commit -m "feat: conversation notifier uses typed AppExceptions for error handling" +``` + +--- + +## Task 6: Update Sessions Screen Error Display + +**Files:** +- Modify: `lib/features/sessions/screens/sessions_screen.dart:56-78` +- Modify: `lib/shared/widgets/error_display.dart:41-47` + +### Step 1: Update SnackBar error messages in sessions screen + +Replace `_onRename` catch block (lines 56-61): + +```dart +} on AppException catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.message)), + ); +} on Exception catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Rename failed: $e')), + ); +} +``` + +Replace `_onDelete` catch block (lines 73-78): + +```dart +} on SessionNotFoundError catch (_) { + // Session already gone — just refresh the list. + ref.read(sessionsProvider.notifier).refresh(); +} on AppException catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.message)), + ); +} on Exception catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Delete failed: $e')), + ); +} +``` + +Add import: +```dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +``` + +### Step 2: Update `ErrorDisplay` to use `AppException.message` + +In `lib/shared/widgets/error_display.dart:41-47`, replace: + +```dart +Text( + error.toString(), +``` + +with: + +```dart +Text( + error is AppException ? error.message : error.toString(), +``` + +Add import: +```dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +``` + +### Step 3: Run all tests + +Run: `flutter test` +Expected: PASS — no regressions + +### Step 4: Commit + +```bash +git add lib/features/sessions/screens/sessions_screen.dart lib/shared/widgets/error_display.dart +git commit -m "feat: sessions screen and ErrorDisplay show human-readable AppException messages" +``` + +--- + +## Task 7: Update Auth Notifier Error Handling + +**Files:** +- Modify: `lib/core/auth/auth_notifier.dart:114-120` + +### Step 1: Update `_isAuthError` to use typed exceptions + +Replace `_isAuthError` (lines 114-120): + +```dart +static bool _isAuthError(Object error) { + return error is AuthExpiredError || error is PermissionDeniedError; +} +``` + +This is a simplification — the `ErrorMappingInterceptor` now converts `GrpcError` with `unauthenticated`/`permissionDenied` codes into these typed exceptions before they reach the auth notifier. + +However, we must also keep the old `GrpcError` check as a fallback since `_isAuthError` may be called from code paths where the interceptor hasn't run (e.g. direct `GrpcError` from health check or token refresh): + +```dart +static bool _isAuthError(Object error) { + if (error is AuthExpiredError || error is PermissionDeniedError) { + return true; + } + if (error is GrpcError) { + return error.code == StatusCode.unauthenticated || + error.code == StatusCode.permissionDenied; + } + return false; +} +``` + +Add import: +```dart +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +``` + +### Step 2: Run auth tests + +Run: `flutter test test/core/auth/` +Expected: PASS + +### Step 3: Commit + +```bash +git add lib/core/auth/auth_notifier.dart +git commit -m "feat: auth notifier recognizes typed AppExceptions for auth error detection" +``` + +--- + +## Task 8: Final Integration Test + +**Files:** +- No new files — verification only + +### Step 1: Run `dart analyze` + +Run: `dart analyze lib/ test/` +Expected: Zero issues + +### Step 2: Run full test suite + +Run: `flutter test` +Expected: All tests pass + +### Step 3: Check lint + +Run: `dart format --set-exit-if-changed lib/ test/` +Expected: No formatting changes needed (or fix any issues) + +### Step 4: Manual testing checklist + +On a device or emulator, verify: +- [ ] Kill the relay → orange "Relay unreachable" banner appears, app does NOT logout +- [ ] Turn on airplane mode → red "No internet" banner appears +- [ ] Restore network → banner disappears smoothly +- [ ] Open a deleted session via deep link → redirects to sessions list with "Session no longer exists" toast +- [ ] Rename with empty name → SnackBar shows human message, not raw GrpcError +- [ ] Normal operations (list sessions, start conversation, send message) work unchanged + +### Step 5: Final commit (if any fixups needed) + +```bash +git add -A +git commit -m "fix: integration fixups for error handling" +``` diff --git a/lib/app.dart b/lib/app.dart index 357b6a5..0cc38d8 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,7 @@ import 'package:betcode_app/core/grpc/relay_reconnect_provider.dart'; import 'package:betcode_app/core/router.dart'; import 'package:betcode_app/shared/theme/theme.dart'; +import 'package:betcode_app/shared/widgets/connectivity_banner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,12 +13,19 @@ class BetCodeApp extends ConsumerWidget { ref.watch(relayAutoReconnectProvider); final router = ref.watch(routerProvider); - return MaterialApp.router( - title: 'BetCode', - debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - routerConfig: router, + return Column( + children: [ + const ConnectivityBanner(), + Expanded( + child: MaterialApp.router( + title: 'BetCode', + debugShowCheckedModeBanner: false, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + routerConfig: router, + ), + ), + ], ); } } diff --git a/lib/core/auth/auth_notifier.dart b/lib/core/auth/auth_notifier.dart index 5f6e277..571e07d 100644 --- a/lib/core/auth/auth_notifier.dart +++ b/lib/core/auth/auth_notifier.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:betcode_app/core/auth/auth_state.dart'; +import 'package:betcode_app/core/grpc/app_exceptions.dart'; import 'package:betcode_app/core/storage/storage.dart'; import 'package:betcode_app/generated/betcode/v1/auth.pbgrpc.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -112,6 +113,11 @@ class AuthNotifier extends Notifier { /// failure (invalid or revoked token). Network and transient errors /// return false — we keep the session alive so reconnection can retry. static bool _isAuthError(Object error) { + if (error is AuthExpiredError || error is PermissionDeniedError) { + return true; + } + // Fallback: _isAuthError may be called from code paths where the + // ErrorMappingInterceptor hasn't run (e.g. health check, token refresh). if (error is GrpcError) { return error.code == StatusCode.unauthenticated || error.code == StatusCode.permissionDenied; diff --git a/lib/core/grpc/app_exceptions.dart b/lib/core/grpc/app_exceptions.dart new file mode 100644 index 0000000..c6d3e17 --- /dev/null +++ b/lib/core/grpc/app_exceptions.dart @@ -0,0 +1,72 @@ +/// Structured exceptions for gRPC and application-level errors. +/// +/// [AppException] is a sealed class so that `switch` on its subtypes is +/// exhaustive, enabling the compiler to verify every error case is handled. +sealed class AppException implements Exception { + /// Creates an [AppException] with the given [message] and optional [cause]. + const AppException({required this.message, this.cause}); + + /// Human-readable description of what went wrong. + final String message; + + /// The underlying error that triggered this exception, if any. + final Object? cause; + + @override + String toString() => message; +} + +/// A network-level failure (DNS, TCP, TLS, timeout). +final class NetworkError extends AppException { + /// Creates a [NetworkError] with the given [message] and optional [cause]. + const NetworkError({required super.message, super.cause}); +} + +/// The relay server could not be reached. +final class RelayUnavailableError extends AppException { + /// Creates a [RelayUnavailableError]. + const RelayUnavailableError({required super.message, super.cause}); +} + +/// The requested session does not exist on the daemon. +final class SessionNotFoundError extends AppException { + /// Creates a [SessionNotFoundError] for the given [sessionId]. + const SessionNotFoundError({ + required super.message, + required this.sessionId, + super.cause, + }); + + /// The ID of the session that was not found. + final String sessionId; +} + +/// The session exists but is in an invalid state (e.g. corrupted, expired). +final class SessionInvalidError extends AppException { + /// Creates a [SessionInvalidError]. + const SessionInvalidError({required super.message, super.cause}); +} + +/// The JWT has expired and the client must re-authenticate. +final class AuthExpiredError extends AppException { + /// Creates an [AuthExpiredError]. + const AuthExpiredError({required super.message, super.cause}); +} + +/// The authenticated user lacks permission for the requested action. +final class PermissionDeniedError extends AppException { + /// Creates a [PermissionDeniedError]. + const PermissionDeniedError({required super.message, super.cause}); +} + +/// An unexpected error on the daemon side (gRPC INTERNAL / UNKNOWN). +final class ServerError extends AppException { + /// Creates a [ServerError]. + const ServerError({required super.message, super.cause}); +} + +/// The client has been rate-limited by the daemon or relay. +final class RateLimitError extends AppException { + /// Creates a [RateLimitError]. + const RateLimitError({required super.message, super.cause}); +} diff --git a/lib/core/grpc/error_mapping.dart b/lib/core/grpc/error_mapping.dart new file mode 100644 index 0000000..709c0f0 --- /dev/null +++ b/lib/core/grpc/error_mapping.dart @@ -0,0 +1,78 @@ +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:grpc/grpc.dart'; + +/// RPC method substrings that indicate session-related operations. +const _sessionMethods = [ + 'Session', // DeleteSession, RenameSession, CompactSession, ResumeSession + 'Converse', // The bidi conversation stream +]; + +/// Maps a [GrpcError] to the appropriate [AppException] subclass. +/// +/// The optional [method] parameter is the gRPC method path +/// (e.g. `/betcode.v1.AgentService/DeleteSession`) and is used to +/// distinguish session-specific NOT_FOUND from generic NOT_FOUND. +AppException mapGrpcError(GrpcError error, {String? method}) { + return switch (error.code) { + StatusCode.unavailable => _mapUnavailable(error), + StatusCode.deadlineExceeded => NetworkError( + message: 'Request timed out. Check your connection and try again.', + cause: error, + ), + StatusCode.notFound => + _isSessionMethod(method) + ? SessionNotFoundError( + message: 'Session no longer exists.', + cause: error, + sessionId: '', + ) + : ServerError( + message: 'The requested resource was not found.', + cause: error, + ), + StatusCode.unauthenticated => AuthExpiredError( + message: 'Your session has expired. Please log in again.', + cause: error, + ), + StatusCode.permissionDenied => PermissionDeniedError( + message: "You don't have permission for this action.", + cause: error, + ), + StatusCode.resourceExhausted => RateLimitError( + message: 'Too many requests. Please wait a moment and try again.', + cause: error, + ), + StatusCode.invalidArgument || + StatusCode.failedPrecondition => SessionInvalidError( + message: error.message ?? 'Invalid request.', + cause: error, + ), + _ => ServerError( + message: 'Something went wrong. Please try again.', + cause: error, + ), + }; +} + +AppException _mapUnavailable(GrpcError error) { + final msg = error.message ?? ''; + if (msg.contains('HandshakeException') || + msg.contains('WRONG_VERSION_NUMBER') || + msg.contains('Channel shutting down') || + msg.contains('TLS') || + msg.contains('CERTIFICATE')) { + return RelayUnavailableError( + message: 'Unable to reach the relay server.', + cause: error, + ); + } + return NetworkError( + message: 'Connection lost. Retrying...', + cause: error, + ); +} + +bool _isSessionMethod(String? method) { + if (method == null) return false; + return _sessionMethods.any(method.contains); +} diff --git a/lib/core/grpc/grpc.dart b/lib/core/grpc/grpc.dart index 257fb93..12a15df 100644 --- a/lib/core/grpc/grpc.dart +++ b/lib/core/grpc/grpc.dart @@ -1,5 +1,7 @@ +export 'app_exceptions.dart'; export 'client_manager.dart'; export 'connection_state.dart'; +export 'error_mapping.dart'; export 'grpc_providers.dart'; export 'interceptors.dart'; export 'relay_config.dart'; diff --git a/lib/core/grpc/interceptors.dart b/lib/core/grpc/interceptors.dart index 1b8b9ca..ef0f2f1 100644 --- a/lib/core/grpc/interceptors.dart +++ b/lib/core/grpc/interceptors.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:betcode_app/core/auth/auth_notifier.dart'; +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:betcode_app/core/grpc/error_mapping.dart'; import 'package:betcode_app/generated/betcode/v1/auth.pbgrpc.dart'; import 'package:flutter/foundation.dart'; import 'package:grpc/grpc.dart'; @@ -233,3 +235,151 @@ class TokenRefreshInterceptor extends ClientInterceptor { return invoker(method, requests, opts); } } + +/// Maps [GrpcError] responses to typed [AppException] subclasses. +/// +/// Must be the **last** interceptor in the chain so it wraps errors from +/// all preceding interceptors (auth refresh, logging, etc.). +class ErrorMappingInterceptor extends ClientInterceptor { + @override + ResponseFuture interceptUnary( + ClientMethod method, + Q request, + CallOptions options, + ClientUnaryInvoker invoker, + ) { + final response = invoker(method, request, options); + return _ErrorMappingResponseFuture(response, method.path); + } + + @override + ResponseStream interceptStreaming( + ClientMethod method, + Stream requests, + CallOptions options, + ClientStreamingInvoker invoker, + ) { + final response = invoker(method, requests, options); + return _ErrorMappingResponseStream(response, method.path); + } +} + +/// Wraps a [ResponseFuture] and maps [GrpcError]s to [AppException]s. +/// +/// Delegates all [Future] and [Response] methods to the underlying +/// [_delegate], intercepting errors in [then] and [catchError] so that +/// any [GrpcError] is replaced with the typed exception from +/// [mapGrpcError]. +class _ErrorMappingResponseFuture implements ResponseFuture { + _ErrorMappingResponseFuture(this._delegate, this._method); + + final ResponseFuture _delegate; + final String _method; + + /// Transforms a [GrpcError] into an [AppException]. Non-[GrpcError] + /// exceptions pass through unchanged. + Object _mapError(Object error) { + if (error is GrpcError) return mapGrpcError(error, method: _method); + return error; + } + + /// The mapped future: maps errors once, then reuses the result. + late final Future _mapped = _delegate.then( + (v) => v, + onError: (Object error, StackTrace stack) => + Error.throwWithStackTrace(_mapError(error), stack), + ); + + @override + Future then( + FutureOr Function(R) onValue, { + Function? onError, + }) => _mapped.then(onValue, onError: onError); + + @override + Future catchError(Function onError, {bool Function(Object)? test}) => + _mapped.catchError(onError, test: test); + + @override + Future whenComplete(FutureOr Function() action) => + _mapped.whenComplete(action); + + @override + Future timeout( + Duration timeLimit, { + FutureOr Function()? onTimeout, + }) => _mapped.timeout(timeLimit, onTimeout: onTimeout); + + @override + Stream asStream() => _mapped.asStream(); + + @override + Future> get headers => _delegate.headers; + + @override + Future> get trailers => _delegate.trailers; + + @override + Future cancel() => _delegate.cancel(); +} + +/// Wraps a [ResponseStream] and maps [GrpcError]s to [AppException]s. +/// +/// Overrides [listen] to intercept errors from the underlying stream, +/// mapping [GrpcError]s to typed [AppException]s. All other [Stream] +/// methods (e.g. [map], [where], [toList]) ultimately go through +/// [listen], so they also benefit from the mapping. +class _ErrorMappingResponseStream extends StreamView + implements ResponseStream { + _ErrorMappingResponseStream(this._delegate, this._method) : super(_delegate); + + final ResponseStream _delegate; + final String _method; + + @override + StreamSubscription listen( + void Function(R)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return super.listen( + onData, + onError: (Object error, StackTrace stackTrace) { + final mapped = error is GrpcError + ? mapGrpcError(error, method: _method) + : error; + if (onError != null) { + if (onError is void Function(Object, StackTrace)) { + onError(mapped, stackTrace); + } else if (onError is void Function(Object)) { + onError(mapped); + } else { + // Best-effort: call with both arguments. + (onError as dynamic)(mapped, stackTrace); + } + } else { + // No onError callback — rethrow via the zone's error handler + // so the subscription can propagate it. + Error.throwWithStackTrace(mapped, stackTrace); + } + }, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + /// [ResponseStream] narrows the return type of [single] from [Future] to + /// [ResponseFuture]. We delegate to the original stream's [single]. + @override + ResponseFuture get single => _delegate.single; + + @override + Future> get headers => _delegate.headers; + + @override + Future> get trailers => _delegate.trailers; + + @override + Future cancel() => _delegate.cancel(); +} diff --git a/lib/features/sessions/screens/sessions_screen.dart b/lib/features/sessions/screens/sessions_screen.dart index 021a0eb..50cfd00 100644 --- a/lib/features/sessions/screens/sessions_screen.dart +++ b/lib/features/sessions/screens/sessions_screen.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + +import 'package:betcode_app/core/grpc/app_exceptions.dart'; import 'package:betcode_app/features/sessions/notifiers/sessions_providers.dart'; +import 'package:betcode_app/features/sessions/widgets/confirm_delete_dialog.dart'; import 'package:betcode_app/features/sessions/widgets/rename_session_dialog.dart'; import 'package:betcode_app/features/sessions/widgets/session_card.dart'; import 'package:betcode_app/generated/betcode/v1/agent.pb.dart'; @@ -31,7 +35,7 @@ class SessionsScreen extends ConsumerWidget { onTap: () => context.go('/sessions/${session.id}'), onRename: (currentName) => _onRename(context, ref, session.id, currentName), - onDelete: () => _onDelete(context), + onDelete: () => _onDelete(context, ref, session.id), ), ), ); @@ -52,17 +56,41 @@ class SessionsScreen extends ConsumerWidget { await ref .read(sessionsProvider.notifier) .renameSession(sessionId: sessionId, name: newName); + } on AppException catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.message)), + ); } on Exception catch (e) { if (!context.mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Rename failed: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Rename failed: $e')), + ); } } - void _onDelete(BuildContext context) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Delete coming soon'))); + Future _onDelete( + BuildContext context, + WidgetRef ref, + String sessionId, + ) async { + final confirmed = await ConfirmDeleteDialog.show(context); + if (confirmed != true) return; + try { + await ref.read(sessionsProvider.notifier).deleteSession(sessionId); + } on SessionNotFoundError catch (_) { + // Session already gone — just refresh the list. + unawaited(ref.read(sessionsProvider.notifier).refresh()); + } on AppException catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.message)), + ); + } on Exception catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Delete failed: $e')), + ); + } } } diff --git a/lib/shared/widgets/connectivity_banner.dart b/lib/shared/widgets/connectivity_banner.dart new file mode 100644 index 0000000..0fc8ed5 --- /dev/null +++ b/lib/shared/widgets/connectivity_banner.dart @@ -0,0 +1,70 @@ +import 'package:betcode_app/core/grpc/connection_state.dart'; +import 'package:betcode_app/core/grpc/grpc_providers.dart'; +import 'package:betcode_app/core/sync/connectivity.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// A persistent banner shown at the top of the app when there are +/// connectivity issues. +/// +/// Watches [networkStatusProvider] and [connectionStatusProvider] to +/// determine what to display. Hidden (zero height) when everything is fine. +class ConnectivityBanner extends ConsumerWidget { + /// Creates a [ConnectivityBanner]. + const ConnectivityBanner({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final networkAsync = ref.watch(networkStatusProvider); + final connectionAsync = ref.watch(connectionStatusProvider); + + final isOffline = networkAsync.value == NetworkStatus.offline; + final connectionStatus = connectionAsync.value; + final isRelayDown = + !isOffline && + (connectionStatus == GrpcConnectionStatus.disconnected || + connectionStatus == GrpcConnectionStatus.reconnecting); + + final String? message; + final Color? backgroundColor; + final IconData? icon; + + if (isOffline) { + message = 'No internet connection'; + backgroundColor = Theme.of(context).colorScheme.error; + icon = Icons.wifi_off; + } else if (isRelayDown) { + message = connectionStatus == GrpcConnectionStatus.reconnecting + ? 'Relay unreachable \u2014 reconnecting...' + : 'Relay unreachable'; + backgroundColor = Theme.of(context).colorScheme.tertiary; + icon = Icons.cloud_off; + } else { + message = null; + backgroundColor = null; + icon = null; + } + + return AnimatedSize( + duration: const Duration(milliseconds: 200), + child: message != null + ? MaterialBanner( + content: Row( + children: [ + Icon(icon, color: Colors.white, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + backgroundColor: backgroundColor, + actions: const [SizedBox.shrink()], + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/lib/shared/widgets/error_display.dart b/lib/shared/widgets/error_display.dart index 56371b6..2ca76e4 100644 --- a/lib/shared/widgets/error_display.dart +++ b/lib/shared/widgets/error_display.dart @@ -1,3 +1,4 @@ +import 'package:betcode_app/core/grpc/app_exceptions.dart'; import 'package:flutter/material.dart'; /// A widget that displays an error state with an icon, message, and an @@ -39,7 +40,9 @@ class ErrorDisplay extends StatelessWidget { Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error), const SizedBox(height: 16), Text( - error.toString(), + error is AppException + ? (error as AppException).message + : error.toString(), textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 4a950de..a51bb1a 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'async_list_scaffold.dart'; export 'confirm_dialog.dart'; export 'connection_indicator.dart'; +export 'connectivity_banner.dart'; export 'dialog_actions.dart'; export 'empty_state.dart'; export 'error_display.dart'; diff --git a/test/core/grpc/app_exceptions_test.dart b/test/core/grpc/app_exceptions_test.dart new file mode 100644 index 0000000..f3c5c41 --- /dev/null +++ b/test/core/grpc/app_exceptions_test.dart @@ -0,0 +1,77 @@ +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppException', () { + test('NetworkError carries message and cause', () { + final cause = Exception('socket closed'); + const message = 'connection lost'; + final error = NetworkError(message: message, cause: cause); + + expect(error.message, message); + expect(error.cause, cause); + expect(error.toString(), message); + }); + + test('NetworkError works without cause', () { + const error = NetworkError(message: 'timeout'); + + expect(error.message, 'timeout'); + expect(error.cause, isNull); + }); + + test('SessionNotFoundError includes sessionId', () { + const error = SessionNotFoundError( + message: 'not found', + sessionId: 'sess-42', + ); + + expect(error.sessionId, 'sess-42'); + expect(error.message, 'not found'); + expect(error.cause, isNull); + }); + + test('all subtypes are AppException', () { + const exceptions = [ + NetworkError(message: 'a'), + RelayUnavailableError(message: 'b'), + SessionNotFoundError(message: 'c', sessionId: 's'), + SessionInvalidError(message: 'd'), + AuthExpiredError(message: 'e'), + PermissionDeniedError(message: 'f'), + ServerError(message: 'g'), + RateLimitError(message: 'h'), + ]; + + for (final e in exceptions) { + expect(e, isA()); + expect(e, isA()); + } + }); + + test('sealed switch is exhaustive', () { + const AppException error = NetworkError(message: 'test'); + + // If a subtype is added to the sealed class without updating this + // switch, the analyzer will report a missing-case warning, proving + // exhaustiveness. + final label = switch (error) { + NetworkError() => 'network', + RelayUnavailableError() => 'relay', + SessionNotFoundError() => 'session_not_found', + SessionInvalidError() => 'session_invalid', + AuthExpiredError() => 'auth', + PermissionDeniedError() => 'permission', + ServerError() => 'server', + RateLimitError() => 'rate_limit', + }; + + expect(label, 'network'); + }); + + test('toString returns message', () { + const error = ServerError(message: 'internal error'); + expect('$error', 'internal error'); + }); + }); +} diff --git a/test/core/grpc/error_mapping_interceptor_test.dart b/test/core/grpc/error_mapping_interceptor_test.dart new file mode 100644 index 0000000..5b66a52 --- /dev/null +++ b/test/core/grpc/error_mapping_interceptor_test.dart @@ -0,0 +1,158 @@ +import 'dart:async'; + +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:betcode_app/core/grpc/interceptors.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:grpc/grpc.dart'; + +import '../../helpers/fake_response_future.dart'; +import '../interceptor_test_helpers.dart'; + +void main() { + group('ErrorMappingInterceptor', () { + late ErrorMappingInterceptor interceptor; + + setUp(() { + interceptor = ErrorMappingInterceptor(); + }); + + // ---- Unary tests ---- + + test('passes through successful unary responses unchanged', () async { + final response = interceptor.interceptUnary( + testMethod('/test/Ok'), + 'request', + CallOptions(), + (method, request, options) => FakeResponseFuture.value('success'), + ); + expect(await response, 'success'); + }); + + test('maps GrpcError to AppException on unary failure', () async { + final response = interceptor.interceptUnary( + testMethod('/betcode.v1.AgentService/DeleteSession'), + 'request', + CallOptions(), + (method, request, options) => + FakeResponseFuture.error(const GrpcError.notFound()), + ); + await expectLater( + response, + throwsA(isA()), + ); + }); + + test('maps UNAVAILABLE to NetworkError on unary', () async { + final response = interceptor.interceptUnary( + testMethod('/test/Rpc'), + 'request', + CallOptions(), + (method, request, options) => + FakeResponseFuture.error(const GrpcError.unavailable()), + ); + await expectLater( + response, + throwsA(isA()), + ); + }); + + test('non-GrpcError passes through unmapped', () async { + final response = interceptor.interceptUnary( + testMethod('/test/Rpc'), + 'request', + CallOptions(), + (method, request, options) => + FakeResponseFuture.error(Exception('not grpc')), + ); + await expectLater( + response, + throwsA( + isA().having( + (e) => e.toString(), + 'toString', + contains('not grpc'), + ), + ), + ); + }); + + test('maps UNAUTHENTICATED to AuthExpiredError on unary', () async { + final response = interceptor.interceptUnary( + testMethod('/test/Rpc'), + 'request', + CallOptions(), + (method, request, options) => + FakeResponseFuture.error(const GrpcError.unauthenticated()), + ); + await expectLater( + response, + throwsA(isA()), + ); + }); + + // ---- Streaming tests ---- + + test('streaming: maps GrpcError to AppException', () async { + final response = interceptor.interceptStreaming( + testMethod('/betcode.v1.AgentService/Converse'), + const Stream.empty(), + CallOptions(), + (method, requests, options) => FakeInterceptorResponseStream( + Stream.error(const GrpcError.notFound()), + ), + ); + + final completer = Completer(); + response.listen( + (_) {}, + onError: (Object error) { + if (!completer.isCompleted) completer.complete(error); + }, + ); + final error = await completer.future; + expect(error, isA()); + }); + + test('streaming: non-GrpcError passes through unmapped', () async { + final response = interceptor.interceptStreaming( + testMethod('/test/Rpc'), + const Stream.empty(), + CallOptions(), + (method, requests, options) => FakeInterceptorResponseStream( + Stream.error(Exception('not grpc')), + ), + ); + + final completer = Completer(); + response.listen( + (_) {}, + onError: (Object error) { + if (!completer.isCompleted) completer.complete(error); + }, + ); + final error = await completer.future; + expect(error, isA()); + expect(error.toString(), contains('not grpc')); + }); + + test('streaming: successful values pass through unchanged', () async { + final response = interceptor.interceptStreaming( + testMethod('/test/Rpc'), + const Stream.empty(), + CallOptions(), + (method, requests, options) => FakeInterceptorResponseStream( + Stream.fromIterable(['a', 'b', 'c']), + ), + ); + + final values = []; + final completer = Completer(); + response.listen( + values.add, + onDone: completer.complete, + ); + await completer.future; + expect(values, ['a', 'b', 'c']); + }); + }); +} diff --git a/test/core/grpc/error_mapping_test.dart b/test/core/grpc/error_mapping_test.dart new file mode 100644 index 0000000..db966aa --- /dev/null +++ b/test/core/grpc/error_mapping_test.dart @@ -0,0 +1,143 @@ +import 'package:betcode_app/core/grpc/app_exceptions.dart'; +import 'package:betcode_app/core/grpc/error_mapping.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:grpc/grpc.dart'; + +void main() { + group('mapGrpcError', () { + test('UNAVAILABLE with handshake error -> RelayUnavailableError', () { + const grpcError = GrpcError.custom( + StatusCode.unavailable, + 'Error connecting: HandshakeException: WRONG_VERSION_NUMBER', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + expect(result.cause, grpcError); + }); + + test('UNAVAILABLE with channel shutting down -> RelayUnavailableError', () { + const grpcError = GrpcError.custom( + StatusCode.unavailable, + 'Channel shutting down.', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('UNAVAILABLE with TLS keyword -> RelayUnavailableError', () { + const grpcError = GrpcError.custom( + StatusCode.unavailable, + 'TLS handshake failed', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('UNAVAILABLE with CERTIFICATE keyword -> RelayUnavailableError', () { + const grpcError = GrpcError.custom( + StatusCode.unavailable, + 'CERTIFICATE_VERIFY_FAILED', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('UNAVAILABLE generic -> NetworkError', () { + const grpcError = GrpcError.custom( + StatusCode.unavailable, + 'Connection timed out', + ); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('NOT_FOUND on session RPC -> SessionNotFoundError', () { + const grpcError = GrpcError.notFound('session not found'); + final result = mapGrpcError( + grpcError, + method: '/betcode.v1.AgentService/DeleteSession', + ); + expect(result, isA()); + }); + + test('NOT_FOUND on Converse RPC -> SessionNotFoundError', () { + const grpcError = GrpcError.notFound('session not found'); + final result = mapGrpcError( + grpcError, + method: '/betcode.v1.AgentService/Converse', + ); + expect(result, isA()); + }); + + test('NOT_FOUND on non-session RPC -> ServerError', () { + const grpcError = GrpcError.notFound('machine not found'); + final result = mapGrpcError( + grpcError, + method: '/betcode.v1.MachineService/ListMachines', + ); + expect(result, isA()); + }); + + test('NOT_FOUND with no method -> ServerError', () { + const grpcError = GrpcError.notFound('not found'); + final result = mapGrpcError(grpcError); + expect(result, isA()); + }); + + test('UNAUTHENTICATED -> AuthExpiredError', () { + final result = mapGrpcError(const GrpcError.unauthenticated()); + expect(result, isA()); + }); + + test('PERMISSION_DENIED -> PermissionDeniedError', () { + final result = mapGrpcError( + const GrpcError.custom(StatusCode.permissionDenied, 'not owner'), + ); + expect(result, isA()); + }); + + test('RESOURCE_EXHAUSTED -> RateLimitError', () { + final result = mapGrpcError(const GrpcError.resourceExhausted()); + expect(result, isA()); + }); + + test('INVALID_ARGUMENT -> SessionInvalidError', () { + final result = mapGrpcError( + const GrpcError.custom(StatusCode.invalidArgument, 'name too long'), + ); + expect(result, isA()); + expect(result.message, 'name too long'); + }); + + test('FAILED_PRECONDITION -> SessionInvalidError', () { + final result = mapGrpcError( + const GrpcError.custom(StatusCode.failedPrecondition, 'session locked'), + ); + expect(result, isA()); + expect(result.message, 'session locked'); + }); + + test('INTERNAL -> ServerError', () { + final result = mapGrpcError(const GrpcError.internal()); + expect(result, isA()); + }); + + test('DEADLINE_EXCEEDED -> NetworkError', () { + final result = mapGrpcError(const GrpcError.deadlineExceeded()); + expect(result, isA()); + }); + + test('UNKNOWN -> ServerError', () { + final result = mapGrpcError( + const GrpcError.custom(StatusCode.unknown, 'something broke'), + ); + expect(result, isA()); + }); + + test('preserves original GrpcError as cause', () { + const grpcError = GrpcError.internal('db crashed'); + final result = mapGrpcError(grpcError); + expect(result.cause, grpcError); + }); + }); +} diff --git a/test/core/grpc/grpc_providers_test.dart b/test/core/grpc/grpc_providers_test.dart index 75f0df6..15a18d3 100644 --- a/test/core/grpc/grpc_providers_test.dart +++ b/test/core/grpc/grpc_providers_test.dart @@ -86,9 +86,33 @@ void main() { ); }); - test('has exactly four interceptors', () { + test('interceptor chain contains ErrorMappingInterceptor', () { final manager = container.read(grpcClientManagerProvider); - expect(manager.interceptors, hasLength(4)); + final interceptors = manager.interceptors; + + expect( + interceptors.whereType(), + hasLength(1), + reason: 'ErrorMappingInterceptor should be present in the chain', + ); + }); + + test('ErrorMappingInterceptor is last in the chain', () { + final manager = container.read(grpcClientManagerProvider); + final interceptors = manager.interceptors; + + expect( + interceptors.last, + isA(), + reason: + 'ErrorMappingInterceptor must be last so it wraps errors from ' + 'all preceding interceptors', + ); + }); + + test('has exactly five interceptors', () { + final manager = container.read(grpcClientManagerProvider); + expect(manager.interceptors, hasLength(5)); }); test('has a health check function configured', () { diff --git a/test/features/conversation/notifiers/conversation_notifier_test.dart b/test/features/conversation/notifiers/conversation_notifier_test.dart index b3f9b8b..e242d19 100644 --- a/test/features/conversation/notifiers/conversation_notifier_test.dart +++ b/test/features/conversation/notifiers/conversation_notifier_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:betcode_app/core/grpc/app_exceptions.dart'; import 'package:betcode_app/core/grpc/service_providers.dart'; import 'package:betcode_app/core/lifecycle/app_lifecycle_notifier.dart'; import 'package:betcode_app/features/conversation/models/conversation_state.dart'; @@ -138,7 +139,10 @@ void main() { final s = fc.read(conversationProvider(null)).value; expect(s, isA()); - expect((s! as ConversationError).message, contains('UNAVAILABLE')); + expect( + (s! as ConversationError).message, + contains('Failed to start conversation'), + ); }); }); @@ -287,12 +291,19 @@ void main() { final n = notifier(); await goActive(n); - eventController.addError(const GrpcError.unauthenticated('expired')); + eventController.addError( + const AuthExpiredError( + message: 'Your session has expired. Please log in again.', + ), + ); await Future.delayed(Duration.zero); final s = stateVal(); expect(s, isA()); - expect((s! as ConversationError).message, contains('expired')); + expect( + (s! as ConversationError).message, + 'Your session has expired. Please log in again.', + ); }); test('stream done triggers reconnection on active session', () async { @@ -430,7 +441,8 @@ void main() { ({ int Function() callCount, StreamController Function() latestController, - }) setupReconnectMock({ + }) + setupReconnectMock({ void Function(StreamController)? onReconnect, bool immediateError = false, GrpcError? throwOnConverse, @@ -440,8 +452,7 @@ void main() { var isFirstCall = true; when(() => mockClient.converse(any())).thenAnswer((inv) { - final reqStream = - inv.positionalArguments[0] as Stream; + final reqStream = inv.positionalArguments[0] as Stream; if (isFirstCall) { // First call is the initial startConversation. @@ -544,9 +555,15 @@ void main() { final mock = setupReconnectMock(); startActive(async); - injectError(async, const GrpcError.unauthenticated('expired')); - async.elapse(const Duration(seconds: 60)); + eventController.addError( + const AuthExpiredError( + message: 'Your session has expired. Please log in again.', + ), + ); + async + ..flushMicrotasks() + ..elapse(const Duration(seconds: 60)); expect(mock.callCount(), 0); diff --git a/test/features/conversation/widgets/input_bar_test.dart b/test/features/conversation/widgets/input_bar_test.dart index 610f73f..d35a798 100644 --- a/test/features/conversation/widgets/input_bar_test.dart +++ b/test/features/conversation/widgets/input_bar_test.dart @@ -15,9 +15,9 @@ import 'package:flutter_test/flutter_test.dart'; /// A notifier that returns canned async value without gRPC calls. class _FakeCommandsNotifier extends CommandsNotifier { + // Never completes — keeps the provider in loading state. @override - Future> build() => - Completer>().future; // never completes → empty + Future> build() => Completer>().future; } Widget _app(Widget child) => ProviderScope( diff --git a/test/shared/widgets/connectivity_banner_test.dart b/test/shared/widgets/connectivity_banner_test.dart new file mode 100644 index 0000000..7a250cf --- /dev/null +++ b/test/shared/widgets/connectivity_banner_test.dart @@ -0,0 +1,78 @@ +import 'package:betcode_app/core/grpc/connection_state.dart'; +import 'package:betcode_app/core/grpc/grpc_providers.dart'; +import 'package:betcode_app/core/sync/connectivity.dart'; +import 'package:betcode_app/shared/widgets/connectivity_banner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ConnectivityBanner', () { + Widget buildApp({ + AsyncValue connectionStatus = const AsyncData( + GrpcConnectionStatus.connected, + ), + AsyncValue networkStatus = const AsyncData( + NetworkStatus.online, + ), + }) { + return ProviderScope( + overrides: [ + connectionStatusProvider.overrideWith( + (_) => connectionStatus.when( + data: Stream.value, + loading: () => const Stream.empty(), + error: Stream.error, + ), + ), + networkStatusProvider.overrideWith( + (_) => networkStatus.when( + data: Stream.value, + loading: () => const Stream.empty(), + error: Stream.error, + ), + ), + ], + child: const MaterialApp( + home: Scaffold( + body: Column( + children: [ + ConnectivityBanner(), + Expanded(child: Placeholder()), + ], + ), + ), + ), + ); + } + + testWidgets('hidden when online and connected', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect(find.text('No internet connection'), findsNothing); + expect(find.text('Relay unreachable'), findsNothing); + }); + + testWidgets('shows offline banner when network is offline', (tester) async { + await tester.pumpWidget( + buildApp( + networkStatus: const AsyncData(NetworkStatus.offline), + ), + ); + await tester.pumpAndSettle(); + expect(find.textContaining('No internet'), findsOneWidget); + }); + + testWidgets('shows relay banner when disconnected but online', ( + tester, + ) async { + await tester.pumpWidget( + buildApp( + connectionStatus: const AsyncData(GrpcConnectionStatus.reconnecting), + ), + ); + await tester.pumpAndSettle(); + expect(find.textContaining('reconnecting'), findsOneWidget); + }); + }); +} From 97d69dce3ae032602d9e5ff53ae25e5e1c391980 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 17:28:21 +0300 Subject: [PATCH 04/13] chore: update pubspec.lock Co-Authored-By: Claude Opus 4.6 --- pubspec.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e9f1717..85209aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" charcode: dependency: transitive description: @@ -571,18 +571,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: From 15d3763a3113b0e4245cd12c4756827bce6d9037 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 18:02:03 +0300 Subject: [PATCH 05/13] fix: prevent historical fatal errors from killing session resume When resuming a session whose history contained a fatal error (e.g. "Claude exited with error"), the event handler treated replayed errors as live ones, transitioning to ConversationError and making the session permanently unrecoverable. Also fixes stream leaks on retry. Co-Authored-By: Claude Opus 4.6 --- .../notifiers/conversation_event_handler.dart | 11 ++++++++++- .../notifiers/conversation_notifier.dart | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/features/conversation/notifiers/conversation_event_handler.dart b/lib/features/conversation/notifiers/conversation_event_handler.dart index 4b82118..e56bddb 100644 --- a/lib/features/conversation/notifiers/conversation_event_handler.dart +++ b/lib/features/conversation/notifiers/conversation_event_handler.dart @@ -18,6 +18,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Separated from [ConversationNotifier] to keep file sizes manageable /// and isolate event-processing logic from stream lifecycle management. mixin ConversationEventHandler on AsyncNotifier { + /// True while replaying historical events via history load. + /// + /// Fatal errors from history (e.g. a previous session crash) must NOT + /// kill the current conversation state — they happened in the past. + bool isReplayingHistory = false; + /// Dispatches a single [pb.AgentEvent] to the appropriate handler. void handleEvent(pb.AgentEvent event) { debugPrint( @@ -437,7 +443,10 @@ mixin ConversationEventHandler on AsyncNotifier { } void _onError(pb.ErrorEvent error, int seq) { - if (error.isFatal) { + // During history replay, fatal errors are from a past session run. + // Show them as a non-fatal banner so the user can still interact + // with the conversation instead of being locked into an error screen. + if (error.isFatal && !isReplayingHistory) { state = AsyncData( ConversationState.error('[${error.code}] ${error.message}'), ); diff --git a/lib/features/conversation/notifiers/conversation_notifier.dart b/lib/features/conversation/notifiers/conversation_notifier.dart index b43d4bb..6437eba 100644 --- a/lib/features/conversation/notifiers/conversation_notifier.dart +++ b/lib/features/conversation/notifiers/conversation_notifier.dart @@ -98,6 +98,12 @@ class ConversationNotifier extends AsyncNotifier state = const AsyncData(ConversationState.connecting()); try { + // Close any existing streams from a previous attempt (e.g. retry + // after error) to prevent leaked subscriptions. + unawaited(_eventSubscription?.cancel()); + _eventSubscription = null; + unawaited(_requestController?.close()); + _requestController = StreamController(); final responseStream = _client.converse(_requestController!.stream); @@ -136,8 +142,15 @@ class ConversationNotifier extends AsyncNotifier // When resuming an existing session, load conversation history from // the daemon's event store via the ResumeSession RPC. Events are // processed through the same handler and deduplicated by sequence. + // History replay uses a flag so _onError treats past fatal errors + // as non-fatal banners instead of killing the conversation state. if (sessionId != null && sessionId!.isNotEmpty) { - await _loadHistory(sessionId!); + isReplayingHistory = true; + try { + await _loadHistory(sessionId!); + } finally { + isReplayingHistory = false; + } } } on Exception catch (e) { final message = e is AppException From a553f89da3f94dd32ff15be2d888f68fdc9e5ffb Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 18:18:27 +0300 Subject: [PATCH 06/13] fix: suppress error banner for historical errors during session resume Co-Authored-By: Claude Opus 4.6 --- .../notifiers/conversation_event_handler.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/features/conversation/notifiers/conversation_event_handler.dart b/lib/features/conversation/notifiers/conversation_event_handler.dart index e56bddb..3c7788a 100644 --- a/lib/features/conversation/notifiers/conversation_event_handler.dart +++ b/lib/features/conversation/notifiers/conversation_event_handler.dart @@ -444,9 +444,16 @@ mixin ConversationEventHandler on AsyncNotifier { void _onError(pb.ErrorEvent error, int seq) { // During history replay, fatal errors are from a past session run. - // Show them as a non-fatal banner so the user can still interact - // with the conversation instead of being locked into an error screen. - if (error.isFatal && !isReplayingHistory) { + // Just update the sequence counter — don't show a banner or kill + // the conversation. The user already knows the previous run failed. + if (isReplayingHistory) { + _updateActive((active) { + return active.copyWith(lastSequence: seq); + }); + return; + } + + if (error.isFatal) { state = AsyncData( ConversationState.error('[${error.code}] ${error.message}'), ); From 99e45b50b989bf569c5b2e692da6db0425f946a8 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Wed, 18 Feb 2026 23:06:19 +0300 Subject: [PATCH 07/13] fix: stale gRPC clients, cancelled stream handling, and diagnostic logging - Service providers now watch connectionStatusProvider so clients rebuild when the channel is replaced during reconnection (fixes rapid-fire "Unable to reach relay" errors from stale channel references) - Separate "Channel shutting down" from TLS errors in error mapping so transient reconnection errors don't show "relay unreachable" banner - Handle StatusCode.cancelled as retryable NetworkError instead of falling through to "Something went wrong" catch-all - LoggingInterceptor now logs streaming errors and completion (was silent) - ErrorMappingInterceptor logs original GrpcError code+message before mapping, and _handleStreamError logs the cause for diagnostics Co-Authored-By: Claude Opus 4.6 --- lib/core/grpc/error_mapping.dart | 17 ++++- lib/core/grpc/interceptors.dart | 75 ++++++++++++++++++- lib/core/grpc/service_providers.dart | 17 +++++ .../notifiers/conversation_notifier.dart | 4 +- pubspec.lock | 12 +-- 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/lib/core/grpc/error_mapping.dart b/lib/core/grpc/error_mapping.dart index 709c0f0..d61d25b 100644 --- a/lib/core/grpc/error_mapping.dart +++ b/lib/core/grpc/error_mapping.dart @@ -14,6 +14,10 @@ const _sessionMethods = [ /// distinguish session-specific NOT_FOUND from generic NOT_FOUND. AppException mapGrpcError(GrpcError error, {String? method}) { return switch (error.code) { + StatusCode.cancelled => NetworkError( + message: 'Connection lost. Retrying...', + cause: error, + ), StatusCode.unavailable => _mapUnavailable(error), StatusCode.deadlineExceeded => NetworkError( message: 'Request timed out. Check your connection and try again.', @@ -56,9 +60,20 @@ AppException mapGrpcError(GrpcError error, {String? method}) { AppException _mapUnavailable(GrpcError error) { final msg = error.message ?? ''; + + // "Channel shutting down" is a transient local error that occurs when the + // ClientChannel is replaced during reconnection. It is NOT a TLS or relay + // issue — the new channel may work fine. Treat it as a retryable network + // error so callers don't display a scary "relay unreachable" banner. + if (msg.contains('Channel shutting down')) { + return NetworkError( + message: 'Connection lost. Retrying...', + cause: error, + ); + } + if (msg.contains('HandshakeException') || msg.contains('WRONG_VERSION_NUMBER') || - msg.contains('Channel shutting down') || msg.contains('TLS') || msg.contains('CERTIFICATE')) { return RelayUnavailableError( diff --git a/lib/core/grpc/interceptors.dart b/lib/core/grpc/interceptors.dart index ef0f2f1..8b01254 100644 --- a/lib/core/grpc/interceptors.dart +++ b/lib/core/grpc/interceptors.dart @@ -152,9 +152,76 @@ class LoggingInterceptor extends ClientInterceptor { CallOptions options, ClientStreamingInvoker invoker, ) { + final stopwatch = Stopwatch()..start(); debugPrint('$_tag -> ${method.path} (stream)'); - return invoker(method, requests, options); + return _LoggingResponseStream( + invoker(method, requests, options), + method.path, + stopwatch, + ); + } +} + +/// Wraps a [ResponseStream] to log errors and completion. +class _LoggingResponseStream extends StreamView + implements ResponseStream { + _LoggingResponseStream(this._delegate, this._method, this._stopwatch) + : super(_delegate); + + final ResponseStream _delegate; + final String _method; + final Stopwatch _stopwatch; + + @override + StreamSubscription listen( + void Function(R)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return super.listen( + onData, + onError: (Object error, StackTrace stackTrace) { + _stopwatch.stop(); + debugPrint( + '[gRPC] <- $_method stream ERROR ' + '(${_stopwatch.elapsedMilliseconds}ms): $error', + ); + if (onError != null) { + if (onError is void Function(Object, StackTrace)) { + onError(error, stackTrace); + } else if (onError is void Function(Object)) { + onError(error); + } else { + (onError as dynamic)(error, stackTrace); + } + } else { + Error.throwWithStackTrace(error, stackTrace); + } + }, + onDone: () { + _stopwatch.stop(); + debugPrint( + '[gRPC] <- $_method stream DONE ' + '(${_stopwatch.elapsedMilliseconds}ms)', + ); + onDone?.call(); + }, + cancelOnError: cancelOnError, + ); } + + @override + ResponseFuture get single => _delegate.single; + + @override + Future> get headers => _delegate.headers; + + @override + Future> get trailers => _delegate.trailers; + + @override + Future cancel() => _delegate.cancel(); } /// Checks token expiry before each RPC and triggers a refresh if needed. @@ -346,6 +413,12 @@ class _ErrorMappingResponseStream extends StreamView return super.listen( onData, onError: (Object error, StackTrace stackTrace) { + if (error is GrpcError) { + debugPrint( + '[gRPC] ErrorMapping $_method: ' + 'code=${error.code} message=${error.message}', + ); + } final mapped = error is GrpcError ? mapGrpcError(error, method: _method) : error; diff --git a/lib/core/grpc/service_providers.dart b/lib/core/grpc/service_providers.dart index bdf9b79..582d5c0 100644 --- a/lib/core/grpc/service_providers.dart +++ b/lib/core/grpc/service_providers.dart @@ -13,8 +13,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Provides the [AgentServiceClient] for conversation streaming, session /// management, and input lock operations. +/// +/// Watches [connectionStatusProvider] so the client is recreated with the +/// current [ClientChannel] whenever [GrpcClientManager.connect] replaces +/// the channel (e.g. during reconnection). Without this, stale clients +/// would hold a reference to the old (shut-down) channel and every RPC +/// would fail with "Channel shutting down". final agentServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return AgentServiceClient( manager.channel, interceptors: manager.interceptors, @@ -26,12 +33,14 @@ final agentServiceProvider = Provider((ref) { /// auth calls themselves are unauthenticated. final authServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return AuthServiceClient(manager.channel); }); /// Provides the [MachineServiceClient] for listing and switching machines. final machineServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return MachineServiceClient( manager.channel, interceptors: manager.interceptors, @@ -41,6 +50,7 @@ final machineServiceProvider = Provider((ref) { /// Provides the [WorktreeServiceClient] for worktree CRUD per machine. final worktreeServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return WorktreeServiceClient( manager.channel, interceptors: manager.interceptors, @@ -51,6 +61,7 @@ final worktreeServiceProvider = Provider((ref) { /// configuration management. final gitRepoServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return GitRepoServiceClient( manager.channel, interceptors: manager.interceptors, @@ -61,6 +72,7 @@ final gitRepoServiceProvider = Provider((ref) { /// server listing. final configServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return ConfigServiceClient( manager.channel, interceptors: manager.interceptors, @@ -70,6 +82,7 @@ final configServiceProvider = Provider((ref) { /// Provides the [GitLabServiceClient] for MR, pipeline, and issue views. final gitlabServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return GitLabServiceClient( manager.channel, interceptors: manager.interceptors, @@ -79,12 +92,14 @@ final gitlabServiceProvider = Provider((ref) { /// Provides the [HealthClient] for daemon health checks. final healthServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return HealthClient(manager.channel, interceptors: manager.interceptors); }); /// Provides the [BetCodeHealthClient] for relay health checks. final betcodeHealthServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return BetCodeHealthClient( manager.channel, interceptors: manager.interceptors, @@ -95,6 +110,7 @@ final betcodeHealthServiceProvider = Provider((ref) { /// discovery. final versionServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return VersionServiceClient( manager.channel, interceptors: manager.interceptors, @@ -105,6 +121,7 @@ final versionServiceProvider = Provider((ref) { /// service command execution, and plugin management. final commandServiceProvider = Provider((ref) { final manager = ref.watch(grpcClientManagerProvider); + ref.watch(connectionStatusProvider); return CommandServiceClient( manager.channel, interceptors: manager.interceptors, diff --git a/lib/features/conversation/notifiers/conversation_notifier.dart b/lib/features/conversation/notifiers/conversation_notifier.dart index 6437eba..1767e4e 100644 --- a/lib/features/conversation/notifiers/conversation_notifier.dart +++ b/lib/features/conversation/notifiers/conversation_notifier.dart @@ -310,7 +310,9 @@ class ConversationNotifier extends AsyncNotifier // --------------------------------------------------------------------------- void _handleStreamError(Object error) { - debugPrint('[Conversation] Stream error: $error'); + final causeInfo = + error is AppException && error.cause != null ? error.cause : ''; + debugPrint('[Conversation] Stream error: $error | cause: $causeInfo'); unawaited(_eventSubscription?.cancel()); _eventSubscription = null; diff --git a/pubspec.lock b/pubspec.lock index 85209aa..e9f1717 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -571,18 +571,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: From 2f132bd4df8e9a9446274ed092fa418aa1c61ad1 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Fri, 20 Feb 2026 21:55:59 +0300 Subject: [PATCH 08/13] fix: startup hang from Riverpod 3 container.read(provider.future) container.read(machinesProvider.future) never resolved on a bare ProviderContainer before runApp(), even though the AsyncNotifier build completed successfully. Replace with container.listen + Completer pattern that reliably awaits async provider state transitions. Co-Authored-By: Claude Opus 4.6 --- lib/main.dart | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index b293db1..1145c49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:betcode_app/app.dart'; import 'package:betcode_app/core/auth/auth.dart'; import 'package:betcode_app/core/grpc/grpc_providers.dart'; import 'package:betcode_app/features/machines/notifiers/machines_providers.dart'; +import 'package:betcode_app/generated/betcode/v1/machine.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -28,8 +31,12 @@ Future main() async { // Load machines and auto-select the sole machine before the app renders, // so gRPC calls that require the x-machine-id header have it available. + // + // NOTE: container.read(provider.future) can hang with Riverpod 3 on a bare + // ProviderContainer (before runApp). Use container.listen to reliably await + // the async build result. try { - await container.read(machinesProvider.future); + await _awaitProvider>(container, machinesProvider); debugPrint('[main] Machines loaded'); } on Exception catch (e) { debugPrint('[main] Machines pre-load failed: $e'); @@ -40,3 +47,34 @@ Future main() async { UncontrolledProviderScope(container: container, child: const BetCodeApp()), ); } + +/// Awaits an [AsyncNotifierProvider]'s build by listening for state transitions. +/// +/// Returns the data value when the provider transitions to [AsyncData], or +/// throws the error if it transitions to [AsyncError]. +Future _awaitProvider( + ProviderContainer container, + AsyncNotifierProvider provider, +) { + final completer = Completer(); + late final ProviderSubscription> sub; + sub = container.listen>( + provider, + (_, next) { + if (completer.isCompleted) return; + next.when( + data: (value) { + completer.complete(value); + sub.close(); + }, + error: (e, st) { + completer.completeError(e, st); + sub.close(); + }, + loading: () {}, + ); + }, + fireImmediately: true, + ); + return completer.future; +} From caa17f93498f816adf532e14db797cdd4d25b0cf Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Sat, 21 Feb 2026 12:54:55 +0300 Subject: [PATCH 09/13] fix: code review fixes for error handling branch - Fix failing tests: error_mapping (Channel shutting down -> NetworkError) and interceptors (LoggingInterceptor wraps stream) - Fix _loadHistory catching AppException in addition to GrpcError so error-mapped exceptions show soft banner instead of killing conversation - Move ConnectivityBanner inside MaterialApp.router builder for proper theme/directionality context - Make SessionNotFoundError.sessionId optional (was always empty string) - Remove duplicate debugPrint in ErrorMappingResponseStream - Remove redundant imports in ConversationEventHandler - Document best-effort health check design decision in GrpcClientManager - Add tests for optional sessionId and AppException history load path Co-Authored-By: Claude Opus 4.6 --- lib/app.dart | 25 +++++++-------- lib/core/grpc/app_exceptions.dart | 6 ++-- lib/core/grpc/client_manager.dart | 4 +++ lib/core/grpc/error_mapping.dart | 1 - lib/core/grpc/interceptors.dart | 6 ---- .../notifiers/conversation_event_handler.dart | 4 --- .../notifiers/conversation_notifier.dart | 8 +++++ test/core/grpc/app_exceptions_test.dart | 7 ++++ test/core/grpc/error_mapping_test.dart | 4 +-- test/core/grpc/interceptors_test.dart | 6 ++-- .../notifiers/conversation_notifier_test.dart | 32 +++++++++++++++++++ 11 files changed, 72 insertions(+), 31 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 0cc38d8..1421248 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -13,19 +13,18 @@ class BetCodeApp extends ConsumerWidget { ref.watch(relayAutoReconnectProvider); final router = ref.watch(routerProvider); - return Column( - children: [ - const ConnectivityBanner(), - Expanded( - child: MaterialApp.router( - title: 'BetCode', - debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - routerConfig: router, - ), - ), - ], + return MaterialApp.router( + title: 'BetCode', + debugShowCheckedModeBanner: false, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + routerConfig: router, + builder: (context, child) => Column( + children: [ + const ConnectivityBanner(), + Expanded(child: child ?? const SizedBox.shrink()), + ], + ), ); } } diff --git a/lib/core/grpc/app_exceptions.dart b/lib/core/grpc/app_exceptions.dart index c6d3e17..9cb20f9 100644 --- a/lib/core/grpc/app_exceptions.dart +++ b/lib/core/grpc/app_exceptions.dart @@ -33,12 +33,12 @@ final class SessionNotFoundError extends AppException { /// Creates a [SessionNotFoundError] for the given [sessionId]. const SessionNotFoundError({ required super.message, - required this.sessionId, + this.sessionId, super.cause, }); - /// The ID of the session that was not found. - final String sessionId; + /// The ID of the session that was not found, if available. + final String? sessionId; } /// The session exists but is in an invalid state (e.g. corrupted, expired). diff --git a/lib/core/grpc/client_manager.dart b/lib/core/grpc/client_manager.dart index 4642068..4db347d 100644 --- a/lib/core/grpc/client_manager.dart +++ b/lib/core/grpc/client_manager.dart @@ -175,6 +175,10 @@ class GrpcClientManager { ), ); + // Health check is best-effort during initial connect: we don't block + // the connection on it because the relay may be up while the daemon + // behind it is still starting. Callers can use healthCheck() explicitly + // for stricter verification (e.g. GrpcLifecycleBridge on app resume). if (_healthCheckFn != null) { try { await _healthCheckFn(_channel!); diff --git a/lib/core/grpc/error_mapping.dart b/lib/core/grpc/error_mapping.dart index d61d25b..266bad0 100644 --- a/lib/core/grpc/error_mapping.dart +++ b/lib/core/grpc/error_mapping.dart @@ -28,7 +28,6 @@ AppException mapGrpcError(GrpcError error, {String? method}) { ? SessionNotFoundError( message: 'Session no longer exists.', cause: error, - sessionId: '', ) : ServerError( message: 'The requested resource was not found.', diff --git a/lib/core/grpc/interceptors.dart b/lib/core/grpc/interceptors.dart index 8b01254..2510ca3 100644 --- a/lib/core/grpc/interceptors.dart +++ b/lib/core/grpc/interceptors.dart @@ -413,12 +413,6 @@ class _ErrorMappingResponseStream extends StreamView return super.listen( onData, onError: (Object error, StackTrace stackTrace) { - if (error is GrpcError) { - debugPrint( - '[gRPC] ErrorMapping $_method: ' - 'code=${error.code} message=${error.message}', - ); - } final mapped = error is GrpcError ? mapGrpcError(error, method: _method) : error; diff --git a/lib/features/conversation/notifiers/conversation_event_handler.dart b/lib/features/conversation/notifiers/conversation_event_handler.dart index 3c7788a..5eeaaae 100644 --- a/lib/features/conversation/notifiers/conversation_event_handler.dart +++ b/lib/features/conversation/notifiers/conversation_event_handler.dart @@ -1,12 +1,8 @@ import 'dart:convert'; -import 'package:betcode_app/features/conversation/conversation.dart' - show ConversationNotifier; import 'package:betcode_app/features/conversation/models/conversation_state.dart'; import 'package:betcode_app/features/conversation/notifiers/conversation_notifier.dart' show ConversationNotifier; -import 'package:betcode_app/features/conversation/notifiers/notifiers.dart' - show ConversationNotifier; import 'package:betcode_app/generated/betcode/v1/agent.pb.dart' as pb; import 'package:betcode_app/generated/betcode/v1/common.pb.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/features/conversation/notifiers/conversation_notifier.dart b/lib/features/conversation/notifiers/conversation_notifier.dart index 1767e4e..6c4136a 100644 --- a/lib/features/conversation/notifiers/conversation_notifier.dart +++ b/lib/features/conversation/notifiers/conversation_notifier.dart @@ -185,6 +185,14 @@ class ConversationNotifier extends AsyncNotifier final current = state.value; final seq = current is ConversationActive ? current.lastSequence : 0; debugPrint('[Conversation] History loaded, lastSequence=$seq'); + } on AppException catch (e) { + debugPrint('[Conversation] History load failed: $e'); + final current = state.value; + if (current is ConversationActive) { + state = AsyncData( + current.copyWith(errorMessage: "Couldn't load message history."), + ); + } } on GrpcError catch (e) { debugPrint('[Conversation] History load failed: $e'); final current = state.value; diff --git a/test/core/grpc/app_exceptions_test.dart b/test/core/grpc/app_exceptions_test.dart index f3c5c41..2f5ceb7 100644 --- a/test/core/grpc/app_exceptions_test.dart +++ b/test/core/grpc/app_exceptions_test.dart @@ -31,6 +31,13 @@ void main() { expect(error.cause, isNull); }); + test('SessionNotFoundError works without sessionId', () { + const error = SessionNotFoundError(message: 'not found'); + + expect(error.sessionId, isNull); + expect(error.message, 'not found'); + }); + test('all subtypes are AppException', () { const exceptions = [ NetworkError(message: 'a'), diff --git a/test/core/grpc/error_mapping_test.dart b/test/core/grpc/error_mapping_test.dart index db966aa..4988cd7 100644 --- a/test/core/grpc/error_mapping_test.dart +++ b/test/core/grpc/error_mapping_test.dart @@ -15,13 +15,13 @@ void main() { expect(result.cause, grpcError); }); - test('UNAVAILABLE with channel shutting down -> RelayUnavailableError', () { + test('UNAVAILABLE with channel shutting down -> NetworkError', () { const grpcError = GrpcError.custom( StatusCode.unavailable, 'Channel shutting down.', ); final result = mapGrpcError(grpcError); - expect(result, isA()); + expect(result, isA()); }); test('UNAVAILABLE with TLS keyword -> RelayUnavailableError', () { diff --git a/test/core/grpc/interceptors_test.dart b/test/core/grpc/interceptors_test.dart index af6540e..d84d70c 100644 --- a/test/core/grpc/interceptors_test.dart +++ b/test/core/grpc/interceptors_test.dart @@ -247,7 +247,7 @@ void main() { expect(captured, same(opts)); }); - test('returns invoker response stream', () { + test('returns invoker response stream wrapped for logging', () async { final interceptor = LoggingInterceptor(); final expected = FakeInterceptorResponseStream( Stream.fromIterable(['d']), @@ -258,7 +258,9 @@ void main() { CallOptions(), (m, r, o) => expected, ); - expect(result, same(expected)); + expect(result, isA>()); + expect(result, isNot(same(expected))); + expect(await result.toList(), ['d']); }); }); diff --git a/test/features/conversation/notifiers/conversation_notifier_test.dart b/test/features/conversation/notifiers/conversation_notifier_test.dart index e242d19..b787d0c 100644 --- a/test/features/conversation/notifiers/conversation_notifier_test.dart +++ b/test/features/conversation/notifiers/conversation_notifier_test.dart @@ -267,6 +267,38 @@ void main() { }); }); + group('history load', () { + test('AppException during history load sets errorMessage, not fatal', () async { + // When the ErrorMappingInterceptor maps a GrpcError to an AppException + // during history load, the conversation should stay active with a soft + // error message rather than transitioning to ConversationError. + const sid = 'sess-history'; + final historyController = StreamController(); + when(() => mockClient.resumeSession(any())).thenAnswer((_) { + historyController.addError( + const NetworkError(message: 'Connection lost. Retrying...'), + ); + return FakeResponseStream(historyController); + }); + + final n = notifier(sid); + await n.startConversation(workingDirectory: '/tmp'); + await Future.delayed(Duration.zero); + + // startConversation sets state to ConversationActive before calling + // _loadHistory. The history load should catch the AppException + // gracefully and set errorMessage without killing the conversation. + final s = stateVal(sid); + expect(s, isA()); + expect( + (s! as ConversationActive).errorMessage, + "Couldn't load message history.", + ); + + await historyController.close(); + }); + }); + group('stream error handling', () { test( 'transient stream error triggers reconnection on active session', From 38e4a4c641f8f5e6fe0a34b1ce94b06ee7a1629e Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Sat, 21 Feb 2026 13:46:29 +0300 Subject: [PATCH 10/13] feat: add GitHub Actions CI and migrate iOS to UIScene lifecycle Add CI workflow with analyze, test, build-android, and build-ios jobs. Migrate iOS from AppDelegate-based window management to UIScene with SceneDelegate. Bump iOS deployment target from 13.0 to 16.0. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 113 +++++++++++++++++++++++++++ CLAUDE.md | 2 +- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Runner.xcodeproj/project.pbxproj | 10 ++- ios/Runner/AppDelegate.swift | 21 ++++- ios/Runner/Info.plist | 19 ++++- ios/Runner/SceneDelegate.swift | 26 ++++++ 7 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 ios/Runner/SceneDelegate.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bdf7e0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + key: pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }} + restore-keys: pub-${{ runner.os }}- + + - run: flutter pub get + + - run: dart format --set-exit-if-changed . + + - run: dart analyze --fatal-infos + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + key: pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }} + restore-keys: pub-${{ runner.os }}- + + - run: flutter pub get + + - run: flutter test + + build-android: + name: Build Android + runs-on: ubuntu-latest + needs: [analyze, test] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + ~/.gradle/caches + ~/.gradle/wrapper + key: android-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'android/build.gradle*', 'android/app/build.gradle*') }} + restore-keys: android-${{ runner.os }}- + + - run: flutter pub get + + - run: flutter build apk --debug + + build-ios: + name: Build iOS + runs-on: macos-latest + needs: [analyze, test] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - uses: actions/cache@v4 + with: + path: | + ~/.pub-cache + ios/Pods + key: ios-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'ios/Podfile.lock') }} + restore-keys: ios-${{ runner.os }}- + + - run: flutter pub get + + - run: flutter build ios --debug --no-codesign diff --git a/CLAUDE.md b/CLAUDE.md index 6db96a3..f70f238 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,7 +164,7 @@ All user-initiated actions include idempotency keys (UUIDv7) to handle duplicate | Platform | Transport | Secure Storage | Background Sync | Push | |----------|-----------|---------------|-----------------|------| | Android (SDK 24+) | OkHttp | Keystore | WorkManager | FCM | -| iOS (15+) | URLSession | Keychain | BGTaskScheduler | APNs | +| iOS (16+) | URLSession | Keychain | BGTaskScheduler | APNs | ## Coding Conventions diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index b5586f2..f598305 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 16.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6d1a5aa..76e547e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + F1A2B3C51ED2DC5600515810 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -47,6 +48,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -116,6 +118,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -269,6 +272,7 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + F1A2B3C51ED2DC5600515810 /* SceneDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -346,7 +350,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -472,7 +476,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -523,7 +527,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 8be1cec..ada6779 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -7,7 +7,26 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) + // Plugin registration is deferred to SceneDelegate.scene(_:willConnectTo:options:) + // because the window (and FlutterViewController) is not available yet + // under the UIScene lifecycle. return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + override func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + return UISceneConfiguration( + name: "Default Configuration", + sessionRole: connectingSceneSession.role + ) + } + + override func application( + _ application: UIApplication, + didDiscardSceneSessions sceneSessions: Set + ) { + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 54bd098..8330fd1 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,8 +26,23 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..5596312 --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,26 @@ +import Flutter +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene, + let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } + + let controller = FlutterViewController(project: nil, nibName: nil, bundle: nil) + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = controller + window.makeKeyAndVisible() + self.window = window + + // Sync with FlutterAppDelegate so plugins can locate the engine. + appDelegate.window = window + GeneratedPluginRegistrant.register(with: appDelegate) + } +} From 4d25f8f071062d1ff051c29589ec942f1792b57e Mon Sep 17 00:00:00 2001 From: sakost Date: Sat, 21 Feb 2026 13:53:33 +0300 Subject: [PATCH 11/13] chore: add iOS/macOS build configs and CocoaPods files Co-Authored-By: Claude Opus 4.6 --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 43 + ios/Podfile.lock | 69 + ios/Runner.xcodeproj/project.pbxproj | 1352 +++++++++-------- .../contents.xcworkspacedata | 17 +- macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Podfile | 42 + 9 files changed, 900 insertions(+), 627 deletions(-) create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock create mode 100644 macos/Podfile diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 0b2d479..dfd2626 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 0b2d479..a97381a 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..724bbd5 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,69 @@ +PODS: + - connectivity_plus (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - integration_test (0.0.1): + - Flutter + - sqlite3 (3.51.1): + - sqlite3/common (= 3.51.1) + - sqlite3/common (3.51.1) + - sqlite3/dbstatvtab (3.51.1): + - sqlite3/common + - sqlite3/fts5 (3.51.1): + - sqlite3/common + - sqlite3/math (3.51.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.51.1): + - sqlite3/common + - sqlite3/rtree (3.51.1): + - sqlite3/common + - sqlite3/session (3.51.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.51.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - sqlite3/session + +DEPENDENCIES: + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - Flutter (from `Flutter`) + - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + +SPEC REPOS: + trunk: + - sqlite3 + +EXTERNAL SOURCES: + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + Flutter: + :path: Flutter + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + +SPEC CHECKSUMS: + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b + sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 76e547e..3df1f77 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -1,620 +1,732 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - F1A2B3C51ED2DC5600515810 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - F1A2B3C51ED2DC5600515810 /* SceneDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5D047C6C4EF47E19EEE443BC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E1AC62DB6A5A840090AF17D /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + EB4C202DA96D5450FBC06ADB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C450E4C41ECB1A1D6B15537 /* Pods_Runner.framework */; }; + F1A2B3C51ED2DC5600515810 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0E1AC62DB6A5A840090AF17D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1851A2292E885E7F7723B997 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 22DCF7218B6E86C0FD3CD542 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 24CE31D94497CB2FEF229A47 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 30406E0CBF89D1B8EF64AB3F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5A25F8957B81DBF896C47999 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7C450E4C41ECB1A1D6B15537 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7CBABE939792D9926A297C91 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7EF05B9695710C9D8071CEDE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5D047C6C4EF47E19EEE443BC /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EB4C202DA96D5450FBC06ADB /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 047F59B8B71EC0608564A43C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7C450E4C41ECB1A1D6B15537 /* Pods_Runner.framework */, + 0E1AC62DB6A5A840090AF17D /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 8F1240ABECAE23A528193361 /* Pods */ = { + isa = PBXGroup; + children = ( + 22DCF7218B6E86C0FD3CD542 /* Pods-Runner.debug.xcconfig */, + 7CBABE939792D9926A297C91 /* Pods-Runner.release.xcconfig */, + 1851A2292E885E7F7723B997 /* Pods-Runner.profile.xcconfig */, + 30406E0CBF89D1B8EF64AB3F /* Pods-RunnerTests.debug.xcconfig */, + 5A25F8957B81DBF896C47999 /* Pods-RunnerTests.release.xcconfig */, + 24CE31D94497CB2FEF229A47 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 8F1240ABECAE23A528193361 /* Pods */, + 047F59B8B71EC0608564A43C /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + F1A2B3C41ED2DC5600515810 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + A53E9055CC399A8BA94790CC /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 7EF05B9695710C9D8071CEDE /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 57E42E1A65C9A88D472BD761 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 67446E1DA34FCBBC13A74DF4 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 57E42E1A65C9A88D472BD761 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 67446E1DA34FCBBC13A74DF4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A53E9055CC399A8BA94790CC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + F1A2B3C51ED2DC5600515810 /* SceneDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 30406E0CBF89D1B8EF64AB3F /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5A25F8957B81DBF896C47999 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 24CE31D94497CB2FEF229A47 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.sakost.betcodeApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 59c6d39..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -1,7 +1,10 @@ - - - - - + + + + + + + diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index f022c34..df4c964 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index f022c34..e79501e 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end From 0c8e3dfeff79a3c7682b26135ef3d724b3fcf724 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Sat, 21 Feb 2026 14:01:45 +0300 Subject: [PATCH 12/13] fix: dart format and update .gitignore for generated files Co-Authored-By: Claude Opus 4.6 --- .gitignore | 14 ++++- lib/core/grpc/interceptors.dart | 2 +- .../notifiers/conversation_notifier.dart | 5 +- macos/Flutter/GeneratedPluginRegistrant.swift | 16 ------ .../notifiers/conversation_notifier_test.dart | 55 ++++++++++--------- 5 files changed, 46 insertions(+), 46 deletions(-) delete mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/.gitignore b/.gitignore index a7638ff..9b3600a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,14 +25,26 @@ migrate_working_dir/ # Flutter/Dart/Pub related **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ +.flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ /coverage/ +# iOS generated & dependencies +ios/Flutter/.last_build_id +ios/Flutter/ephemeral/ +ios/Flutter/flutter_export_environment.sh +ios/Flutter/Generated.xcconfig +ios/Pods/ + +# macOS generated & dependencies +macos/Flutter/ephemeral/ +macos/Flutter/GeneratedPluginRegistrant.swift +macos/Pods/ + # Symbolication related app.*.symbols diff --git a/lib/core/grpc/interceptors.dart b/lib/core/grpc/interceptors.dart index 2510ca3..a46d213 100644 --- a/lib/core/grpc/interceptors.dart +++ b/lib/core/grpc/interceptors.dart @@ -166,7 +166,7 @@ class LoggingInterceptor extends ClientInterceptor { class _LoggingResponseStream extends StreamView implements ResponseStream { _LoggingResponseStream(this._delegate, this._method, this._stopwatch) - : super(_delegate); + : super(_delegate); final ResponseStream _delegate; final String _method; diff --git a/lib/features/conversation/notifiers/conversation_notifier.dart b/lib/features/conversation/notifiers/conversation_notifier.dart index 6c4136a..31813ec 100644 --- a/lib/features/conversation/notifiers/conversation_notifier.dart +++ b/lib/features/conversation/notifiers/conversation_notifier.dart @@ -318,8 +318,9 @@ class ConversationNotifier extends AsyncNotifier // --------------------------------------------------------------------------- void _handleStreamError(Object error) { - final causeInfo = - error is AppException && error.cause != null ? error.cause : ''; + final causeInfo = error is AppException && error.cause != null + ? error.cause + : ''; debugPrint('[Conversation] Stream error: $error | cause: $causeInfo'); unawaited(_eventSubscription?.cancel()); _eventSubscription = null; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 8ac2845..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import connectivity_plus -import flutter_secure_storage_darwin -import sqlite3_flutter_libs - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) - FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) - Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) -} diff --git a/test/features/conversation/notifiers/conversation_notifier_test.dart b/test/features/conversation/notifiers/conversation_notifier_test.dart index b787d0c..753ea62 100644 --- a/test/features/conversation/notifiers/conversation_notifier_test.dart +++ b/test/features/conversation/notifiers/conversation_notifier_test.dart @@ -268,35 +268,38 @@ void main() { }); group('history load', () { - test('AppException during history load sets errorMessage, not fatal', () async { - // When the ErrorMappingInterceptor maps a GrpcError to an AppException - // during history load, the conversation should stay active with a soft - // error message rather than transitioning to ConversationError. - const sid = 'sess-history'; - final historyController = StreamController(); - when(() => mockClient.resumeSession(any())).thenAnswer((_) { - historyController.addError( - const NetworkError(message: 'Connection lost. Retrying...'), - ); - return FakeResponseStream(historyController); - }); + test( + 'AppException during history load sets errorMessage, not fatal', + () async { + // When the ErrorMappingInterceptor maps a GrpcError to an AppException + // during history load, the conversation should stay active with a soft + // error message rather than transitioning to ConversationError. + const sid = 'sess-history'; + final historyController = StreamController(); + when(() => mockClient.resumeSession(any())).thenAnswer((_) { + historyController.addError( + const NetworkError(message: 'Connection lost. Retrying...'), + ); + return FakeResponseStream(historyController); + }); - final n = notifier(sid); - await n.startConversation(workingDirectory: '/tmp'); - await Future.delayed(Duration.zero); + final n = notifier(sid); + await n.startConversation(workingDirectory: '/tmp'); + await Future.delayed(Duration.zero); - // startConversation sets state to ConversationActive before calling - // _loadHistory. The history load should catch the AppException - // gracefully and set errorMessage without killing the conversation. - final s = stateVal(sid); - expect(s, isA()); - expect( - (s! as ConversationActive).errorMessage, - "Couldn't load message history.", - ); + // startConversation sets state to ConversationActive before calling + // _loadHistory. The history load should catch the AppException + // gracefully and set errorMessage without killing the conversation. + final s = stateVal(sid); + expect(s, isA()); + expect( + (s! as ConversationActive).errorMessage, + "Couldn't load message history.", + ); - await historyController.close(); - }); + await historyController.close(); + }, + ); }); group('stream error handling', () { From 3305fc9b86469f7f557839ad21500d1f571b78c9 Mon Sep 17 00:00:00 2001 From: Konstantin Sazhenov Date: Sat, 21 Feb 2026 14:07:32 +0300 Subject: [PATCH 13/13] fix: resolve dart analyze infos (comment refs, line length) Co-Authored-By: Claude Opus 4.6 --- lib/core/grpc/service_providers.dart | 4 ++-- lib/main.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/core/grpc/service_providers.dart b/lib/core/grpc/service_providers.dart index 582d5c0..6e96ea8 100644 --- a/lib/core/grpc/service_providers.dart +++ b/lib/core/grpc/service_providers.dart @@ -14,8 +14,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Provides the [AgentServiceClient] for conversation streaming, session /// management, and input lock operations. /// -/// Watches [connectionStatusProvider] so the client is recreated with the -/// current [ClientChannel] whenever [GrpcClientManager.connect] replaces +/// Watches `connectionStatusProvider` so the client is recreated with the +/// current `ClientChannel` whenever `GrpcClientManager.connect` replaces /// the channel (e.g. during reconnection). Without this, stale clients /// would hold a reference to the old (shut-down) channel and every RPC /// would fail with "Channel shutting down". diff --git a/lib/main.dart b/lib/main.dart index 1145c49..411746e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,7 +48,8 @@ Future main() async { ); } -/// Awaits an [AsyncNotifierProvider]'s build by listening for state transitions. +/// Awaits an [AsyncNotifierProvider]'s build by listening for state +/// transitions. /// /// Returns the data value when the provider transitions to [AsyncData], or /// throws the error if it transitions to [AsyncError].