diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 6d8d91ee..cf2be8d2 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -5,6 +5,9 @@ PODS:
- file_selector_ios (0.0.1):
- Flutter
- Flutter (1.0.0)
+ - flutter_webrtc (0.12.6):
+ - Flutter
+ - WebRTC-SDK (= 125.6422.06)
- integration_test (0.0.1):
- Flutter
- irondash_engine_context (0.0.1):
@@ -20,11 +23,13 @@ PODS:
- FlutterMacOS
- super_native_extensions (0.0.1):
- Flutter
+ - WebRTC-SDK (125.6422.06)
DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
+ - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -35,6 +40,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- CocoaAsyncSocket
+ - WebRTC-SDK
EXTERNAL SOURCES:
device_info_plus:
@@ -43,6 +49,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_selector_ios/ios"
Flutter:
:path: Flutter
+ flutter_webrtc:
+ :path: ".symlinks/plugins/flutter_webrtc/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
@@ -61,12 +69,14 @@ SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
+ WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index d7a158b4..2657ab1f 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -32,6 +32,8 @@
XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network.
NSCameraUsageDescription
XWorkmate uses the camera only when you explicitly scan a gateway pairing QR code.
+ NSMicrophoneUsageDescription
+ XWorkmate requires microphone access for voice interactions and WebRTC connections.
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
diff --git a/lib/app/app_logger.dart b/lib/app/app_logger.dart
new file mode 100644
index 00000000..8181f9a7
--- /dev/null
+++ b/lib/app/app_logger.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/foundation.dart';
+
+class AppLogger {
+ static final AppLogger _instance = AppLogger._internal();
+
+ factory AppLogger() {
+ return _instance;
+ }
+
+ AppLogger._internal();
+
+ final List _logs = [];
+ final int maxLines = 200;
+
+ void log(String message) {
+ final timestamp = DateTime.now().toIso8601String();
+ final logLine = '[$timestamp] APP: $message';
+ _logs.add(logLine);
+ if (_logs.length > maxLines) {
+ _logs.removeAt(0);
+ }
+ debugPrint(logLine);
+ }
+
+ List getLogs() {
+ return List.unmodifiable(_logs);
+ }
+}
+
+// Global helper for easy logging
+void appLog(String message) {
+ AppLogger().log(message);
+}
diff --git a/lib/features/desktop/desktop_client.dart b/lib/features/desktop/desktop_client.dart
index 007c9ac8..e66640b0 100644
--- a/lib/features/desktop/desktop_client.dart
+++ b/lib/features/desktop/desktop_client.dart
@@ -63,7 +63,7 @@ class DesktopClient {
_stateController.add(state.toString().split('.').last);
};
- // Create data channel for inputs
+ // Create data channel for inputs BEFORE creating offer
final dcConfig = RTCDataChannelInit()..ordered = true;
_dataChannel =
await _peerConnection!.createDataChannel('input', dcConfig);
@@ -75,11 +75,14 @@ class DesktopClient {
}
};
+ // Add transceiver for receiving video (required for unified-plan)
+ await _peerConnection!.addTransceiver(
+ kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
+ init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly),
+ );
+
// Create SDP Offer
- final offer = await _peerConnection!.createOffer({
- 'offerToReceiveVideo': true,
- 'offerToReceiveAudio': false,
- });
+ final offer = await _peerConnection!.createOffer({});
await _peerConnection!.setLocalDescription(offer);
// Send SDP Offer to Bridge
diff --git a/lib/features/settings/settings_logs_panel.dart b/lib/features/settings/settings_logs_panel.dart
index 986b021f..2bada5d1 100644
--- a/lib/features/settings/settings_logs_panel.dart
+++ b/lib/features/settings/settings_logs_panel.dart
@@ -1,5 +1,7 @@
+import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
+import '../../app/app_logger.dart';
import '../../i18n/app_language.dart';
import '../../theme/app_palette.dart';
@@ -13,10 +15,117 @@ class SettingsLogsPanel extends StatefulWidget {
}
class _SettingsLogsPanelState extends State {
+ Timer? _timer;
+ String _bridgeStatus = 'unknown';
+ String _gatewayStatus = 'unknown';
+ List _bridgeLogs = [];
+ final ScrollController _scrollController = ScrollController();
+
+ @override
+ void initState() {
+ super.initState();
+ _fetchStatus();
+ _timer = Timer.periodic(const Duration(seconds: 3), (_) {
+ _fetchStatus();
+ });
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ _scrollController.dispose();
+ super.dispose();
+ }
+
+ Future _fetchStatus() async {
+ try {
+ final res = await widget.controller.gatewayAcpClientInternal.fetchSystemStatus();
+ if (mounted) {
+ setState(() {
+ _bridgeStatus = res['bridgeStatus']?.toString() ?? 'error';
+ _gatewayStatus = res['gatewayStatus']?.toString() ?? 'error';
+
+ final logs = res['bridgeLogs'];
+ if (logs is List) {
+ _bridgeLogs = logs.map((e) => e.toString()).toList();
+ }
+ });
+ // Auto scroll to bottom
+ if (_scrollController.hasClients) {
+ _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _bridgeStatus = 'error';
+ _gatewayStatus = 'error';
+ });
+ }
+ }
+ }
+
+ Widget _buildStatusCard(String title, String status, AppPalette palette) {
+ final isOk = status.toLowerCase() == 'ok' || status.toLowerCase() == 'connected' || status.toLowerCase() == 'running';
+ final color = isOk ? Colors.green : (status == 'unknown' ? Colors.grey : Colors.redAccent);
+
+ return Expanded(
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ decoration: BoxDecoration(
+ color: palette.surfaceSecondary,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: palette.stroke),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ color: palette.textSecondary,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Container(
+ width: 8,
+ height: 8,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: color,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ status.toUpperCase(),
+ style: TextStyle(
+ color: palette.textPrimary,
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ )
+ ],
+ ),
+ ),
+ );
+ }
+
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
+
+ // Combine local app logs with bridge logs
+ final appLogs = AppLogger().getLogs();
return Column(
key: const ValueKey('settings-logs-panel'),
@@ -37,28 +146,42 @@ class _SettingsLogsPanelState extends State {
],
),
const SizedBox(height: 12),
+ Row(
+ children: [
+ _buildStatusCard('App Status', 'Running', palette),
+ const SizedBox(width: 8),
+ _buildStatusCard('Bridge', _bridgeStatus, palette),
+ const SizedBox(width: 8),
+ _buildStatusCard('Gateway', _gatewayStatus, palette),
+ ],
+ ),
+ const SizedBox(height: 12),
Container(
height: 400,
decoration: BoxDecoration(
- color: palette.surfaceContainerHighest,
+ color: const Color(0xFF1E1E1E), // Dark terminal background
borderRadius: BorderRadius.circular(8),
- border: Border.all(color: palette.outlineVariant),
+ border: Border.all(color: palette.stroke),
),
- padding: const EdgeInsets.all(16),
- child: Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.monitor_heart_outlined, size: 48, color: palette.textSecondary.withOpacity(0.5)),
- const SizedBox(height: 16),
- Text(
- appText('暂无日志数据', 'No log data available'),
- style: theme.textTheme.bodyMedium?.copyWith(
- color: palette.textSecondary,
+ padding: const EdgeInsets.all(12),
+ child: ListView.builder(
+ controller: _scrollController,
+ itemCount: appLogs.length + _bridgeLogs.length,
+ itemBuilder: (context, index) {
+ final isAppLog = index < appLogs.length;
+ final logText = isAppLog ? appLogs[index] : _bridgeLogs[index - appLogs.length];
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 4.0),
+ child: Text(
+ logText,
+ style: const TextStyle(
+ fontFamily: 'monospace',
+ fontSize: 12,
+ color: Color(0xFFCCCCCC),
),
),
- ],
- ),
+ );
+ },
),
),
],
diff --git a/lib/features/settings/settings_remote_desktop_panel.dart b/lib/features/settings/settings_remote_desktop_panel.dart
index 5da69c8b..5581c573 100644
--- a/lib/features/settings/settings_remote_desktop_panel.dart
+++ b/lib/features/settings/settings_remote_desktop_panel.dart
@@ -17,33 +17,30 @@ class SettingsRemoteDesktopPanel extends StatefulWidget {
class _SettingsRemoteDesktopPanelState extends State {
final GlobalKey _desktopViewKey = GlobalKey();
bool _isMaximized = false;
+ OverlayEntry? _overlayEntry;
void _toggleMaximize() {
if (_isMaximized) {
- Navigator.of(context).pop();
+ _overlayEntry?.remove();
+ _overlayEntry = null;
+ setState(() => _isMaximized = false);
} else {
setState(() => _isMaximized = true);
- showDialog(
- context: context,
- useSafeArea: false,
- barrierDismissible: false,
+ _overlayEntry = OverlayEntry(
builder: (context) {
- return Dialog.fullscreen(
- child: DesktopView(
- key: _desktopViewKey,
- controller: widget.controller,
- isMaximized: true,
- onToggleMaximize: () {
- Navigator.of(context).pop();
- },
+ return Material(
+ child: SafeArea(
+ child: DesktopView(
+ key: _desktopViewKey,
+ controller: widget.controller,
+ isMaximized: true,
+ onToggleMaximize: _toggleMaximize,
+ ),
),
);
},
- ).then((_) {
- if (mounted) {
- setState(() => _isMaximized = false);
- }
- });
+ );
+ Overlay.of(context).insert(_overlayEntry!);
}
}
diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart
index 16c6182f..6afe84d6 100644
--- a/lib/runtime/gateway_acp_client.dart
+++ b/lib/runtime/gateway_acp_client.dart
@@ -124,6 +124,18 @@ class GatewayAcpClient {
const GatewayAcpCapabilities.empty();
DateTime? _capabilitiesRefreshedAt;
+ Future