react-native-ai-devtools 1.1.4

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 (147) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +1250 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +108 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  8. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  9. package/build/__tests__/integration/connection-health.test.js +151 -0
  10. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  11. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  12. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/execute-in-app.test.js +115 -0
  14. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.d.ts +2 -0
  16. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/tools.test.js +228 -0
  18. package/build/__tests__/integration/tools.test.js.map +1 -0
  19. package/build/__tests__/setup.d.ts +2 -0
  20. package/build/__tests__/setup.d.ts.map +1 -0
  21. package/build/__tests__/setup.js +11 -0
  22. package/build/__tests__/setup.js.map +1 -0
  23. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  24. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/bundle.test.js +53 -0
  26. package/build/__tests__/unit/bundle.test.js.map +1 -0
  27. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  28. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/connection-health.test.js +28 -0
  30. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  31. package/build/__tests__/unit/executor.test.d.ts +2 -0
  32. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/executor.test.js +79 -0
  34. package/build/__tests__/unit/executor.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.d.ts +2 -0
  36. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  37. package/build/__tests__/unit/logs.test.js +81 -0
  38. package/build/__tests__/unit/logs.test.js.map +1 -0
  39. package/build/__tests__/unit/metro.test.d.ts +2 -0
  40. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/metro.test.js +61 -0
  42. package/build/__tests__/unit/metro.test.js.map +1 -0
  43. package/build/__tests__/unit/network.test.d.ts +2 -0
  44. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/network.test.js +102 -0
  46. package/build/__tests__/unit/network.test.js.map +1 -0
  47. package/build/__tests__/unit/tap.test.d.ts +2 -0
  48. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/tap.test.js +157 -0
  50. package/build/__tests__/unit/tap.test.js.map +1 -0
  51. package/build/core/android.d.ts +265 -0
  52. package/build/core/android.d.ts.map +1 -0
  53. package/build/core/android.js +1413 -0
  54. package/build/core/android.js.map +1 -0
  55. package/build/core/bundle.d.ts +49 -0
  56. package/build/core/bundle.d.ts.map +1 -0
  57. package/build/core/bundle.js +368 -0
  58. package/build/core/bundle.js.map +1 -0
  59. package/build/core/connection.d.ts +43 -0
  60. package/build/core/connection.d.ts.map +1 -0
  61. package/build/core/connection.js +963 -0
  62. package/build/core/connection.js.map +1 -0
  63. package/build/core/connectionState.d.ts +108 -0
  64. package/build/core/connectionState.d.ts.map +1 -0
  65. package/build/core/connectionState.js +284 -0
  66. package/build/core/connectionState.js.map +1 -0
  67. package/build/core/errorScreenParser.d.ts +30 -0
  68. package/build/core/errorScreenParser.d.ts.map +1 -0
  69. package/build/core/errorScreenParser.js +198 -0
  70. package/build/core/errorScreenParser.js.map +1 -0
  71. package/build/core/executor.d.ts +113 -0
  72. package/build/core/executor.d.ts.map +1 -0
  73. package/build/core/executor.js +1877 -0
  74. package/build/core/executor.js.map +1 -0
  75. package/build/core/format.d.ts +8 -0
  76. package/build/core/format.d.ts.map +1 -0
  77. package/build/core/format.js +34 -0
  78. package/build/core/format.js.map +1 -0
  79. package/build/core/guides.d.ts +14 -0
  80. package/build/core/guides.d.ts.map +1 -0
  81. package/build/core/guides.js +261 -0
  82. package/build/core/guides.js.map +1 -0
  83. package/build/core/httpServer.d.ts +14 -0
  84. package/build/core/httpServer.d.ts.map +1 -0
  85. package/build/core/httpServer.js +2459 -0
  86. package/build/core/httpServer.js.map +1 -0
  87. package/build/core/httpServerProcess.d.ts +25 -0
  88. package/build/core/httpServerProcess.d.ts.map +1 -0
  89. package/build/core/httpServerProcess.js +153 -0
  90. package/build/core/httpServerProcess.js.map +1 -0
  91. package/build/core/index.d.ts +25 -0
  92. package/build/core/index.d.ts.map +1 -0
  93. package/build/core/index.js +53 -0
  94. package/build/core/index.js.map +1 -0
  95. package/build/core/ios.d.ts +214 -0
  96. package/build/core/ios.d.ts.map +1 -0
  97. package/build/core/ios.js +1232 -0
  98. package/build/core/ios.js.map +1 -0
  99. package/build/core/logs.d.ts +43 -0
  100. package/build/core/logs.d.ts.map +1 -0
  101. package/build/core/logs.js +144 -0
  102. package/build/core/logs.js.map +1 -0
  103. package/build/core/metro.d.ts +23 -0
  104. package/build/core/metro.d.ts.map +1 -0
  105. package/build/core/metro.js +96 -0
  106. package/build/core/metro.js.map +1 -0
  107. package/build/core/network.d.ts +43 -0
  108. package/build/core/network.d.ts.map +1 -0
  109. package/build/core/network.js +217 -0
  110. package/build/core/network.js.map +1 -0
  111. package/build/core/networkInterceptor.d.ts +3 -0
  112. package/build/core/networkInterceptor.d.ts.map +1 -0
  113. package/build/core/networkInterceptor.js +203 -0
  114. package/build/core/networkInterceptor.js.map +1 -0
  115. package/build/core/ocr.d.ts +69 -0
  116. package/build/core/ocr.d.ts.map +1 -0
  117. package/build/core/ocr.js +212 -0
  118. package/build/core/ocr.js.map +1 -0
  119. package/build/core/state.d.ts +17 -0
  120. package/build/core/state.d.ts.map +1 -0
  121. package/build/core/state.js +50 -0
  122. package/build/core/state.js.map +1 -0
  123. package/build/core/tap.d.ts +91 -0
  124. package/build/core/tap.d.ts.map +1 -0
  125. package/build/core/tap.js +542 -0
  126. package/build/core/tap.js.map +1 -0
  127. package/build/core/telemetry.d.ts +4 -0
  128. package/build/core/telemetry.d.ts.map +1 -0
  129. package/build/core/telemetry.js +289 -0
  130. package/build/core/telemetry.js.map +1 -0
  131. package/build/core/types.d.ts +134 -0
  132. package/build/core/types.d.ts.map +1 -0
  133. package/build/core/types.js +2 -0
  134. package/build/core/types.js.map +1 -0
  135. package/build/httpServerStandalone.d.ts +7 -0
  136. package/build/httpServerStandalone.d.ts.map +1 -0
  137. package/build/httpServerStandalone.js +31 -0
  138. package/build/httpServerStandalone.js.map +1 -0
  139. package/build/index.d.ts +3 -0
  140. package/build/index.d.ts.map +1 -0
  141. package/build/index.js +3012 -0
  142. package/build/index.js.map +1 -0
  143. package/build/pro/tap.d.ts +91 -0
  144. package/build/pro/tap.d.ts.map +1 -0
  145. package/build/pro/tap.js +542 -0
  146. package/build/pro/tap.js.map +1 -0
  147. package/package.json +63 -0
