react-native-debug-toolkit 3.1.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +88 -65
  2. package/README.zh-CN.md +87 -64
  3. package/bin/debug-toolkit.js +10 -2
  4. package/lib/commonjs/core/initialize.js +5 -3
  5. package/lib/commonjs/core/initialize.js.map +1 -1
  6. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +146 -0
  7. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -0
  8. package/lib/commonjs/features/devConnect/DevConnectTab.js +426 -0
  9. package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -0
  10. package/lib/commonjs/features/devConnect/cameraKit.js +54 -0
  11. package/lib/commonjs/features/devConnect/cameraKit.js.map +1 -0
  12. package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -0
  13. package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -0
  14. package/lib/commonjs/features/devConnect/devConnectUtils.js +53 -0
  15. package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -0
  16. package/lib/commonjs/features/devConnect/index.js +92 -0
  17. package/lib/commonjs/features/devConnect/index.js.map +1 -0
  18. package/lib/commonjs/features/devConnect/platformDetect.js +30 -0
  19. package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -0
  20. package/lib/commonjs/features/devConnect/types.js +2 -0
  21. package/lib/commonjs/features/devConnect/types.js.map +1 -0
  22. package/lib/commonjs/features/network/NetworkLogTab.js +7 -3
  23. package/lib/commonjs/features/network/NetworkLogTab.js.map +1 -1
  24. package/lib/commonjs/index.js +7 -0
  25. package/lib/commonjs/index.js.map +1 -1
  26. package/lib/commonjs/ui/DebugView.js +1 -0
  27. package/lib/commonjs/ui/DebugView.js.map +1 -1
  28. package/lib/commonjs/ui/panel/DebugPanel.js +0 -25
  29. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  30. package/lib/commonjs/utils/DaemonClient.js +25 -1
  31. package/lib/commonjs/utils/DaemonClient.js.map +1 -1
  32. package/lib/commonjs/utils/debugPreferences.js +2 -1
  33. package/lib/commonjs/utils/debugPreferences.js.map +1 -1
  34. package/lib/commonjs/utils/deviceReport.js +1 -0
  35. package/lib/commonjs/utils/deviceReport.js.map +1 -1
  36. package/lib/module/core/initialize.js +5 -3
  37. package/lib/module/core/initialize.js.map +1 -1
  38. package/lib/module/features/devConnect/DevConnectQrScanner.js +141 -0
  39. package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -0
  40. package/lib/module/features/devConnect/DevConnectTab.js +421 -0
  41. package/lib/module/features/devConnect/DevConnectTab.js.map +1 -0
  42. package/lib/module/features/devConnect/cameraKit.js +49 -0
  43. package/lib/module/features/devConnect/cameraKit.js.map +1 -0
  44. package/lib/module/features/devConnect/devConnectPreferences.js +29 -0
  45. package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -0
  46. package/lib/module/features/devConnect/devConnectUtils.js +47 -0
  47. package/lib/module/features/devConnect/devConnectUtils.js.map +1 -0
  48. package/lib/module/features/devConnect/index.js +52 -0
  49. package/lib/module/features/devConnect/index.js.map +1 -0
  50. package/lib/module/features/devConnect/platformDetect.js +26 -0
  51. package/lib/module/features/devConnect/platformDetect.js.map +1 -0
  52. package/lib/module/features/devConnect/types.js +2 -0
  53. package/lib/module/features/devConnect/types.js.map +1 -0
  54. package/lib/module/features/network/NetworkLogTab.js +7 -3
  55. package/lib/module/features/network/NetworkLogTab.js.map +1 -1
  56. package/lib/module/index.js +1 -1
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/ui/DebugView.js +1 -0
  59. package/lib/module/ui/DebugView.js.map +1 -1
  60. package/lib/module/ui/panel/DebugPanel.js +1 -26
  61. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  62. package/lib/module/utils/DaemonClient.js +25 -1
  63. package/lib/module/utils/DaemonClient.js.map +1 -1
  64. package/lib/module/utils/debugPreferences.js +2 -1
  65. package/lib/module/utils/debugPreferences.js.map +1 -1
  66. package/lib/module/utils/deviceReport.js +1 -0
  67. package/lib/module/utils/deviceReport.js.map +1 -1
  68. package/lib/typescript/src/core/initialize.d.ts +1 -0
  69. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  70. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +9 -0
  71. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -0
  72. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts +5 -0
  73. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -0
  74. package/lib/typescript/src/features/devConnect/cameraKit.d.ts +47 -0
  75. package/lib/typescript/src/features/devConnect/cameraKit.d.ts.map +1 -0
  76. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +7 -0
  77. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -0
  78. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +12 -0
  79. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -0
  80. package/lib/typescript/src/features/devConnect/index.d.ts +7 -0
  81. package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -0
  82. package/lib/typescript/src/features/devConnect/platformDetect.d.ts +2 -0
  83. package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -0
  84. package/lib/typescript/src/features/devConnect/types.d.ts +7 -0
  85. package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -0
  86. package/lib/typescript/src/features/network/NetworkLogTab.d.ts.map +1 -1
  87. package/lib/typescript/src/index.d.ts +2 -0
  88. package/lib/typescript/src/index.d.ts.map +1 -1
  89. package/lib/typescript/src/types/feature.d.ts +1 -1
  90. package/lib/typescript/src/types/feature.d.ts.map +1 -1
  91. package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
  92. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  93. package/lib/typescript/src/utils/DaemonClient.d.ts +1 -0
  94. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -1
  95. package/lib/typescript/src/utils/debugPreferences.d.ts +1 -0
  96. package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
  97. package/lib/typescript/src/utils/deviceReport.d.ts +6 -0
  98. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -1
  99. package/node/daemon/src/console/console.html +166 -27
  100. package/node/daemon/src/store.js +45 -6
  101. package/package.json +15 -3
  102. package/src/core/initialize.ts +7 -1
  103. package/src/features/devConnect/DevConnectQrScanner.tsx +122 -0
  104. package/src/features/devConnect/DevConnectTab.tsx +357 -0
  105. package/src/features/devConnect/cameraKit.ts +93 -0
  106. package/src/features/devConnect/devConnectPreferences.ts +33 -0
  107. package/src/features/devConnect/devConnectUtils.ts +59 -0
  108. package/src/features/devConnect/index.ts +64 -0
  109. package/src/features/devConnect/platformDetect.ts +26 -0
  110. package/src/features/devConnect/types.ts +6 -0
  111. package/src/features/network/NetworkLogTab.tsx +6 -3
  112. package/src/index.ts +2 -0
  113. package/src/types/feature.ts +2 -1
  114. package/src/ui/DebugView.tsx +1 -0
  115. package/src/ui/panel/DebugPanel.tsx +1 -23
  116. package/src/utils/DaemonClient.ts +26 -1
  117. package/src/utils/debugPreferences.ts +1 -0
  118. package/src/utils/deviceReport.ts +8 -1
  119. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +0 -495
  120. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +0 -1
  121. package/lib/module/ui/panel/StreamingSettingsModal.js +0 -490
  122. package/lib/module/ui/panel/StreamingSettingsModal.js.map +0 -1
  123. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +0 -8
  124. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +0 -1
  125. package/src/ui/panel/StreamingSettingsModal.tsx +0 -528
