import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'app_metadata.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/gateway_runtime.dart'; import '../runtime/runtime_controllers.dart'; import '../runtime/runtime_models.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/runtime_coordinator.dart'; import '../runtime/codex_runtime.dart'; import '../runtime/codex_config_bridge.dart'; import '../runtime/code_agent_node_orchestrator.dart'; import '../runtime/mode_switcher.dart'; import '../runtime/agent_registry.dart'; enum CodexCooperationState { notStarted, bridgeOnly, registered } class AppController extends ChangeNotifier { AppController({ SecureConfigStore? store, RuntimeCoordinator? runtimeCoordinator, }) { _store = store ?? SecureConfigStore(); final resolvedRuntimeCoordinator = runtimeCoordinator ?? RuntimeCoordinator( gateway: GatewayRuntime( store: _store, identityStore: DeviceIdentityStore(_store), ), codex: CodexRuntime(), configBridge: CodexConfigBridge(), ); _runtimeCoordinator = resolvedRuntimeCoordinator; _codeAgentNodeOrchestrator = CodeAgentNodeOrchestrator(_runtimeCoordinator); _codeAgentBridgeRegistry = AgentRegistry(_runtimeCoordinator.gateway); _settingsController = SettingsController(_store); _agentsController = GatewayAgentsController(_runtimeCoordinator.gateway); _sessionsController = GatewaySessionsController( _runtimeCoordinator.gateway, ); _chatController = GatewayChatController(_runtimeCoordinator.gateway); _instancesController = InstancesController(_runtimeCoordinator.gateway); _skillsController = SkillsController(_runtimeCoordinator.gateway); _connectorsController = ConnectorsController(_runtimeCoordinator.gateway); _modelsController = ModelsController( _runtimeCoordinator.gateway, _settingsController, ); _cronJobsController = CronJobsController(_runtimeCoordinator.gateway); _devicesController = DevicesController(_runtimeCoordinator.gateway); _tasksController = DerivedTasksController(); _attachChildListeners(); unawaited(_initialize()); } late final SecureConfigStore _store; late final RuntimeCoordinator _runtimeCoordinator; late final CodeAgentNodeOrchestrator _codeAgentNodeOrchestrator; late final AgentRegistry _codeAgentBridgeRegistry; late final SettingsController _settingsController; late final GatewayAgentsController _agentsController; late final GatewaySessionsController _sessionsController; late final GatewayChatController _chatController; late final InstancesController _instancesController; late final SkillsController _skillsController; late final ConnectorsController _connectorsController; late final ModelsController _modelsController; late final CronJobsController _cronJobsController; late final DevicesController _devicesController; late final DerivedTasksController _tasksController; WorkspaceDestination _destination = WorkspaceDestination.assistant; ThemeMode _themeMode = ThemeMode.light; AppSidebarState _sidebarState = AppSidebarState.expanded; DetailPanelData? _detailPanel; bool _initializing = true; String? _bootstrapError; StreamSubscription? _runtimeEventsSubscription; bool _disposed = false; WorkspaceDestination get destination => _destination; ThemeMode get themeMode => _themeMode; AppSidebarState get sidebarState => _sidebarState; DetailPanelData? get detailPanel => _detailPanel; bool get initializing => _initializing; String? get bootstrapError => _bootstrapError; RuntimeCoordinator get runtimeCoordinator => _runtimeCoordinator; GatewayRuntime get _runtime => _runtimeCoordinator.gateway; GatewayRuntime get runtime => _runtime; /// Whether Codex bridge is enabled and configured bool get isCodexBridgeEnabled => _isCodexBridgeEnabled; bool _isCodexBridgeEnabled = false; bool _isCodexBridgeBusy = false; String? _codexBridgeError; String? _codexRuntimeWarning; String? _resolvedCodexCliPath; CodexCooperationState _codexCooperationState = CodexCooperationState.notStarted; SettingsController get settingsController => _settingsController; GatewayAgentsController get agentsController => _agentsController; GatewaySessionsController get sessionsController => _sessionsController; GatewayChatController get chatController => _chatController; InstancesController get instancesController => _instancesController; SkillsController get skillsController => _skillsController; ConnectorsController get connectorsController => _connectorsController; ModelsController get modelsController => _modelsController; CronJobsController get cronJobsController => _cronJobsController; DevicesController get devicesController => _devicesController; DerivedTasksController get tasksController => _tasksController; GatewayConnectionSnapshot get connection => _runtime.snapshot; SettingsSnapshot get settings => _settingsController.snapshot; List get agents => _agentsController.agents; List get sessions => _sessionsController.sessions; List get instances => _instancesController.items; List get skills => _skillsController.items; List get connectors => _connectorsController.items; List get models => _modelsController.items; List get cronJobs => _cronJobsController.items; GatewayDevicePairingList get devices => _devicesController.items; String get selectedAgentId => _agentsController.selectedAgentId; String get activeAgentName => _agentsController.activeAgentName; String get currentSessionKey => _sessionsController.currentSessionKey; String? get activeRunId => _chatController.activeRunId; AppLanguage get appLanguage => settings.appLanguage; AssistantExecutionTarget get assistantExecutionTarget => settings.assistantExecutionTarget; AssistantPermissionLevel get assistantPermissionLevel => settings.assistantPermissionLevel; bool get hasStoredGatewayCredential => _settingsController.secureRefs.containsKey('gateway_token') || _settingsController.secureRefs.containsKey('gateway_password') || _settingsController.secureRefs.containsKey( 'gateway_device_token_operator', ); bool get hasStoredGatewayToken => _settingsController.secureRefs.containsKey('gateway_token'); String? get storedGatewayTokenMask => _settingsController.secureRefs['gateway_token']; String get aiGatewayUrl => settings.aiGateway.baseUrl.trim(); bool get isCodexBridgeBusy => _isCodexBridgeBusy; String? get codexBridgeError => _codexBridgeError; String? get codexRuntimeWarning => _codexRuntimeWarning; String? get resolvedCodexCliPath => _resolvedCodexCliPath; bool get hasDetectedCodexCli => _resolvedCodexCliPath != null; String get configuredCodexCliPath => settings.codexCliPath.trim(); CodeAgentRuntimeMode get configuredCodeAgentRuntimeMode => settings.codeAgentRuntimeMode; CodeAgentRuntimeMode get effectiveCodeAgentRuntimeMode => configuredCodeAgentRuntimeMode; CodexCooperationState get codexCooperationState => _codexCooperationState; Future loadAiGatewayApiKey() async { return (await _store.loadAiGatewayApiKey())?.trim() ?? ''; } Future openOnlineWorkspace() async { const url = 'https://www.svc.plus/Xworkmate'; try { if (Platform.isMacOS) { await Process.run('open', [url]); return; } if (Platform.isWindows) { await Process.run('cmd', ['/c', 'start', '', url]); return; } if (Platform.isLinux) { await Process.run('xdg-open', [url]); } } catch (_) { // Best effort only. Do not surface a blocking error from a convenience link. } } List get aiGatewayModelChoices { final selected = settings.aiGateway.selectedModels .where(settings.aiGateway.availableModels.contains) .toList(growable: false); if (selected.isNotEmpty) { return selected; } final available = settings.aiGateway.availableModels .take(5) .toList(growable: false); if (available.isNotEmpty) { return available; } return _modelsController.items .map((item) => item.id) .toList(growable: false); } String get resolvedDefaultModel { final current = settings.defaultModel.trim(); final choices = aiGatewayModelChoices; if (choices.contains(current)) { return current; } if (choices.isNotEmpty) { return choices.first; } return current; } bool get canQuickConnectGateway { final profile = settings.gateway; if (profile.useSetupCode && profile.setupCode.trim().isNotEmpty) { return true; } final host = profile.host.trim(); if (host.isEmpty || profile.port <= 0) { return false; } if (profile.mode == RuntimeConnectionMode.local) { return true; } final defaults = GatewayConnectionProfile.defaults(); return hasStoredGatewayCredential || host != defaults.host || profile.port != defaults.port || profile.tls != defaults.tls || profile.mode != defaults.mode; } List get secretReferences => _settingsController.buildSecretReferences(); List get secretAuditTrail => _settingsController.auditTrail; List get runtimeLogs => _runtime.logs; List get assistantNavigationDestinations => normalizeAssistantNavigationDestinations( settings.assistantNavigationDestinations, ); List get chatMessages { final items = List.from(_chatController.messages); final streaming = _chatController.streamingAssistantText?.trim() ?? ''; if (streaming.isNotEmpty) { items.add( GatewayChatMessage( id: 'streaming', role: 'assistant', text: streaming, timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), toolCallId: null, toolName: null, stopReason: null, pending: true, error: false, ), ); } return items; } void navigateTo(WorkspaceDestination destination) { if (_destination == destination) { return; } _destination = destination; _detailPanel = null; notifyListeners(); } void navigateHome() { final mainSessionKey = _runtime.snapshot.mainSessionKey?.trim().isNotEmpty == true ? _runtime.snapshot.mainSessionKey!.trim() : 'main'; final destinationChanged = _destination != WorkspaceDestination.assistant; final detailChanged = _detailPanel != null; _destination = WorkspaceDestination.assistant; _detailPanel = null; if (destinationChanged || detailChanged) { notifyListeners(); } if (_sessionsController.currentSessionKey != mainSessionKey) { unawaited(switchSession(mainSessionKey)); } } void cycleSidebarState() { _sidebarState = switch (_sidebarState) { AppSidebarState.expanded => AppSidebarState.collapsed, AppSidebarState.collapsed => AppSidebarState.hidden, AppSidebarState.hidden => AppSidebarState.expanded, }; notifyListeners(); } void setSidebarState(AppSidebarState state) { if (_sidebarState == state) { return; } _sidebarState = state; notifyListeners(); } void setThemeMode(ThemeMode mode) { if (_themeMode == mode) { return; } _themeMode = mode; notifyListeners(); } Future toggleAppLanguage() async { await setAppLanguage( settings.appLanguage == AppLanguage.zh ? AppLanguage.en : AppLanguage.zh, ); } Future setAppLanguage(AppLanguage language) async { if (settings.appLanguage == language) { return; } setActiveAppLanguage(language); await saveSettings( settings.copyWith(appLanguage: language), refreshAfterSave: false, ); } void openDetail(DetailPanelData detailPanel) { _detailPanel = detailPanel; notifyListeners(); } void closeDetail() { if (_detailPanel == null) { return; } _detailPanel = null; notifyListeners(); } Future connectWithSetupCode({ required String setupCode, String token = '', String password = '', }) async { final decoded = decodeGatewaySetupCode(setupCode); final resolvedToken = token.trim().isNotEmpty ? token.trim() : (decoded?.token.trim() ?? ''); final resolvedPassword = password.trim().isNotEmpty ? password.trim() : (decoded?.password.trim() ?? ''); await _settingsController.saveGatewaySecrets( token: resolvedToken, password: resolvedPassword, ); final nextProfile = settings.gateway.copyWith( useSetupCode: true, setupCode: setupCode.trim(), host: decoded?.host ?? settings.gateway.host, port: decoded?.port ?? settings.gateway.port, tls: decoded?.tls ?? settings.gateway.tls, mode: _modeFromHost(decoded?.host ?? settings.gateway.host), ); await saveSettings( settings.copyWith(gateway: nextProfile), refreshAfterSave: false, ); await _connectProfile( nextProfile, authTokenOverride: resolvedToken, authPasswordOverride: resolvedPassword, ); } Future connectManual({ required String host, required int port, required bool tls, required RuntimeConnectionMode mode, String token = '', String password = '', }) async { await _settingsController.saveGatewaySecrets( token: token.trim(), password: password.trim(), ); final resolvedHost = host.trim().isEmpty && mode == RuntimeConnectionMode.local ? '127.0.0.1' : host.trim(); final resolvedPort = mode == RuntimeConnectionMode.local && port <= 0 ? 18789 : port; final nextProfile = settings.gateway.copyWith( mode: mode, useSetupCode: false, setupCode: '', host: resolvedHost, port: resolvedPort <= 0 ? 443 : resolvedPort, tls: mode == RuntimeConnectionMode.local ? false : tls, ); await saveSettings( settings.copyWith(gateway: nextProfile), refreshAfterSave: false, ); await _connectProfile( nextProfile, authTokenOverride: token.trim(), authPasswordOverride: password.trim(), ); } Future disconnectGateway() async { _clearCodexGatewayRegistration(); await _runtime.disconnect(clearDesiredProfile: false); await _settingsController.refreshDerivedState(); await _agentsController.refresh(); await _sessionsController.refresh(); _chatController.clear(); await _instancesController.refresh(); await _skillsController.refresh(); await _connectorsController.refresh(); await _modelsController.refresh(); await _cronJobsController.refresh(); _devicesController.clear(); _recomputeTasks(); } Future connectSavedGateway() async { await _connectProfile(settings.gateway); } Future clearStoredGatewayToken() async { await _settingsController.clearGatewaySecrets(token: true); } Future refreshGatewayHealth() async { if (!_runtime.isConnected) { return; } try { await _runtime.health(); } catch (_) {} try { await _runtime.status(); } catch (_) {} notifyListeners(); } Future refreshDevices({bool quiet = false}) async { await _devicesController.refresh(quiet: quiet); } Future approveDevicePairing(String requestId) async { await _devicesController.approve(requestId); await _settingsController.refreshDerivedState(); } Future rejectDevicePairing(String requestId) async { await _devicesController.reject(requestId); } Future removePairedDevice(String deviceId) async { await _devicesController.remove(deviceId); await _settingsController.refreshDerivedState(); } Future rotateDeviceRoleToken({ required String deviceId, required String role, List scopes = const [], }) async { final token = await _devicesController.rotateToken( deviceId: deviceId, role: role, scopes: scopes, ); await _settingsController.refreshDerivedState(); return token; } Future revokeDeviceRoleToken({ required String deviceId, required String role, }) async { await _devicesController.revokeToken(deviceId: deviceId, role: role); await _settingsController.refreshDerivedState(); } Future refreshAgents() async { await _agentsController.refresh(); _sessionsController.configure( mainSessionKey: _runtime.snapshot.mainSessionKey ?? 'main', selectedAgentId: _agentsController.selectedAgentId, defaultAgentId: '', ); _recomputeTasks(); } Future selectAgent(String? agentId) async { _agentsController.selectAgent(agentId); final nextProfile = settings.gateway.copyWith( selectedAgentId: _agentsController.selectedAgentId, ); await saveSettings( settings.copyWith(gateway: nextProfile), refreshAfterSave: false, ); _sessionsController.configure( mainSessionKey: _runtime.snapshot.mainSessionKey ?? 'main', selectedAgentId: _agentsController.selectedAgentId, defaultAgentId: '', ); await _chatController.loadSession(_sessionsController.currentSessionKey); await _skillsController.refresh( agentId: _agentsController.selectedAgentId.isEmpty ? null : _agentsController.selectedAgentId, ); _recomputeTasks(); } Future refreshSessions() async { _sessionsController.configure( mainSessionKey: _runtime.snapshot.mainSessionKey ?? 'main', selectedAgentId: _agentsController.selectedAgentId, defaultAgentId: '', ); await _sessionsController.refresh(); await _chatController.loadSession(_sessionsController.currentSessionKey); _recomputeTasks(); } Future switchSession(String sessionKey) async { await _sessionsController.switchSession(sessionKey); await _chatController.loadSession(_sessionsController.currentSessionKey); _recomputeTasks(); } Future sendChatMessage( String message, { String thinking = 'off', List attachments = const [], }) async { final dispatch = _codeAgentNodeOrchestrator.buildGatewayDispatch( _buildCodeAgentNodeState(), ); await _chatController.sendMessage( sessionKey: _sessionsController.currentSessionKey, message: message, thinking: thinking, attachments: attachments, agentId: dispatch.agentId, metadata: dispatch.metadata, ); _recomputeTasks(); } Future abortRun() async { await _chatController.abortRun(); } Future setAssistantExecutionTarget( AssistantExecutionTarget target, ) async { if (settings.assistantExecutionTarget == target) { return; } await saveSettings( settings.copyWith(assistantExecutionTarget: target), refreshAfterSave: false, ); } Future setAssistantPermissionLevel( AssistantPermissionLevel level, ) async { if (settings.assistantPermissionLevel == level) { return; } await saveSettings( settings.copyWith(assistantPermissionLevel: level), refreshAfterSave: false, ); } Future selectDefaultModel(String modelId) async { final trimmed = modelId.trim(); if (trimmed.isEmpty || settings.defaultModel == trimmed) { return; } await saveSettings( settings.copyWith(defaultModel: trimmed), refreshAfterSave: false, ); } Future updateAiGatewaySelection(List selectedModels) async { final available = settings.aiGateway.availableModels; final normalized = selectedModels .map((item) => item.trim()) .where((item) => item.isNotEmpty && available.contains(item)) .toList(growable: false); final fallbackSelection = normalized.isNotEmpty ? normalized : available.isNotEmpty ? [available.first] : const []; final currentDefaultModel = settings.defaultModel.trim(); final resolvedDefaultModel = fallbackSelection.contains(currentDefaultModel) ? currentDefaultModel : fallbackSelection.isNotEmpty ? fallbackSelection.first : ''; await saveSettings( settings.copyWith( aiGateway: settings.aiGateway.copyWith( selectedModels: fallbackSelection, ), defaultModel: resolvedDefaultModel, ), refreshAfterSave: false, ); } Future syncAiGatewayCatalog( AiGatewayProfile profile, { String apiKeyOverride = '', }) async { final synced = await _settingsController.syncAiGatewayCatalog( profile, apiKeyOverride: apiKeyOverride, ); _modelsController.restoreFromSettings( _settingsController.snapshot.aiGateway, ); _recomputeTasks(); return synced; } Future saveSettings( SettingsSnapshot snapshot, { bool refreshAfterSave = true, }) async { final current = settings; final sanitized = _sanitizeCodeAgentSettings(snapshot); setActiveAppLanguage(sanitized.appLanguage); await _settingsController.saveSnapshot(sanitized); _agentsController.restoreSelection(sanitized.gateway.selectedAgentId); _modelsController.restoreFromSettings(sanitized.aiGateway); if (current.codexCliPath != sanitized.codexCliPath || current.codeAgentRuntimeMode != sanitized.codeAgentRuntimeMode) { _registerCodexExternalProvider(codexPath: sanitized.codexCliPath); await _refreshCodexCliAvailability(); } if (refreshAfterSave) { _recomputeTasks(); } } Future toggleAssistantNavigationDestination( WorkspaceDestination destination, ) async { if (!kAssistantNavigationDestinationCandidates.contains(destination)) { return; } final current = assistantNavigationDestinations; final next = current.contains(destination) ? current.where((item) => item != destination).toList(growable: false) : [...current, destination]; await saveSettings( settings.copyWith(assistantNavigationDestinations: next), refreshAfterSave: false, ); } Future testOllamaConnection({required bool cloud}) { return _settingsController.testOllamaConnection(cloud: cloud); } Future testVaultConnection() { return _settingsController.testVaultConnection(); } void clearRuntimeLogs() { _runtimeCoordinator.gateway.clearLogs(); _notifyIfActive(); } List taskItemsForTab(String tab) => switch (tab) { 'Queue' => _tasksController.queue, 'Running' => _tasksController.running, 'History' => _tasksController.history, 'Failed' => _tasksController.failed, 'Scheduled' => _tasksController.scheduled, _ => _tasksController.queue, }; /// Enable Codex ↔ Gateway bridge Future enableCodexBridge() async { if (_isCodexBridgeEnabled || _isCodexBridgeBusy) return; _isCodexBridgeBusy = true; _codexBridgeError = null; try { final gatewayUrl = aiGatewayUrl; final apiKey = await loadAiGatewayApiKey(); if (gatewayUrl.isEmpty) { throw StateError( appText('AI Gateway URL 未配置', 'AI Gateway URL not configured'), ); } final runtimeMode = effectiveCodeAgentRuntimeMode; String? codexPath; if (runtimeMode == CodeAgentRuntimeMode.externalCli) { codexPath = await _resolveCodexCliPath(); if (codexPath == null) { throw StateError( appText( '未找到 Codex CLI。请先安装或填写可执行文件路径。', 'Codex CLI not found. Install it or set a manual binary path.', ), ); } } await _runtimeCoordinator.configureCodexForGateway( gatewayUrl: gatewayUrl, apiKey: apiKey, ); await _runtimeCoordinator.startCodeAgentRuntime( runtimeMode: runtimeMode, codexPath: codexPath, workingDirectory: _resolveCodexWorkingDirectory(), ); _registerCodexExternalProvider(codexPath: codexPath); _isCodexBridgeEnabled = true; _codexCooperationState = CodexCooperationState.bridgeOnly; await _ensureCodexGatewayRegistration(); notifyListeners(); } catch (e) { _codexBridgeError = e.toString(); notifyListeners(); rethrow; } finally { _isCodexBridgeBusy = false; notifyListeners(); } } /// Disable Codex ↔ Gateway bridge Future disableCodexBridge() async { if (!_isCodexBridgeEnabled || _isCodexBridgeBusy) return; _isCodexBridgeBusy = true; try { if (_runtime.isConnected && _codeAgentBridgeRegistry.isRegistered) { await _codeAgentBridgeRegistry.unregister(); } else { _codeAgentBridgeRegistry.clearRegistration(); } await _runtimeCoordinator.stopCodeAgentRuntime(); _isCodexBridgeEnabled = false; _codexCooperationState = CodexCooperationState.notStarted; _codexBridgeError = null; notifyListeners(); } catch (e) { _codexBridgeError = e.toString(); notifyListeners(); rethrow; } finally { _isCodexBridgeBusy = false; notifyListeners(); } } @override void dispose() { if (_disposed) { return; } _disposed = true; _runtimeEventsSubscription?.cancel(); _detachChildListeners(); _runtimeCoordinator.dispose(); _settingsController.dispose(); _agentsController.dispose(); _sessionsController.dispose(); _chatController.dispose(); _instancesController.dispose(); _skillsController.dispose(); _connectorsController.dispose(); _modelsController.dispose(); _cronJobsController.dispose(); _devicesController.dispose(); _tasksController.dispose(); _store.dispose(); super.dispose(); } Future _initialize() async { try { await _settingsController.initialize(); if (_disposed) { return; } final bootstrap = await RuntimeBootstrapConfig.load( workspacePathHint: settings.workspacePath, cliPathHint: settings.cliPath, ); if (_disposed) { return; } final seeded = bootstrap.mergeIntoSettings(settings); if (seeded.toJsonString() != settings.toJsonString()) { await _settingsController.saveSnapshot(seeded); if (_disposed) { return; } } final normalized = _sanitizeCodeAgentSettings( _settingsController.snapshot, ); if (normalized.toJsonString() != _settingsController.snapshot.toJsonString()) { await _settingsController.saveSnapshot(normalized); if (_disposed) { return; } } _modelsController.restoreFromSettings(settings.aiGateway); setActiveAppLanguage(settings.appLanguage); _registerCodexExternalProvider(); await _refreshCodexCliAvailability(); if (_disposed) { return; } _agentsController.restoreSelection(settings.gateway.selectedAgentId); _sessionsController.configure( mainSessionKey: _runtime.snapshot.mainSessionKey ?? 'main', selectedAgentId: _agentsController.selectedAgentId, defaultAgentId: '', ); _runtimeEventsSubscription = _runtimeCoordinator.gateway.events.listen( _handleRuntimeEvent, ); final shouldAutoConnect = settings.gateway.useSetupCode && settings.gateway.setupCode.trim().isNotEmpty; if (shouldAutoConnect) { try { await _connectProfile(settings.gateway); } catch (_) { // Keep the shell usable when auto-connect fails. } } } catch (error) { if (_disposed) { return; } _bootstrapError = error.toString(); } finally { if (!_disposed) { _initializing = false; _notifyIfActive(); } } } Future _connectProfile( GatewayConnectionProfile profile, { String authTokenOverride = '', String authPasswordOverride = '', }) async { await _runtime.connectProfile( profile, authTokenOverride: authTokenOverride, authPasswordOverride: authPasswordOverride, ); await refreshGatewayHealth(); await refreshAgents(); await refreshSessions(); await _instancesController.refresh(); await _skillsController.refresh( agentId: _agentsController.selectedAgentId.isEmpty ? null : _agentsController.selectedAgentId, ); await _connectorsController.refresh(); await _modelsController.refresh(); await _cronJobsController.refresh(); await _devicesController.refresh(quiet: true); await _settingsController.refreshDerivedState(); await _ensureCodexGatewayRegistration(); _recomputeTasks(); } void _handleRuntimeEvent(GatewayPushEvent event) { _chatController.handleEvent(event); if (event.event == 'chat') { final payload = asMap(event.payload); final state = stringValue(payload['state']); if (state == 'final' || state == 'aborted' || state == 'error') { unawaited(refreshSessions()); } } if (event.event == 'seqGap') { unawaited(refreshSessions()); } if (event.event == 'device.pair.requested' || event.event == 'device.pair.resolved') { unawaited(refreshDevices(quiet: true)); } } SettingsSnapshot _sanitizeCodeAgentSettings(SettingsSnapshot snapshot) { _codexRuntimeWarning = snapshot.codeAgentRuntimeMode == CodeAgentRuntimeMode.builtIn ? appText( '内置 Codex 仍处于实验阶段;建议优先使用 External Codex CLI。', 'Built-in Codex is still experimental; External Codex CLI is recommended.', ) : null; final normalizedPath = snapshot.codexCliPath.trim(); if (normalizedPath == snapshot.codexCliPath) { return snapshot; } return snapshot.copyWith(codexCliPath: normalizedPath); } Future _refreshCodexCliAvailability() async { _resolvedCodexCliPath = await _runtimeCoordinator.resolveCodexPath( codexPath: settings.codexCliPath, ); _notifyIfActive(); } Future _resolveCodexCliPath() async { if (_resolvedCodexCliPath != null) { return _resolvedCodexCliPath; } await _refreshCodexCliAvailability(); return _resolvedCodexCliPath; } String? _resolveCodexWorkingDirectory() { final candidate = settings.workspacePath.trim(); if (candidate.isEmpty) { return null; } final directory = Directory(candidate); return directory.existsSync() ? directory.path : null; } void _registerCodexExternalProvider({String? codexPath}) { _runtimeCoordinator.registerExternalCodeAgent( ExternalCodeAgentProvider( id: 'codex', name: 'Codex CLI', command: (codexPath?.trim().isNotEmpty ?? false) ? codexPath!.trim() : 'codex', defaultArgs: const ['app-server', '--listen', 'stdio://'], capabilities: const [ 'chat', 'code-edit', 'gateway-bridge', 'memory-sync', ], ), ); } CodeAgentNodeState _buildCodeAgentNodeState() { return CodeAgentNodeState( selectedAgentId: _agentsController.selectedAgentId, gatewayConnected: _runtime.isConnected, executionTarget: settings.assistantExecutionTarget, runtimeMode: effectiveCodeAgentRuntimeMode, bridgeEnabled: _isCodexBridgeEnabled, bridgeState: _codexCooperationState.name, preferredProviderId: 'codex', resolvedCodexCliPath: _resolvedCodexCliPath, configuredCodexCliPath: configuredCodexCliPath, ); } GatewayMode _bridgeGatewayMode() { return switch (settings.gateway.mode) { RuntimeConnectionMode.local => GatewayMode.local, RuntimeConnectionMode.remote => GatewayMode.remote, RuntimeConnectionMode.unconfigured => GatewayMode.offline, }; } Future _ensureCodexGatewayRegistration() async { if (!_isCodexBridgeEnabled) { return; } if (!_runtime.isConnected) { _codexCooperationState = CodexCooperationState.bridgeOnly; _codeAgentBridgeRegistry.clearRegistration(); notifyListeners(); return; } if (_codeAgentBridgeRegistry.isRegistered) { _codexCooperationState = CodexCooperationState.registered; notifyListeners(); return; } try { final dispatch = _codeAgentNodeOrchestrator.buildGatewayDispatch( _buildCodeAgentNodeState(), ); await _codeAgentBridgeRegistry.register( agentType: 'code-agent-bridge', name: 'XWorkmate Codex Bridge', version: kAppVersion, transport: 'stdio-bridge', capabilities: const [ AgentCapability( name: 'chat', description: 'Bridge external Codex CLI chat turns.', ), AgentCapability( name: 'code-edit', description: 'Bridge code editing tasks through Codex CLI.', ), AgentCapability( name: 'memory-sync', description: 'Coordinate memory sync through OpenClaw Gateway.', ), ], metadata: { ...dispatch.metadata, 'providerId': 'codex', 'runtimeMode': effectiveCodeAgentRuntimeMode.name, 'gatewayMode': _bridgeGatewayMode().name, 'binaryConfigured': (resolvedCodexCliPath ?? configuredCodexCliPath) .trim() .isNotEmpty, 'capabilities': const [ 'chat', 'code-edit', 'gateway-bridge', 'memory-sync', ], }, ); _codexCooperationState = CodexCooperationState.registered; _codexBridgeError = null; } catch (error) { _codexCooperationState = CodexCooperationState.bridgeOnly; _codexBridgeError = error.toString(); } notifyListeners(); } void _clearCodexGatewayRegistration() { _codeAgentBridgeRegistry.clearRegistration(); if (_isCodexBridgeEnabled) { _codexCooperationState = CodexCooperationState.bridgeOnly; } else { _codexCooperationState = CodexCooperationState.notStarted; } notifyListeners(); } void _recomputeTasks() { _tasksController.recompute( sessions: _sessionsController.sessions, cronJobs: _cronJobsController.items, currentSessionKey: _sessionsController.currentSessionKey, hasPendingRun: _chatController.hasPendingRun, activeAgentName: _agentsController.activeAgentName, ); } void _attachChildListeners() { _runtimeCoordinator.addListener(_relayChildChange); _settingsController.addListener(_relayChildChange); _agentsController.addListener(_relayChildChange); _sessionsController.addListener(_relayChildChange); _chatController.addListener(_relayChildChange); _instancesController.addListener(_relayChildChange); _skillsController.addListener(_relayChildChange); _connectorsController.addListener(_relayChildChange); _modelsController.addListener(_relayChildChange); _cronJobsController.addListener(_relayChildChange); _devicesController.addListener(_relayChildChange); _tasksController.addListener(_relayChildChange); } void _detachChildListeners() { _runtimeCoordinator.removeListener(_relayChildChange); _settingsController.removeListener(_relayChildChange); _agentsController.removeListener(_relayChildChange); _sessionsController.removeListener(_relayChildChange); _chatController.removeListener(_relayChildChange); _instancesController.removeListener(_relayChildChange); _skillsController.removeListener(_relayChildChange); _connectorsController.removeListener(_relayChildChange); _modelsController.removeListener(_relayChildChange); _cronJobsController.removeListener(_relayChildChange); _devicesController.removeListener(_relayChildChange); _tasksController.removeListener(_relayChildChange); } void _relayChildChange() { _notifyIfActive(); } void _notifyIfActive() { if (_disposed) { return; } notifyListeners(); } RuntimeConnectionMode _modeFromHost(String host) { final trimmed = host.trim().toLowerCase(); if (trimmed == '127.0.0.1' || trimmed == 'localhost') { return RuntimeConnectionMode.local; } return RuntimeConnectionMode.remote; } }