diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 197dab58..5dca6fa4 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -454,12 +454,12 @@ class AppController extends ChangeNotifier { DesktopIntegrationState get desktopIntegration => desktopPlatformServiceInternal.state; bool get supportsDesktopIntegration => desktopIntegration.isSupported; - bool get desktopPlatformBusy => desktopPlatformBusyInternal; + bool get desktopPlatformBusy => _desktopPlatformBusyInternal; set desktopPlatformBusyInternal(bool value) { - desktopPlatformBusyInternal = value; + _desktopPlatformBusyInternal = value; notifyListeners(); } - bool desktopPlatformBusyInternal = false; + bool _desktopPlatformBusyInternal = false; GatewayConnectionSnapshot get connection => runtimeInternal.snapshot; SettingsSnapshot get settings => settingsControllerInternal.snapshot; diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart index a0a34423..7f9f1874 100644 --- a/lib/app/app_controller_desktop_external_acp_routing.dart +++ b/lib/app/app_controller_desktop_external_acp_routing.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_gateway.dart b/lib/app/app_controller_desktop_gateway.dart index 763a29f7..569813f7 100644 --- a/lib/app/app_controller_desktop_gateway.dart +++ b/lib/app/app_controller_desktop_gateway.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_navigation.dart b/lib/app/app_controller_desktop_navigation.dart index 72f7f650..87199f4d 100644 --- a/lib/app/app_controller_desktop_navigation.dart +++ b/lib/app/app_controller_desktop_navigation.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 3718d197..c4c50d6b 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 86585943..9dc1dc8c 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index b3df783a..d62291e8 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart index f1777487..562eace7 100644 --- a/lib/app/app_controller_desktop_settings_runtime.dart +++ b/lib/app/app_controller_desktop_settings_runtime.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; @@ -755,8 +755,6 @@ extension AppControllerDesktopSettingsRuntime on AppController { await settingsControllerInternal.saveSnapshot(sanitized); settingsDraftInternal = sanitized; settingsDraftInitializedInternal = true; - } - Future applyPersistedSettingsSideEffectsInternal({ required SettingsSnapshot previous, required SettingsSnapshot current, @@ -774,7 +772,20 @@ extension AppControllerDesktopSettingsRuntime on AppController { ?.selectedAgentId ?? '', ); - modelsControllerInternal.restoreFromSettings(current.aiGateway); + themeModeInternal = current.themeMode; + + final bridgeChanged = + previous.acpBridgeServerModeConfig.toJsonString() != + current.acpBridgeServerModeConfig.toJsonString(); + + if (refreshAfterSave || bridgeChanged) { + // Re-trigger Bridge capability discovery if the mode or endpoint changed. + unawaited(refreshAcpCapabilitiesInternal(quiet: true)); + } + + notifyListeners(); + } + if (disposedInternal) { return; } diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 56e0b29f..b7478a1c 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index 09bfc8a8..33753e85 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index ef9501f2..bb678e8b 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 34f2a0ad..bc168e2b 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart index f122f7e1..8ceadb60 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index 5c6436ed..aee301e4 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 66b2aa08..7e2f28b2 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/aris_bundle.dart'; + import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/runtime/gateway_runtime_helpers.dart b/lib/runtime/gateway_runtime_helpers.dart index 4aa807ae..b29d62d7 100644 --- a/lib/runtime/gateway_runtime_helpers.dart +++ b/lib/runtime/gateway_runtime_helpers.dart @@ -207,11 +207,15 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier { ) { final text = raw is String ? raw : utf8.decode(raw as List); final decoded = jsonDecode(text) as Map; + + // Handle Events / Notifications + final event = stringValue(decoded['event']) ?? stringValue(decoded['method']); final type = stringValue(decoded['type']); - if (type == 'event') { - final event = stringValue(decoded['event']) ?? ''; - final payload = decoded['payload']; - if (event == 'connect.challenge') { + + if (event != null || type == 'event') { + final resolvedEvent = event ?? ''; + final payload = decoded['payload'] ?? decoded['params']; + if (resolvedEvent == 'connect.challenge') { final nonce = stringValue(asMap(payload)['nonce']); if (nonce != null && !challenge.isCompleted) { challenge.complete(nonce); @@ -219,36 +223,35 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier { appendLogInternal(runtime, 'debug', 'connect', 'challenge received'); return; } - if (event == 'health') { + if (resolvedEvent == 'health') { runtime.snapshotInternal = runtime.snapshotInternal.copyWith( healthPayload: asMap(payload), ); appendLogInternal(runtime, 'debug', 'health', 'push health update'); runtime.notifyListeners(); - } else if (event == 'device.pair.requested' || - event == 'device.pair.resolved') { + } else if (resolvedEvent == 'device.pair.requested' || + resolvedEvent == 'device.pair.resolved') { final eventPayload = asMap(payload); appendLogInternal( runtime, 'info', 'pairing', - '$event | request: ${stringValue(eventPayload['requestId']) ?? 'unknown'} | device: ${stringValue(eventPayload['deviceId']) ?? 'unknown'}', + '$resolvedEvent | request: ${stringValue(eventPayload['requestId']) ?? 'unknown'} | device: ${stringValue(eventPayload['deviceId']) ?? 'unknown'}', ); - } else if (event == 'seqGap') { + } else if (resolvedEvent == 'seqGap') { appendLogInternal(runtime, 'warn', 'sync', 'sequence gap detected'); } runtime.eventsInternal.add( GatewayPushEvent( - event: event, + event: resolvedEvent, payload: payload, - sequence: intValue(decoded['seq']), + sequence: intValue(decoded['seq']) ?? 0, ), ); return; } - if (type != 'res') { - return; - } + + // Handle Responses final id = stringValue(decoded['id']); if (id == null) { return; @@ -257,9 +260,13 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier { if (completer == null || completer.isCompleted) { return; } - final ok = boolValue(decoded['ok']) ?? false; - final payload = decoded['payload']; + + final hasResult = decoded.containsKey('result'); + final hasError = decoded.containsKey('error'); + final ok = boolValue(decoded['ok']) ?? (hasResult && !hasError); + final payload = decoded['payload'] ?? decoded['result']; final error = asMap(decoded['error']); + if (!ok) { appendLogInternal( runtime, diff --git a/lib/runtime/multi_agent_orchestrator_protocol.dart b/lib/runtime/multi_agent_orchestrator_protocol.dart index ca3b4baa..7909cd4c 100644 --- a/lib/runtime/multi_agent_orchestrator_protocol.dart +++ b/lib/runtime/multi_agent_orchestrator_protocol.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'aris_bundle.dart'; + import 'embedded_agent_launch_policy.dart'; import 'go_core.dart'; import 'aris_llm_chat_client.dart'; diff --git a/lib/runtime/multi_agent_orchestrator_support.dart b/lib/runtime/multi_agent_orchestrator_support.dart index ad7b4b13..eee781bf 100644 --- a/lib/runtime/multi_agent_orchestrator_support.dart +++ b/lib/runtime/multi_agent_orchestrator_support.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'aris_bundle.dart'; + import 'embedded_agent_launch_policy.dart'; import 'go_core.dart'; import 'aris_llm_chat_client.dart'; diff --git a/lib/runtime/multi_agent_orchestrator_workflow.dart b/lib/runtime/multi_agent_orchestrator_workflow.dart index 5fe385ce..ea577c92 100644 --- a/lib/runtime/multi_agent_orchestrator_workflow.dart +++ b/lib/runtime/multi_agent_orchestrator_workflow.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'aris_bundle.dart'; + import 'embedded_agent_launch_policy.dart'; import 'go_core.dart'; import 'aris_llm_chat_client.dart'; diff --git a/lib/runtime/secure_config_store.dart b/lib/runtime/secure_config_store.dart index f37c9825..9646851c 100644 --- a/lib/runtime/secure_config_store.dart +++ b/lib/runtime/secure_config_store.dart @@ -1,9 +1,8 @@ import 'dart:convert'; import 'dart:io'; -export 'file_store_support.dart'; -export 'secret_store.dart'; -export 'settings_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path_provider/package_info_plus.dart'; import 'file_store_support.dart'; import 'runtime_models.dart'; @@ -12,319 +11,71 @@ import 'settings_store.dart'; class SecureConfigStore { SecureConfigStore({ - Future Function()? secretRootPathResolver, - Future Function()? appDataRootPathResolver, - Future Function()? supportRootPathResolver, - SecureStorageClient? secureStorage, - bool enableSecureStorage = true, - }) { - final layoutResolver = StoreLayoutResolver( - appDataRootPathResolver: appDataRootPathResolver, - secretRootPathResolver: secretRootPathResolver, - supportRootPathResolver: supportRootPathResolver, - ); - _layoutResolver = layoutResolver; - _secretStore = SecretStore( - secretRootPathResolver: secretRootPathResolver, - appDataRootPathResolver: appDataRootPathResolver, - supportRootPathResolver: supportRootPathResolver, - secureStorage: secureStorage, - enableSecureStorage: enableSecureStorage, - layoutResolver: layoutResolver, - ); - _settingsStore = SettingsStore( - appDataRootPathResolver: appDataRootPathResolver, - supportRootPathResolver: supportRootPathResolver, - layoutResolver: layoutResolver, - ); - } + StoreLayoutResolver? layoutResolver, + SettingsStore? settingsStore, + SecretStore? secretStore, + }) : _layoutResolver = layoutResolver ?? StoreLayoutResolver(), + _settingsStore = settingsStore ?? SettingsStore(layoutResolver ?? StoreLayoutResolver()), + _secretStore = secretStore ?? SecretStore(layoutResolver ?? StoreLayoutResolver()); - late final SecretStore _secretStore; - late final SettingsStore _settingsStore; - late final StoreLayoutResolver _layoutResolver; + final StoreLayoutResolver _layoutResolver; + final SettingsStore _settingsStore; + final SecretStore _secretStore; Future initialize() async { - await _secretStore.initialize(); await _settingsStore.initialize(); + await _secretStore.initialize(); } - Future loadSettingsSnapshot() { - return _settingsStore.loadSettingsSnapshot(); - } + Future loadSettingsSnapshot() => _settingsStore.loadSnapshot(); + Future saveSettingsSnapshot(SettingsSnapshot snapshot) => _settingsStore.saveSnapshot(snapshot); + Future reloadSettingsSnapshotResult() => _settingsStore.reloadSnapshotResult(); - Future reloadSettingsSnapshot() { - return _settingsStore.reloadSettingsSnapshot(); - } + Future> loadAccountManagedSecrets() => _secretStore.loadAccountManagedSecrets(); + Future saveAccountManagedSecret({required String target, required String value}) => _secretStore.saveAccountManagedSecret(target: target, value: value); + Future clearAccountManagedSecret({required String target}) => _secretStore.clearAccountManagedSecret(target: target); + Future clearAccountManagedSecrets() => _secretStore.clearAccountManagedSecrets(); - Future reloadSettingsSnapshotResult() { - return _settingsStore.reloadSettingsSnapshotResult(); - } + Future loadAccountSessionToken() => _secretStore.loadAccountSessionToken(); + Future saveAccountSessionToken(String value) => _secretStore.saveAccountSessionToken(value); + Future clearAccountSessionToken() => _secretStore.clearAccountSessionToken(); - Future saveSettingsSnapshot(SettingsSnapshot snapshot) { - return _settingsStore.saveSettingsSnapshot(snapshot); - } + Future loadAccountSessionExpiresAtMs() => _secretStore.loadAccountSessionExpiresAtMs(); + Future saveAccountSessionExpiresAtMs(int value) => _secretStore.saveAccountSessionExpiresAtMs(value); + Future clearAccountSessionExpiresAtMs() => _secretStore.clearAccountSessionExpiresAtMs(); - Future resolvedSettingsFile() { - return _settingsStore.resolvedSettingsFile(); - } + Future loadAccountSessionUserId() => _secretStore.loadAccountSessionUserId(); + Future saveAccountSessionUserId(String value) => _secretStore.saveAccountSessionUserId(value); + Future clearAccountSessionUserId() => _secretStore.clearAccountSessionUserId(); - Future resolvedSettingsWatchDirectory() { - return _settingsStore.resolvedSettingsWatchDirectory(); - } + Future loadAccountSessionIdentifier() => _secretStore.loadAccountSessionIdentifier(); + Future saveAccountSessionIdentifier(String value) => _secretStore.saveAccountSessionIdentifier(value); + Future clearAccountSessionIdentifier() => _secretStore.clearAccountSessionIdentifier(); - Future> loadTaskThreads() { - return _settingsStore.loadTaskThreads(); - } + Future loadAccountSessionSummary() => _secretStore.loadAccountSessionSummary(); + Future saveAccountSessionSummary(AccountSessionSummary value) => _secretStore.saveAccountSessionSummary(value); + Future clearAccountSessionSummary() => _secretStore.clearAccountSessionSummary(); - List get lastSkippedInvalidTaskThreadRecords => - _settingsStore.lastSkippedInvalidTaskThreadRecords; + Future loadAccountSyncState() => _secretStore.loadAccountSyncState(); + Future saveAccountSyncState(AccountSyncState value) => _secretStore.saveAccountSyncState(value); + Future clearAccountSyncState() => _secretStore.clearAccountSyncState(); - List get lastSkippedInvalidTaskThreadIds => - _settingsStore.lastSkippedInvalidTaskThreadIds; + Future> loadTaskThreads() => _settingsStore.loadTaskThreads(); + Future saveTaskThreads(Map threads) => _settingsStore.saveTaskThreads(threads); - Future saveTaskThreads(List records) { - return _settingsStore.saveTaskThreads(records); - } + Future> loadAuditTrail() => _secretStore.loadAuditTrail(); + Future appendAudit(SecretAuditEntry entry) => _secretStore.appendAudit(entry); - Future clearAssistantLocalState() { - return _settingsStore.clearAssistantLocalState(); - } + Future resolvedSettingsFile() => _layoutResolver.resolve().then((l) => File('${l.configDirectory.path}/settings.yaml')); + Future resolvedSettingsWatchDirectory() => _layoutResolver.resolve().then((l) => l.configDirectory); - Future> loadAuditTrail() { - return _settingsStore.loadAuditTrail(); - } - - Future appendAudit(SecretAuditEntry entry) { - return _settingsStore.appendAudit(entry); - } - - Future?> loadSupportJson(String relativePath) async { - final file = await supportFile(relativePath); - if (file == null || !await file.exists()) { - return null; - } - final raw = await file.readAsString(); - final decoded = jsonDecode(raw); - return decoded is Map ? decoded : null; - } - - Future saveSupportJson( - String relativePath, - Map payload, - ) async { - final file = await supportFile(relativePath); - if (file == null) { - return; - } - await atomicWriteString(file, jsonEncode(payload), ownerOnly: true); - } - - Future supportFile(String relativePath) async { - final normalized = relativePath.trim(); - if (normalized.isEmpty) { - return null; - } - final layout = await _layoutResolver.resolve(); - return File('${layout.rootDirectory.path}/$normalized'); - } - - Future> loadSecureRefs() { - return _secretStore.loadSecureRefs(); - } - - Future loadSecretValueByRef(String refName) => - _secretStore.loadSecretValueByRef(refName); - - Future saveSecretValueByRef(String refName, String value) => - _secretStore.saveSecretValueByRef(refName, value); - - Future clearSecretValueByRef(String refName) => - _secretStore.clearSecretValueByRef(refName); - - Future loadGatewayToken({int? profileIndex}) => - _secretStore.loadGatewayToken(profileIndex: profileIndex); - - Future saveGatewayToken(String value, {int? profileIndex}) => - _secretStore.saveGatewayToken(value, profileIndex: profileIndex); - - Future clearGatewayToken({int? profileIndex}) => - _secretStore.clearGatewayToken(profileIndex: profileIndex); - - Future loadGatewayPassword({int? profileIndex}) => - _secretStore.loadGatewayPassword(profileIndex: profileIndex); - - Future saveGatewayPassword(String value, {int? profileIndex}) => - _secretStore.saveGatewayPassword(value, profileIndex: profileIndex); - - Future clearGatewayPassword({int? profileIndex}) => - _secretStore.clearGatewayPassword(profileIndex: profileIndex); - - Future loadOllamaCloudApiKey() => - _secretStore.loadOllamaCloudApiKey(); - - Future saveOllamaCloudApiKey(String value) => - _secretStore.saveOllamaCloudApiKey(value); - - Future loadVaultToken() => _secretStore.loadVaultToken(); - - Future saveVaultToken(String value) => - _secretStore.saveVaultToken(value); - - Future loadAiGatewayApiKey() => _secretStore.loadAiGatewayApiKey(); - - Future saveAiGatewayApiKey(String value) => - _secretStore.saveAiGatewayApiKey(value); - - Future clearAiGatewayApiKey() => _secretStore.clearAiGatewayApiKey(); - - Future loadAccountSessionToken() => - _secretStore.loadAccountSessionToken(); - - Future saveAccountSessionToken(String value) => - _secretStore.saveAccountSessionToken(value); - - Future clearAccountSessionToken() => - _secretStore.clearAccountSessionToken(); - - Future loadAccountSessionExpiresAtMs() => - _secretStore.loadAccountSessionExpiresAtMs(); - - Future saveAccountSessionExpiresAtMs(int value) => - _secretStore.saveAccountSessionExpiresAtMs(value); - - Future clearAccountSessionExpiresAtMs() => - _secretStore.clearAccountSessionExpiresAtMs(); - - Future loadAccountSessionUserId() => - _secretStore.loadAccountSessionUserId(); - - Future saveAccountSessionUserId(String value) => - _secretStore.saveAccountSessionUserId(value); - - Future clearAccountSessionUserId() => - _secretStore.clearAccountSessionUserId(); - - Future loadAccountSessionIdentifier() => - _secretStore.loadAccountSessionIdentifier(); - - Future saveAccountSessionIdentifier(String value) => - _secretStore.saveAccountSessionIdentifier(value); - - Future clearAccountSessionIdentifier() => - _secretStore.clearAccountSessionIdentifier(); - - Future loadAccountSessionSummary() => - _secretStore.loadAccountSessionSummary(); - - Future saveAccountSessionSummary(AccountSessionSummary value) => - _secretStore.saveAccountSessionSummary(value); - - Future clearAccountSessionSummary() => - _secretStore.clearAccountSessionSummary(); - - Future loadAccountSyncState() async { - final payload = await loadSupportJson('account/sync_state.json'); - if (payload == null) { - return null; - } - return AccountSyncState.fromJson(payload); - } - - Future saveAccountSyncState(AccountSyncState value) => - saveSupportJson('account/sync_state.json', value.toJson()); - - Future clearAccountSyncState() async { - final file = await supportFile('account/sync_state.json'); - if (file != null && await file.exists()) { - await file.delete(); - } - } - - Future loadAppUiState() async { - final payload = await loadSupportJson('ui/state.json'); - if (payload == null) { - return AppUiState.defaults(); - } - try { - return AppUiState.fromJson(payload); - } catch (_) { - return AppUiState.defaults(); - } - } - - Future saveAppUiState(AppUiState value) => - saveSupportJson('ui/state.json', value.toJson()); - - Future clearAppUiState() async { - final file = await supportFile('ui/state.json'); - if (file == null) { - return; - } - await deleteIfExists(file); - } - - Future loadAccountManagedSecret({required String target}) => - _secretStore.loadAccountManagedSecret(target: target); - - Future saveAccountManagedSecret({ - required String target, - required String value, - }) => _secretStore.saveAccountManagedSecret(target: target, value: value); - - Future clearAccountManagedSecret({required String target}) => - _secretStore.clearAccountManagedSecret(target: target); - - Future clearAccountManagedSecrets() => - _secretStore.clearAccountManagedSecrets(); - - Future loadDeviceIdentity() { - return _secretStore.loadDeviceIdentity(); - } - - Future saveDeviceIdentity(LocalDeviceIdentity identity) { - return _secretStore.saveDeviceIdentity(identity); - } - - Future loadDeviceToken({ - required String deviceId, - required String role, - }) { - return _secretStore.loadDeviceToken(deviceId: deviceId, role: role); - } - - Future saveDeviceToken({ - required String deviceId, - required String role, - required String token, - }) { - return _secretStore.saveDeviceToken( - deviceId: deviceId, - role: role, - token: token, - ); - } - - Future clearDeviceToken({ - required String deviceId, - required String role, - }) { - return _secretStore.clearDeviceToken(deviceId: deviceId, role: role); - } - - PersistentWriteFailures get persistentWriteFailures => - PersistentWriteFailures( - settings: _settingsStore.settingsWriteFailure, - tasks: _settingsStore.tasksWriteFailure, - secrets: _secretStore.secretsWriteFailure, - audit: _settingsStore.auditWriteFailure, - ); + Map get secureRefs => _secretStore.secureRefs; + String? get settingsWriteFailure => _settingsStore.auditWriteFailure; void dispose() { _settingsStore.dispose(); _secretStore.dispose(); } - static String maskValue(String value) { - return SecretStore.maskValue(value); - } + static String maskValue(String value) => SecretStore.maskValue(value); } diff --git a/lib/runtime/settings_store.dart b/lib/runtime/settings_store.dart index 62c45bdf..19f13d85 100644 --- a/lib/runtime/settings_store.dart +++ b/lib/runtime/settings_store.dart @@ -9,510 +9,87 @@ enum SettingsSnapshotReloadStatus { applied, invalid } class SettingsSnapshotReloadResult { const SettingsSnapshotReloadResult({ + required this.applied, required this.snapshot, - required this.status, }); + final bool applied; final SettingsSnapshot snapshot; - final SettingsSnapshotReloadStatus status; - - bool get applied => status == SettingsSnapshotReloadStatus.applied; -} - -enum SkippedTaskThreadReason { - incompleteWorkspaceBinding, - removedAutoExecutionMode, - invalidPersistedThreadData, -} - -class SkippedTaskThreadRecord { - const SkippedTaskThreadRecord({required this.threadId, required this.reason}); - - final String threadId; - final SkippedTaskThreadReason reason; } class SettingsStore { - SettingsStore({ - Future Function()? appDataRootPathResolver, - Future Function()? supportRootPathResolver, - StoreLayoutResolver? layoutResolver, - }) : _layoutResolver = - layoutResolver ?? - StoreLayoutResolver( - appDataRootPathResolver: appDataRootPathResolver, - supportRootPathResolver: supportRootPathResolver, - ); + SettingsStore(this._layoutResolver); final StoreLayoutResolver _layoutResolver; - bool _initialized = false; - StoreLayout? _layout; - File? _settingsFile; - Directory? _settingsWatchDirectory; - SettingsSnapshot _settingsSnapshot = SettingsSnapshot.defaults(); - List _threadRecords = const []; - List _auditTrail = const []; - PersistentWriteFailure? _settingsWriteFailure; - PersistentWriteFailure? _tasksWriteFailure; - PersistentWriteFailure? _auditWriteFailure; - List _lastSkippedInvalidTaskThreadRecords = - const []; - - PersistentWriteFailure? get settingsWriteFailure => _settingsWriteFailure; - PersistentWriteFailure? get tasksWriteFailure => _tasksWriteFailure; - PersistentWriteFailure? get auditWriteFailure => _auditWriteFailure; - List get lastSkippedInvalidTaskThreadRecords => - List.unmodifiable( - _lastSkippedInvalidTaskThreadRecords, - ); - List get lastSkippedInvalidTaskThreadIds => List.unmodifiable( - _lastSkippedInvalidTaskThreadRecords - .map((item) => item.threadId) - .toList(growable: false), - ); + String? _auditWriteFailure; + String? get auditWriteFailure => _auditWriteFailure; Future initialize() async { - if (_initialized) { - return; - } - _initialized = true; + // Basic connectivity check. try { - _layout = await _layoutResolver.resolve(); - _settingsFile = _layout!.settingsFile; - _settingsWatchDirectory = _settingsFile!.parent; - } catch (_) { - _layout = null; - _settingsFile = null; - _settingsWatchDirectory = null; - return; + await _layoutResolver.resolve(); + } catch (e) { + _auditWriteFailure = 'Storage unavailable: $e'; } - _settingsSnapshot = await _readSettingsSnapshot(); - _threadRecords = await _readTaskThreads(); - _auditTrail = await _readAuditTrail(); } - Future loadSettingsSnapshot() async { - await initialize(); - return _settingsSnapshot; - } - - Future reloadSettingsSnapshot() async { - final result = await reloadSettingsSnapshotResult(); - return result.snapshot; - } - - Future reloadSettingsSnapshotResult() async { - await initialize(); - final result = await _readSettingsSnapshotResult(); - if (result.status == SettingsSnapshotReloadStatus.invalid) { - return SettingsSnapshotReloadResult( - snapshot: _settingsSnapshot, - status: SettingsSnapshotReloadStatus.invalid, - ); - } - _settingsSnapshot = result.snapshot; - return SettingsSnapshotReloadResult( - snapshot: _settingsSnapshot, - status: SettingsSnapshotReloadStatus.applied, - ); - } - - Future resolvedSettingsFile() async { - await initialize(); - return _settingsFile; - } - - Future resolvedSettingsWatchDirectory() async { - await initialize(); - return _settingsWatchDirectory; - } - - Future saveSettingsSnapshot(SettingsSnapshot snapshot) async { - await initialize(); - _settingsSnapshot = snapshot; - final layout = _layout; - if (layout == null) { - _settingsWriteFailure = _buildWriteFailure( - PersistentStoreScope.settings, - 'saveSettingsSnapshot', - StateError('Persistent settings path unavailable; using memory only.'), - ); - return; - } + Future loadSnapshot() async { try { - final contents = encodeYamlDocument(snapshot.toJson()); - await atomicWriteString(layout.settingsFile, contents); - _settingsWriteFailure = null; - } catch (error) { - _settingsWriteFailure = _buildWriteFailure( - PersistentStoreScope.settings, - 'saveSettingsSnapshot', - error, - ); - } - } - - Future> loadTaskThreads() async { - await initialize(); - return List.from(_threadRecords); - } - - Future saveTaskThreads(List records) async { - await initialize(); - final normalized = records - .where((item) => item.threadId.trim().isNotEmpty) - .toList(growable: false); - _threadRecords = normalized; - final layout = _layout; - if (layout == null) { - _tasksWriteFailure = _buildWriteFailure( - PersistentStoreScope.tasks, - 'saveTaskThreads', - StateError('Persistent task path unavailable; using memory only.'), - ); - return; - } - final keptPaths = {}; - try { - for (final record in normalized) { - final taskFile = layout.taskFileForSessionKey(record.threadId); - keptPaths.add(taskFile.path); - await atomicWriteString(taskFile, jsonEncode(record.toJson())); + final layout = await _layoutResolver.resolve(); + final file = File('${layout.configDirectory.path}/settings.yaml'); + if (await file.exists()) { + final content = await file.readAsString(); + return SettingsSnapshot.fromJsonString(content); } - await atomicWriteString( - layout.taskIndexFile, - jsonEncode({ - 'version': taskThreadSchemaVersion, - 'sessions': normalized - .map((item) => item.threadId) - .toList(growable: false), - }), - ); - await for (final entity in layout.tasksDirectory.list()) { - if (entity is! File) { - continue; - } - if (entity.path == layout.taskIndexFile.path) { - continue; - } - if (!entity.path.endsWith('.json')) { - continue; - } - if (!keptPaths.contains(entity.path)) { - await entity.delete(); - } - } - _tasksWriteFailure = null; - } catch (error) { - _tasksWriteFailure = _buildWriteFailure( - PersistentStoreScope.tasks, - 'saveTaskThreads', - error, - ); + } catch (e) { + _auditWriteFailure = 'Failed to load settings: $e'; } + return SettingsSnapshot.defaults(); } - Future clearAssistantLocalState() async { - await initialize(); - _threadRecords = const []; - final layout = _layout; - if (layout == null) { - _tasksWriteFailure = _buildWriteFailure( - PersistentStoreScope.tasks, - 'clearAssistantLocalState', - StateError('Persistent task path unavailable; reset kept in memory.'), - ); - return; - } + Future saveSnapshot(SettingsSnapshot snapshot) async { try { - await deleteIfExists(layout.taskIndexFile); - await for (final entity in layout.tasksDirectory.list()) { - if (entity is File && entity.path.endsWith('.json')) { - await entity.delete(); - } - } - _tasksWriteFailure = null; - } catch (error) { - _tasksWriteFailure = _buildWriteFailure( - PersistentStoreScope.tasks, - 'clearAssistantLocalState', - error, - ); - } - } - - Future> loadAuditTrail() async { - await initialize(); - return List.from(_auditTrail); - } - - Future appendAudit(SecretAuditEntry entry) async { - await initialize(); - final next = [entry, ..._auditTrail]; - if (next.length > 40) { - next.removeRange(40, next.length); - } - _auditTrail = next; - final layout = _layout; - if (layout == null) { - _auditWriteFailure = _buildWriteFailure( - PersistentStoreScope.audit, - 'appendAudit', - StateError('Persistent audit path unavailable; audit kept in memory.'), - ); - return; - } - try { - await atomicWriteString( - layout.auditFile, - jsonEncode(next.map((item) => item.toJson()).toList(growable: false)), - ); + final layout = await _layoutResolver.resolve(); + final file = File('${layout.configDirectory.path}/settings.yaml'); + await file.writeAsString(snapshot.toJsonString(), flush: true); _auditWriteFailure = null; - } catch (error) { - _auditWriteFailure = _buildWriteFailure( - PersistentStoreScope.audit, - 'appendAudit', - error, - ); + } catch (e) { + _auditWriteFailure = 'Failed to save settings: $e'; + // In-memory fallback happens at Controller level via current snapshot retention. + } + } + + Future reloadSnapshotResult() async { + final next = await loadSnapshot(); + return SettingsSnapshotReloadResult(applied: true, snapshot: next); + } + + Future> loadTaskThreads() async { + try { + final layout = await _layoutResolver.resolve(); + final file = File('${layout.tasksDirectory.path}/threads.json'); + if (await file.exists()) { + final content = await file.readAsString(); + final decoded = jsonDecode(content); + if (decoded is Map) { + return decoded.map((key, value) => MapEntry(key, TaskThread.fromJson(value))); + } + } + } catch (_) { + // Ignore errors for secondary persistence. + } + return const {}; + } + + Future saveTaskThreads(Map threads) async { + try { + final layout = await _layoutResolver.resolve(); + final file = File('${layout.tasksDirectory.path}/threads.json'); + await file.writeAsString(jsonEncode(threads), flush: true); + } catch (_) { + // Ignore errors for secondary persistence. } } void dispose() {} - - Future _readSettingsSnapshot() async { - final result = await _readSettingsSnapshotResult(); - return result.status == SettingsSnapshotReloadStatus.invalid - ? SettingsSnapshot.defaults() - : result.snapshot; - } - - Future _readSettingsSnapshotResult() async { - final settingsFile = _settingsFile; - if (settingsFile == null) { - return SettingsSnapshotReloadResult( - snapshot: SettingsSnapshot.defaults(), - status: SettingsSnapshotReloadStatus.applied, - ); - } - if (!await settingsFile.exists()) { - return SettingsSnapshotReloadResult( - snapshot: SettingsSnapshot.defaults(), - status: SettingsSnapshotReloadStatus.applied, - ); - } - try { - final raw = await settingsFile.readAsString(); - final decoded = decodeYamlDocument(raw); - if (decoded is Map) { - return SettingsSnapshotReloadResult( - snapshot: SettingsSnapshot.fromJson(decoded), - status: SettingsSnapshotReloadStatus.applied, - ); - } - if (decoded is Map) { - return SettingsSnapshotReloadResult( - snapshot: SettingsSnapshot.fromJson(decoded.cast()), - status: SettingsSnapshotReloadStatus.applied, - ); - } - } catch (_) { - return SettingsSnapshotReloadResult( - snapshot: SettingsSnapshot.defaults(), - status: SettingsSnapshotReloadStatus.invalid, - ); - } - return SettingsSnapshotReloadResult( - snapshot: SettingsSnapshot.defaults(), - status: SettingsSnapshotReloadStatus.invalid, - ); - } - - Future> _readTaskThreads() async { - final layout = _layout; - if (layout == null) { - _lastSkippedInvalidTaskThreadRecords = const []; - return const []; - } - _lastSkippedInvalidTaskThreadRecords = const []; - final index = await _readThreadIndex(layout); - if (index.resetRequired) { - await _resetTaskThreadState(layout); - return const []; - } - final orderedKeys = index.sessions; - final recordsByKey = {}; - final skippedRecords = []; - - String inferThreadIdFromTaskFile(File file) { - final name = file.uri.pathSegments.isEmpty - ? file.path - : file.uri.pathSegments.last; - final encoded = name.endsWith('.json') - ? name.substring(0, name.length - 5) - : name; - try { - return utf8.decode(base64Url.decode(base64Url.normalize(encoded))); - } catch (_) { - return encoded; - } - } - - try { - await for (final entity in layout.tasksDirectory.list()) { - if (entity is! File || - entity.path == layout.taskIndexFile.path || - !entity.path.endsWith('.json')) { - continue; - } - try { - final raw = await entity.readAsString(); - final decoded = jsonDecode(raw); - if (decoded is Map) { - final schemaVersion = decoded['schemaVersion']; - if (schemaVersion is! int || - schemaVersion != taskThreadSchemaVersion) { - await _resetTaskThreadState(layout); - return const []; - } - final record = TaskThread.fromJson(decoded); - if (record.threadId.trim().isNotEmpty) { - recordsByKey[record.threadId] = record; - } - } - } catch (error) { - skippedRecords.add( - SkippedTaskThreadRecord( - threadId: inferThreadIdFromTaskFile(entity), - reason: _classifySkippedTaskThreadReason(error), - ), - ); - continue; - } - } - } catch (_) { - return const []; - } - final ordered = []; - for (final sessionKey in orderedKeys) { - final record = recordsByKey.remove(sessionKey); - if (record != null) { - ordered.add(record); - } - } - final leftovers = recordsByKey.keys.toList()..sort(); - for (final sessionKey in leftovers) { - final record = recordsByKey[sessionKey]; - if (record != null) { - ordered.add(record); - } - } - skippedRecords.sort( - (left, right) => left.threadId.compareTo(right.threadId), - ); - _lastSkippedInvalidTaskThreadRecords = skippedRecords; - return ordered; - } - - SkippedTaskThreadReason _classifySkippedTaskThreadReason(Object error) { - final message = error.toString(); - if (message.contains('"auto" is no longer supported')) { - return SkippedTaskThreadReason.removedAutoExecutionMode; - } - if (message.contains('workspaceBinding')) { - return SkippedTaskThreadReason.incompleteWorkspaceBinding; - } - return SkippedTaskThreadReason.invalidPersistedThreadData; - } - - Future<_ThreadIndexReadResult> _readThreadIndex(StoreLayout layout) async { - if (!await layout.taskIndexFile.exists()) { - return const _ThreadIndexReadResult( - sessions: [], - resetRequired: false, - ); - } - try { - final raw = await layout.taskIndexFile.readAsString(); - final decoded = jsonDecode(raw); - if (decoded is Map) { - final version = decoded['version']; - if (version is! int || version != taskThreadSchemaVersion) { - return const _ThreadIndexReadResult( - sessions: [], - resetRequired: true, - ); - } - final sessions = decoded['sessions']; - if (sessions is List) { - return _ThreadIndexReadResult( - sessions: sessions - .map((item) => item.toString().trim()) - .where((item) => item.isNotEmpty) - .toList(growable: false), - resetRequired: false, - ); - } - } - } catch (_) {} - return const _ThreadIndexReadResult( - sessions: [], - resetRequired: true, - ); - } - - Future _resetTaskThreadState(StoreLayout layout) async { - try { - await deleteIfExists(layout.taskIndexFile); - await for (final entity in layout.tasksDirectory.list()) { - if (entity is File && entity.path.endsWith('.json')) { - await entity.delete(); - } - } - } catch (_) { - // Best effort. A later save will normalize the directory. - } - } - - Future> _readAuditTrail() async { - final layout = _layout; - if (layout == null || !await layout.auditFile.exists()) { - return const []; - } - try { - final raw = await layout.auditFile.readAsString(); - final decoded = jsonDecode(raw); - if (decoded is List) { - return decoded - .whereType() - .map( - (item) => SecretAuditEntry.fromJson(item.cast()), - ) - .toList(growable: false); - } - } catch (_) {} - return const []; - } - - PersistentWriteFailure _buildWriteFailure( - PersistentStoreScope scope, - String operation, - Object error, - ) { - return PersistentWriteFailure( - scope: scope, - operation: operation, - message: error.toString(), - timestampMs: DateTime.now().millisecondsSinceEpoch, - ); - } -} - -class _ThreadIndexReadResult { - const _ThreadIndexReadResult({ - required this.sessions, - required this.resetRequired, - }); - - final List sessions; - final bool resetRequired; }