@@ -28,15 +28,33 @@ function slugPart(value) {
28
28
  .slice(0, 80) || 'unknown';
29
29
  }
30
30
 
31
+ function ipTail(ip) {
32
+ if (!ip || typeof ip !== 'string') return '0';
33
+ const parts = ip.split('.');
34
+ return parts.length >= 2 ? parts[parts.length - 1] : slugPart(ip);
35
+ }
36
+
37
+ function isSimulatorIp(ip) {
38
+ return ip === '127.0.0.1' || ip === '::1' || ip === '10.0.2.2' || ip === 'localhost';
39
+ }
40
+
31
41
  function createDeviceId(report, source) {
32
42
  const device = report && typeof report === 'object' && report.device && typeof report.device === 'object'
33
43
  ? report.device
34
44
  : {};
35
- return [
36
- slugPart(device.platform),
37
- slugPart(device.model),
38
- slugPart(source && source.ip),
39
- ].join('_');
45
+ const platform = slugPart(device.platform);
46
+ const ip = source && source.ip ? String(source.ip) : '';
47
+ const sim = isSimulatorIp(ip);
48
+ let model = slugPart(device.model);
49
+ if (model === 'unknown' && platform !== 'unknown') {
50
+ model = sim ? 'sim' : 'device';
51
+ }
52
+ const ver = device.appVersion ? slugPart(device.appVersion) : '';
53
+ const tail = sim ? 'sim' : ipTail(ip);
54
+ const parts = [platform, model];
55
+ if (ver && ver !== 'unknown') parts.push(ver);
56
+ parts.push(tail);
57
+ return parts.join('_');
40
58
  }
