react-native-debug-toolkit 2.3.0 → 3.1.2

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 (93) hide show
  1. package/README.md +115 -97
  2. package/README.zh-CN.md +113 -95
  3. package/bin/debug-toolkit.js +114 -0
  4. package/lib/commonjs/core/initialize.js +5 -0
  5. package/lib/commonjs/core/initialize.js.map +1 -1
  6. package/lib/commonjs/features/network/index.js +28 -2
  7. package/lib/commonjs/features/network/index.js.map +1 -1
  8. package/lib/commonjs/features/network/networkInterceptor.js +14 -6
  9. package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
  10. package/lib/commonjs/index.js +56 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
  13. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  14. package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
  15. package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
  16. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
  17. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
  18. package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
  19. package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
  20. package/lib/commonjs/utils/DaemonClient.js +721 -0
  21. package/lib/commonjs/utils/DaemonClient.js.map +1 -0
  22. package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
  23. package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
  24. package/lib/commonjs/utils/deviceReport.js +132 -0
  25. package/lib/commonjs/utils/deviceReport.js.map +1 -0
  26. package/lib/module/core/initialize.js +6 -0
  27. package/lib/module/core/initialize.js.map +1 -1
  28. package/lib/module/features/network/index.js +25 -1
  29. package/lib/module/features/network/index.js.map +1 -1
  30. package/lib/module/features/network/networkInterceptor.js +14 -6
  31. package/lib/module/features/network/networkInterceptor.js.map +1 -1
  32. package/lib/module/index.js +3 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/panel/DebugPanel.js +26 -1
  35. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  36. package/lib/module/ui/panel/FloatPanelView.js +16 -63
  37. package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
  38. package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
  39. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
  40. package/lib/module/ui/panel/useTabAnimation.js +67 -0
  41. package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
  42. package/lib/module/utils/DaemonClient.js +703 -0
  43. package/lib/module/utils/DaemonClient.js.map +1 -0
  44. package/lib/module/utils/createPersistedObservableStore.js +23 -3
  45. package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
  46. package/lib/module/utils/deviceReport.js +128 -0
  47. package/lib/module/utils/deviceReport.js.map +1 -0
  48. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  49. package/lib/typescript/src/features/network/index.d.ts +2 -0
  50. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  51. package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
  52. package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
  53. package/lib/typescript/src/index.d.ts +5 -0
  54. package/lib/typescript/src/index.d.ts.map +1 -1
  55. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  56. package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
  57. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
  58. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
  59. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
  60. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
  61. package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
  62. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
  63. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
  64. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
  65. package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
  66. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
  67. package/node/daemon/src/cli.js +82 -0
  68. package/node/daemon/src/console/console.html +1662 -0
  69. package/node/daemon/src/console/index.js +47 -0
  70. package/node/daemon/src/constants.js +38 -0
  71. package/node/daemon/src/index.js +11 -0
  72. package/node/daemon/src/server.js +447 -0
  73. package/node/daemon/src/store.js +187 -0
  74. package/node/mcp/src/cli.js +31 -0
  75. package/node/mcp/src/constants.js +13 -0
  76. package/node/mcp/src/daemonClient.js +132 -0
  77. package/node/mcp/src/httpClient.js +49 -0
  78. package/node/mcp/src/index.js +15 -0
  79. package/node/mcp/src/logs.js +96 -0
  80. package/node/mcp/src/server.js +144 -0
  81. package/node/mcp/src/tools.js +84 -0
  82. package/package.json +8 -3
  83. package/src/core/initialize.ts +8 -0
  84. package/src/features/network/index.ts +30 -3
  85. package/src/features/network/networkInterceptor.ts +19 -6
  86. package/src/index.ts +22 -0
  87. package/src/ui/panel/DebugPanel.tsx +23 -1
  88. package/src/ui/panel/FloatPanelView.tsx +10 -68
  89. package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
  90. package/src/ui/panel/useTabAnimation.ts +77 -0
  91. package/src/utils/DaemonClient.ts +887 -0
  92. package/src/utils/createPersistedObservableStore.ts +16 -3
  93. package/src/utils/deviceReport.ts +203 -0
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const { getDaemonOrigin, readDevice, readDevices } = require('./daemonClient');
4
+ const { KNOWN_LOG_TYPES, createToolPayload } = require('./logs');
5
+
6
+ const getAppLogsTool = {
7
+ name: 'get_app_logs',
8
+ description: 'Read React Native Debug Toolkit logs from the local daemon. Tip: if you have shell access, curl http://127.0.0.1:3799/devices/latest is more efficient.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ deviceId: { type: 'string' },
13
+ logType: {
14
+ type: 'string',
15
+ enum: KNOWN_LOG_TYPES,
16
+ },
17
+ limit: { type: 'number', default: 50 },
18
+ failedOnly: { type: 'boolean', default: false },
19
+ includeBodies: { type: 'boolean', default: true },
20
+ },
21
+ },
22
+ };
23
+
24
+ const listAppDevicesTool = {
25
+ name: 'list_app_devices',
26
+ description: 'List React Native Debug Toolkit devices available in the local daemon. Tip: if you have shell access, curl http://127.0.0.1:3799/devices is more efficient.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {},
30
+ },
31
+ };
32
+
33
+ const tools = [getAppLogsTool, listAppDevicesTool];
34
+
35
+ async function callTool(name, args = {}, context = {}) {
36
+ const ensureDaemon = context.ensureDaemon || (async () => ({ ok: true, origin: getDaemonOrigin() }));
37
+ const daemon = await ensureDaemon();
38
+ if (!daemon.ok) {
39
+ return {
40
+ ok: false,
41
+ error: daemon.error || 'Debug toolkit daemon is not available',
42
+ origin: daemon.origin,
43
+ };
44
+ }
45
+
46
+ try {
47
+ if (name === listAppDevicesTool.name) {
48
+ const readDevicesImpl = context.readDevices || readDevices;
49
+ const result = await readDevicesImpl(daemon.origin);
50
+ const devices = Array.isArray(result.devices) ? result.devices : [];
51
+ return {
52
+ ok: true,
53
+ origin: daemon.origin,
54
+ devices,
55
+ count: devices.length,
56
+ };
57
+ }
58
+
59
+ if (name !== getAppLogsTool.name) {
60
+ throw new Error(`Unknown tool: ${name}`);
61
+ }
62
+
63
+ const device = await readDevice(daemon.origin, args.deviceId);
64
+ return createToolPayload(device, {
65
+ logType: args.logType,
66
+ limit: args.limit,
67
+ failedOnly: args.failedOnly,
68
+ includeBodies: args.includeBodies,
69
+ });
70
+ } catch (error) {
71
+ return {
72
+ ok: false,
73
+ error: error.message || 'Failed to read debug toolkit logs',
74
+ origin: daemon.origin,
75
+ };
76
+ }
77
+ }
78
+
79
+ module.exports = {
80
+ callTool,
81
+ getAppLogsTool,
82
+ listAppDevicesTool,
83
+ tools,
84
+ };
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "2.3.0",
4
- "description": "A dev-only floating debug panel for React Native with network, console, Zustand, navigation, and event logs",
3
+ "version": "3.1.2",
4
+ "description": "A local-first React Native debugging bridge with in-app logs, desktop daemon, Web Console, HTTP API, and MCP support",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/src/index.d.ts",
8
8
  "files": [
9
9
  "src",
10
10
  "lib",
11
+ "bin",
12
+ "node",
11
13
  "README.md",
12
14
  "LICENSE",
13
15
  "!**/__tests__",
@@ -19,9 +21,12 @@
19
21
  "typescript": "npm run typecheck",
20
22
  "test": "jest",
21
23
  "build": "bob build",
22
- "lint": "eslint \"src/**/*.{js,ts,tsx}\"",
24
+ "lint": "eslint \"src/**/*.{js,ts,tsx}\" \"node/**/*.js\" \"bin/**/*.js\"",
23
25
  "prepare": "bob build"
24
26
  },