@@ -0,0 +1,2459 @@
1
+ import { createServer } from "http";
2
+ import { logBuffer, networkBuffer, bundleErrorBuffer, connectedApps } from "./state.js";
3
+ import { listAndroidDevices, androidScreenshot, androidGetScreenSize, androidTap } from "./android.js";
4
+ import { listIOSSimulators, iosScreenshot, iosTap } from "./ios.js";
5
+ import { recognizeText, inferIOSDevicePixelRatio } from "./ocr.js";
6
+ import { getAllConnectionStates, getContextHealth } from "./connectionState.js";
7
+ import { executeInApp, getComponentTree, listDebugGlobals, inspectGlobal } from "./executor.js";
8
+ const DEFAULT_HTTP_PORT = 3456;
9
+ const MAX_PORT_ATTEMPTS = 20;
10
+ // Store the active port for querying via MCP tool
11
+ let activeDebugServerPort = null;
12
+ let tapVerifierMarkers = [];
13
+ /**
14
+ * Get the port the debug HTTP server is running on (if started)
15
+ */
16
+ export function getDebugServerPort() {
17
+ return activeDebugServerPort;
18
+ }
19
+ // HTML template with highlight.js and auto-refresh
20
+ function htmlTemplate(title, content, refreshInterval = 3000) {
21
+ return `<!DOCTYPE html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="UTF-8">
25
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
26
+ <title>${title} - RN Debugger</title>
27
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
28
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
29
+ <style>
30
+ * { box-sizing: border-box; margin: 0; padding: 0; }
31
+ body {
32
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
33
+ background: #0d1117;
34
+ color: #c9d1d9;
35
+ padding: 20px;
36
+ line-height: 1.5;
37
+ }
38
+ nav {
39
+ background: #161b22;
40
+ padding: 12px 20px;
41
+ margin: -20px -20px 20px -20px;
42
+ border-bottom: 1px solid #30363d;
43
+ display: flex;
44
+ gap: 20px;
45
+ align-items: center;
46
+ }
47
+ nav a {
48
+ color: #58a6ff;
49
+ text-decoration: none;
50
+ padding: 6px 12px;
51
+ border-radius: 6px;
52
+ transition: background 0.2s;
53
+ }
54
+ nav a:hover { background: #21262d; }
55
+ nav a.active { background: #388bfd; color: white; }
56
+ .logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
57
+ h1 { margin-bottom: 16px; font-size: 1.5em; }
58
+ .stats {
59
+ display: flex;
60
+ gap: 16px;
61
+ margin-bottom: 20px;
62
+ flex-wrap: wrap;
63
+ }
64
+ .stat {
65
+ background: #161b22;
66
+ padding: 12px 20px;
67
+ border-radius: 8px;
68
+ border: 1px solid #30363d;
69
+ }
70
+ .stat-value { font-size: 1.5em; font-weight: 600; color: #58a6ff; }
71
+ .stat-label { font-size: 0.85em; color: #8b949e; }
72
+ pre {
73
+ background: #161b22;
74
+ border: 1px solid #30363d;
75
+ border-radius: 8px;
76
+ padding: 16px;
77
+ overflow-x: auto;
78
+ font-size: 13px;
79
+ }
80
+ code { font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; }
81
+ .log-entry {
82
+ padding: 8px 12px;
83
+ border-bottom: 1px solid #21262d;
84
+ font-family: 'SF Mono', Consolas, monospace;
85
+ font-size: 13px;
86
+ }
87
+ .log-entry:last-child { border-bottom: none; }
88
+ .log-entry.log { color: #c9d1d9; }
89
+ .log-entry.info { color: #58a6ff; }
90
+ .log-entry.warn { color: #d29922; background: #d299221a; }
91
+ .log-entry.error { color: #f85149; background: #f851491a; }
92
+ .log-entry.debug { color: #8b949e; }
93
+ .log-time { color: #6e7681; margin-right: 12px; }
94
+ .log-level {
95
+ display: inline-block;
96
+ width: 50px;
97
+ text-transform: uppercase;
98
+ font-size: 11px;
99
+ font-weight: 600;
100
+ }
101
+ .network-item { border-bottom: 1px solid #21262d; }
102
+ .network-item:last-child { border-bottom: none; }
103
+ .network-entry {
104
+ padding: 12px;
105
+ display: grid;
106
+ grid-template-columns: 70px 60px 1fr 100px 30px;
107
+ gap: 8px 12px;
108
+ align-items: start;
109
+ font-size: 13px;
110
+ cursor: pointer;
111
+ transition: background 0.15s;
112
+ }
113
+ .network-entry:hover { background: #21262d; }
114
+ .network-main-row {
115
+ display: contents;
116
+ }
117
+ .url-cell {
118
+ display: flex;
119
+ flex-direction: column;
120
+ overflow: hidden;
121
+ }
122
+ .method { font-weight: 600; font-family: monospace; }
123
+ .method.GET { color: #58a6ff; }
124
+ .method.POST { color: #3fb950; }
125
+ .method.PUT { color: #d29922; }
126
+ .method.DELETE { color: #f85149; }
127
+ .method.PATCH { color: #a371f7; }
128
+ .status { font-family: monospace; font-weight: 600; }
129
+ .status.s2xx { color: #3fb950; }
130
+ .status.s3xx { color: #58a6ff; }
131
+ .status.s4xx { color: #d29922; }
132
+ .status.s5xx { color: #f85149; }
133
+ .url { color: #c9d1d9; word-break: break-all; font-family: monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
134
+ .duration { color: #8b949e; text-align: right; }
135
+ .expand-icon { color: #6e7681; text-align: center; transition: transform 0.2s; }
136
+ .network-item.expanded .expand-icon { transform: rotate(90deg); }
137
+ .network-details {
138
+ display: none;
139
+ padding: 12px 16px;
140
+ background: #0d1117;
141
+ border-top: 1px solid #21262d;
142
+ font-size: 12px;
143
+ }
144
+ .network-item.expanded .network-details { display: block; }
145
+ .detail-section { margin-bottom: 12px; }
146
+ .detail-section:last-child { margin-bottom: 0; }
147
+ .detail-label { color: #8b949e; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; font-weight: 600; }
148
+ .detail-value { font-family: 'SF Mono', Consolas, monospace; white-space: pre-wrap; word-break: break-all; }
149
+ .detail-value.url-full { color: #58a6ff; }
150
+ .headers-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; }
151
+ .header-name { color: #a371f7; }
152
+ .header-value { color: #c9d1d9; word-break: break-all; }
153
+ .operation-info {
154
+ font-size: 11px;
155
+ color: #8b949e;
156
+ margin-top: 2px;
157
+ font-family: 'SF Mono', Consolas, monospace;
158
+ overflow: hidden;
159
+ text-overflow: ellipsis;
160
+ white-space: nowrap;
161
+ }
162
+ .operation-name { color: #d2a8ff; font-weight: 500; }
163
+ .operation-vars { color: #7ee787; }
164
+ .empty { color: #8b949e; text-align: center; padding: 40px; }
165
+ .app-card {
166
+ background: #161b22;
167
+ border: 1px solid #30363d;
168
+ border-radius: 8px;
169
+ padding: 16px;
170
+ margin-bottom: 12px;
171
+ }
172
+ .app-card h3 { margin-bottom: 8px; }
173
+ .app-status { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
174
+ .app-status.connected { background: #238636; color: white; }
175
+ .app-status.disconnected { background: #6e7681; color: white; }
176
+ .app-detail { color: #8b949e; font-size: 13px; margin-top: 4px; }
177
+ #content { min-height: 200px; }
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <nav>
182
+ <span class="logo">RN Debugger</span>
183
+ <a href="/" ${title === 'Dashboard' ? 'class="active"' : ''}>Dashboard</a>
184
+ <a href="/logs" ${title === 'Logs' ? 'class="active"' : ''}>Logs</a>
185
+ <a href="/network" ${title === 'Network' ? 'class="active"' : ''}>Network</a>
186
+ <a href="/bundle-errors" ${title === 'Bundle Errors' ? 'class="active"' : ''}>Errors</a>
187
+ <a href="/apps" ${title === 'Apps' ? 'class="active"' : ''}>Apps</a>
188
+ <a href="/repl" ${title === 'REPL' ? 'class="active"' : ''}>REPL</a>
189
+ <a href="/component-tree" ${title === 'Component Tree' ? 'class="active"' : ''}>Components</a>
190
+ <a href="/globals" ${title === 'Globals' ? 'class="active"' : ''}>Globals</a>
191
+ <a href="/tap-verifier" ${title === 'Tap Verifier' ? 'class="active"' : ''}>Tap Verifier</a>
192
+ </nav>
193
+ <div id="content">${content}</div>
194
+ <script>
195
+ hljs.highlightAll();
196
+
197
+ function toggleNetworkItem(el) {
198
+ el.closest('.network-item').classList.toggle('expanded');
199
+ }
200
+
201
+ ${refreshInterval > 0 ? `
202
+ setInterval(() => {
203
+ fetch(window.location.pathname + '?t=' + Date.now(), {
204
+ headers: { 'Accept': 'text/html' }
205
+ })
206
+ .then(r => r.text())
207
+ .then(html => {
208
+ const parser = new DOMParser();
209
+ const doc = parser.parseFromString(html, 'text/html');
210
+ const newContent = doc.getElementById('content');
211
+ const oldContent = document.getElementById('content');
212
+ if (newContent && oldContent && newContent.innerHTML !== oldContent.innerHTML) {
213
+ // Preserve expanded state
214
+ const expanded = new Set();
215
+ oldContent.querySelectorAll('.network-item.expanded').forEach(el => {
216
+ const id = el.getAttribute('data-id');
217
+ if (id) expanded.add(id);
218
+ });
219
+
220
+ oldContent.innerHTML = newContent.innerHTML;
221
+
222
+ // Restore expanded state
223
+ expanded.forEach(id => {
224
+ const el = oldContent.querySelector('.network-item[data-id="' + id + '"]');
225
+ if (el) el.classList.add('expanded');
226
+ });
227
+
228
+ hljs.highlightAll();
229
+ }
230
+ });
231
+ }, ${refreshInterval});
232
+ ` : ''}
233
+ </script>
234
+ </body>
235
+ </html>`;
236
+ }
237
+ function formatTime(date) {
238
+ return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
239
+ }
240
+ function escapeHtml(str) {
241
+ return str
242
+ .replace(/&/g, '&amp;')
243
+ .replace(/</g, '&lt;')
244
+ .replace(/>/g, '&gt;')
245
+ .replace(/"/g, '&quot;');
246
+ }
247
+ function renderDashboard() {
248
+ const logs = logBuffer.size;
249
+ const network = networkBuffer.size;
250
+ const errors = bundleErrorBuffer.get().length;
251
+ const apps = connectedApps.size;
252
+ const status = bundleErrorBuffer.getStatus();
253
+ return htmlTemplate('Dashboard', `
254
+ <h1>Dashboard</h1>
255
+ <div class="stats">
256
+ <div class="stat">
257
+ <div class="stat-value">${logs}</div>
258
+ <div class="stat-label">Console Logs</div>
259
+ </div>
260
+ <div class="stat">
261
+ <div class="stat-value">${network}</div>
262
+ <div class="stat-label">Network Requests</div>
263
+ </div>
264
+ <div class="stat">
265
+ <div class="stat-value">${errors}</div>
266
+ <div class="stat-label">Bundle Errors</div>
267
+ </div>
268
+ <div class="stat">
269
+ <div class="stat-value">${apps}</div>
270
+ <div class="stat-label">Connected Apps</div>
271
+ </div>
272
+ </div>
273
+ <h2 style="margin: 20px 0 12px;">Bundle Status</h2>
274
+ <pre><code class="language-json">${escapeHtml(JSON.stringify(status, null, 2))}</code></pre>
275
+ `);
276
+ }
277
+ function renderLogs() {
278
+ const logs = logBuffer.getAll();
279
+ if (logs.length === 0) {
280
+ return htmlTemplate('Logs', '<div class="empty">No logs captured yet. Connect to a Metro server and interact with your app.</div>');
281
+ }
282
+ const logsHtml = logs.map(log => {
283
+ const time = formatTime(log.timestamp);
284
+ const message = escapeHtml(log.message);
285
+ return `<div class="log-entry ${log.level}">
286
+ <span class="log-time">${time}</span>
287
+ <span class="log-level">${log.level}</span>
288
+ ${message}
289
+ </div>`;
290
+ }).join('');
291
+ return htmlTemplate('Logs', `
292
+ <h1>Console Logs <span style="color: #8b949e; font-weight: normal;">(${logs.length})</span></h1>
293
+ <pre style="padding: 0;">${logsHtml}</pre>
294
+ `);
295
+ }
296
+ function formatHeaders(headers) {
297
+ if (!headers || Object.keys(headers).length === 0) {
298
+ return '<span style="color: #6e7681;">No headers</span>';
299
+ }
300
+ return Object.entries(headers)
301
+ .map(([name, value]) => `<span class="header-name">${escapeHtml(name)}:</span> <span class="header-value">${escapeHtml(value)}</span>`)
302
+ .join('<br>');
303
+ }
304
+ function parseRequestBody(postData) {
305
+ if (!postData)
306
+ return null;
307
+ try {
308
+ const parsed = JSON.parse(postData);
309
+ // Check if it's GraphQL
310
+ if (parsed.query || parsed.operationName) {
311
+ return {
312
+ isGraphQL: true,
313
+ operationName: parsed.operationName,
314
+ variables: parsed.variables
315
+ };
316
+ }
317
+ // REST API - return body preview
318
+ const preview = JSON.stringify(parsed);
319
+ return {
320
+ isGraphQL: false,
321
+ bodyPreview: preview.length > 100 ? preview.substring(0, 100) + '...' : preview
322
+ };
323
+ }
324
+ catch {
325
+ // Not JSON - return raw preview
326
+ return {
327
+ isGraphQL: false,
328
+ bodyPreview: postData.length > 100 ? postData.substring(0, 100) + '...' : postData
329
+ };
330
+ }
331
+ }
332
+ function formatVariablesCompact(variables) {
333
+ if (!variables || Object.keys(variables).length === 0)
334
+ return '';
335
+ const parts = Object.entries(variables).map(([key, value]) => {
336
+ let valStr;
337
+ if (typeof value === 'string') {
338
+ valStr = `"${value.length > 15 ? value.substring(0, 15) + '...' : value}"`;
339
+ }
340
+ else if (typeof value === 'object' && value !== null) {
341
+ valStr = Array.isArray(value) ? `[${value.length}]` : '{...}';
342
+ }
343
+ else {
344
+ valStr = String(value);
345
+ }
346
+ return `${key}: ${valStr}`;
347
+ });
348
+ const result = parts.join(', ');
349
+ return result.length > 60 ? result.substring(0, 60) + '...' : result;
350
+ }
351
+ function renderNetwork() {
352
+ const requests = networkBuffer.getAll({});
353
+ if (requests.length === 0) {
354
+ return htmlTemplate('Network', '<div class="empty">No network requests captured yet. Connect to a Metro server and interact with your app.</div>');
355
+ }
356
+ const requestsHtml = requests.map(req => {
357
+ const statusClass = req.status ? `s${Math.floor(req.status / 100)}xx` : '';
358
+ const duration = req.timing?.duration ? `${Math.round(req.timing.duration)}ms` : '-';
359
+ const url = escapeHtml(req.url);
360
+ const requestId = escapeHtml(req.requestId);
361
+ // Parse body for operation info
362
+ const parsedBody = parseRequestBody(req.postData);
363
+ // Build details section
364
+ const details = [];
365
+ // Full URL
366
+ details.push(`
367
+ <div class="detail-section">
368
+ <div class="detail-label">URL</div>
369
+ <div class="detail-value url-full">${url}</div>
370
+ </div>
371
+ `);
372
+ // Timing
373
+ if (req.timing) {
374
+ details.push(`
375
+ <div class="detail-section">
376
+ <div class="detail-label">Timing</div>
377
+ <div class="detail-value">Duration: ${duration}</div>
378
+ </div>
379
+ `);
380
+ }
381
+ // Request Headers
382
+ details.push(`
383
+ <div class="detail-section">
384
+ <div class="detail-label">Request Headers</div>
385
+ <div class="detail-value">${formatHeaders(req.headers)}</div>
386
+ </div>
387
+ `);
388
+ // Request Body (POST data)
389
+ if (req.postData) {
390
+ let formattedBody = escapeHtml(req.postData);
391
+ try {
392
+ const parsed = JSON.parse(req.postData);
393
+ formattedBody = `<code class="language-json">${escapeHtml(JSON.stringify(parsed, null, 2))}</code>`;
394
+ }
395
+ catch {
396
+ // Not JSON, use as-is
397
+ }
398
+ details.push(`
399
+ <div class="detail-section">
400
+ <div class="detail-label">Request Body</div>
401
+ <pre style="margin: 0; padding: 8px; font-size: 11px;">${formattedBody}</pre>
402
+ </div>
403
+ `);
404
+ }
405
+ // Response Headers
406
+ if (req.responseHeaders && Object.keys(req.responseHeaders).length > 0) {
407
+ details.push(`
408
+ <div class="detail-section">
409
+ <div class="detail-label">Response Headers</div>
410
+ <div class="detail-value">${formatHeaders(req.responseHeaders)}</div>
411
+ </div>
412
+ `);
413
+ }
414
+ // Response info
415
+ if (req.mimeType || req.contentLength) {
416
+ const info = [];
417
+ if (req.mimeType)
418
+ info.push(`Type: ${escapeHtml(req.mimeType)}`);
419
+ if (req.contentLength)
420
+ info.push(`Size: ${req.contentLength} bytes`);
421
+ details.push(`
422
+ <div class="detail-section">
423
+ <div class="detail-label">Response Info</div>
424
+ <div class="detail-value">${info.join(' | ')}</div>
425
+ </div>
426
+ `);
427
+ }
428
+ // Error
429
+ if (req.error) {
430
+ details.push(`
431
+ <div class="detail-section">
432
+ <div class="detail-label" style="color: #f85149;">Error</div>
433
+ <div class="detail-value" style="color: #f85149;">${escapeHtml(req.error)}</div>
434
+ </div>
435
+ `);
436
+ }
437
+ // Build operation info line for compact view
438
+ let operationInfo = '';
439
+ if (parsedBody) {
440
+ if (parsedBody.isGraphQL && parsedBody.operationName) {
441
+ const varsStr = formatVariablesCompact(parsedBody.variables);
442
+ operationInfo = `<div class="operation-info"><span class="operation-name">${escapeHtml(parsedBody.operationName)}</span>${varsStr ? ` <span class="operation-vars">(${escapeHtml(varsStr)})</span>` : ''}</div>`;
443
+ }
444
+ else if (!parsedBody.isGraphQL && parsedBody.bodyPreview) {
445
+ operationInfo = `<div class="operation-info">${escapeHtml(parsedBody.bodyPreview)}</div>`;
446
+ }
447
+ }
448
+ return `<div class="network-item" data-id="${requestId}">
449
+ <div class="network-entry" onclick="toggleNetworkItem(this)">
450
+ <span class="method ${req.method}">${req.method}</span>
451
+ <span class="status ${statusClass}">${req.status || '-'}</span>
452
+ <div class="url-cell">
453
+ <span class="url" title="${url}">${url}</span>
454
+ ${operationInfo}
455
+ </div>
456
+ <span class="duration">${duration}</span>
457
+ <span class="expand-icon">▶</span>
458
+ </div>
459
+ <div class="network-details">
460
+ ${details.join('')}
461
+ </div>
462
+ </div>`;
463
+ }).join('');
464
+ return htmlTemplate('Network', `
465
+ <h1>Network Requests <span style="color: #8b949e; font-weight: normal;">(${requests.length})</span></h1>
466
+ <div style="background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden;">${requestsHtml}</div>
467
+ `);
468
+ }
469
+ function renderApps() {
470
+ const apps = Array.from(connectedApps.entries()).map(([id, app]) => ({
471
+ id,
472
+ deviceInfo: app.deviceInfo,
473
+ port: app.port,
474
+ connected: app.ws.readyState === 1
475
+ }));
476
+ if (apps.length === 0) {
477
+ return htmlTemplate('Apps', '<div class="empty">No apps connected. Use scan_metro to connect to a running Metro server.</div>');
478
+ }
479
+ const connectionStates = getAllConnectionStates();
480
+ const appsHtml = apps.map(app => {
481
+ const state = connectionStates.get(app.id);
482
+ const health = getContextHealth(app.id);
483
+ let uptimeStr = '-';
484
+ if (state?.lastConnectedTime) {
485
+ const uptimeMs = Date.now() - state.lastConnectedTime.getTime();
486
+ const uptimeSec = Math.floor(uptimeMs / 1000);
487
+ if (uptimeSec < 60)
488
+ uptimeStr = `${uptimeSec}s`;
489
+ else if (uptimeSec < 3600)
490
+ uptimeStr = `${Math.floor(uptimeSec / 60)}m ${uptimeSec % 60}s`;
491
+ else
492
+ uptimeStr = `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
493
+ }
494
+ const healthStatus = health?.isStale ? 'stale' : 'healthy';
495
+ const healthClass = health?.isStale ? 'health-stale' : 'health-ok';
496
+ // Get gaps for this specific app
497
+ const appGaps = state?.connectionGaps || [];
498
+ const recentAppGaps = appGaps.slice(-3);
499
+ const gapsHtml = recentAppGaps.length > 0 ? `
500
+ <div class="app-detail" style="margin-top: 8px;">
501
+ <strong>Recent Gaps:</strong>
502
+ ${recentAppGaps.map(gap => {
503
+ const duration = gap.durationMs ? `${Math.round(gap.durationMs / 1000)}s` : 'ongoing';
504
+ return `<div style="color: #d29922; font-size: 12px; margin-left: 8px;">• ${escapeHtml(gap.reason)} (${duration})</div>`;
505
+ }).join('')}
506
+ </div>
507
+ ` : '';
508
+ return `
509
+ <div class="app-card">
510
+ <h3>${escapeHtml(app.deviceInfo.title)}</h3>
511
+ <span class="app-status ${app.connected ? 'connected' : 'disconnected'}">
512
+ ${app.connected ? 'Connected' : 'Disconnected'}
513
+ </span>
514
+ <span class="app-status ${healthClass}" style="margin-left: 8px;">
515
+ Context: ${healthStatus}
516
+ </span>
517
+ <div class="app-detail">Device: ${escapeHtml(app.deviceInfo.deviceName)}</div>
518
+ <div class="app-detail">Metro Port: ${app.port}</div>
519
+ <div class="app-detail">Uptime: ${uptimeStr}</div>
520
+ <div class="app-detail">ID: ${escapeHtml(app.id)}</div>
521
+ ${gapsHtml}
522
+ </div>
523
+ `;
524
+ }).join('');
525
+ return htmlTemplate('Apps', `
526
+ <style>
527
+ .health-ok { background: #238636; color: white; }
528
+ .health-stale { background: #d29922; color: #333; }
529
+ </style>
530
+ <h1>Connected Apps</h1>
531
+ ${appsHtml}
532
+ `);
533
+ }
534
+ function renderBundleErrors() {
535
+ const errors = bundleErrorBuffer.get();
536
+ const status = bundleErrorBuffer.getStatus();
537
+ const statusText = status.hasError ? 'Build Failed' : 'Build OK';
538
+ let content = `
539
+ <h1>Bundle Errors</h1>
540
+ <div class="stats">
541
+ <div class="stat">
542
+ <div class="stat-value" style="color: ${status.hasError ? '#f85149' : '#3fb950'};">${statusText}</div>
543
+ <div class="stat-label">Build Status</div>
544
+ </div>
545
+ <div class="stat">
546
+ <div class="stat-value">${errors.length}</div>
547
+ <div class="stat-label">Errors</div>
548
+ </div>
549
+ ${status.lastBuildTimestamp ? `
550
+ <div class="stat">
551
+ <div class="stat-value">${formatTime(status.lastBuildTimestamp)}</div>
552
+ <div class="stat-label">Last Build</div>
553
+ </div>
554
+ ` : ''}
555
+ </div>
556
+ `;
557
+ if (errors.length === 0) {
558
+ content += '<div class="empty" style="margin-top: 20px;">No bundle errors. Your app is building successfully!</div>';
559
+ }
560
+ else {
561
+ const errorsHtml = errors.map((error, index) => {
562
+ const location = error.line ? `Line ${error.line}${error.column ? `:${error.column}` : ''}` : '';
563
+ return `
564
+ <div class="error-card" style="background: #161b22; border: 1px solid #f85149; border-radius: 8px; padding: 16px; margin-bottom: 12px;">
565
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
566
+ <strong style="color: #f85149;">Error ${index + 1}</strong>
567
+ ${location ? `<span style="color: #8b949e; font-size: 12px;">${location}</span>` : ''}
568
+ </div>
569
+ ${error.file ? `<div style="color: #58a6ff; font-size: 13px; margin-bottom: 8px; font-family: monospace;">${escapeHtml(error.file)}</div>` : ''}
570
+ <pre style="margin: 0; padding: 12px; background: #0d1117; border-radius: 4px; overflow-x: auto; white-space: pre-wrap;"><code style="color: #f85149;">${escapeHtml(error.message)}</code></pre>
571
+ ${error.codeFrame ? `
572
+ <div style="margin-top: 12px;">
573
+ <div style="color: #8b949e; font-size: 11px; margin-bottom: 4px;">Code Frame:</div>
574
+ <pre style="margin: 0; padding: 12px; background: #0d1117; border-radius: 4px; overflow-x: auto;"><code class="language-javascript">${escapeHtml(error.codeFrame)}</code></pre>
575
+ </div>
576
+ ` : ''}
577
+ </div>
578
+ `;
579
+ }).join('');
580
+ content += `<div style="margin-top: 20px;">${errorsHtml}</div>`;
581
+ }
582
+ return htmlTemplate('Bundle Errors', content);
583
+ }
584
+ function renderRepl() {
585
+ return `<!DOCTYPE html>
586
+ <html lang="en">
587
+ <head>
588
+ <meta charset="UTF-8">
589
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
590
+ <title>REPL - RN Debugger</title>
591
+ <style>
592
+ * { box-sizing: border-box; margin: 0; padding: 0; }
593
+ body {
594
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
595
+ background: #0d1117;
596
+ color: #c9d1d9;
597
+ padding: 20px;
598
+ line-height: 1.5;
599
+ }
600
+ nav {
601
+ background: #161b22;
602
+ padding: 12px 20px;
603
+ margin: -20px -20px 20px -20px;
604
+ border-bottom: 1px solid #30363d;
605
+ display: flex;
606
+ gap: 20px;
607
+ align-items: center;
608
+ }
609
+ nav a {
610
+ color: #58a6ff;
611
+ text-decoration: none;
612
+ padding: 6px 12px;
613
+ border-radius: 6px;
614
+ transition: background 0.2s;
615
+ }
616
+ nav a:hover { background: #21262d; }
617
+ nav a.active { background: #388bfd; color: white; }
618
+ .logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
619
+ h1 { margin-bottom: 16px; font-size: 1.5em; }
620
+ .repl-container {
621
+ display: flex;
622
+ flex-direction: column;
623
+ gap: 16px;
624
+ height: calc(100vh - 140px);
625
+ }
626
+ .input-section {
627
+ display: flex;
628
+ flex-direction: column;
629
+ gap: 8px;
630
+ }
631
+ .input-section label {
632
+ color: #8b949e;
633
+ font-size: 13px;
634
+ }
635
+ #codeInput {
636
+ width: 100%;
637
+ height: 150px;
638
+ padding: 12px;
639
+ font-family: 'SF Mono', Consolas, monospace;
640
+ font-size: 14px;
641
+ background: #161b22;
642
+ color: #c9d1d9;
643
+ border: 1px solid #30363d;
644
+ border-radius: 8px;
645
+ resize: vertical;
646
+ }
647
+ #codeInput:focus {
648
+ outline: none;
649
+ border-color: #58a6ff;
650
+ }
651
+ .controls {
652
+ display: flex;
653
+ gap: 12px;
654
+ align-items: center;
655
+ }
656
+ .btn {
657
+ padding: 8px 16px;
658
+ border: none;
659
+ border-radius: 6px;
660
+ font-size: 14px;
661
+ font-weight: 500;
662
+ cursor: pointer;
663
+ transition: background 0.2s;
664
+ }
665
+ .btn-primary {
666
+ background: #238636;
667
+ color: white;
668
+ }
669
+ .btn-primary:hover { background: #2ea043; }
670
+ .btn-primary:disabled { background: #21262d; color: #6e7681; cursor: not-allowed; }
671
+ .btn-secondary {
672
+ background: #21262d;
673
+ color: #c9d1d9;
674
+ }
675
+ .btn-secondary:hover { background: #30363d; }
676
+ .checkbox-label {
677
+ display: flex;
678
+ align-items: center;
679
+ gap: 6px;
680
+ color: #8b949e;
681
+ font-size: 13px;
682
+ }
683
+ .output-section {
684
+ flex: 1;
685
+ display: flex;
686
+ flex-direction: column;
687
+ gap: 8px;
688
+ min-height: 0;
689
+ }
690
+ .output-header {
691
+ display: flex;
692
+ justify-content: space-between;
693
+ align-items: center;
694
+ }
695
+ .output-header label {
696
+ color: #8b949e;
697
+ font-size: 13px;
698
+ }
699
+ #output {
700
+ flex: 1;
701
+ padding: 12px;
702
+ font-family: 'SF Mono', Consolas, monospace;
703
+ font-size: 13px;
704
+ background: #161b22;
705
+ color: #c9d1d9;
706
+ border: 1px solid #30363d;
707
+ border-radius: 8px;
708
+ overflow: auto;
709
+ white-space: pre-wrap;
710
+ word-break: break-word;
711
+ }
712
+ .output-success { color: #3fb950; }
713
+ .output-error { color: #f85149; }
714
+ .history-section {
715
+ margin-top: 16px;
716
+ }
717
+ .history-section h3 {
718
+ color: #8b949e;
719
+ font-size: 13px;
720
+ margin-bottom: 8px;
721
+ }
722
+ .history-list {
723
+ display: flex;
724
+ flex-wrap: wrap;
725
+ gap: 8px;
726
+ }
727
+ .history-item {
728
+ padding: 4px 10px;
729
+ background: #21262d;
730
+ border-radius: 4px;
731
+ font-size: 12px;
732
+ font-family: monospace;
733
+ cursor: pointer;
734
+ max-width: 200px;
735
+ overflow: hidden;
736
+ text-overflow: ellipsis;
737
+ white-space: nowrap;
738
+ }
739
+ .history-item:hover { background: #30363d; }
740
+ </style>
741
+ </head>
742
+ <body>
743
+ <nav>
744
+ <span class="logo">RN Debugger</span>
745
+ <a href="/">Dashboard</a>
746
+ <a href="/logs">Logs</a>
747
+ <a href="/network">Network</a>
748
+ <a href="/bundle-errors">Errors</a>
749
+ <a href="/apps">Apps</a>
750
+ <a href="/repl" class="active">REPL</a>
751
+ <a href="/component-tree">Components</a>
752
+ <a href="/globals">Globals</a>
753
+ <a href="/tap-verifier">Tap Verifier</a>
754
+ </nav>
755
+ <h1>JavaScript REPL</h1>
756
+ <div class="repl-container">
757
+ <div class="input-section">
758
+ <label>Enter JavaScript expression to execute in the app:</label>
759
+ <textarea id="codeInput" placeholder="// Example: get current navigation state
760
+ global.__REACT_NAVIGATION__?.current?.getRootState()
761
+
762
+ // Or inspect React DevTools hook
763
+ Object.keys(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ || {})"></textarea>
764
+ <div class="controls">
765
+ <button class="btn btn-primary" id="executeBtn" onclick="executeCode()">Execute</button>
766
+ <button class="btn btn-secondary" onclick="clearOutput()">Clear Output</button>
767
+ <label class="checkbox-label">
768
+ <input type="checkbox" id="awaitPromise" checked>
769
+ Await Promises
770
+ </label>
771
+ </div>
772
+ </div>
773
+ <div class="output-section">
774
+ <div class="output-header">
775
+ <label>Output:</label>
776
+ <span id="execTime" style="color: #6e7681; font-size: 12px;"></span>
777
+ </div>
778
+ <div id="output"><span style="color: #6e7681;">Output will appear here...</span></div>
779
+ </div>
780
+ <div class="history-section">
781
+ <h3>History</h3>
782
+ <div class="history-list" id="historyList"></div>
783
+ </div>
784
+ </div>
785
+ <script>
786
+ const MAX_HISTORY = 10;
787
+ let history = JSON.parse(localStorage.getItem('repl-history') || '[]');
788
+
789
+ function updateHistoryUI() {
790
+ const list = document.getElementById('historyList');
791
+ list.innerHTML = history.map((item, i) =>
792
+ '<div class="history-item" onclick="loadHistory(' + i + ')" title="' + item.replace(/"/g, '&quot;') + '">' + item.slice(0, 50) + (item.length > 50 ? '...' : '') + '</div>'
793
+ ).join('');
794
+ }
795
+
796
+ function loadHistory(index) {
797
+ document.getElementById('codeInput').value = history[index];
798
+ }
799
+
800
+ function addToHistory(code) {
801
+ // Remove if already exists
802
+ history = history.filter(h => h !== code);
803
+ // Add to front
804
+ history.unshift(code);
805
+ // Limit size
806
+ history = history.slice(0, MAX_HISTORY);
807
+ localStorage.setItem('repl-history', JSON.stringify(history));
808
+ updateHistoryUI();
809
+ }
810
+
811
+ async function executeCode() {
812
+ const code = document.getElementById('codeInput').value.trim();
813
+ if (!code) return;
814
+
815
+ const output = document.getElementById('output');
816
+ const execTime = document.getElementById('execTime');
817
+ const btn = document.getElementById('executeBtn');
818
+ const awaitPromise = document.getElementById('awaitPromise').checked;
819
+
820
+ btn.disabled = true;
821
+ btn.textContent = 'Executing...';
822
+ output.innerHTML = '<span style="color: #8b949e;">Executing...</span>';
823
+ execTime.textContent = '';
824
+
825
+ const startTime = Date.now();
826
+
827
+ try {
828
+ const res = await fetch('/api/execute', {
829
+ method: 'POST',
830
+ headers: { 'Content-Type': 'application/json' },
831
+ body: JSON.stringify({ expression: code, awaitPromise })
832
+ });
833
+ const data = await res.json();
834
+ const elapsed = Date.now() - startTime;
835
+ execTime.textContent = elapsed + 'ms';
836
+
837
+ if (data.success) {
838
+ output.innerHTML = '<span class="output-success">' + formatOutput(data.result) + '</span>';
839
+ addToHistory(code);
840
+ } else {
841
+ output.innerHTML = '<span class="output-error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</span>';
842
+ }
843
+ } catch (err) {
844
+ output.innerHTML = '<span class="output-error">Request failed: ' + escapeHtml(err.message) + '</span>';
845
+ } finally {
846
+ btn.disabled = false;
847
+ btn.textContent = 'Execute';
848
+ }
849
+ }
850
+
851
+ function formatOutput(result) {
852
+ if (result === undefined || result === 'undefined') return 'undefined';
853
+ if (result === null || result === 'null') return 'null';
854
+ try {
855
+ const parsed = typeof result === 'string' ? JSON.parse(result) : result;
856
+ return escapeHtml(JSON.stringify(parsed, null, 2));
857
+ } catch {
858
+ return escapeHtml(String(result));
859
+ }
860
+ }
861
+
862
+ function escapeHtml(str) {
863
+ return String(str)
864
+ .replace(/&/g, '&amp;')
865
+ .replace(/</g, '&lt;')
866
+ .replace(/>/g, '&gt;')
867
+ .replace(/"/g, '&quot;');
868
+ }
869
+
870
+ function clearOutput() {
871
+ document.getElementById('output').innerHTML = '<span style="color: #6e7681;">Output will appear here...</span>';
872
+ document.getElementById('execTime').textContent = '';
873
+ }
874
+
875
+ // Keyboard shortcut: Ctrl/Cmd + Enter to execute
876
+ document.getElementById('codeInput').addEventListener('keydown', (e) => {
877
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
878
+ e.preventDefault();
879
+ executeCode();
880
+ }
881
+ });
882
+
883
+ // Init history
884
+ updateHistoryUI();
885
+ </script>
886
+ </body>
887
+ </html>`;
888
+ }
889
+ function renderComponentTree() {
890
+ return `<!DOCTYPE html>
891
+ <html lang="en">
892
+ <head>
893
+ <meta charset="UTF-8">
894
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
895
+ <title>Component Tree - RN Debugger</title>
896
+ <style>
897
+ * { box-sizing: border-box; margin: 0; padding: 0; }
898
+ body {
899
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
900
+ background: #0d1117;
901
+ color: #c9d1d9;
902
+ padding: 20px;
903
+ line-height: 1.5;
904
+ }
905
+ nav {
906
+ background: #161b22;
907
+ padding: 12px 20px;
908
+ margin: -20px -20px 20px -20px;
909
+ border-bottom: 1px solid #30363d;
910
+ display: flex;
911
+ gap: 20px;
912
+ align-items: center;
913
+ }
914
+ nav a {
915
+ color: #58a6ff;
916
+ text-decoration: none;
917
+ padding: 6px 12px;
918
+ border-radius: 6px;
919
+ transition: background 0.2s;
920
+ }
921
+ nav a:hover { background: #21262d; }
922
+ nav a.active { background: #388bfd; color: white; }
923
+ .logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
924
+ h1 { margin-bottom: 16px; font-size: 1.5em; }
925
+ .controls {
926
+ display: flex;
927
+ gap: 12px;
928
+ align-items: center;
929
+ margin-bottom: 16px;
930
+ flex-wrap: wrap;
931
+ }
932
+ .btn {
933
+ padding: 8px 16px;
934
+ border: none;
935
+ border-radius: 6px;
936
+ font-size: 14px;
937
+ font-weight: 500;
938
+ cursor: pointer;
939
+ transition: background 0.2s;
940
+ }
941
+ .btn-primary {
942
+ background: #238636;
943
+ color: white;
944
+ }
945
+ .btn-primary:hover { background: #2ea043; }
946
+ .btn-primary:disabled { background: #21262d; color: #6e7681; cursor: not-allowed; }
947
+ .btn-secondary {
948
+ background: #21262d;
949
+ color: #c9d1d9;
950
+ }
951
+ .btn-secondary:hover { background: #30363d; }
952
+ .checkbox-label {
953
+ display: flex;
954
+ align-items: center;
955
+ gap: 6px;
956
+ color: #8b949e;
957
+ font-size: 13px;
958
+ }
959
+ #searchInput {
960
+ padding: 8px 12px;
961
+ border: 1px solid #30363d;
962
+ border-radius: 6px;
963
+ background: #161b22;
964
+ color: #c9d1d9;
965
+ font-size: 14px;
966
+ width: 200px;
967
+ }
968
+ #searchInput:focus {
969
+ outline: none;
970
+ border-color: #58a6ff;
971
+ }
972
+ .tree-container {
973
+ background: #161b22;
974
+ border: 1px solid #30363d;
975
+ border-radius: 8px;
976
+ padding: 16px;
977
+ overflow: auto;
978
+ max-height: calc(100vh - 220px);
979
+ }
980
+ .tree-content {
981
+ font-family: 'SF Mono', Consolas, monospace;
982
+ font-size: 13px;
983
+ white-space: pre;
984
+ line-height: 1.6;
985
+ }
986
+ .tree-content .component { color: #7ee787; }
987
+ .tree-content .props { color: #d2a8ff; }
988
+ .tree-content .layout { color: #79c0ff; }
989
+ .loading { color: #8b949e; text-align: center; padding: 40px; }
990
+ .error { color: #f85149; padding: 20px; }
991
+ .focused-screen {
992
+ background: #388bfd33;
993
+ padding: 8px 12px;
994
+ border-radius: 6px;
995
+ margin-bottom: 12px;
996
+ font-size: 14px;
997
+ }
998
+ .focused-screen strong { color: #58a6ff; }
999
+ .stats {
1000
+ color: #8b949e;
1001
+ font-size: 12px;
1002
+ margin-bottom: 12px;
1003
+ }
1004
+ </style>
1005
+ </head>
1006
+ <body>
1007
+ <nav>
1008
+ <span class="logo">RN Debugger</span>
1009
+ <a href="/">Dashboard</a>
1010
+ <a href="/logs">Logs</a>
1011
+ <a href="/network">Network</a>
1012
+ <a href="/bundle-errors">Errors</a>
1013
+ <a href="/apps">Apps</a>
1014
+ <a href="/repl">REPL</a>
1015
+ <a href="/component-tree" class="active">Components</a>
1016
+ <a href="/globals">Globals</a>
1017
+ <a href="/tap-verifier">Tap Verifier</a>
1018
+ </nav>
1019
+ <h1>React Component Tree</h1>
1020
+ <div class="controls">
1021
+ <button class="btn btn-primary" id="refreshBtn" onclick="loadTree()">Refresh</button>
1022
+ <input type="text" id="searchInput" placeholder="Filter components..." oninput="filterTree()">
1023
+ <label class="checkbox-label">
1024
+ <input type="checkbox" id="focusedOnly" checked onchange="loadTree()">
1025
+ Focused Screen Only
1026
+ </label>
1027
+ <label class="checkbox-label">
1028
+ <input type="checkbox" id="structureOnly" checked onchange="loadTree()">
1029
+ Structure Only (Compact)
1030
+ </label>
1031
+ <label class="checkbox-label">
1032
+ <input type="checkbox" id="includeProps" onchange="loadTree()">
1033
+ Include Props
1034
+ </label>
1035
+ </div>
1036
+ <div id="focusedInfo"></div>
1037
+ <div id="stats" class="stats"></div>
1038
+ <div class="tree-container">
1039
+ <div class="tree-content" id="treeContent">
1040
+ <div class="loading">Click "Refresh" to load the component tree...</div>
1041
+ </div>
1042
+ </div>
1043
+ <script>
1044
+ let fullTree = '';
1045
+
1046
+ async function loadTree() {
1047
+ const content = document.getElementById('treeContent');
1048
+ const focusedInfo = document.getElementById('focusedInfo');
1049
+ const stats = document.getElementById('stats');
1050
+ const btn = document.getElementById('refreshBtn');
1051
+
1052
+ const focusedOnly = document.getElementById('focusedOnly').checked;
1053
+ const structureOnly = document.getElementById('structureOnly').checked;
1054
+ const includeProps = document.getElementById('includeProps').checked;
1055
+
1056
+ btn.disabled = true;
1057
+ btn.textContent = 'Loading...';
1058
+ content.innerHTML = '<div class="loading">Loading component tree...</div>';
1059
+ focusedInfo.innerHTML = '';
1060
+ stats.textContent = '';
1061
+
1062
+ try {
1063
+ const params = new URLSearchParams({
1064
+ focusedOnly: focusedOnly.toString(),
1065
+ structureOnly: structureOnly.toString(),
1066
+ includeProps: includeProps.toString(),
1067
+ maxDepth: structureOnly ? '50' : '100'
1068
+ });
1069
+
1070
+ const res = await fetch('/api/component-tree?' + params);
1071
+ const data = await res.json();
1072
+
1073
+ if (data.success) {
1074
+ fullTree = data.result || '';
1075
+
1076
+ // Check for focused screen info
1077
+ const lines = fullTree.split('\\n');
1078
+ if (lines[0] && lines[0].startsWith('Focused:')) {
1079
+ focusedInfo.innerHTML = '<div class="focused-screen"><strong>' + escapeHtml(lines[0]) + '</strong></div>';
1080
+ fullTree = lines.slice(2).join('\\n');
1081
+ }
1082
+
1083
+ // Count components
1084
+ const lineCount = fullTree.split('\\n').filter(l => l.trim()).length;
1085
+ stats.textContent = lineCount + ' components';
1086
+
1087
+ displayTree(fullTree);
1088
+ } else {
1089
+ content.innerHTML = '<div class="error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</div>';
1090
+ }
1091
+ } catch (err) {
1092
+ content.innerHTML = '<div class="error">Request failed: ' + escapeHtml(err.message) + '</div>';
1093
+ } finally {
1094
+ btn.disabled = false;
1095
+ btn.textContent = 'Refresh';
1096
+ }
1097
+ }
1098
+
1099
+ function displayTree(tree) {
1100
+ const content = document.getElementById('treeContent');
1101
+ // Syntax highlight the tree
1102
+ const highlighted = tree
1103
+ .split('\\n')
1104
+ .map(line => {
1105
+ // Component names (at start of line after indentation)
1106
+ let result = line.replace(/^(\\s*)(\\S+)/, '$1<span class="component">$2</span>');
1107
+ // Props in parentheses
1108
+ result = result.replace(/\\(([^)]+)\\)/g, '<span class="props">($1)</span>');
1109
+ // Layout in brackets
1110
+ result = result.replace(/\\[([^\\]]+)\\]/g, '<span class="layout">[$1]</span>');
1111
+ return result;
1112
+ })
1113
+ .join('\\n');
1114
+ content.innerHTML = highlighted || '<div class="loading">No components found</div>';
1115
+ }
1116
+
1117
+ function filterTree() {
1118
+ const filter = document.getElementById('searchInput').value.toLowerCase();
1119
+ if (!filter) {
1120
+ displayTree(fullTree);
1121
+ return;
1122
+ }
1123
+
1124
+ const filtered = fullTree
1125
+ .split('\\n')
1126
+ .filter(line => line.toLowerCase().includes(filter))
1127
+ .join('\\n');
1128
+
1129
+ displayTree(filtered || 'No matching components');
1130
+ }
1131
+
1132
+ function escapeHtml(str) {
1133
+ return String(str)
1134
+ .replace(/&/g, '&amp;')
1135
+ .replace(/</g, '&lt;')
1136
+ .replace(/>/g, '&gt;');
1137
+ }
1138
+ </script>
1139
+ </body>
1140
+ </html>`;
1141
+ }
1142
+ function renderGlobals() {
1143
+ return `<!DOCTYPE html>
1144
+ <html lang="en">
1145
+ <head>
1146
+ <meta charset="UTF-8">
1147
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1148
+ <title>Debug Globals - RN Debugger</title>
1149
+ <style>
1150
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1151
+ body {
1152
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1153
+ background: #0d1117;
1154
+ color: #c9d1d9;
1155
+ padding: 20px;
1156
+ line-height: 1.5;
1157
+ }
1158
+ nav {
1159
+ background: #161b22;
1160
+ padding: 12px 20px;
1161
+ margin: -20px -20px 20px -20px;
1162
+ border-bottom: 1px solid #30363d;
1163
+ display: flex;
1164
+ gap: 20px;
1165
+ align-items: center;
1166
+ }
1167
+ nav a {
1168
+ color: #58a6ff;
1169
+ text-decoration: none;
1170
+ padding: 6px 12px;
1171
+ border-radius: 6px;
1172
+ transition: background 0.2s;
1173
+ }
1174
+ nav a:hover { background: #21262d; }
1175
+ nav a.active { background: #388bfd; color: white; }
1176
+ .logo { font-weight: 600; color: #f0f6fc; margin-right: auto; }
1177
+ h1 { margin-bottom: 16px; font-size: 1.5em; }
1178
+ .controls {
1179
+ display: flex;
1180
+ gap: 12px;
1181
+ align-items: center;
1182
+ margin-bottom: 16px;
1183
+ }
1184
+ .btn {
1185
+ padding: 8px 16px;
1186
+ border: none;
1187
+ border-radius: 6px;
1188
+ font-size: 14px;
1189
+ font-weight: 500;
1190
+ cursor: pointer;
1191
+ transition: background 0.2s;
1192
+ }
1193
+ .btn-primary {
1194
+ background: #238636;
1195
+ color: white;
1196
+ }
1197
+ .btn-primary:hover { background: #2ea043; }
1198
+ .btn-primary:disabled { background: #21262d; color: #6e7681; cursor: not-allowed; }
1199
+ .category {
1200
+ background: #161b22;
1201
+ border: 1px solid #30363d;
1202
+ border-radius: 8px;
1203
+ margin-bottom: 16px;
1204
+ overflow: hidden;
1205
+ }
1206
+ .category-header {
1207
+ padding: 12px 16px;
1208
+ background: #21262d;
1209
+ cursor: pointer;
1210
+ display: flex;
1211
+ justify-content: space-between;
1212
+ align-items: center;
1213
+ }
1214
+ .category-header:hover { background: #30363d; }
1215
+ .category-title {
1216
+ font-weight: 600;
1217
+ color: #58a6ff;
1218
+ }
1219
+ .category-count {
1220
+ color: #8b949e;
1221
+ font-size: 13px;
1222
+ }
1223
+ .category-content {
1224
+ padding: 12px 16px;
1225
+ display: none;
1226
+ }
1227
+ .category.expanded .category-content { display: block; }
1228
+ .global-item {
1229
+ padding: 8px 12px;
1230
+ margin: 4px 0;
1231
+ background: #0d1117;
1232
+ border-radius: 4px;
1233
+ font-family: 'SF Mono', Consolas, monospace;
1234
+ font-size: 13px;
1235
+ cursor: pointer;
1236
+ display: flex;
1237
+ justify-content: space-between;
1238
+ align-items: center;
1239
+ }
1240
+ .global-item:hover { background: #21262d; }
1241
+ .global-name { color: #7ee787; }
1242
+ .inspect-btn {
1243
+ padding: 4px 8px;
1244
+ background: #388bfd;
1245
+ color: white;
1246
+ border: none;
1247
+ border-radius: 4px;
1248
+ font-size: 11px;
1249
+ cursor: pointer;
1250
+ }
1251
+ .inspect-btn:hover { background: #58a6ff; }
1252
+ .loading { color: #8b949e; text-align: center; padding: 40px; }
1253
+ .error { color: #f85149; padding: 20px; }
1254
+ .modal {
1255
+ display: none;
1256
+ position: fixed;
1257
+ top: 0;
1258
+ left: 0;
1259
+ right: 0;
1260
+ bottom: 0;
1261
+ background: rgba(0, 0, 0, 0.8);
1262
+ z-index: 1000;
1263
+ padding: 40px;
1264
+ overflow: auto;
1265
+ }
1266
+ .modal.visible { display: block; }
1267
+ .modal-content {
1268
+ max-width: 800px;
1269
+ margin: 0 auto;
1270
+ background: #161b22;
1271
+ border: 1px solid #30363d;
1272
+ border-radius: 8px;
1273
+ overflow: hidden;
1274
+ }
1275
+ .modal-header {
1276
+ padding: 12px 16px;
1277
+ background: #21262d;
1278
+ display: flex;
1279
+ justify-content: space-between;
1280
+ align-items: center;
1281
+ }
1282
+ .modal-title {
1283
+ font-weight: 600;
1284
+ color: #58a6ff;
1285
+ font-family: monospace;
1286
+ }
1287
+ .close-btn {
1288
+ background: none;
1289
+ border: none;
1290
+ color: #8b949e;
1291
+ font-size: 24px;
1292
+ cursor: pointer;
1293
+ }
1294
+ .close-btn:hover { color: #f85149; }
1295
+ .modal-body {
1296
+ padding: 16px;
1297
+ max-height: 70vh;
1298
+ overflow: auto;
1299
+ }
1300
+ .modal-body pre {
1301
+ margin: 0;
1302
+ padding: 12px;
1303
+ background: #0d1117;
1304
+ border-radius: 4px;
1305
+ overflow-x: auto;
1306
+ font-size: 12px;
1307
+ }
1308
+ .prop-item {
1309
+ padding: 8px;
1310
+ border-bottom: 1px solid #21262d;
1311
+ }
1312
+ .prop-item:last-child { border-bottom: none; }
1313
+ .prop-name { color: #d2a8ff; }
1314
+ .prop-type { color: #8b949e; font-size: 11px; margin-left: 8px; }
1315
+ .prop-value { color: #79c0ff; font-family: monospace; font-size: 12px; margin-top: 4px; }
1316
+ </style>
1317
+ </head>
1318
+ <body>
1319
+ <nav>
1320
+ <span class="logo">RN Debugger</span>
1321
+ <a href="/">Dashboard</a>
1322
+ <a href="/logs">Logs</a>
1323
+ <a href="/network">Network</a>
1324
+ <a href="/bundle-errors">Errors</a>
1325
+ <a href="/apps">Apps</a>
1326
+ <a href="/repl">REPL</a>
1327
+ <a href="/component-tree">Components</a>
1328
+ <a href="/globals" class="active">Globals</a>
1329
+ <a href="/tap-verifier">Tap Verifier</a>
1330
+ </nav>
1331
+ <h1>Debug Globals Explorer</h1>
1332
+ <div class="controls">
1333
+ <button class="btn btn-primary" id="refreshBtn" onclick="loadGlobals()">Refresh</button>
1334
+ </div>
1335
+ <div id="content">
1336
+ <div class="loading">Click "Refresh" to discover debug globals...</div>
1337
+ </div>
1338
+
1339
+ <div class="modal" id="inspectModal" onclick="closeModal(event)">
1340
+ <div class="modal-content" onclick="event.stopPropagation()">
1341
+ <div class="modal-header">
1342
+ <span class="modal-title" id="modalTitle">Global</span>
1343
+ <button class="close-btn" onclick="closeModal()">&times;</button>
1344
+ </div>
1345
+ <div class="modal-body" id="modalBody">
1346
+ Loading...
1347
+ </div>
1348
+ </div>
1349
+ </div>
1350
+
1351
+ <script>
1352
+ let globalsData = null;
1353
+
1354
+ async function loadGlobals() {
1355
+ const content = document.getElementById('content');
1356
+ const btn = document.getElementById('refreshBtn');
1357
+
1358
+ btn.disabled = true;
1359
+ btn.textContent = 'Loading...';
1360
+ content.innerHTML = '<div class="loading">Scanning for debug globals...</div>';
1361
+
1362
+ try {
1363
+ const res = await fetch('/api/globals');
1364
+ const data = await res.json();
1365
+
1366
+ if (data.success && data.result) {
1367
+ globalsData = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
1368
+ renderGlobals(globalsData);
1369
+ } else {
1370
+ content.innerHTML = '<div class="error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</div>';
1371
+ }
1372
+ } catch (err) {
1373
+ content.innerHTML = '<div class="error">Request failed: ' + escapeHtml(err.message) + '</div>';
1374
+ } finally {
1375
+ btn.disabled = false;
1376
+ btn.textContent = 'Refresh';
1377
+ }
1378
+ }
1379
+
1380
+ function renderGlobals(categories) {
1381
+ const content = document.getElementById('content');
1382
+ const html = Object.entries(categories)
1383
+ .filter(([_, items]) => items && items.length > 0)
1384
+ .map(([category, items]) => {
1385
+ const itemsHtml = items.map(name =>
1386
+ '<div class="global-item">' +
1387
+ '<span class="global-name">' + escapeHtml(name) + '</span>' +
1388
+ '<button class="inspect-btn" onclick="inspectGlobal(\\'' + escapeHtml(name).replace(/'/g, "\\\\'") + '\\')">Inspect</button>' +
1389
+ '</div>'
1390
+ ).join('');
1391
+ return '<div class="category" onclick="toggleCategory(this)">' +
1392
+ '<div class="category-header">' +
1393
+ '<span class="category-title">' + escapeHtml(category) + '</span>' +
1394
+ '<span class="category-count">' + items.length + ' items</span>' +
1395
+ '</div>' +
1396
+ '<div class="category-content" onclick="event.stopPropagation()">' + itemsHtml + '</div>' +
1397
+ '</div>';
1398
+ }).join('');
1399
+
1400
+ if (!html) {
1401
+ content.innerHTML = '<div class="loading">No debug globals found. Make sure your app has debugging tools enabled (e.g., React DevTools, Apollo Client, Redux).</div>';
1402
+ } else {
1403
+ content.innerHTML = html;
1404
+ }
1405
+ }
1406
+
1407
+ function toggleCategory(el) {
1408
+ el.classList.toggle('expanded');
1409
+ }
1410
+
1411
+ async function inspectGlobal(name) {
1412
+ const modal = document.getElementById('inspectModal');
1413
+ const title = document.getElementById('modalTitle');
1414
+ const body = document.getElementById('modalBody');
1415
+
1416
+ title.textContent = name;
1417
+ body.innerHTML = '<div class="loading">Loading...</div>';
1418
+ modal.classList.add('visible');
1419
+
1420
+ try {
1421
+ const res = await fetch('/api/globals/' + encodeURIComponent(name));
1422
+ const data = await res.json();
1423
+
1424
+ if (data.success && data.result) {
1425
+ const result = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
1426
+ if (result.error) {
1427
+ body.innerHTML = '<div class="error">' + escapeHtml(result.error) + '</div>';
1428
+ } else {
1429
+ const propsHtml = Object.entries(result).map(([key, info]) => {
1430
+ const typeInfo = typeof info === 'object' && info !== null ? info : { type: typeof info, value: info };
1431
+ return '<div class="prop-item">' +
1432
+ '<span class="prop-name">' + escapeHtml(key) + '</span>' +
1433
+ '<span class="prop-type">' + escapeHtml(typeInfo.type || 'unknown') + (typeInfo.callable ? ' (callable)' : '') + '</span>' +
1434
+ (typeInfo.preview || typeInfo.value !== undefined ?
1435
+ '<div class="prop-value">' + escapeHtml(String(typeInfo.preview || typeInfo.value)).slice(0, 200) + '</div>' : '') +
1436
+ '</div>';
1437
+ }).join('');
1438
+ body.innerHTML = propsHtml || '<div class="loading">Empty object</div>';
1439
+ }
1440
+ } else {
1441
+ body.innerHTML = '<div class="error">Error: ' + escapeHtml(data.error || 'Unknown error') + '</div>';
1442
+ }
1443
+ } catch (err) {
1444
+ body.innerHTML = '<div class="error">Request failed: ' + escapeHtml(err.message) + '</div>';
1445
+ }
1446
+ }
1447
+
1448
+ function closeModal(event) {
1449
+ if (!event || event.target === document.getElementById('inspectModal')) {
1450
+ document.getElementById('inspectModal').classList.remove('visible');
1451
+ }
1452
+ }
1453
+
1454
+ function escapeHtml(str) {
1455
+ return String(str)
1456
+ .replace(/&/g, '&amp;')
1457
+ .replace(/</g, '&lt;')
1458
+ .replace(/>/g, '&gt;')
1459
+ .replace(/"/g, '&quot;');
1460
+ }
1461
+
1462
+ // Close modal on Escape key
1463
+ document.addEventListener('keydown', (e) => {
1464
+ if (e.key === 'Escape') closeModal();
1465
+ });
1466
+ </script>
1467
+ </body>
1468
+ </html>`;
1469
+ }
1470
+ function renderTapVerifier() {
1471
+ return `<!DOCTYPE html>
1472
+ <html lang="en">
1473
+ <head>
1474
+ <meta charset="UTF-8">
1475
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1476
+ <title>Tap Test Page - RN Debugger</title>
1477
+ <style>
1478
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1479
+ body {
1480
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1481
+ background: #1a1a2e;
1482
+ color: #eee;
1483
+ min-height: 100vh;
1484
+ }
1485
+ .page-container {
1486
+ position: relative;
1487
+ width: 100%;
1488
+ min-height: 100vh;
1489
+ }
1490
+
1491
+ /* Sample UI Elements for Testing */
1492
+ .header {
1493
+ background: #16213e;
1494
+ padding: 20px;
1495
+ text-align: center;
1496
+ border-bottom: 2px solid #0f3460;
1497
+ }
1498
+ .header h1 { font-size: 24px; margin-bottom: 8px; }
1499
+ .header p { color: #888; font-size: 14px; }
1500
+
1501
+ .button-grid {
1502
+ display: grid;
1503
+ grid-template-columns: repeat(3, 1fr);
1504
+ gap: 16px;
1505
+ padding: 20px;
1506
+ max-width: 400px;
1507
+ margin: 0 auto;
1508
+ }
1509
+ .test-btn {
1510
+ padding: 20px;
1511
+ border: none;
1512
+ border-radius: 12px;
1513
+ font-size: 14px;
1514
+ font-weight: 600;
1515
+ cursor: pointer;
1516
+ transition: transform 0.1s;
1517
+ }
1518
+ .test-btn:active { transform: scale(0.95); }
1519
+ .btn-red { background: #e94560; color: white; }
1520
+ .btn-blue { background: #0f3460; color: white; }
1521
+ .btn-green { background: #1eb980; color: white; }
1522
+ .btn-yellow { background: #f39c12; color: #333; }
1523
+ .btn-purple { background: #9b59b6; color: white; }
1524
+ .btn-cyan { background: #00cec9; color: #333; }
1525
+
1526
+ /* Grid Overlay */
1527
+ .grid-overlay {
1528
+ position: fixed;
1529
+ top: 0;
1530
+ left: 0;
1531
+ width: 100%;
1532
+ height: 100%;
1533
+ pointer-events: none;
1534
+ z-index: 9998;
1535
+ display: none;
1536
+ }
1537
+ .grid-overlay.visible { display: block; }
1538
+ .grid-line-v {
1539
+ position: absolute;
1540
+ top: 0;
1541
+ bottom: 0;
1542
+ width: 1px;
1543
+ background: rgba(255, 255, 0, 0.6);
1544
+ }
1545
+ .grid-line-h {
1546
+ position: absolute;
1547
+ left: 0;
1548
+ right: 0;
1549
+ height: 1px;
1550
+ background: rgba(255, 255, 0, 0.6);
1551
+ }
1552
+ .grid-label {
1553
+ position: absolute;
1554
+ background: rgba(0, 0, 0, 0.8);
1555
+ color: #ffff00;
1556
+ font-size: 10px;
1557
+ font-family: monospace;
1558
+ padding: 2px 4px;
1559
+ border-radius: 2px;
1560
+ }
1561
+ .grid-toggle {
1562
+ position: fixed;
1563
+ top: 10px;
1564
+ left: 10px;
1565
+ background: #ffff00;
1566
+ color: #000;
1567
+ border: none;
1568
+ padding: 6px 10px;
1569
+ border-radius: 4px;
1570
+ font-size: 11px;
1571
+ font-weight: bold;
1572
+ cursor: pointer;
1573
+ z-index: 10001;
1574
+ opacity: 0.8;
1575
+ }
1576
+
1577
+ .nav-bar {
1578
+ display: flex;
1579
+ justify-content: space-around;
1580
+ background: #16213e;
1581
+ padding: 16px;
1582
+ position: fixed;
1583
+ bottom: 0;
1584
+ left: 0;
1585
+ right: 0;
1586
+ border-top: 2px solid #0f3460;
1587
+ }
1588
+ .nav-item {
1589
+ display: flex;
1590
+ flex-direction: column;
1591
+ align-items: center;
1592
+ color: #888;
1593
+ font-size: 12px;
1594
+ padding: 8px 16px;
1595
+ border-radius: 8px;
1596
+ cursor: pointer;
1597
+ }
1598
+ .nav-item:hover { background: #0f3460; color: #fff; }
1599
+ .nav-icon { font-size: 24px; margin-bottom: 4px; }
1600
+
1601
+ .card {
1602
+ background: #16213e;
1603
+ margin: 20px;
1604
+ padding: 20px;
1605
+ border-radius: 12px;
1606
+ border: 1px solid #0f3460;
1607
+ }
1608
+ .card h2 { margin-bottom: 12px; font-size: 18px; }
1609
+ .card p { color: #888; line-height: 1.6; }
1610
+
1611
+ .input-group {
1612
+ margin: 20px;
1613
+ }
1614
+ .input-group label {
1615
+ display: block;
1616
+ margin-bottom: 8px;
1617
+ color: #888;
1618
+ }
1619
+ .input-group input {
1620
+ width: 100%;
1621
+ padding: 14px;
1622
+ border: 2px solid #0f3460;
1623
+ border-radius: 8px;
1624
+ background: #16213e;
1625
+ color: #fff;
1626
+ font-size: 16px;
1627
+ }
1628
+
1629
+ /* Canvas Overlay for Markers */
1630
+ #markerCanvas {
1631
+ position: fixed;
1632
+ top: 0;
1633
+ left: 0;
1634
+ width: 100%;
1635
+ height: 100%;
1636
+ pointer-events: auto;
1637
+ z-index: 9999;
1638
+ cursor: crosshair;
1639
+ }
1640
+
1641
+ /* Marker info panel */
1642
+ .marker-panel {
1643
+ position: fixed;
1644
+ top: 10px;
1645
+ right: 10px;
1646
+ background: rgba(0,0,0,0.8);
1647
+ padding: 12px;
1648
+ border-radius: 8px;
1649
+ font-size: 12px;
1650
+ z-index: 10000;
1651
+ max-width: 250px;
1652
+ }
1653
+ .marker-panel h3 {
1654
+ color: #ff6b6b;
1655
+ margin-bottom: 8px;
1656
+ font-size: 14px;
1657
+ }
1658
+ .marker-info {
1659
+ color: #ccc;
1660
+ margin-bottom: 4px;
1661
+ }
1662
+ .clear-btn {
1663
+ margin-top: 8px;
1664
+ padding: 6px 12px;
1665
+ background: #e94560;
1666
+ border: none;
1667
+ border-radius: 4px;
1668
+ color: white;
1669
+ cursor: pointer;
1670
+ font-size: 12px;
1671
+ }
1672
+ </style>
1673
+ </head>
1674
+ <body>
1675
+ <div class="page-container">
1676
+ <div class="header">
1677
+ <h1>Tap Test Page</h1>
1678
+ <p>Agent marks coordinates here to verify accuracy</p>
1679
+ </div>
1680
+
1681
+ <div class="button-grid">
1682
+ <button class="test-btn btn-red" onclick="btnClick('Red')">Red</button>
1683
+ <button class="test-btn btn-blue" onclick="btnClick('Blue')">Blue</button>
1684
+ <button class="test-btn btn-green" onclick="btnClick('Green')">Green</button>
1685
+ <button class="test-btn btn-yellow" onclick="btnClick('Yellow')">Yellow</button>
1686
+ <button class="test-btn btn-purple" onclick="btnClick('Purple')">Purple</button>
1687
+ <button class="test-btn btn-cyan" onclick="btnClick('Cyan')">Cyan</button>
1688
+ </div>
1689
+
1690
+ <div class="card">
1691
+ <h2>Test Card</h2>
1692
+ <p>This is a sample card element. The agent can try to tap on this text or the card itself to test coordinate accuracy.</p>
1693
+ </div>
1694
+
1695
+ <div class="input-group">
1696
+ <label>Test Input Field</label>
1697
+ <input type="text" placeholder="Tap here to focus...">
1698
+ </div>
1699
+
1700
+ <div class="card">
1701
+ <h2>Instructions</h2>
1702
+ <p>1. Agent takes screenshot of this page<br>
1703
+ 2. Agent identifies element and calculates coordinates<br>
1704
+ 3. Agent calls /api/tap-verifier/mark with coordinates<br>
1705
+ 4. Marker appears on canvas overlay<br>
1706
+ 5. Verify if marker aligns with intended element</p>
1707
+ </div>
1708
+
1709
+ <div class="nav-bar">
1710
+ <div class="nav-item" onclick="navClick('Home')">
1711
+ <span class="nav-icon">🏠</span>
1712
+ Home
1713
+ </div>
1714
+ <div class="nav-item" onclick="navClick('Search')">
1715
+ <span class="nav-icon">🔍</span>
1716
+ Search
1717
+ </div>
1718
+ <div class="nav-item" onclick="navClick('Profile')">
1719
+ <span class="nav-icon">👤</span>
1720
+ Profile
1721
+ </div>
1722
+ <div class="nav-item" onclick="navClick('Settings')">
1723
+ <span class="nav-icon">⚙️</span>
1724
+ Settings
1725
+ </div>
1726
+ </div>
1727
+ </div>
1728
+
1729
+ <!-- Grid overlay for coordinate reference -->
1730
+ <div class="grid-overlay" id="gridOverlay"></div>
1731
+ <button class="grid-toggle" onclick="toggleGrid()">Grid</button>
1732
+
1733
+ <!-- Transparent canvas overlay for markers -->
1734
+ <canvas id="markerCanvas"></canvas>
1735
+
1736
+ <!-- Marker info panel -->
1737
+ <div class="marker-panel" id="markerPanel" style="display: none;">
1738
+ <h3>Agent Markers</h3>
1739
+ <div id="markerList"></div>
1740
+ <button class="clear-btn" onclick="clearMarkers()">Clear All</button>
1741
+ </div>
1742
+
1743
+ <script>
1744
+ const canvas = document.getElementById('markerCanvas');
1745
+ const ctx = canvas.getContext('2d');
1746
+ const markerPanel = document.getElementById('markerPanel');
1747
+ const markerList = document.getElementById('markerList');
1748
+
1749
+ let markers = [];
1750
+
1751
+ // Resize canvas to match window
1752
+ function resizeCanvas() {
1753
+ canvas.width = window.innerWidth;
1754
+ canvas.height = window.innerHeight;
1755
+ drawMarkers();
1756
+ }
1757
+ window.addEventListener('resize', resizeCanvas);
1758
+ resizeCanvas();
1759
+
1760
+ // Click on canvas to add marker
1761
+ canvas.addEventListener('click', async (e) => {
1762
+ const x = e.clientX;
1763
+ const y = e.clientY;
1764
+ try {
1765
+ await fetch('/api/tap-verifier/mark', {
1766
+ method: 'POST',
1767
+ headers: { 'Content-Type': 'application/json' },
1768
+ body: JSON.stringify({ x, y, label: 'Click', color: '#00ff88' })
1769
+ });
1770
+ pollMarkers();
1771
+ } catch (err) {
1772
+ console.error('Failed to add marker:', err);
1773
+ }
1774
+ });
1775
+
1776
+ // Button click feedback
1777
+ function btnClick(name) {
1778
+ console.log('Button clicked:', name);
1779
+ }
1780
+ function navClick(name) {
1781
+ console.log('Nav clicked:', name);
1782
+ }
1783
+
1784
+ // Draw all markers
1785
+ function drawMarkers() {
1786
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1787
+
1788
+ markers.forEach((marker, i) => {
1789
+ const color = marker.color || '#ff6b6b';
1790
+ const x = marker.x;
1791
+ const y = marker.y;
1792
+
1793
+ // Draw outer ring (dashed)
1794
+ ctx.beginPath();
1795
+ ctx.arc(x, y, 30, 0, Math.PI * 2);
1796
+ ctx.setLineDash([6, 4]);
1797
+ ctx.strokeStyle = color;
1798
+ ctx.lineWidth = 3;
1799
+ ctx.stroke();
1800
+ ctx.setLineDash([]);
1801
+
1802
+ // Draw inner circle
1803
+ ctx.beginPath();
1804
+ ctx.arc(x, y, 8, 0, Math.PI * 2);
1805
+ ctx.fillStyle = color;
1806
+ ctx.fill();
1807
+
1808
+ // Draw crosshair
1809
+ ctx.beginPath();
1810
+ ctx.moveTo(x - 20, y);
1811
+ ctx.lineTo(x + 20, y);
1812
+ ctx.moveTo(x, y - 20);
1813
+ ctx.lineTo(x, y + 20);
1814
+ ctx.strokeStyle = color;
1815
+ ctx.lineWidth = 2;
1816
+ ctx.stroke();
1817
+
1818
+ // Draw label
1819
+ const label = marker.label || 'Marker ' + (i + 1);
1820
+ const text = label + ' (' + x + ', ' + y + ')';
1821
+ ctx.font = 'bold 12px monospace';
1822
+ const textWidth = ctx.measureText(text).width;
1823
+
1824
+ ctx.fillStyle = 'rgba(0,0,0,0.8)';
1825
+ ctx.fillRect(x + 35, y - 10, textWidth + 10, 22);
1826
+ ctx.strokeStyle = color;
1827
+ ctx.lineWidth = 1;
1828
+ ctx.strokeRect(x + 35, y - 10, textWidth + 10, 22);
1829
+
1830
+ ctx.fillStyle = color;
1831
+ ctx.fillText(text, x + 40, y + 5);
1832
+ });
1833
+
1834
+ updateMarkerPanel();
1835
+ }
1836
+
1837
+ // Update marker panel
1838
+ function updateMarkerPanel() {
1839
+ if (markers.length === 0) {
1840
+ markerPanel.style.display = 'none';
1841
+ return;
1842
+ }
1843
+
1844
+ markerPanel.style.display = 'block';
1845
+ markerList.innerHTML = markers.map((m, i) =>
1846
+ '<div class="marker-info">' + (m.label || 'Marker ' + (i+1)) + ': (' + m.x + ', ' + m.y + ')</div>'
1847
+ ).join('');
1848
+ }
1849
+
1850
+ // Poll for markers from server
1851
+ async function pollMarkers() {
1852
+ try {
1853
+ const res = await fetch('/api/tap-verifier/markers');
1854
+ const data = await res.json();
1855
+ if (data.markers && JSON.stringify(data.markers) !== JSON.stringify(markers)) {
1856
+ markers = data.markers;
1857
+ drawMarkers();
1858
+ }
1859
+ } catch (err) {
1860
+ // Ignore
1861
+ }
1862
+ }
1863
+
1864
+ // Clear markers
1865
+ async function clearMarkers() {
1866
+ try {
1867
+ await fetch('/api/tap-verifier/clear-markers', { method: 'POST' });
1868
+ markers = [];
1869
+ drawMarkers();
1870
+ } catch (err) {
1871
+ console.error('Failed to clear:', err);
1872
+ }
1873
+ }
1874
+
1875
+ // Grid overlay functionality
1876
+ const gridOverlay = document.getElementById('gridOverlay');
1877
+ let gridVisible = false;
1878
+
1879
+ function toggleGrid() {
1880
+ gridVisible = !gridVisible;
1881
+ if (gridVisible) {
1882
+ gridOverlay.classList.add('visible');
1883
+ createGrid();
1884
+ } else {
1885
+ gridOverlay.classList.remove('visible');
1886
+ }
1887
+ }
1888
+
1889
+ function createGrid() {
1890
+ gridOverlay.innerHTML = '';
1891
+ const w = window.innerWidth;
1892
+ const h = window.innerHeight;
1893
+
1894
+ // Calculate button grid boundaries
1895
+ // Grid: max-width 400px, centered, padding 20px, gap 16px, 3 columns
1896
+ const gridWidth = Math.min(400, w);
1897
+ const gridLeft = (w - gridWidth) / 2;
1898
+ const gridRight = gridLeft + gridWidth;
1899
+ const padding = 20;
1900
+ const gap = 16;
1901
+ const colWidth = (gridWidth - 2 * padding - 2 * gap) / 3;
1902
+
1903
+ // Column positions (centers)
1904
+ const col1Center = gridLeft + padding + colWidth / 2;
1905
+ const col2Center = gridLeft + padding + colWidth + gap + colWidth / 2;
1906
+ const col3Center = gridLeft + padding + 2 * colWidth + 2 * gap + colWidth / 2;
1907
+
1908
+ // Column boundaries
1909
+ const col1Left = gridLeft + padding;
1910
+ const col1Right = col1Left + colWidth;
1911
+ const col2Left = col1Right + gap;
1912
+ const col2Right = col2Left + colWidth;
1913
+ const col3Left = col2Right + gap;
1914
+ const col3Right = col3Left + colWidth;
1915
+
1916
+ // Draw vertical lines at column boundaries
1917
+ const vLines = [
1918
+ { x: gridLeft, label: Math.round(gridLeft) },
1919
+ { x: col1Left, label: Math.round(col1Left) },
1920
+ { x: col1Center, label: Math.round(col1Center) + ' (C1)', isCenter: true },
1921
+ { x: col1Right, label: Math.round(col1Right) },
1922
+ { x: col2Left, label: Math.round(col2Left) },
1923
+ { x: col2Center, label: Math.round(col2Center) + ' (C2)', isCenter: true },
1924
+ { x: col2Right, label: Math.round(col2Right) },
1925
+ { x: col3Left, label: Math.round(col3Left) },
1926
+ { x: col3Center, label: Math.round(col3Center) + ' (C3)', isCenter: true },
1927
+ { x: col3Right, label: Math.round(col3Right) },
1928
+ { x: gridRight, label: Math.round(gridRight) }
1929
+ ];
1930
+
1931
+ vLines.forEach((line, i) => {
1932
+ const div = document.createElement('div');
1933
+ div.className = 'grid-line-v';
1934
+ div.style.left = line.x + 'px';
1935
+ if (line.isCenter) {
1936
+ div.style.background = 'rgba(0, 255, 0, 0.8)';
1937
+ div.style.width = '2px';
1938
+ }
1939
+ gridOverlay.appendChild(div);
1940
+
1941
+ // Label
1942
+ const label = document.createElement('div');
1943
+ label.className = 'grid-label';
1944
+ label.textContent = line.label;
1945
+ label.style.left = (line.x + 3) + 'px';
1946
+ label.style.top = (70 + (i % 3) * 14) + 'px';
1947
+ if (line.isCenter) label.style.color = '#00ff00';
1948
+ gridOverlay.appendChild(label);
1949
+ });
1950
+
1951
+ // Horizontal lines every 50px with labels
1952
+ for (let y = 0; y <= h; y += 50) {
1953
+ const div = document.createElement('div');
1954
+ div.className = 'grid-line-h';
1955
+ div.style.top = y + 'px';
1956
+ if (y % 100 === 0) {
1957
+ div.style.background = 'rgba(255, 255, 0, 0.8)';
1958
+ }
1959
+ gridOverlay.appendChild(div);
1960
+
1961
+ if (y % 100 === 0) {
1962
+ const label = document.createElement('div');
1963
+ label.className = 'grid-label';
1964
+ label.textContent = 'y=' + y;
1965
+ label.style.left = '3px';
1966
+ label.style.top = (y + 2) + 'px';
1967
+ gridOverlay.appendChild(label);
1968
+ }
1969
+ }
1970
+ }
1971
+
1972
+ // Recreate grid on resize
1973
+ window.addEventListener('resize', () => {
1974
+ if (gridVisible) createGrid();
1975
+ });
1976
+
1977
+ // Start polling
1978
+ setInterval(pollMarkers, 500);
1979
+ pollMarkers();
1980
+ </script>
1981
+ </body>
1982
+ </html>`;
1983
+ }
1984
+ function createRequestHandler() {
1985
+ return async (req, res) => {
1986
+ // Set CORS headers for browser access
1987
+ res.setHeader("Access-Control-Allow-Origin", "*");
1988
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1989
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1990
+ if (req.method === "OPTIONS") {
1991
+ res.statusCode = 204;
1992
+ res.end();
1993
+ return;
1994
+ }
1995
+ const fullUrl = req.url ?? "/";
1996
+ const [urlPath, queryString] = fullUrl.split('?');
1997
+ const url = urlPath; // Path without query params
1998
+ const params = new URLSearchParams(queryString || '');
1999
+ try {
2000
+ // HTML endpoints
2001
+ if (url === "/") {
2002
+ res.setHeader("Content-Type", "text/html");
2003
+ res.end(renderDashboard());
2004
+ return;
2005
+ }
2006
+ if (url === "/logs") {
2007
+ res.setHeader("Content-Type", "text/html");
2008
+ res.end(renderLogs());
2009
+ return;
2010
+ }
2011
+ if (url === "/network") {
2012
+ res.setHeader("Content-Type", "text/html");
2013
+ res.end(renderNetwork());
2014
+ return;
2015
+ }
2016
+ if (url === "/apps") {
2017
+ res.setHeader("Content-Type", "text/html");
2018
+ res.end(renderApps());
2019
+ return;
2020
+ }
2021
+ if (url === "/tap-verifier") {
2022
+ res.setHeader("Content-Type", "text/html");
2023
+ res.end(renderTapVerifier());
2024
+ return;
2025
+ }
2026
+ if (url === "/bundle-errors") {
2027
+ res.setHeader("Content-Type", "text/html");
2028
+ res.end(renderBundleErrors());
2029
+ return;
2030
+ }
2031
+ if (url === "/repl") {
2032
+ res.setHeader("Content-Type", "text/html");
2033
+ res.end(renderRepl());
2034
+ return;
2035
+ }
2036
+ if (url === "/component-tree") {
2037
+ res.setHeader("Content-Type", "text/html");
2038
+ res.end(renderComponentTree());
2039
+ return;
2040
+ }
2041
+ if (url === "/globals") {
2042
+ res.setHeader("Content-Type", "text/html");
2043
+ res.end(renderGlobals());
2044
+ return;
2045
+ }
2046
+ // JSON API endpoints
2047
+ res.setHeader("Content-Type", "application/json");
2048
+ if (url === "/api/logs" || url === "/api/logs/") {
2049
+ const logs = logBuffer.getAll();
2050
+ res.end(JSON.stringify({ count: logs.length, logs }, null, 2));
2051
+ }
2052
+ else if (url === "/api/network" || url === "/api/network/") {
2053
+ const requests = networkBuffer.getAll({});
2054
+ res.end(JSON.stringify({ count: requests.length, requests }, null, 2));
2055
+ }
2056
+ else if (url === "/api/bundle-errors" || url === "/api/bundle-errors/") {
2057
+ const errors = bundleErrorBuffer.get();
2058
+ const status = bundleErrorBuffer.getStatus();
2059
+ res.end(JSON.stringify({ status, count: errors.length, errors }, null, 2));
2060
+ }
2061
+ else if (url === "/api/apps" || url === "/api/apps/") {
2062
+ const apps = Array.from(connectedApps.entries()).map(([id, app]) => ({
2063
+ id,
2064
+ deviceInfo: app.deviceInfo,
2065
+ port: app.port,
2066
+ connected: app.ws.readyState === 1 // WebSocket.OPEN
2067
+ }));
2068
+ res.end(JSON.stringify({ count: apps.length, apps }, null, 2));
2069
+ }
2070
+ else if (url === "/api/status" || url === "/api/status/") {
2071
+ const status = {
2072
+ logs: logBuffer.size,
2073
+ networkRequests: networkBuffer.size,
2074
+ bundleErrors: bundleErrorBuffer.get().length,
2075
+ connectedApps: connectedApps.size,
2076
+ bundleStatus: bundleErrorBuffer.getStatus()
2077
+ };
2078
+ res.end(JSON.stringify(status, null, 2));
2079
+ }
2080
+ else if (url === "/api/connection-status") {
2081
+ // Connection health API endpoint
2082
+ const states = {};
2083
+ const health = {};
2084
+ for (const [appKey] of connectedApps.entries()) {
2085
+ const state = getAllConnectionStates().get(appKey);
2086
+ const contextHealth = getContextHealth(appKey);
2087
+ if (state)
2088
+ states[appKey] = state;
2089
+ if (contextHealth)
2090
+ health[appKey] = contextHealth;
2091
+ }
2092
+ res.end(JSON.stringify({ states, health }, null, 2));
2093
+ }
2094
+ else if (url === "/api/execute" && req.method === "POST") {
2095
+ // REPL execute API endpoint
2096
+ let body = '';
2097
+ req.on('data', chunk => { body += chunk; });
2098
+ req.on('end', async () => {
2099
+ try {
2100
+ const data = JSON.parse(body);
2101
+ const { expression, awaitPromise = true } = data;
2102
+ if (!expression || typeof expression !== 'string') {
2103
+ res.end(JSON.stringify({ success: false, error: 'expression is required' }));
2104
+ return;
2105
+ }
2106
+ const result = await executeInApp(expression, awaitPromise);
2107
+ res.end(JSON.stringify(result, null, 2));
2108
+ }
2109
+ catch (err) {
2110
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2111
+ }
2112
+ });
2113
+ return;
2114
+ }
2115
+ else if (url === "/api/component-tree") {
2116
+ // Component tree API endpoint
2117
+ const maxDepth = parseInt(params.get('maxDepth') || '50', 10);
2118
+ const focusedOnly = params.get('focusedOnly') === 'true';
2119
+ const structureOnly = params.get('structureOnly') === 'true';
2120
+ const includeProps = params.get('includeProps') === 'true';
2121
+ try {
2122
+ const result = await getComponentTree({
2123
+ maxDepth,
2124
+ focusedOnly,
2125
+ structureOnly,
2126
+ includeProps
2127
+ });
2128
+ res.end(JSON.stringify(result, null, 2));
2129
+ }
2130
+ catch (err) {
2131
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2132
+ }
2133
+ }
2134
+ else if (url === "/api/globals") {
2135
+ // Debug globals list API endpoint
2136
+ try {
2137
+ const result = await listDebugGlobals();
2138
+ res.end(JSON.stringify(result, null, 2));
2139
+ }
2140
+ catch (err) {
2141
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2142
+ }
2143
+ }
2144
+ else if (url.startsWith("/api/globals/")) {
2145
+ // Inspect specific global API endpoint
2146
+ const globalName = decodeURIComponent(url.replace("/api/globals/", ""));
2147
+ if (!globalName) {
2148
+ res.end(JSON.stringify({ success: false, error: 'Global name required' }));
2149
+ }
2150
+ else {
2151
+ try {
2152
+ const result = await inspectGlobal(globalName);
2153
+ res.end(JSON.stringify(result, null, 2));
2154
+ }
2155
+ catch (err) {
2156
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2157
+ }
2158
+ }
2159
+ }
2160
+ else if (url === "/api/tap-verifier/devices") {
2161
+ const platform = params.get('platform') || 'android';
2162
+ try {
2163
+ if (platform === 'android') {
2164
+ const result = await listAndroidDevices();
2165
+ if (result.success && result.devices) {
2166
+ const devices = result.devices.map(d => ({
2167
+ id: d.id,
2168
+ name: `${d.model || d.id} (${d.status})`
2169
+ }));
2170
+ res.end(JSON.stringify({ devices }));
2171
+ }
2172
+ else {
2173
+ res.end(JSON.stringify({ devices: [], error: result.error }));
2174
+ }
2175
+ }
2176
+ else {
2177
+ const result = await listIOSSimulators();
2178
+ if (result.success && result.simulators) {
2179
+ const devices = result.simulators
2180
+ .filter(s => s.state === 'Booted')
2181
+ .map(s => ({
2182
+ id: s.udid,
2183
+ name: `${s.name} (${s.runtime})`
2184
+ }));
2185
+ res.end(JSON.stringify({ devices }));
2186
+ }
2187
+ else {
2188
+ res.end(JSON.stringify({ devices: [], error: result.error }));
2189
+ }
2190
+ }
2191
+ }
2192
+ catch (err) {
2193
+ res.end(JSON.stringify({ devices: [], error: String(err) }));
2194
+ }
2195
+ }
2196
+ else if (url === "/api/tap-verifier/screen-size") {
2197
+ const platform = params.get('platform') || 'android';
2198
+ const deviceId = params.get('deviceId') || undefined;
2199
+ try {
2200
+ if (platform === 'android') {
2201
+ const result = await androidGetScreenSize(deviceId);
2202
+ if (result.success && result.width && result.height) {
2203
+ res.end(JSON.stringify({ width: result.width, height: result.height }));
2204
+ }
2205
+ else {
2206
+ res.end(JSON.stringify({ error: result.error || 'Failed to get screen size' }));
2207
+ }
2208
+ }
2209
+ else {
2210
+ // iOS doesn't have a direct screen size function, use common sizes
2211
+ // We'll get the actual size from the screenshot
2212
+ res.end(JSON.stringify({ width: 390, height: 844, note: 'Default iPhone size. Load screenshot for actual dimensions.' }));
2213
+ }
2214
+ }
2215
+ catch (err) {
2216
+ res.end(JSON.stringify({ error: String(err) }));
2217
+ }
2218
+ }
2219
+ else if (url === "/api/tap-verifier/screenshot") {
2220
+ const platform = params.get('platform') || 'android';
2221
+ const deviceId = params.get('deviceId') || undefined;
2222
+ try {
2223
+ if (platform === 'android') {
2224
+ const result = await androidScreenshot(deviceId);
2225
+ if (result.success && result.data) {
2226
+ const base64 = result.data.toString('base64');
2227
+ res.end(JSON.stringify({ success: true, image: base64 }));
2228
+ }
2229
+ else {
2230
+ res.end(JSON.stringify({ success: false, error: result.error || 'Failed to take screenshot' }));
2231
+ }
2232
+ }
2233
+ else {
2234
+ const result = await iosScreenshot(undefined, deviceId);
2235
+ if (result.success && result.data) {
2236
+ const base64 = result.data.toString('base64');
2237
+ res.end(JSON.stringify({ success: true, image: base64 }));
2238
+ }
2239
+ else {
2240
+ res.end(JSON.stringify({ success: false, error: result.error || 'Failed to take screenshot' }));
2241
+ }
2242
+ }
2243
+ }
2244
+ catch (err) {
2245
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2246
+ }
2247
+ }
2248
+ else if (url === "/api/tap-verifier/execute" && req.method === "POST") {
2249
+ // Parse POST body
2250
+ let body = '';
2251
+ req.on('data', chunk => { body += chunk; });
2252
+ req.on('end', async () => {
2253
+ try {
2254
+ const data = JSON.parse(body);
2255
+ const { platform, x, y, deviceId } = data;
2256
+ if (typeof x !== 'number' || typeof y !== 'number') {
2257
+ res.end(JSON.stringify({ success: false, error: 'x and y must be numbers' }));
2258
+ return;
2259
+ }
2260
+ if (platform === 'android') {
2261
+ const result = await androidTap(x, y, deviceId);
2262
+ res.end(JSON.stringify({ success: result.success, error: result.error }));
2263
+ }
2264
+ else {
2265
+ const result = await iosTap(x, y, { udid: deviceId });
2266
+ res.end(JSON.stringify({ success: result.success, error: result.error }));
2267
+ }
2268
+ }
2269
+ catch (err) {
2270
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2271
+ }
2272
+ });
2273
+ return; // Important: return here since we're handling the response asynchronously
2274
+ }
2275
+ else if (url === "/api/tap-verifier/mark" && req.method === "POST") {
2276
+ // Add a marker to the tap verifier (agent can mark calculated coordinates)
2277
+ let body = '';
2278
+ req.on('data', chunk => { body += chunk; });
2279
+ req.on('end', () => {
2280
+ try {
2281
+ const data = JSON.parse(body);
2282
+ const { x, y, label, color } = data;
2283
+ if (typeof x !== 'number' || typeof y !== 'number') {
2284
+ res.end(JSON.stringify({ success: false, error: 'x and y must be numbers' }));
2285
+ return;
2286
+ }
2287
+ const marker = {
2288
+ x,
2289
+ y,
2290
+ label: label || undefined,
2291
+ color: color || undefined,
2292
+ timestamp: Date.now()
2293
+ };
2294
+ tapVerifierMarkers.push(marker);
2295
+ res.end(JSON.stringify({
2296
+ success: true,
2297
+ marker,
2298
+ totalMarkers: tapVerifierMarkers.length
2299
+ }));
2300
+ }
2301
+ catch (err) {
2302
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2303
+ }
2304
+ });
2305
+ return;
2306
+ }
2307
+ else if (url === "/api/tap-verifier/markers") {
2308
+ // Get all markers added by the agent
2309
+ res.end(JSON.stringify({ markers: tapVerifierMarkers }));
2310
+ }
2311
+ else if (url === "/api/tap-verifier/clear-markers" && req.method === "POST") {
2312
+ // Clear all agent-added markers
2313
+ tapVerifierMarkers = [];
2314
+ res.end(JSON.stringify({ success: true, message: 'All markers cleared' }));
2315
+ }
2316
+ else if (url === "/api/ocr") {
2317
+ // OCR endpoint - takes screenshot and runs OCR
2318
+ const platform = params.get('platform') || 'ios';
2319
+ const deviceId = params.get('deviceId') || undefined;
2320
+ console.log(`[OCR] Request: platform=${platform}, deviceId=${deviceId || 'auto'}`);
2321
+ try {
2322
+ // Take screenshot
2323
+ let screenshotResult;
2324
+ if (platform === 'android') {
2325
+ screenshotResult = await androidScreenshot(deviceId);
2326
+ }
2327
+ else {
2328
+ screenshotResult = await iosScreenshot(undefined, deviceId);
2329
+ }
2330
+ if (!screenshotResult.success || !screenshotResult.data) {
2331
+ console.log(`[OCR] Screenshot failed: ${screenshotResult.error || 'No image data'}`);
2332
+ res.end(JSON.stringify({
2333
+ success: false,
2334
+ error: `Screenshot failed: ${screenshotResult.error || 'No image data'}`
2335
+ }));
2336
+ return;
2337
+ }
2338
+ // Infer device pixel ratio from screenshot dimensions (iOS only)
2339
+ const devicePixelRatio = platform === 'ios' && screenshotResult.originalWidth && screenshotResult.originalHeight
2340
+ ? inferIOSDevicePixelRatio(screenshotResult.originalWidth, screenshotResult.originalHeight)
2341
+ : 1; // Android uses raw pixels
2342
+ console.log(`[OCR] Screenshot captured, size=${screenshotResult.data.length} bytes, scaleFactor=${screenshotResult.scaleFactor}, devicePixelRatio=${devicePixelRatio}`);
2343
+ // Run OCR with scale factor, platform, and device pixel ratio
2344
+ const scaleFactor = screenshotResult.scaleFactor || 1;
2345
+ const ocrResult = await recognizeText(screenshotResult.data, {
2346
+ scaleFactor,
2347
+ platform: platform,
2348
+ devicePixelRatio
2349
+ });
2350
+ console.log(`[OCR] Complete: engine=${ocrResult.engine}, words=${ocrResult.words.length}, time=${ocrResult.processingTimeMs}ms`);
2351
+ res.end(JSON.stringify({
2352
+ success: ocrResult.success,
2353
+ platform,
2354
+ engine: ocrResult.engine || "unknown",
2355
+ processingTimeMs: ocrResult.processingTimeMs,
2356
+ fullText: ocrResult.fullText,
2357
+ confidence: ocrResult.confidence,
2358
+ wordsCount: ocrResult.words.length,
2359
+ linesCount: ocrResult.lines.length,
2360
+ words: ocrResult.words,
2361
+ lines: ocrResult.lines,
2362
+ imageScaleFactor: scaleFactor,
2363
+ devicePixelRatio
2364
+ }, null, 2));
2365
+ }
2366
+ catch (err) {
2367
+ console.log(`[OCR] Error: ${err}`);
2368
+ res.end(JSON.stringify({ success: false, error: String(err) }));
2369
+ }
2370
+ }
2371
+ else if (url === "/api" || url === "/api/") {
2372
+ const endpoints = {
2373
+ message: "React Native AI Debugger - Debug HTTP Server",
2374
+ html: {
2375
+ "/": "Dashboard",
2376
+ "/logs": "Console logs (colored)",
2377
+ "/network": "Network requests",
2378
+ "/bundle-errors": "Bundle/compilation errors",
2379
+ "/apps": "Connected apps with connection health",
2380
+ "/repl": "JavaScript REPL for code execution",
2381
+ "/component-tree": "React component tree viewer",
2382
+ "/globals": "Debug globals explorer",
2383
+ "/tap-verifier": "Tap coordinate verification tool"
2384
+ },
2385
+ api: {
2386
+ "/api/status": "Overall server status and buffer sizes",
2387
+ "/api/logs": "All captured console logs (JSON)",
2388
+ "/api/network": "All captured network requests (JSON)",
2389
+ "/api/bundle-errors": "Metro bundle/compilation errors (JSON)",
2390
+ "/api/apps": "Connected React Native apps (JSON)",
2391
+ "/api/connection-status": "Connection states and context health for all apps",
2392
+ "/api/execute": "Execute JavaScript in the app (POST: expression, awaitPromise?)",
2393
+ "/api/component-tree": "Get React component tree (query: maxDepth, focusedOnly, structureOnly, includeProps)",
2394
+ "/api/globals": "List available debug globals",
2395
+ "/api/globals/:name": "Inspect a specific global object",
2396
+ "/api/tap-verifier/devices": "List available devices (query: platform=android|ios)",
2397
+ "/api/tap-verifier/screen-size": "Get device screen size (query: platform, deviceId)",
2398
+ "/api/tap-verifier/screenshot": "Get device screenshot as base64 (query: platform, deviceId)",
2399
+ "/api/tap-verifier/execute": "Execute tap at coordinates (POST: platform, x, y, deviceId)",
2400
+ "/api/tap-verifier/mark": "Add a marker to visualize agent-calculated coordinates (POST: x, y, label?, color?)",
2401
+ "/api/tap-verifier/markers": "Get all agent-added markers (GET)",
2402
+ "/api/tap-verifier/clear-markers": "Clear all agent-added markers (POST)",
2403
+ "/api/ocr": "Take screenshot and run OCR to extract text with coordinates (query: platform=ios|android, deviceId?)"
2404
+ }
2405
+ };
2406
+ res.end(JSON.stringify(endpoints, null, 2));
2407
+ }
2408
+ else {
2409
+ res.statusCode = 404;
2410
+ res.end(JSON.stringify({ error: "Not found", path: url }));
2411
+ }
2412
+ }
2413
+ catch (error) {
2414
+ res.statusCode = 500;
2415
+ res.end(JSON.stringify({ error: String(error) }));
2416
+ }
2417
+ };
2418
+ }
2419
+ function tryListenOnPort(server, port) {
2420
+ return new Promise((resolve, reject) => {
2421
+ const onError = (err) => {
2422
+ server.removeListener("error", onError);
2423
+ if (err.code === "EADDRINUSE") {
2424
+ reject(new Error(`Port ${port} in use`));
2425
+ }
2426
+ else {
2427
+ reject(err);
2428
+ }
2429
+ };
2430
+ server.once("error", onError);
2431
+ server.listen(port, () => {
2432
+ server.removeListener("error", onError);
2433
+ resolve(port);
2434
+ });
2435
+ });
2436
+ }
2437
+ /**
2438
+ * Start a debug HTTP server to expose buffer contents.
2439
+ * Automatically finds an available port starting from the default.
2440
+ */
2441
+ export async function startDebugHttpServer(options = {}) {
2442
+ const startPort = options.port ?? DEFAULT_HTTP_PORT;
2443
+ const server = createServer(createRequestHandler());
2444
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
2445
+ const port = startPort + attempt;
2446
+ try {
2447
+ await tryListenOnPort(server, port);
2448
+ activeDebugServerPort = port;
2449
+ console.error(`[rn-ai-debugger] Debug HTTP server running on http://localhost:${port}`);
2450
+ return port;
2451
+ }
2452
+ catch {
2453
+ // Port in use, try next one
2454
+ }
2455
+ }
2456
+ console.error(`[rn-ai-debugger] Could not find available port for debug HTTP server (tried ${startPort}-${startPort + MAX_PORT_ATTEMPTS - 1})`);
2457
+ return null;
2458
+ }
2459
+ //# sourceMappingURL=httpServer.js.map