41
59
 
42
60
  function readPersistedDevices(storagePath, maxDevices) {
@@ -92,6 +110,18 @@ function createMemoryStore(options = {}) {
92
110
  const deviceId = createDeviceId(report, source);
93
111
  const existingIndex = devices.findIndex((item) => item.deviceId === deviceId);
94
112
  const existing = existingIndex >= 0 ? devices[existingIndex] : null;
113
+ const reportSessionId = report.session ? report.session.id : null;
114
+ if (reportSessionId && report.logs) {
115
+ Object.entries(report.logs).forEach(function(pair) {
116
+ if (!Array.isArray(pair[1])) return;
117
+ report.logs[pair[0]] = pair[1].map(function(entry) {
118
+ if (entry && typeof entry === 'object' && !entry.sessionId) {
119
+ return Object.assign({}, entry, { sessionId: reportSessionId });
120
+ }
121
+ return entry;
122
+ });
123
+ });
124
+ }
95
125
  const deviceLog = {
96
126
  deviceId,
97
127
  firstSeenAt: existing ? existing.firstSeenAt : receivedAt,
@@ -99,6 +129,7 @@ function createMemoryStore(options = {}) {
99
129
  receivedAt,
100
130
  source,
101
131
  device: report.device || null,
132
+ session: report.session || null,
102
133
  report,
103
134
  logCount: createLogCount(report),
104
135
  };
@@ -124,6 +155,7 @@ function createMemoryStore(options = {}) {
124
155
  }
125
156
 
126
157
  const deltaLogs = (delta && delta.logs) || {};
158
+ const currentSessionId = deviceLog.session ? deviceLog.session.id : null;
127
159
  Object.entries(deltaLogs).forEach(([type, entries]) => {
128
160
  if (!Array.isArray(entries)) {
129
161
  return;
@@ -131,7 +163,13 @@ function createMemoryStore(options = {}) {
131
163
  if (!deviceLog.report.logs[type]) {
132
164
  deviceLog.report.logs[type] = [];
133
165
  }
134
- deviceLog.report.logs[type].push(...entries);
166
+ const tagged = entries.map(function(entry) {
167
+ if (entry && typeof entry === 'object' && currentSessionId && !entry.sessionId) {
168
+ return Object.assign({}, entry, { sessionId: currentSessionId });
169
+ }
170
+ return entry;
171
+ });
172
+ deviceLog.report.logs[type].push(...tagged);
135
173
  });
136
174
 
137
175
  deviceLog.lastSeenAt = new Date(Date.now()).toISOString();
@@ -153,6 +191,7 @@ function createMemoryStore(options = {}) {
153
191
  receivedAt: deviceLog.receivedAt,
154
192
  device: deviceLog.device || null,
155
193
  source: deviceLog.source || null,
194
+ session: deviceLog.session || null,
156
195
  logCount: deviceLog.logCount,
157
196
  }));
158
197
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "3.1.4",
4
- "description": "A local-first React Native debugging bridge with in-app logs, desktop daemon, Web Console, HTTP API, and MCP support",
3
+ "version": "3.2.0",
4
+ "description": "A local-first React Native debug toolkit with Web Console, HTTP API, and MCP support for AI-readable app logs",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/src/index.d.ts",
@@ -30,8 +30,12 @@
30
30
  "keywords": [
31
31
  "react-native",
32
32
  "debug",
33
+ "log-bridge",
34
+ "web-console",
35
+ "ai-debugging",
33
36
  "toolkit",
34
37
  "http-inspector",
38
+ "mcp",
35
39
  "development-tools",
36
40
  "floating-panel"
37
41
  ],
@@ -48,11 +52,19 @@
48
52
  "peerDependencies": {
49
53
  "@react-native-clipboard/clipboard": ">=1.0.0",
50
54
  "react": ">=18.0.0",
51
- "react-native": ">=0.72.0"
55
+ "react-native": ">=0.72.0",
56
+ "react-native-camera-kit": ">=18.0.0",
57
+ "expo-camera": ">=15.0.0"
52
58
  },
53
59
  "peerDependenciesMeta": {
54
60
  "@react-native-clipboard/clipboard": {
55
61
  "optional": true
62
+ },
63
+ "react-native-camera-kit": {
64
+ "optional": true
65
+ },
66
+ "expo-camera": {
67
+ "optional": true
56
68
  }
57
69
  },
58
70
  "devDependencies": {
@@ -11,6 +11,7 @@ import { createTrackFeature } from '../features/track';
11
11
  import type { TrackFeatureConfig } from '../features/track';
12
12
  import { createEnvironmentFeature } from '../features/environment';
13
13
  import { createClipboardFeature } from '../features/clipboard';
14
+ import { createDevConnectFeature, restoreDevConnectSettingsToDaemon } from '../features/devConnect';
14
15
  import { daemonClient } from '../utils/DaemonClient';
15
16
  import type { AnyDebugFeature, BuiltInFeatureName } from '../types';
16
17
 
@@ -25,6 +26,7 @@ export interface FeatureConfigs {
25
26
  track?: boolean | TrackFeatureConfig;
26
27
  environment?: Parameters<typeof createEnvironmentFeature>[0];
27
28
  clipboard?: boolean;
29
+ devConnect?: boolean;
28
30
  }
29
31
 
30
32
  export interface InitializeOptions {
@@ -43,6 +45,7 @@ const featureRegistry: Record<BuiltInFeatureName, (config?: any) => AnyDebugFeat
43
45
  track: createTrackFeature,
44
46
  environment: createEnvironmentFeature,
45
47
  clipboard: createClipboardFeature,
48
+ devConnect: createDevConnectFeature,
46
49
  };
47
50
 
48
51
  const DEFAULT_FEATURES: BuiltInFeatureName[] = [
@@ -52,6 +55,7 @@ const DEFAULT_FEATURES: BuiltInFeatureName[] = [
52
55
  'zustand',
53
56
  'track',
54
57
  'clipboard',
58
+ 'devConnect',
55
59
  ];
56
60
 
57
61
  function resolveFeatureConfigs(configs: FeatureConfigs): AnyDebugFeature[] {
@@ -119,7 +123,9 @@ export function initializeDebugToolkit(
119
123
  DebugToolkit.hideLauncher();
120
124
  }
121
125
 
122
- daemonClient.restore().catch(() => {});
126
+ restoreDevConnectSettingsToDaemon()
127
+ .then(() => daemonClient.restore(), () => daemonClient.restore())
128
+ .catch(() => {});
123
129
 
124
130
  return DebugToolkit;
125
131
  } catch (error) {
@@ -0,0 +1,122 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ } from 'react-native';
10
+
11
+ import { Colors } from '../../ui/theme/colors';
12
+ import {
13
+ getScannerModule,
14
+ type CameraKitReadCodeEvent,
15
+ type ExpoCameraScanResult,
16
+ } from './cameraKit';
17
+ import { parseMetroQrPayload } from './devConnectUtils';
18
+
19
+ interface DevConnectQrScannerProps {
20
+ visible: boolean;
21
+ onClose: () => void;
22
+ onScanHost: (host: string) => void;
23
+ }
24
+
25
+ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnectQrScannerProps) {
26
+ const scannedRef = useRef(false);
27
+ const [error, setError] = useState<string | null>(null);
28
+ const scanner = getScannerModule();
29
+
30
+ useEffect(() => {
31
+ if (visible) {
32
+ scannedRef.current = false;
33
+ setError(null);
34
+ }
35
+ }, [visible]);
36
+
37
+ const handleScanned = useCallback((rawValue: string) => {
38
+ if (scannedRef.current) return;
39
+ if (typeof rawValue !== 'string') return;
40
+
41
+ const parsed = parseMetroQrPayload(rawValue);
42
+ if (!parsed) {
43
+ setError('QR code does not contain a supported Metro URL.');
44
+ return;
45
+ }
46
+
47
+ scannedRef.current = true;
48
+ setError(null);
49
+ onScanHost(parsed.computerHost);
50
+ onClose();
51
+ }, [onClose, onScanHost]);
52
+
53
+ const handleCameraKitRead = useCallback((event: CameraKitReadCodeEvent) => {
54
+ handleScanned(event.nativeEvent?.codeStringValue ?? '');
55
+ }, [handleScanned]);
56
+
57
+ const handleExpoScanned = useCallback((result: ExpoCameraScanResult) => {
58
+ handleScanned(result.value ?? '');
59
+ }, [handleScanned]);
60
+
61
+ if (!visible || !scanner) return null;
62
+
63
+ return (
64
+ <Modal visible={visible} animationType="slide" onRequestClose={onClose}>
65
+ <View style={styles.container}>
66
+ {scanner.kind === 'camera-kit' && scanner.CameraKit ? (
67
+ <scanner.CameraKit.Camera
68
+ style={styles.camera}
69
+ cameraType={scanner.CameraKit.CameraType?.Back}
70
+ scanBarcode
71
+ onReadCode={handleCameraKitRead}
72
+ showFrame
73
+ laserColor={Colors.primary}
74
+ frameColor={Colors.primary}
75
+ allowedBarcodeTypes={['qr']}
76
+ />
77
+ ) : scanner.kind === 'expo-camera' && scanner.ExpoCamera ? (
78
+ <scanner.ExpoCamera.Camera
79
+ style={styles.camera}
80
+ onBarCodeScanned={handleExpoScanned}
81
+ barCodeScannerSettings={{ barCodeTypes: ['qr'] }}
82
+ />
83
+ ) : null}
84
+ <View style={styles.footer}>
85
+ {error ? <Text style={styles.error}>{error}</Text> : <Text style={styles.hint}>Scan a Metro QR code.</Text>}
86
+ <TouchableOpacity style={styles.closeButton} onPress={onClose} activeOpacity={0.7}>
87
+ <Text style={styles.closeButtonText}>Close</Text>
88
+ </TouchableOpacity>
89
+ </View>
90
+ <Pressable style={styles.topClose} onPress={onClose}>
91
+ <Text style={styles.topCloseText}>Close</Text>
92
+ </Pressable>
93
+ </View>
94
+ </Modal>
95
+ );
96
+ }
97
+
98
+ const styles = StyleSheet.create({
99
+ container: { flex: 1, backgroundColor: '#000' },
100
+ camera: { flex: 1 },
101
+ footer: { padding: 16, backgroundColor: Colors.surface },
102
+ hint: { fontSize: 13, color: Colors.textSecondary, marginBottom: 12 },
103
+ error: { fontSize: 13, color: Colors.error, marginBottom: 12 },
104
+ closeButton: {
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ paddingVertical: 11,
108
+ borderRadius: 10,
109
+ backgroundColor: Colors.primary,
110
+ },
111
+ closeButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
112
+ topClose: {
113
+ position: 'absolute',
114
+ top: 48,
115
+ right: 16,
116
+ paddingHorizontal: 12,
117
+ paddingVertical: 8,
118
+ borderRadius: 8,
119
+ backgroundColor: 'rgba(0,0,0,0.55)',
120
+ },
121
+ topCloseText: { color: '#fff', fontSize: 13, fontWeight: '600' },
122
+ });
@@ -0,0 +1,357 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ KeyboardAvoidingView,
4
+ Platform,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ TextInput,
9
+ TouchableOpacity,
10
+ View,
11
+ } from 'react-native';
12
+
13
+ import type { DebugFeatureRenderProps } from '../../types';
14
+ import { Colors } from '../../ui/theme/colors';
15
+ import { copyToComputer } from '../../utils/copyToComputer';
16
+ import {
17
+ buildDeviceDaemonEndpoint,
18
+ daemonClient,
19
+ getDefaultDaemonEndpoint,
20
+ normalizeDaemonSettings,
21
+ type DaemonSettings,
22
+ } from '../../utils/DaemonClient';
23
+ import { buildMetroUrls, normalizeComputerHost } from './devConnectUtils';
24
+ import { saveComputerHost } from './devConnectPreferences';
25
+ import type { DevConnectState } from './types';
26
+ import { DevConnectQrScanner } from './DevConnectQrScanner';
27
+
28
+ const CONNECTION_TIMEOUT_MS = 2000;
29
+ const METRO_PORT = '8081';
30
+
31
+ type SyncUiState = 'idle' | 'checking' | 'connected' | 'retrying' | 'failed' | 'running';
32
+
33
+ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectState>) {
34
+ const inputRef = useRef<TextInput>(null);
35
+ const [computerHost, setComputerHost] = useState(snapshot.computerHost);
36
+ const [streaming, setStreaming] = useState(snapshot.streaming);
37
+ const [syncState, setSyncState] = useState<SyncUiState>(snapshot.streaming ? 'running' : 'idle');
38
+ const [message, setMessage] = useState<string | null>(null);
39
+ const [sending, setSending] = useState(false);
40
+ const [qrVisible, setQrVisible] = useState(false);
41
+
42
+ const isSim = snapshot.isSimulator;
43
+
44
+ useEffect(() => {
45
+ setComputerHost(snapshot.computerHost);
46
+ setStreaming(snapshot.streaming);
47
+ setSyncState(snapshot.streaming ? 'running' : 'idle');
48
+ }, [snapshot.computerHost, snapshot.streaming]);
49
+
50
+ const metroUrls = isSim
51
+ ? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
52
+ : buildMetroUrls(computerHost);
53
+
54
+ const handleHostChange = useCallback((value: string) => {
55
+ setComputerHost(value);
56
+ const normalized = normalizeComputerHost(value);
57
+ if (normalized) {
58
+ saveComputerHost(normalized).catch(() => {});
59
+ }
60
+ setSyncState((prev) => (prev === 'failed' ? 'idle' : prev));
61
+ setMessage(null);
62
+ }, []);
63
+
64
+ const handleQrHost = useCallback((host: string) => {
65
+ setComputerHost(host);
66
+ saveComputerHost(host).catch(() => {});
67
+ setMessage('Computer IP updated from QR code.');
68
+ }, []);
69
+
70
+ const validateSettings = useCallback((): boolean => {
71
+ if (!isSim && !normalizeComputerHost(computerHost)) {
72
+ setMessage('Enter your computer IP first.');
73
+ return false;
74
+ }
75
+ return true;
76
+ }, [computerHost, isSim]);
77
+
78
+ const configureDaemon = useCallback(() => {
79
+ const normalizedHost = isSim ? '' : (normalizeComputerHost(computerHost) ?? '');
80
+ const settings: DaemonSettings = {
81
+ mode: isSim ? 'simulator' : 'device',
82
+ endpoint: '',
83
+ deviceHost: normalizedHost,
84
+ token: '',
85
+ };
86
+ daemonClient.configure(settings);
87
+ const normalized = normalizeDaemonSettings(settings);
88
+ const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(normalizedHost));
89
+ return { ...normalized, endpoint };
90
+ }, [computerHost, isSim]);
91
+
92
+ const toggleLiveSync = useCallback(async () => {
93
+ if (streaming) {
94
+ daemonClient.disconnect();
95
+ daemonClient.setStreamingEnabled(false);
96
+ setStreaming(false);
97
+ setSyncState('idle');
98
+ setMessage(null);
99
+ return;
100
+ }
101
+
102
+ if (!validateSettings()) return;
103
+
104
+ const daemonOptions = configureDaemon();
105
+ setMessage('Checking desktop connection...');
106
+ setSyncState('checking');
107
+
108
+ const connection = await daemonClient.checkConnection({
109
+ ...daemonOptions,
110
+ timeoutMs: CONNECTION_TIMEOUT_MS,
111
+ });
112
+ if (!connection.ok) {
113
+ setStreaming(false);
114
+ setSyncState('failed');
115
+ setMessage('Cannot reach desktop. Try /health in phone browser.');
116
+ return;
117
+ }
118
+
119
+ daemonClient.setStreamingEnabled(true);
120
+ daemonClient.connect({
121
+ ...daemonOptions,
122
+ timeoutMs: 3000,
123
+ onStatus: (status) => {
124
+ if (status.state === 'connected') {
125
+ setStreaming(true);
126
+ setSyncState('connected');
127
+ setMessage(null);
128
+ } else if (status.state === 'retrying') {
129
+ setSyncState('retrying');
130
+ setMessage('Desktop not reachable. Retrying...');
131
+ } else if (status.state === 'failed') {
132
+ setStreaming(false);
133
+ setSyncState('failed');
134
+ setMessage(status.reason === 'auth' ? 'Desktop token rejected.' : 'Desktop not reachable after multiple retries.');
135
+ } else {
136
+ setSyncState('checking');
137
+ }
138
+ },
139
+ });
140
+ setStreaming(true);
141
+ }, [configureDaemon, streaming, validateSettings]);
142
+
143
+ const sendOnce = useCallback(async () => {
144
+ if (!validateSettings()) return;
145
+
146
+ const daemonOptions = configureDaemon();
147
+ setSending(true);
148
+ setMessage('Checking desktop connection...');
149
+
150
+ try {
151
+ const connection = await daemonClient.checkConnection({
152
+ ...daemonOptions,
153
+ timeoutMs: CONNECTION_TIMEOUT_MS,
154
+ });
155
+ if (!connection.ok) {
156
+ setMessage('Cannot reach desktop. Try /health in phone browser.');
157
+ return;
158
+ }
159
+
160
+ setMessage('Sending logs...');
161
+ const result = await daemonClient.reportOnce({
162
+ ...daemonOptions,
163
+ timeoutMs: 2000,
164
+ });
165
+
166
+ if (result.ok) {
167
+ const totalLogs = Object.values(result.logCount ?? {}).reduce((total, count) => total + count, 0);
168
+ setMessage(`Sent ${totalLogs} logs.`);
169
+ } else {
170
+ setMessage(result.error ? `Send failed: ${result.error}` : 'Send failed.');
171
+ }
172
+ } finally {
173
+ setSending(false);
174
+ }
175
+ }, [configureDaemon, validateSettings]);
176
+
177
+ const copyUrl = useCallback((label: string, url: string) => {
178
+ copyToComputer(url, { label });
179
+ setMessage('Copied to computer output.');
180
+ setTimeout(() => setMessage(null), 1500);
181
+ }, []);
182
+
183
+ const canConnect = isSim || Boolean(normalizeComputerHost(computerHost));
184
+ const busy = sending || syncState === 'checking';
185
+
186
+ return (
187
+ <KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
188
+ <ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
189
+
190
+ {isSim ? (
191
+ <View style={styles.badge}>
192
+ <Text style={styles.badgeText}>Simulator — using localhost</Text>
193
+ </View>
194
+ ) : (
195
+ <View style={styles.section}>
196
+ <Text style={styles.label}>Computer IP</Text>
197
+ <View style={styles.inputRow}>
198
+ <TextInput
199
+ ref={inputRef}
200
+ style={styles.input}
201
+ value={computerHost}
202
+ onChangeText={handleHostChange}
203
+ placeholder="192.168.1.10"
204
+ placeholderTextColor={Colors.textLight}
205
+ autoCapitalize="none"
206
+ autoCorrect={false}
207
+ keyboardType="numbers-and-punctuation"
208
+ returnKeyType="done"
209
+ onSubmitEditing={() => inputRef.current?.blur()}
210
+ editable={!streaming}
211
+ />
212
+ {snapshot.qrAvailable ? (
213
+ <TouchableOpacity style={styles.scanButton} onPress={() => setQrVisible(true)} disabled={streaming} activeOpacity={0.7}>
214
+ <Text style={styles.scanButtonText}>Scan</Text>
215
+ </TouchableOpacity>
216
+ ) : null}
217
+ </View>
218
+ </View>
219
+ )}
220
+
221
+ <View style={styles.actions}>
222
+ <TouchableOpacity
223
+ style={[styles.primaryButton, (!canConnect || busy) && styles.buttonDisabled]}
224
+ onPress={toggleLiveSync}
225
+ disabled={!canConnect || busy}
226
+ activeOpacity={0.75}
227
+ >
228
+ <Text style={styles.primaryButtonText}>
229
+ {streaming ? 'Stop' : busy ? 'Checking...' : 'Live Sync'}
230
+ </Text>
231
+ </TouchableOpacity>
232
+ <TouchableOpacity
233
+ style={[styles.secondaryButton, (!canConnect || busy) && styles.buttonDisabled]}
234
+ onPress={sendOnce}
235
+ disabled={!canConnect || busy}
236
+ activeOpacity={0.75}
237
+ >
238
+ <Text style={styles.secondaryButtonText}>
239
+ {sending ? 'Sending...' : 'Send Once'}
240
+ </Text>
241
+ </TouchableOpacity>
242
+ </View>
243
+
244
+ {message ? <Text style={styles.message}>{message}</Text> : null}
245
+
246
+ <View style={styles.section}>
247
+ <Text style={styles.sectionTitle}>Metro Bundler</Text>
248
+ {metroUrls ? (
249
+ <>
250
+ <View style={styles.urlRow}>
251
+ <Text style={styles.urlText} numberOfLines={1}>{metroUrls.expUrl}</Text>
252
+ <TouchableOpacity style={styles.copyButton} onPress={() => copyUrl('Metro exp URL', metroUrls.expUrl)} activeOpacity={0.7}>
253
+ <Text style={styles.copyButtonText}>Copy</Text>
254
+ </TouchableOpacity>
255
+ </View>
256
+ <View style={styles.urlRow}>
257
+ <Text style={styles.urlText} numberOfLines={1}>{metroUrls.httpUrl}</Text>
258
+ <TouchableOpacity style={styles.copyButton} onPress={() => copyUrl('Metro HTTP URL', metroUrls.httpUrl)} activeOpacity={0.7}>
259
+ <Text style={styles.copyButtonText}>Copy</Text>
260
+ </TouchableOpacity>
261
+ </View>
262
+ </>
263
+ ) : (
264
+ <Text style={styles.hint}>Enter a computer IP to show Metro URLs.</Text>
265
+ )}
266
+ </View>
267
+ </ScrollView>
268
+ <DevConnectQrScanner
269
+ visible={qrVisible}
270
+ onClose={() => setQrVisible(false)}
271
+ onScanHost={handleQrHost}
272
+ />
273
+ </KeyboardAvoidingView>
274
+ );
275
+ }
276
+
277
+ const styles = StyleSheet.create({
278
+ container: { flex: 1 },
279
+ scrollContent: { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 24 },
280
+ badge: {
281
+ paddingHorizontal: 12,
282
+ paddingVertical: 8,
283
+ borderRadius: 8,
284
+ backgroundColor: `${Colors.primary}15`,
285
+ borderWidth: 1,
286
+ borderColor: `${Colors.primary}30`,
287
+ marginBottom: 14,
288
+ },
289
+ badgeText: { fontSize: 13, fontWeight: '500', color: Colors.primary },
290
+ section: { marginBottom: 14 },
291
+ sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 8 },
292
+ label: { fontSize: 13, fontWeight: '500', color: Colors.textSecondary, marginBottom: 6 },
293
+ inputRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
294
+ input: {
295
+ flex: 1,
296
+ backgroundColor: Colors.surface,
297
+ borderWidth: 1,
298
+ borderColor: Colors.border,
299
+ borderRadius: 8,
300
+ paddingHorizontal: 12,
301
+ paddingVertical: 10,
302
+ fontSize: 14,
303
+ color: Colors.text,
304
+ fontFamily: 'Courier',
305
+ },
306
+ scanButton: {
307
+ minWidth: 62,
308
+ alignItems: 'center',
309
+ justifyContent: 'center',
310
+ paddingHorizontal: 12,
311
+ paddingVertical: 10,
312
+ borderRadius: 8,
313
+ backgroundColor: Colors.surface,
314
+ borderWidth: 1,
315
+ borderColor: Colors.primary,
316
+ },
317
+ scanButtonText: { color: Colors.primary, fontSize: 13, fontWeight: '600' },
318
+ actions: { flexDirection: 'row', gap: 10, marginTop: 4, marginBottom: 12 },
319
+ primaryButton: {
320
+ flex: 1,
321
+ alignItems: 'center',
322
+ justifyContent: 'center',
323
+ paddingVertical: 11,
324
+ borderRadius: 10,
325
+ backgroundColor: Colors.primary,
326
+ },
327
+ primaryButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
328
+ secondaryButton: {
329
+ flex: 1,
330
+ alignItems: 'center',
331
+ justifyContent: 'center',
332
+ paddingVertical: 11,
333
+ borderRadius: 10,
334
+ backgroundColor: Colors.surface,
335
+ borderWidth: 1,
336
+ borderColor: Colors.border,
337
+ },
338
+ secondaryButtonText: { color: Colors.primary, fontSize: 14, fontWeight: '600' },
339
+ buttonDisabled: { opacity: 0.5 },
340
+ message: { fontSize: 12, lineHeight: 17, color: Colors.textSecondary, marginBottom: 12 },
341
+ hint: { fontSize: 12, color: Colors.textLight },
342
+ urlRow: {
343
+ flexDirection: 'row',
344
+ alignItems: 'center',
345
+ backgroundColor: Colors.surface,
346
+ borderWidth: 1,
347
+ borderColor: Colors.border,
348
+ borderRadius: 8,
349
+ paddingLeft: 12,
350
+ paddingRight: 4,
351
+ paddingVertical: 4,
352
+ marginBottom: 8,
353
+ },
354
+ urlText: { flex: 1, fontSize: 13, fontFamily: 'Courier', color: Colors.text, paddingVertical: 6 },
355
+ copyButton: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 6, backgroundColor: Colors.primary },
356
+ copyButtonText: { color: '#fff', fontSize: 12, fontWeight: '600' },
357
+ });