27
+ "bin": {
28
+ "debug-toolkit": "bin/debug-toolkit.js"
29
+ },
25
30
  "keywords": [
26
31
  "react-native",
27
32
  "debug",
@@ -11,10 +11,16 @@ 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 { daemonClient, restoreDaemonStreaming } from '../utils/DaemonClient';
15
+ import { _addDaemonEndpointToNetworkBlacklist } from '../features/network';
14
16
  import type { AnyDebugFeature, BuiltInFeatureName } from '../types';
15
17
 
16
18
  const isDebugMode = __DEV__;
17
19
 
20
+ daemonClient.setEndpointDetector((url) => {
21
+ _addDaemonEndpointToNetworkBlacklist(url);
22
+ });
23
+
18
24
  /** Feature-specific configuration map */
19
25
  export interface FeatureConfigs {
20
26
  network?: boolean | NetworkFeatureConfig;
@@ -114,6 +120,8 @@ export function initializeDebugToolkit(
114
120
  DebugToolkit.hideLauncher();
115
121
  }
116
122
 
123
+ restoreDaemonStreaming().catch(() => {});
124
+
117
125
  return DebugToolkit;
118
126
  } catch (error) {
119
127
  console.error('[DebugToolkit] Initialization failed:', error);
@@ -9,8 +9,7 @@ import {
9
9
  startXMLHttpRequest,
10
10
  resetInterceptors,
11
11
  } from './networkInterceptor';
12
-
13
- type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
12
+ import type { NetworkLogPayload } from './networkInterceptor';
14
13
 
15
14
  // ─── Utilities ────────────────────────────────────────
16
15
 
@@ -37,6 +36,7 @@ function emitNetworkLog(entry: NetworkLogPayload): void {
37
36
  // ─── Feature factory ──────────────────────────────────
38
37
 
39
38
  const DEFAULT_MAX_LOGS = 200;
39
+ const daemonEndpointBlacklist: Array<string | RegExp> = [];
40
40
 
41
41
  export interface NetworkFeatureConfig {
42
42
  /** Maximum number of network logs to keep (default: 200) */
@@ -57,7 +57,7 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
57
57
  let stopXhrFn: (() => void) | null = null;
58
58
 
59
59
  const handleLog = (entry: NetworkLogPayload) => {
60
- if (isUrlBlacklisted(entry.request.url, blacklist)) {
60
+ if (isUrlBlacklisted(entry.request.url, [...blacklist, ...daemonEndpointBlacklist])) {
61
61
  return;
62
62
  }
63
63
  logStore.push({ ...entry, id: logStore.nextId() }, maxLogs);
@@ -95,8 +95,35 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
95
95
  };
96
96
  };
97
97
 
98
+ function normalizeDaemonEndpoint(endpoint: string): string {
99
+ const trimmed = endpoint.trim().replace(/\/+$/, '');
100
+ if (!trimmed) {
101
+ return trimmed;
102
+ }
103
+
104
+ try {
105
+ const url = new URL(trimmed);
106
+ return `${url.origin}${url.pathname === '/' ? '' : url.pathname}`;
107
+ } catch {
108
+ return trimmed;
109
+ }
110
+ }
111
+
112
+ export function _addDaemonEndpointToNetworkBlacklist(endpoint: string): void {
113
+ const normalized = normalizeDaemonEndpoint(endpoint);
114
+ if (!normalized || daemonEndpointBlacklist.includes(normalized)) {
115
+ return;
116
+ }
117
+ daemonEndpointBlacklist.push(normalized);
118
+ }
119
+
120
+ export function _isNetworkUrlBlacklistedForTesting(url: string): boolean {
121
+ return isUrlBlacklisted(url, daemonEndpointBlacklist);
122
+ }
123
+
98
124
  /** Reset module-level state for testing */
99
125
  export function _resetNetworkForTesting(): void {
100
126
  networkChannel = createEventChannel<NetworkLogPayload>();
127
+ daemonEndpointBlacklist.splice(0, daemonEndpointBlacklist.length);
101
128
  resetInterceptors();
102
129
  }
@@ -3,6 +3,8 @@ import { urlRewriter } from '../../utils/urlRewriterRegistry';
3
3
 
4
4
  type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
5
5
 
6
+ export type { NetworkLogPayload };
7
+
6
8
  // Intercepts React Native's XMLHttpRequest transport layer.
7
9
  // RN fetch and axios (default adapter) both go through XHR — one hook captures everything.
8
10
 
@@ -113,6 +115,15 @@ function safeRead<T>(read: () => T): T | undefined {
113
115
  }
114
116
  }
115
117
 
118
+ // URLs matching these patterns are skipped (Metro dev server internals).
119
+ const IGNORED_URL_PATTERNS = [
120
+ /\/symbolicate$/,
121
+ ];
122
+
123
+ function shouldIgnoreUrl(url: string): boolean {
124
+ return IGNORED_URL_PATTERNS.some((p) => p.test(url));
125
+ }
126
+
116
127
  function getGlobalXMLHttpRequest(): XMLHttpRequestConstructorLike | undefined {
117
128
  return (globalThis as { XMLHttpRequest?: XMLHttpRequestConstructorLike }).XMLHttpRequest;
118
129
  }
@@ -183,6 +194,9 @@ export function startXMLHttpRequest(
183
194
  ...args: unknown[]
184
195
  ) {
185
196
  const rewrittenUrl = urlRewriter.get() ? rewriteUrl(url) : url;
197
+ if (shouldIgnoreUrl(rewrittenUrl)) {
198
+ return originalXhrOpen!.call(this, method, rewrittenUrl, ...args);
199
+ }
186
200
  pendingXhrRequests.set(this, {
187
201
  method: (method || 'GET').toUpperCase(),
188
202
  url: rewrittenUrl,
@@ -209,12 +223,11 @@ export function startXMLHttpRequest(
209
223
  body?: unknown,
210
224
  ) {
211
225
  const that = this;
212
- const state = pendingXhrRequests.get(that) ?? {
213
- method: 'GET',
214
- url: '',
215
- headers: {},
216
- timestamp: Date.now(),
217
- };
226
+ const existingState = pendingXhrRequests.get(that);
227
+ if (!existingState) {
228
+ return originalXhrSend!.call(that, body);
229
+ }
230
+ const state = existingState;
218
231
  state.body = body;
219
232
  state.timestamp = Date.now();
220
233
  pendingXhrRequests.set(that, state);
package/src/index.ts CHANGED
@@ -28,6 +28,28 @@ export { useNavigationLogger } from './features/navigation/useNavigationLogger';
28
28
  export { safeStringify } from './utils/safeStringify';
29
29
  export { copyToComputer, logToComputer, fmt } from './utils/copyToComputer';
30
30
  export type { CopyResult, CopyOptions, CopyMethod } from './utils/copyToComputer';
31
+ export { createDebugDeviceReport } from './utils/deviceReport';
32
+ export type { DebugDeviceReport, DebugDeviceReportOptions } from './utils/deviceReport';
33
+ export { DaemonClient, daemonClient } from './utils/DaemonClient';
34
+ export type {
35
+ DaemonSettings,
36
+ DaemonConnectionMode,
37
+ DaemonConnectionFailureReason,
38
+ DaemonConnectionOptions,
39
+ DaemonConnectionResult,
40
+ StreamStatus,
41
+ StreamToDaemonOptions,
42
+ ReportResult,
43
+ ReportToDaemonOptions,
44
+ } from './utils/DaemonClient';
45
+ export {
46
+ getDefaultDaemonEndpoint,
47
+ reportDebugDeviceToDaemon,
48
+ checkDaemonConnection,
49
+ startStreaming,
50
+ stopStreaming,
51
+ isStreaming,
52
+ } from './utils/DaemonClient';
31
53
 
32
54
  // Types
33
55
  export type {
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -10,6 +10,7 @@ import {
10
10
  useWindowDimensions,
11
11
  } from 'react-native';
12
12
  import { Colors } from '../theme/colors';
13
+ import { StreamingSettingsModal } from './StreamingSettingsModal';
13
14
 
14
15
  interface DebugPanelProps {
15
16
  onClose: () => void;
@@ -21,6 +22,7 @@ export function DebugPanel({ onClose, onClearAll, children }: DebugPanelProps) {
21
22
  const { height: screenHeight } = useWindowDimensions();
22
23
  const panelTranslateY = useRef(new Animated.Value(screenHeight)).current;
23
24
  const backdropOpacity = useRef(new Animated.Value(0)).current;
25
+ const [settingsVisible, setSettingsVisible] = useState(false);
24
26
 
25
27
  useEffect(() => {
26
28
  requestAnimationFrame(() => {
@@ -101,6 +103,13 @@ export function DebugPanel({ onClose, onClearAll, children }: DebugPanelProps) {
101
103
  <View style={styles.header}>
102
104
  <Text style={styles.headerTitle}>Debug Toolkit</Text>
103
105
  <View style={styles.headerButtons}>
106
+ <TouchableOpacity
107
+ onPress={() => setSettingsVisible(true)}
108
+ style={styles.settingsButton}
109
+ activeOpacity={0.6}
110
+ >
111
+ <Text style={styles.settingsButtonText}>⚙</Text>
112
+ </TouchableOpacity>
104
113
  <TouchableOpacity
105
114
  onPress={() => {
106
115
  onClearAll();
@@ -119,6 +128,7 @@ export function DebugPanel({ onClose, onClearAll, children }: DebugPanelProps) {
119
128
  </View>
120
129
  <View style={styles.panelContent}>{children}</View>
121
130
  </Animated.View>
131
+ <StreamingSettingsModal visible={settingsVisible} onClose={() => setSettingsVisible(false)} />
122
132
  </View>
123
133
  );
124
134
  }
@@ -198,6 +208,18 @@ const styles = StyleSheet.create({
198
208
  fontSize: 14,
199
209
  fontWeight: '500',
200
210
  },
211
+ settingsButton: {
212
+ width: 32,
213
+ height: 32,
214
+ borderRadius: 16,
215
+ backgroundColor: Colors.background,
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ },
219
+ settingsButtonText: {
220
+ fontSize: 16,
221
+ color: Colors.textSecondary,
222
+ },
201
223
  closeButton: {
202
224
  width: 30,
203
225
  height: 30,
@@ -4,8 +4,6 @@ import {
4
4
  Text,
5
5
  StyleSheet,
6
6
  Animated,
7
- PanResponder,
8
- Easing,
9
7
  } from 'react-native';
10
8
  import type { AnyDebugFeature } from '../../types';
11
9
  import { getPreference, setPreference, KEYS } from '../../utils/debugPreferences';
@@ -13,6 +11,7 @@ import { FloatIcon } from '../floating/FloatIcon';
13
11
  import { DebugPanel } from './DebugPanel';
14
12
  import { FeatureTabBar } from './FeatureTabBar';
15
13
  import type { TabItem } from './FeatureTabBar';
14
+ import { useTabAnimation } from './useTabAnimation';
16
15
 
17
16
  // ─── Error Boundary ────────────────────────────────────
18
17
  interface ErrorBoundaryState {
@@ -66,34 +65,14 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
66
65
  return () => { mounted = false; };
67
66
  }, []);
68
67
 
69
- // Content slide animation
70
- const contentOpacity = useRef(new Animated.Value(1)).current;
71
- const contentTranslateX = useRef(new Animated.Value(0)).current;
72
- const isSwitchingTab = useRef(false);
73
-
74
- // Refs to avoid stale closures in PanResponder
75
- const activeTabRef = useRef(0);
76
- activeTabRef.current = activeTab;
77
- const featuresLengthRef = useRef(features.length);
78
- featuresLengthRef.current = features.length;
79
- const switchTabRef = useRef<(index: number) => void>(() => {});
80
-
81
- // Swipe-to-switch responder
82
- const swipeResponder = useRef(
83
- PanResponder.create({
84
- onStartShouldSetPanResponder: () => false,
85
- onMoveShouldSetPanResponder: (_, gs) => {
86
- if (isSwitchingTab.current) return false;
87
- return Math.abs(gs.dx) > 25 && Math.abs(gs.dx) > Math.abs(gs.dy) * 2.5;
88
- },
89
- onPanResponderRelease: (_, gs) => {
90
- const tab = activeTabRef.current;
91
- if (gs.dx < -40 && tab < featuresLengthRef.current - 1) switchTabRef.current(tab + 1);
92
- else if (gs.dx > 40 && tab > 0) switchTabRef.current(tab - 1);
93
- },
94
- onPanResponderTerminationRequest: () => true,
95
- }),
96
- ).current;
68
+ const { contentOpacity, contentTranslateX, panHandlers, switchTab } = useTabAnimation({
69
+ activeTab,
70
+ tabCount: features.length,
71
+ onTabChange: useCallback((index: number) => {
72
+ setActiveTab(index);
73
+ setPreference(KEYS.lastTab, String(index));
74
+ }, []),
75
+ });
97
76
 
98
77
  // Feature subscription → re-render on data changes
99
78
  const [, setTick] = useState(0);
@@ -122,43 +101,6 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
122
101
  }
123
102
  }, [features.length, activeTab]);
124
103
 
125
- // Tab switching with content animation
126
- const switchTab = useCallback(
127
- (index: number) => {
128
- if (isSwitchingTab.current || index === activeTabRef.current) return;
129
- isSwitchingTab.current = true;
130
- const direction = index > activeTabRef.current ? 1 : -1;
131
-
132
- Animated.parallel([
133
- Animated.timing(contentOpacity, { toValue: 0, duration: 80, useNativeDriver: true }),
134
- Animated.timing(contentTranslateX, {
135
- toValue: -direction * 40,
136
- duration: 80,
137
- useNativeDriver: true,
138
- }),
139
- ]).start(() => {
140
- setActiveTab(index);
141
- setPreference(KEYS.lastTab, String(index));
142
- contentTranslateX.setValue(direction * 40);
143
- Animated.parallel([
144
- Animated.timing(contentOpacity, { toValue: 1, duration: 150, useNativeDriver: true }),
145
- Animated.timing(contentTranslateX, {
146
- toValue: 0,
147
- duration: 200,
148
- easing: Easing.out(Easing.cubic),
149
- useNativeDriver: true,
150
- }),
151
- ]).start(() => {
152
- isSwitchingTab.current = false;
153
- });
154
- });
155
- },
156
- [contentOpacity, contentTranslateX],
157
- );
158
-
159
- // Keep ref in sync
160
- switchTabRef.current = switchTab;
161
-
162
104
  // Badge (first feature that returns one)
163
105
  const envBadge = features.map((f) => f.badge?.()).find((b) => b != null) ?? null;
164
106
  const tabs: TabItem[] = features.map((f) => ({ label: f.label, id: f.name }));
@@ -195,7 +137,7 @@ export function FloatPanelView({ features, panelOpen, onOpenPanel, onClosePanel,
195
137
  styles.contentContainer,
196
138
  { opacity: contentOpacity, transform: [{ translateX: contentTranslateX }] },
197
139
  ]}
198
- {...swipeResponder.panHandlers}
140
+ {...panHandlers}
199
141
  >
200
142
  {renderFeatureContent()}
201
143
  </Animated.View>