import 'dart:convert'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; enum RuntimeConnectionMode { unconfigured, local, remote } extension RuntimeConnectionModeCopy on RuntimeConnectionMode { String get label => switch (this) { RuntimeConnectionMode.unconfigured => appText('未配置', 'Unconfigured'), RuntimeConnectionMode.local => appText('本地', 'Local'), RuntimeConnectionMode.remote => appText('远程', 'Remote'), }; static RuntimeConnectionMode fromJsonValue(String? value) { return RuntimeConnectionMode.values.firstWhere( (item) => item.name == value, orElse: () => RuntimeConnectionMode.unconfigured, ); } } enum RuntimeConnectionStatus { offline, connecting, connected, error } extension RuntimeConnectionStatusCopy on RuntimeConnectionStatus { String get label => switch (this) { RuntimeConnectionStatus.offline => appText('离线', 'Offline'), RuntimeConnectionStatus.connecting => appText('连接中', 'Connecting'), RuntimeConnectionStatus.connected => appText('已连接', 'Connected'), RuntimeConnectionStatus.error => appText('错误', 'Error'), }; } enum AssistantExecutionTarget { local, remote } extension AssistantExecutionTargetCopy on AssistantExecutionTarget { String get label => switch (this) { AssistantExecutionTarget.local => appText('本地', 'Local'), AssistantExecutionTarget.remote => appText('远程', 'Remote'), }; String get promptValue => switch (this) { AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', }; static AssistantExecutionTarget fromJsonValue(String? value) { return AssistantExecutionTarget.values.firstWhere( (item) => item.name == value, orElse: () => AssistantExecutionTarget.local, ); } } enum AssistantPermissionLevel { defaultAccess, fullAccess } extension AssistantPermissionLevelCopy on AssistantPermissionLevel { String get label => switch (this) { AssistantPermissionLevel.defaultAccess => appText('默认权限', 'Default Access'), AssistantPermissionLevel.fullAccess => appText('完全访问权限', 'Full Access'), }; String get promptValue => switch (this) { AssistantPermissionLevel.defaultAccess => 'default', AssistantPermissionLevel.fullAccess => 'full-access', }; static AssistantPermissionLevel fromJsonValue(String? value) { return AssistantPermissionLevel.values.firstWhere( (item) => item.name == value, orElse: () => AssistantPermissionLevel.defaultAccess, ); } } enum CodeAgentRuntimeMode { builtIn, externalCli } extension CodeAgentRuntimeModeCopy on CodeAgentRuntimeMode { String get label => switch (this) { CodeAgentRuntimeMode.externalCli => appText( '外部 Codex CLI', 'External Codex CLI', ), CodeAgentRuntimeMode.builtIn => appText('内置 Codex', 'Built-in Codex'), }; static CodeAgentRuntimeMode fromJsonValue(String? value) { return CodeAgentRuntimeMode.values.firstWhere( (item) => item.name == value, orElse: () => CodeAgentRuntimeMode.externalCli, ); } } enum VpnMode { tunnel, proxy } extension VpnModeCopy on VpnMode { String get label => switch (this) { VpnMode.tunnel => appText('隧道', 'Tunnel'), VpnMode.proxy => appText('代理', 'Proxy'), }; static VpnMode fromJsonValue(String? value) { return VpnMode.values.firstWhere( (item) => item.name == value, orElse: () => VpnMode.proxy, ); } } enum DesktopEnvironment { unknown, gnome, kde } extension DesktopEnvironmentCopy on DesktopEnvironment { String get label => switch (this) { DesktopEnvironment.unknown => appText('未知桌面', 'Unknown Desktop'), DesktopEnvironment.gnome => 'GNOME', DesktopEnvironment.kde => 'KDE Plasma', }; static DesktopEnvironment fromJsonValue(String? value) { return DesktopEnvironment.values.firstWhere( (item) => item.name == value, orElse: () => DesktopEnvironment.unknown, ); } } class LinuxDesktopConfig { const LinuxDesktopConfig({ required this.preferredMode, required this.vpnConnectionName, required this.proxyHost, required this.proxyPort, required this.trayEnabled, }); final VpnMode preferredMode; final String vpnConnectionName; final String proxyHost; final int proxyPort; final bool trayEnabled; factory LinuxDesktopConfig.defaults() { return const LinuxDesktopConfig( preferredMode: VpnMode.proxy, vpnConnectionName: 'XWorkmate Tunnel', proxyHost: '127.0.0.1', proxyPort: 7890, trayEnabled: true, ); } LinuxDesktopConfig copyWith({ VpnMode? preferredMode, String? vpnConnectionName, String? proxyHost, int? proxyPort, bool? trayEnabled, }) { return LinuxDesktopConfig( preferredMode: preferredMode ?? this.preferredMode, vpnConnectionName: vpnConnectionName ?? this.vpnConnectionName, proxyHost: proxyHost ?? this.proxyHost, proxyPort: proxyPort ?? this.proxyPort, trayEnabled: trayEnabled ?? this.trayEnabled, ); } Map toJson() { return { 'preferredMode': preferredMode.name, 'vpnConnectionName': vpnConnectionName, 'proxyHost': proxyHost, 'proxyPort': proxyPort, 'trayEnabled': trayEnabled, }; } factory LinuxDesktopConfig.fromJson(Map json) { final defaults = LinuxDesktopConfig.defaults(); return LinuxDesktopConfig( preferredMode: VpnModeCopy.fromJsonValue( json['preferredMode'] as String?, ), vpnConnectionName: json['vpnConnectionName'] as String? ?? defaults.vpnConnectionName, proxyHost: json['proxyHost'] as String? ?? defaults.proxyHost, proxyPort: json['proxyPort'] as int? ?? defaults.proxyPort, trayEnabled: json['trayEnabled'] as bool? ?? defaults.trayEnabled, ); } } class SystemProxyState { const SystemProxyState({ required this.enabled, required this.host, required this.port, required this.backend, required this.lastAppliedMode, }); final bool enabled; final String host; final int port; final String backend; final VpnMode lastAppliedMode; factory SystemProxyState.defaults({LinuxDesktopConfig? config}) { final resolvedConfig = config ?? LinuxDesktopConfig.defaults(); return SystemProxyState( enabled: resolvedConfig.preferredMode == VpnMode.proxy, host: resolvedConfig.proxyHost, port: resolvedConfig.proxyPort, backend: '', lastAppliedMode: resolvedConfig.preferredMode, ); } SystemProxyState copyWith({ bool? enabled, String? host, int? port, String? backend, VpnMode? lastAppliedMode, }) { return SystemProxyState( enabled: enabled ?? this.enabled, host: host ?? this.host, port: port ?? this.port, backend: backend ?? this.backend, lastAppliedMode: lastAppliedMode ?? this.lastAppliedMode, ); } Map toJson() { return { 'enabled': enabled, 'host': host, 'port': port, 'backend': backend, 'lastAppliedMode': lastAppliedMode.name, }; } factory SystemProxyState.fromJson( Map json, { LinuxDesktopConfig? config, }) { final defaults = SystemProxyState.defaults(config: config); return SystemProxyState( enabled: json['enabled'] as bool? ?? defaults.enabled, host: json['host'] as String? ?? defaults.host, port: json['port'] as int? ?? defaults.port, backend: json['backend'] as String? ?? defaults.backend, lastAppliedMode: VpnModeCopy.fromJsonValue( json['lastAppliedMode'] as String?, ), ); } } class TunnelSessionState { const TunnelSessionState({ required this.available, required this.connected, required this.connectionName, required this.backend, required this.lastError, }); final bool available; final bool connected; final String connectionName; final String backend; final String lastError; factory TunnelSessionState.defaults({LinuxDesktopConfig? config}) { final resolvedConfig = config ?? LinuxDesktopConfig.defaults(); return TunnelSessionState( available: false, connected: false, connectionName: resolvedConfig.vpnConnectionName, backend: '', lastError: '', ); } TunnelSessionState copyWith({ bool? available, bool? connected, String? connectionName, String? backend, String? lastError, }) { return TunnelSessionState( available: available ?? this.available, connected: connected ?? this.connected, connectionName: connectionName ?? this.connectionName, backend: backend ?? this.backend, lastError: lastError ?? this.lastError, ); } Map toJson() { return { 'available': available, 'connected': connected, 'connectionName': connectionName, 'backend': backend, 'lastError': lastError, }; } factory TunnelSessionState.fromJson( Map json, { LinuxDesktopConfig? config, }) { final defaults = TunnelSessionState.defaults(config: config); return TunnelSessionState( available: json['available'] as bool? ?? defaults.available, connected: json['connected'] as bool? ?? defaults.connected, connectionName: json['connectionName'] as String? ?? defaults.connectionName, backend: json['backend'] as String? ?? defaults.backend, lastError: json['lastError'] as String? ?? defaults.lastError, ); } } class DesktopIntegrationState { const DesktopIntegrationState({ required this.isSupported, required this.environment, required this.mode, required this.trayAvailable, required this.trayEnabled, required this.autostartEnabled, required this.networkManagerAvailable, required this.systemProxy, required this.tunnel, required this.statusMessage, }); final bool isSupported; final DesktopEnvironment environment; final VpnMode mode; final bool trayAvailable; final bool trayEnabled; final bool autostartEnabled; final bool networkManagerAvailable; final SystemProxyState systemProxy; final TunnelSessionState tunnel; final String statusMessage; factory DesktopIntegrationState.loading() { final config = LinuxDesktopConfig.defaults(); return DesktopIntegrationState( isSupported: true, environment: DesktopEnvironment.unknown, mode: config.preferredMode, trayAvailable: false, trayEnabled: config.trayEnabled, autostartEnabled: false, networkManagerAvailable: false, systemProxy: SystemProxyState.defaults(config: config), tunnel: TunnelSessionState.defaults(config: config), statusMessage: '', ); } factory DesktopIntegrationState.unsupported({ LinuxDesktopConfig? config, String message = '', }) { final resolvedConfig = config ?? LinuxDesktopConfig.defaults(); return DesktopIntegrationState( isSupported: false, environment: DesktopEnvironment.unknown, mode: resolvedConfig.preferredMode, trayAvailable: false, trayEnabled: false, autostartEnabled: false, networkManagerAvailable: false, systemProxy: SystemProxyState.defaults(config: resolvedConfig), tunnel: TunnelSessionState.defaults(config: resolvedConfig), statusMessage: message, ); } DesktopIntegrationState copyWith({ bool? isSupported, DesktopEnvironment? environment, VpnMode? mode, bool? trayAvailable, bool? trayEnabled, bool? autostartEnabled, bool? networkManagerAvailable, SystemProxyState? systemProxy, TunnelSessionState? tunnel, String? statusMessage, }) { return DesktopIntegrationState( isSupported: isSupported ?? this.isSupported, environment: environment ?? this.environment, mode: mode ?? this.mode, trayAvailable: trayAvailable ?? this.trayAvailable, trayEnabled: trayEnabled ?? this.trayEnabled, autostartEnabled: autostartEnabled ?? this.autostartEnabled, networkManagerAvailable: networkManagerAvailable ?? this.networkManagerAvailable, systemProxy: systemProxy ?? this.systemProxy, tunnel: tunnel ?? this.tunnel, statusMessage: statusMessage ?? this.statusMessage, ); } Map toJson() { return { 'isSupported': isSupported, 'environment': environment.name, 'mode': mode.name, 'trayAvailable': trayAvailable, 'trayEnabled': trayEnabled, 'autostartEnabled': autostartEnabled, 'networkManagerAvailable': networkManagerAvailable, 'systemProxy': systemProxy.toJson(), 'tunnel': tunnel.toJson(), 'statusMessage': statusMessage, }; } factory DesktopIntegrationState.fromJson( Map json, { LinuxDesktopConfig? fallbackConfig, }) { final config = fallbackConfig ?? LinuxDesktopConfig.defaults(); return DesktopIntegrationState( isSupported: json['isSupported'] as bool? ?? true, environment: DesktopEnvironmentCopy.fromJsonValue( json['environment'] as String?, ), mode: VpnModeCopy.fromJsonValue(json['mode'] as String?), trayAvailable: json['trayAvailable'] as bool? ?? false, trayEnabled: json['trayEnabled'] as bool? ?? config.trayEnabled, autostartEnabled: json['autostartEnabled'] as bool? ?? false, networkManagerAvailable: json['networkManagerAvailable'] as bool? ?? false, systemProxy: SystemProxyState.fromJson( (json['systemProxy'] as Map?)?.cast() ?? const {}, config: config, ), tunnel: TunnelSessionState.fromJson( (json['tunnel'] as Map?)?.cast() ?? const {}, config: config, ), statusMessage: json['statusMessage'] as String? ?? '', ); } } class GatewayConnectionProfile { const GatewayConnectionProfile({ required this.mode, required this.useSetupCode, required this.setupCode, required this.host, required this.port, required this.tls, required this.selectedAgentId, }); final RuntimeConnectionMode mode; final bool useSetupCode; final String setupCode; final String host; final int port; final bool tls; final String selectedAgentId; factory GatewayConnectionProfile.defaults() { return const GatewayConnectionProfile( mode: RuntimeConnectionMode.remote, useSetupCode: false, setupCode: '', host: 'openclaw.svc.plus', port: 443, tls: true, selectedAgentId: '', ); } GatewayConnectionProfile copyWith({ RuntimeConnectionMode? mode, bool? useSetupCode, String? setupCode, String? host, int? port, bool? tls, String? selectedAgentId, }) { final normalized = _normalizeGatewayManualEndpoint( host: host ?? this.host, port: port ?? this.port, tls: tls ?? this.tls, ); return GatewayConnectionProfile( mode: mode ?? this.mode, useSetupCode: useSetupCode ?? this.useSetupCode, setupCode: setupCode ?? this.setupCode, host: normalized.host, port: normalized.port, tls: normalized.tls, selectedAgentId: selectedAgentId ?? this.selectedAgentId, ); } Map toJson() { return { 'mode': mode.name, 'useSetupCode': useSetupCode, 'setupCode': setupCode, 'host': host, 'port': port, 'tls': tls, 'selectedAgentId': selectedAgentId, }; } factory GatewayConnectionProfile.fromJson(Map json) { final defaults = GatewayConnectionProfile.defaults(); final normalized = _normalizeGatewayManualEndpoint( host: json['host'] as String? ?? defaults.host, port: json['port'] as int? ?? defaults.port, tls: json['tls'] as bool? ?? defaults.tls, ); return GatewayConnectionProfile( mode: RuntimeConnectionModeCopy.fromJsonValue(json['mode'] as String?), useSetupCode: json['useSetupCode'] as bool? ?? false, setupCode: json['setupCode'] as String? ?? '', host: normalized.host, port: normalized.port, tls: normalized.tls, selectedAgentId: json['selectedAgentId'] as String? ?? '', ); } } ({String host, int port, bool tls}) _normalizeGatewayManualEndpoint({ required String host, required int port, required bool tls, }) { final trimmedHost = host.trim(); if (trimmedHost.isEmpty) { return (host: trimmedHost, port: port, tls: tls); } final normalizedInput = trimmedHost.contains('://') ? trimmedHost : '${tls ? 'https' : 'http'}://$trimmedHost:${port > 0 ? port : (tls ? 443 : 18789)}'; final uri = Uri.tryParse(normalizedInput); final normalizedHost = uri?.host.trim() ?? trimmedHost; if (normalizedHost.isEmpty) { return (host: trimmedHost, port: port, tls: tls); } final scheme = uri?.scheme.trim().toLowerCase() ?? (tls ? 'https' : 'http'); final normalizedTls = switch (scheme) { 'ws' || 'http' => false, _ => true, }; final normalizedPort = uri?.hasPort == true ? uri!.port : normalizedTls ? 443 : 18789; return ( host: normalizedHost, port: normalizedPort > 0 ? normalizedPort : port, tls: normalizedTls, ); } class OllamaLocalConfig { const OllamaLocalConfig({ required this.endpoint, required this.defaultModel, required this.autoDiscover, }); final String endpoint; final String defaultModel; final bool autoDiscover; factory OllamaLocalConfig.defaults() { return const OllamaLocalConfig( endpoint: 'http://127.0.0.1:11434', defaultModel: 'qwen2.5-coder:latest', autoDiscover: true, ); } OllamaLocalConfig copyWith({ String? endpoint, String? defaultModel, bool? autoDiscover, }) { return OllamaLocalConfig( endpoint: endpoint ?? this.endpoint, defaultModel: defaultModel ?? this.defaultModel, autoDiscover: autoDiscover ?? this.autoDiscover, ); } Map toJson() { return { 'endpoint': endpoint, 'defaultModel': defaultModel, 'autoDiscover': autoDiscover, }; } factory OllamaLocalConfig.fromJson(Map json) { return OllamaLocalConfig( endpoint: json['endpoint'] as String? ?? OllamaLocalConfig.defaults().endpoint, defaultModel: json['defaultModel'] as String? ?? OllamaLocalConfig.defaults().defaultModel, autoDiscover: json['autoDiscover'] as bool? ?? true, ); } } class OllamaCloudConfig { const OllamaCloudConfig({ required this.baseUrl, required this.organization, required this.workspace, required this.defaultModel, required this.apiKeyRef, }); final String baseUrl; final String organization; final String workspace; final String defaultModel; final String apiKeyRef; factory OllamaCloudConfig.defaults() { return const OllamaCloudConfig( baseUrl: 'https://ollama.svc.plus', organization: '', workspace: '', defaultModel: 'gpt-oss:120b', apiKeyRef: 'ollama_cloud_api_key', ); } OllamaCloudConfig copyWith({ String? baseUrl, String? organization, String? workspace, String? defaultModel, String? apiKeyRef, }) { return OllamaCloudConfig( baseUrl: baseUrl ?? this.baseUrl, organization: organization ?? this.organization, workspace: workspace ?? this.workspace, defaultModel: defaultModel ?? this.defaultModel, apiKeyRef: apiKeyRef ?? this.apiKeyRef, ); } Map toJson() { return { 'baseUrl': baseUrl, 'organization': organization, 'workspace': workspace, 'defaultModel': defaultModel, 'apiKeyRef': apiKeyRef, }; } factory OllamaCloudConfig.fromJson(Map json) { return OllamaCloudConfig( baseUrl: json['baseUrl'] as String? ?? OllamaCloudConfig.defaults().baseUrl, organization: json['organization'] as String? ?? '', workspace: json['workspace'] as String? ?? '', defaultModel: json['defaultModel'] as String? ?? OllamaCloudConfig.defaults().defaultModel, apiKeyRef: json['apiKeyRef'] as String? ?? OllamaCloudConfig.defaults().apiKeyRef, ); } } class VaultConfig { const VaultConfig({ required this.address, required this.namespace, required this.authMode, required this.tokenRef, }); final String address; final String namespace; final String authMode; final String tokenRef; factory VaultConfig.defaults() { return const VaultConfig( address: 'http://127.0.0.1:8200', namespace: 'default', authMode: 'token', tokenRef: 'vault_token', ); } VaultConfig copyWith({ String? address, String? namespace, String? authMode, String? tokenRef, }) { return VaultConfig( address: address ?? this.address, namespace: namespace ?? this.namespace, authMode: authMode ?? this.authMode, tokenRef: tokenRef ?? this.tokenRef, ); } Map toJson() { return { 'address': address, 'namespace': namespace, 'authMode': authMode, 'tokenRef': tokenRef, }; } factory VaultConfig.fromJson(Map json) { return VaultConfig( address: json['address'] as String? ?? VaultConfig.defaults().address, namespace: json['namespace'] as String? ?? VaultConfig.defaults().namespace, authMode: json['authMode'] as String? ?? VaultConfig.defaults().authMode, tokenRef: json['tokenRef'] as String? ?? VaultConfig.defaults().tokenRef, ); } } class AiGatewayProfile { const AiGatewayProfile({ required this.name, required this.baseUrl, required this.apiKeyRef, required this.availableModels, required this.selectedModels, required this.syncState, required this.syncMessage, }); final String name; final String baseUrl; final String apiKeyRef; final List availableModels; final List selectedModels; final String syncState; final String syncMessage; factory AiGatewayProfile.defaults() { return const AiGatewayProfile( name: 'AI Gateway', baseUrl: '', apiKeyRef: 'ai_gateway_api_key', availableModels: [], selectedModels: [], syncState: 'idle', syncMessage: 'Ready to sync models', ); } AiGatewayProfile copyWith({ String? name, String? baseUrl, String? apiKeyRef, List? availableModels, List? selectedModels, String? syncState, String? syncMessage, }) { return AiGatewayProfile( name: name ?? this.name, baseUrl: baseUrl ?? this.baseUrl, apiKeyRef: apiKeyRef ?? this.apiKeyRef, availableModels: availableModels ?? this.availableModels, selectedModels: selectedModels ?? this.selectedModels, syncState: syncState ?? this.syncState, syncMessage: syncMessage ?? this.syncMessage, ); } Map toJson() { return { 'name': name, 'baseUrl': baseUrl, 'apiKeyRef': apiKeyRef, 'availableModels': availableModels, 'selectedModels': selectedModels, 'syncState': syncState, 'syncMessage': syncMessage, }; } factory AiGatewayProfile.fromJson(Map json) { List normalizeList(Object? value) { if (value is! List) { return const []; } return value .map((item) => item.toString().trim()) .where((item) => item.isNotEmpty) .toList(growable: false); } final defaults = AiGatewayProfile.defaults(); final availableModels = normalizeList(json['availableModels']); final selectedModels = normalizeList(json['selectedModels']) .where( (item) => availableModels.isEmpty || availableModels.contains(item), ) .toList(growable: false); final legacyFilePath = json['filePath'] as String?; final legacyBaseUrl = legacyFilePath != null && legacyFilePath.trim().startsWith('http') ? legacyFilePath.trim() : null; return AiGatewayProfile( name: json['name'] as String? ?? defaults.name, baseUrl: json['baseUrl'] as String? ?? legacyBaseUrl ?? defaults.baseUrl, apiKeyRef: json['apiKeyRef'] as String? ?? defaults.apiKeyRef, availableModels: availableModels, selectedModels: selectedModels, syncState: json['syncState'] as String? ?? defaults.syncState, syncMessage: json['syncMessage'] as String? ?? defaults.syncMessage, ); } } class AiGatewayConnectionCheck { const AiGatewayConnectionCheck({ required this.state, required this.message, required this.endpoint, required this.modelCount, }); final String state; final String message; final String endpoint; final int modelCount; bool get success => state == 'ready' || state == 'empty'; } class SettingsSnapshot { const SettingsSnapshot({ required this.appLanguage, required this.appActive, required this.launchAtLogin, required this.showDockIcon, required this.workspacePath, required this.remoteProjectRoot, required this.cliPath, required this.codeAgentRuntimeMode, required this.codexCliPath, required this.defaultModel, required this.defaultProvider, required this.gateway, required this.ollamaLocal, required this.ollamaCloud, required this.vault, required this.aiGateway, required this.experimentalCanvas, required this.experimentalBridge, required this.experimentalDebug, required this.accountBaseUrl, required this.accountUsername, required this.accountWorkspace, required this.accountLocalMode, required this.linuxDesktop, required this.assistantExecutionTarget, required this.assistantPermissionLevel, required this.assistantNavigationDestinations, }); final AppLanguage appLanguage; final bool appActive; final bool launchAtLogin; final bool showDockIcon; final String workspacePath; final String remoteProjectRoot; final String cliPath; final CodeAgentRuntimeMode codeAgentRuntimeMode; final String codexCliPath; final String defaultModel; final String defaultProvider; final GatewayConnectionProfile gateway; final OllamaLocalConfig ollamaLocal; final OllamaCloudConfig ollamaCloud; final VaultConfig vault; final AiGatewayProfile aiGateway; final bool experimentalCanvas; final bool experimentalBridge; final bool experimentalDebug; final String accountBaseUrl; final String accountUsername; final String accountWorkspace; final bool accountLocalMode; final LinuxDesktopConfig linuxDesktop; final AssistantExecutionTarget assistantExecutionTarget; final AssistantPermissionLevel assistantPermissionLevel; final List assistantNavigationDestinations; factory SettingsSnapshot.defaults() { return SettingsSnapshot( appLanguage: AppLanguage.zh, appActive: true, launchAtLogin: false, showDockIcon: true, workspacePath: '/opt/data', remoteProjectRoot: '/opt/data/workspace', cliPath: 'openclaw', codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli, codexCliPath: '', defaultModel: '', defaultProvider: 'gateway', gateway: GatewayConnectionProfile.defaults(), ollamaLocal: OllamaLocalConfig.defaults(), ollamaCloud: OllamaCloudConfig.defaults(), vault: VaultConfig.defaults(), aiGateway: AiGatewayProfile.defaults(), experimentalCanvas: false, experimentalBridge: false, experimentalDebug: false, accountBaseUrl: 'https://accounts.svc.plus', accountUsername: '', accountWorkspace: 'Default Workspace', accountLocalMode: true, linuxDesktop: LinuxDesktopConfig.defaults(), assistantExecutionTarget: AssistantExecutionTarget.local, assistantPermissionLevel: AssistantPermissionLevel.defaultAccess, assistantNavigationDestinations: kAssistantNavigationDestinationDefaults, ); } SettingsSnapshot copyWith({ AppLanguage? appLanguage, bool? appActive, bool? launchAtLogin, bool? showDockIcon, String? workspacePath, String? remoteProjectRoot, String? cliPath, CodeAgentRuntimeMode? codeAgentRuntimeMode, String? codexCliPath, String? defaultModel, String? defaultProvider, GatewayConnectionProfile? gateway, OllamaLocalConfig? ollamaLocal, OllamaCloudConfig? ollamaCloud, VaultConfig? vault, AiGatewayProfile? aiGateway, bool? experimentalCanvas, bool? experimentalBridge, bool? experimentalDebug, String? accountBaseUrl, String? accountUsername, String? accountWorkspace, bool? accountLocalMode, LinuxDesktopConfig? linuxDesktop, AssistantExecutionTarget? assistantExecutionTarget, AssistantPermissionLevel? assistantPermissionLevel, List? assistantNavigationDestinations, }) { return SettingsSnapshot( appLanguage: appLanguage ?? this.appLanguage, appActive: appActive ?? this.appActive, launchAtLogin: launchAtLogin ?? this.launchAtLogin, showDockIcon: showDockIcon ?? this.showDockIcon, workspacePath: workspacePath ?? this.workspacePath, remoteProjectRoot: remoteProjectRoot ?? this.remoteProjectRoot, cliPath: cliPath ?? this.cliPath, codeAgentRuntimeMode: codeAgentRuntimeMode ?? this.codeAgentRuntimeMode, codexCliPath: codexCliPath ?? this.codexCliPath, defaultModel: defaultModel ?? this.defaultModel, defaultProvider: defaultProvider ?? this.defaultProvider, gateway: gateway ?? this.gateway, ollamaLocal: ollamaLocal ?? this.ollamaLocal, ollamaCloud: ollamaCloud ?? this.ollamaCloud, vault: vault ?? this.vault, aiGateway: aiGateway ?? this.aiGateway, experimentalCanvas: experimentalCanvas ?? this.experimentalCanvas, experimentalBridge: experimentalBridge ?? this.experimentalBridge, experimentalDebug: experimentalDebug ?? this.experimentalDebug, accountBaseUrl: accountBaseUrl ?? this.accountBaseUrl, accountUsername: accountUsername ?? this.accountUsername, accountWorkspace: accountWorkspace ?? this.accountWorkspace, accountLocalMode: accountLocalMode ?? this.accountLocalMode, linuxDesktop: linuxDesktop ?? this.linuxDesktop, assistantExecutionTarget: assistantExecutionTarget ?? this.assistantExecutionTarget, assistantPermissionLevel: assistantPermissionLevel ?? this.assistantPermissionLevel, assistantNavigationDestinations: assistantNavigationDestinations ?? this.assistantNavigationDestinations, ); } Map toJson() { return { 'appLanguage': appLanguage.name, 'appActive': appActive, 'launchAtLogin': launchAtLogin, 'showDockIcon': showDockIcon, 'workspacePath': workspacePath, 'remoteProjectRoot': remoteProjectRoot, 'cliPath': cliPath, 'codeAgentRuntimeMode': codeAgentRuntimeMode.name, 'codexCliPath': codexCliPath, 'defaultModel': defaultModel, 'defaultProvider': defaultProvider, 'gateway': gateway.toJson(), 'ollamaLocal': ollamaLocal.toJson(), 'ollamaCloud': ollamaCloud.toJson(), 'vault': vault.toJson(), 'aiGateway': aiGateway.toJson(), 'experimentalCanvas': experimentalCanvas, 'experimentalBridge': experimentalBridge, 'experimentalDebug': experimentalDebug, 'accountBaseUrl': accountBaseUrl, 'accountUsername': accountUsername, 'accountWorkspace': accountWorkspace, 'accountLocalMode': accountLocalMode, 'linuxDesktop': linuxDesktop.toJson(), 'assistantExecutionTarget': assistantExecutionTarget.name, 'assistantPermissionLevel': assistantPermissionLevel.name, 'assistantNavigationDestinations': assistantNavigationDestinations .map((item) => item.name) .toList(growable: false), }; } factory SettingsSnapshot.fromJson(Map json) { final rawAssistantNavigationDestinations = json['assistantNavigationDestinations']; final assistantNavigationDestinations = rawAssistantNavigationDestinations is List ? normalizeAssistantNavigationDestinations( rawAssistantNavigationDestinations .map( (item) => WorkspaceDestinationCopy.fromJsonValue(item?.toString()), ) .whereType(), ) : kAssistantNavigationDestinationDefaults; return SettingsSnapshot( appLanguage: AppLanguageCopy.fromJsonValue( json['appLanguage'] as String?, ), appActive: json['appActive'] as bool? ?? true, launchAtLogin: json['launchAtLogin'] as bool? ?? false, showDockIcon: json['showDockIcon'] as bool? ?? true, workspacePath: json['workspacePath'] as String? ?? SettingsSnapshot.defaults().workspacePath, remoteProjectRoot: json['remoteProjectRoot'] as String? ?? SettingsSnapshot.defaults().remoteProjectRoot, cliPath: json['cliPath'] as String? ?? SettingsSnapshot.defaults().cliPath, codeAgentRuntimeMode: CodeAgentRuntimeModeCopy.fromJsonValue( json['codeAgentRuntimeMode'] as String?, ), codexCliPath: json['codexCliPath'] as String? ?? SettingsSnapshot.defaults().codexCliPath, defaultModel: json['defaultModel'] as String? ?? SettingsSnapshot.defaults().defaultModel, defaultProvider: json['defaultProvider'] as String? ?? SettingsSnapshot.defaults().defaultProvider, gateway: GatewayConnectionProfile.fromJson( (json['gateway'] as Map?)?.cast() ?? const {}, ), ollamaLocal: OllamaLocalConfig.fromJson( (json['ollamaLocal'] as Map?)?.cast() ?? const {}, ), ollamaCloud: OllamaCloudConfig.fromJson( (json['ollamaCloud'] as Map?)?.cast() ?? const {}, ), vault: VaultConfig.fromJson( (json['vault'] as Map?)?.cast() ?? const {}, ), aiGateway: AiGatewayProfile.fromJson( (json['aiGateway'] as Map?)?.cast() ?? (json['apisix'] as Map?)?.cast() ?? const {}, ), experimentalCanvas: json['experimentalCanvas'] as bool? ?? false, experimentalBridge: json['experimentalBridge'] as bool? ?? false, experimentalDebug: json['experimentalDebug'] as bool? ?? false, accountBaseUrl: json['accountBaseUrl'] as String? ?? SettingsSnapshot.defaults().accountBaseUrl, accountUsername: json['accountUsername'] as String? ?? '', accountWorkspace: json['accountWorkspace'] as String? ?? SettingsSnapshot.defaults().accountWorkspace, accountLocalMode: json['accountLocalMode'] as bool? ?? true, linuxDesktop: LinuxDesktopConfig.fromJson( (json['linuxDesktop'] as Map?)?.cast() ?? const {}, ), assistantExecutionTarget: AssistantExecutionTargetCopy.fromJsonValue( json['assistantExecutionTarget'] as String?, ), assistantPermissionLevel: AssistantPermissionLevelCopy.fromJsonValue( json['assistantPermissionLevel'] as String?, ), assistantNavigationDestinations: assistantNavigationDestinations, ); } static SettingsSnapshot fromJsonString(String? raw) { if (raw == null || raw.trim().isEmpty) { return SettingsSnapshot.defaults(); } try { final decoded = jsonDecode(raw) as Map; return SettingsSnapshot.fromJson(decoded); } catch (_) { return SettingsSnapshot.defaults(); } } String toJsonString() => jsonEncode(toJson()); } class GatewayConnectionSnapshot { const GatewayConnectionSnapshot({ required this.status, required this.mode, required this.statusText, required this.serverName, required this.remoteAddress, required this.mainSessionKey, required this.lastError, required this.lastErrorCode, required this.lastErrorDetailCode, required this.lastConnectedAtMs, required this.deviceId, required this.authRole, required this.authScopes, required this.connectAuthMode, required this.connectAuthFields, required this.connectAuthSources, required this.hasSharedAuth, required this.hasDeviceToken, required this.healthPayload, required this.statusPayload, }); final RuntimeConnectionStatus status; final RuntimeConnectionMode mode; final String statusText; final String? serverName; final String? remoteAddress; final String? mainSessionKey; final String? lastError; final String? lastErrorCode; final String? lastErrorDetailCode; final int? lastConnectedAtMs; final String? deviceId; final String? authRole; final List authScopes; final String? connectAuthMode; final List connectAuthFields; final List connectAuthSources; final bool hasSharedAuth; final bool hasDeviceToken; final Map? healthPayload; final Map? statusPayload; factory GatewayConnectionSnapshot.initial({ RuntimeConnectionMode mode = RuntimeConnectionMode.unconfigured, }) { return GatewayConnectionSnapshot( status: RuntimeConnectionStatus.offline, mode: mode, statusText: 'Offline', serverName: null, remoteAddress: null, mainSessionKey: null, lastError: null, lastErrorCode: null, lastErrorDetailCode: null, lastConnectedAtMs: null, deviceId: null, authRole: null, authScopes: const [], connectAuthMode: null, connectAuthFields: const [], connectAuthSources: const [], hasSharedAuth: false, hasDeviceToken: false, healthPayload: null, statusPayload: null, ); } GatewayConnectionSnapshot copyWith({ RuntimeConnectionStatus? status, RuntimeConnectionMode? mode, String? statusText, String? serverName, String? remoteAddress, String? mainSessionKey, String? lastError, String? lastErrorCode, String? lastErrorDetailCode, int? lastConnectedAtMs, String? deviceId, String? authRole, List? authScopes, String? connectAuthMode, List? connectAuthFields, List? connectAuthSources, bool? hasSharedAuth, bool? hasDeviceToken, Map? healthPayload, Map? statusPayload, bool clearServerName = false, bool clearRemoteAddress = false, bool clearMainSessionKey = false, bool clearLastError = false, bool clearLastErrorCode = false, bool clearLastErrorDetailCode = false, }) { return GatewayConnectionSnapshot( status: status ?? this.status, mode: mode ?? this.mode, statusText: statusText ?? this.statusText, serverName: clearServerName ? null : (serverName ?? this.serverName), remoteAddress: clearRemoteAddress ? null : (remoteAddress ?? this.remoteAddress), mainSessionKey: clearMainSessionKey ? null : (mainSessionKey ?? this.mainSessionKey), lastError: clearLastError ? null : (lastError ?? this.lastError), lastErrorCode: clearLastErrorCode ? null : (lastErrorCode ?? this.lastErrorCode), lastErrorDetailCode: clearLastErrorDetailCode ? null : (lastErrorDetailCode ?? this.lastErrorDetailCode), lastConnectedAtMs: lastConnectedAtMs ?? this.lastConnectedAtMs, deviceId: deviceId ?? this.deviceId, authRole: authRole ?? this.authRole, authScopes: authScopes ?? this.authScopes, connectAuthMode: connectAuthMode ?? this.connectAuthMode, connectAuthFields: connectAuthFields ?? this.connectAuthFields, connectAuthSources: connectAuthSources ?? this.connectAuthSources, hasSharedAuth: hasSharedAuth ?? this.hasSharedAuth, hasDeviceToken: hasDeviceToken ?? this.hasDeviceToken, healthPayload: healthPayload ?? this.healthPayload, statusPayload: statusPayload ?? this.statusPayload, ); } bool get pairingRequired { final detailCode = lastErrorDetailCode?.trim().toUpperCase(); final errorCode = lastErrorCode?.trim().toUpperCase(); final errorText = lastError?.toLowerCase() ?? ''; return status != RuntimeConnectionStatus.connected && (detailCode == 'PAIRING_REQUIRED' || errorCode == 'NOT_PAIRED' || errorText.contains('pairing required')); } bool get gatewayTokenMissing { final detailCode = lastErrorDetailCode?.trim().toUpperCase(); final errorText = lastError?.toLowerCase() ?? ''; return detailCode == 'AUTH_TOKEN_MISSING' || errorText.contains('gateway token missing'); } String get connectAuthSummary { final mode = connectAuthMode?.trim() ?? 'none'; final fields = connectAuthFields.isEmpty ? 'none' : connectAuthFields.join(', '); final sources = connectAuthSources.isEmpty ? 'none' : connectAuthSources.join(' · '); return '$mode | fields: $fields | sources: $sources'; } } class RuntimePackageInfo { const RuntimePackageInfo({ required this.appName, required this.packageName, required this.version, required this.buildNumber, }); final String appName; final String packageName; final String version; final String buildNumber; } class RuntimeDeviceInfo { const RuntimeDeviceInfo({ required this.platform, required this.platformVersion, required this.deviceFamily, required this.modelIdentifier, }); final String platform; final String platformVersion; final String deviceFamily; final String modelIdentifier; String get platformLabel { final version = platformVersion.trim(); if (version.isEmpty) { return platform; } return '$platform $version'; } } class RuntimeLogEntry { const RuntimeLogEntry({ required this.timestampMs, required this.level, required this.category, required this.message, }); final int timestampMs; final String level; final String category; final String message; String get timeLabel { final date = DateTime.fromMillisecondsSinceEpoch(timestampMs); return '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}:${date.second.toString().padLeft(2, '0')}'; } String get line => '[$timeLabel] ${level.toUpperCase()} $category $message'; } class GatewayAgentSummary { const GatewayAgentSummary({ required this.id, required this.name, required this.emoji, required this.theme, }); final String id; final String name; final String emoji; final String theme; } class GatewaySessionSummary { const GatewaySessionSummary({ required this.key, required this.kind, required this.displayName, required this.surface, required this.subject, required this.room, required this.space, required this.updatedAtMs, required this.sessionId, required this.systemSent, required this.abortedLastRun, required this.thinkingLevel, required this.verboseLevel, required this.inputTokens, required this.outputTokens, required this.totalTokens, required this.model, required this.contextTokens, required this.derivedTitle, required this.lastMessagePreview, }); final String key; final String? kind; final String? displayName; final String? surface; final String? subject; final String? room; final String? space; final double? updatedAtMs; final String? sessionId; final bool? systemSent; final bool? abortedLastRun; final String? thinkingLevel; final String? verboseLevel; final int? inputTokens; final int? outputTokens; final int? totalTokens; final String? model; final int? contextTokens; final String? derivedTitle; final String? lastMessagePreview; String get label { final candidates = [derivedTitle, displayName, subject, room, space, key]; return candidates.firstWhere( (item) => item != null && item.trim().isNotEmpty, orElse: () => key, )!; } } class GatewayChatMessage { const GatewayChatMessage({ required this.id, required this.role, required this.text, required this.timestampMs, required this.toolCallId, required this.toolName, required this.stopReason, required this.pending, required this.error, }); final String id; final String role; final String text; final double? timestampMs; final String? toolCallId; final String? toolName; final String? stopReason; final bool pending; final bool error; GatewayChatMessage copyWith({ String? id, String? role, String? text, double? timestampMs, String? toolCallId, String? toolName, String? stopReason, bool? pending, bool? error, }) { return GatewayChatMessage( id: id ?? this.id, role: role ?? this.role, text: text ?? this.text, timestampMs: timestampMs ?? this.timestampMs, toolCallId: toolCallId ?? this.toolCallId, toolName: toolName ?? this.toolName, stopReason: stopReason ?? this.stopReason, pending: pending ?? this.pending, error: error ?? this.error, ); } } class GatewayChatAttachmentPayload { const GatewayChatAttachmentPayload({ required this.type, required this.mimeType, required this.fileName, required this.content, }); final String type; final String mimeType; final String fileName; final String content; Map toJson() { return { 'type': type, 'mimeType': mimeType, 'fileName': fileName, 'content': content, }; } } class GatewayInstanceSummary { const GatewayInstanceSummary({ required this.id, required this.host, required this.ip, required this.version, required this.platform, required this.deviceFamily, required this.modelIdentifier, required this.lastInputSeconds, required this.mode, required this.reason, required this.text, required this.timestampMs, }); final String id; final String? host; final String? ip; final String? version; final String? platform; final String? deviceFamily; final String? modelIdentifier; final int? lastInputSeconds; final String? mode; final String? reason; final String text; final double timestampMs; } class GatewaySkillSummary { const GatewaySkillSummary({ required this.name, required this.description, required this.source, required this.skillKey, required this.primaryEnv, required this.eligible, required this.disabled, required this.missingBins, required this.missingEnv, required this.missingConfig, }); final String name; final String description; final String source; final String skillKey; final String? primaryEnv; final bool eligible; final bool disabled; final List missingBins; final List missingEnv; final List missingConfig; } class GatewayConnectorSummary { const GatewayConnectorSummary({ required this.id, required this.label, required this.detailLabel, required this.accountName, required this.configured, required this.enabled, required this.running, required this.connected, required this.status, required this.lastError, required this.meta, }); final String id; final String label; final String detailLabel; final String? accountName; final bool configured; final bool enabled; final bool running; final bool connected; final String status; final String? lastError; final List meta; } class GatewayModelSummary { const GatewayModelSummary({ required this.id, required this.name, required this.provider, required this.contextWindow, required this.maxOutputTokens, }); final String id; final String name; final String provider; final int? contextWindow; final int? maxOutputTokens; } class GatewayCronJobSummary { const GatewayCronJobSummary({ required this.id, required this.name, required this.description, required this.enabled, required this.agentId, required this.scheduleLabel, required this.nextRunAtMs, required this.lastRunAtMs, required this.lastStatus, required this.lastError, }); final String id; final String name; final String? description; final bool enabled; final String? agentId; final String scheduleLabel; final int? nextRunAtMs; final int? lastRunAtMs; final String? lastStatus; final String? lastError; } class GatewayDevicePairingList { const GatewayDevicePairingList({required this.pending, required this.paired}); final List pending; final List paired; const GatewayDevicePairingList.empty() : pending = const [], paired = const []; } class GatewayPendingDevice { const GatewayPendingDevice({ required this.requestId, required this.deviceId, required this.displayName, required this.role, required this.scopes, required this.remoteIp, required this.isRepair, required this.requestedAtMs, }); final String requestId; final String deviceId; final String? displayName; final String? role; final List scopes; final String? remoteIp; final bool isRepair; final int? requestedAtMs; String get label { final display = displayName?.trim() ?? ''; return display.isEmpty ? deviceId : display; } } class GatewayPairedDevice { const GatewayPairedDevice({ required this.deviceId, required this.displayName, required this.roles, required this.scopes, required this.remoteIp, required this.tokens, required this.createdAtMs, required this.approvedAtMs, required this.currentDevice, }); final String deviceId; final String? displayName; final List roles; final List scopes; final String? remoteIp; final List tokens; final int? createdAtMs; final int? approvedAtMs; final bool currentDevice; String get label { final display = displayName?.trim() ?? ''; return display.isEmpty ? deviceId : display; } } class GatewayDeviceTokenSummary { const GatewayDeviceTokenSummary({ required this.role, required this.scopes, required this.createdAtMs, required this.rotatedAtMs, required this.revokedAtMs, required this.lastUsedAtMs, }); final String role; final List scopes; final int? createdAtMs; final int? rotatedAtMs; final int? revokedAtMs; final int? lastUsedAtMs; bool get revoked => revokedAtMs != null; } class SecretReferenceEntry { const SecretReferenceEntry({ required this.name, required this.provider, required this.module, required this.maskedValue, required this.status, }); final String name; final String provider; final String module; final String maskedValue; final String status; } class SecretAuditEntry { const SecretAuditEntry({ required this.timeLabel, required this.action, required this.provider, required this.target, required this.module, required this.status, }); final String timeLabel; final String action; final String provider; final String target; final String module; final String status; Map toJson() { return { 'timeLabel': timeLabel, 'action': action, 'provider': provider, 'target': target, 'module': module, 'status': status, }; } factory SecretAuditEntry.fromJson(Map json) { return SecretAuditEntry( timeLabel: json['timeLabel'] as String? ?? '', action: json['action'] as String? ?? '', provider: json['provider'] as String? ?? '', target: json['target'] as String? ?? '', module: json['module'] as String? ?? '', status: json['status'] as String? ?? '', ); } } class DerivedTaskItem { const DerivedTaskItem({ required this.id, required this.title, required this.owner, required this.status, required this.surface, required this.startedAtLabel, required this.durationLabel, required this.summary, required this.sessionKey, }); final String id; final String title; final String owner; final String status; final String surface; final String startedAtLabel; final String durationLabel; final String summary; final String sessionKey; } class LocalDeviceIdentity { const LocalDeviceIdentity({ required this.deviceId, required this.publicKeyBase64Url, required this.privateKeyBase64Url, required this.createdAtMs, }); final String deviceId; final String publicKeyBase64Url; final String privateKeyBase64Url; final int createdAtMs; Map toJson() { return { 'deviceId': deviceId, 'publicKeyBase64Url': publicKeyBase64Url, 'privateKeyBase64Url': privateKeyBase64Url, 'createdAtMs': createdAtMs, }; } factory LocalDeviceIdentity.fromJson(Map json) { return LocalDeviceIdentity( deviceId: json['deviceId'] as String? ?? '', publicKeyBase64Url: json['publicKeyBase64Url'] as String? ?? '', privateKeyBase64Url: json['privateKeyBase64Url'] as String? ?? '', createdAtMs: (json['createdAtMs'] as num?)?.toInt() ?? 0, ); } }