appium-mcp 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.github/dependabot.yml +17 -0
  2. package/.github/workflows/ci.yml +7 -3
  3. package/.github/workflows/pr-title.yml +4 -9
  4. package/.github/workflows/publish.yml +4 -4
  5. package/CHANGELOG.md +12 -0
  6. package/dist/tools/app-management/list-apps.js +5 -1
  7. package/dist/tools/app-management/list-apps.js.map +1 -1
  8. package/dist/tools/context/get-contexts.js +5 -1
  9. package/dist/tools/context/get-contexts.js.map +1 -1
  10. package/dist/tools/interactions/get-page-source.js +5 -1
  11. package/dist/tools/interactions/get-page-source.js.map +1 -1
  12. package/dist/tools/interactions/screenshot.js +5 -1
  13. package/dist/tools/interactions/screenshot.js.map +1 -1
  14. package/dist/tools/session/create-session.js +18 -3
  15. package/dist/tools/session/create-session.js.map +1 -1
  16. package/dist/tools/session/select-device.js +9 -2
  17. package/dist/tools/session/select-device.js.map +1 -1
  18. package/dist/tools/test-generation/locators.js +5 -1
  19. package/dist/tools/test-generation/locators.js.map +1 -1
  20. package/dist/ui/mcp-ui-utils.d.ts +111 -0
  21. package/dist/ui/mcp-ui-utils.js +1542 -0
  22. package/dist/ui/mcp-ui-utils.js.map +1 -0
  23. package/package.json +5 -5
  24. package/src/tools/app-management/list-apps.ts +14 -1
  25. package/src/tools/context/get-contexts.ts +14 -1
  26. package/src/tools/interactions/get-page-source.ts +14 -1
  27. package/src/tools/interactions/screenshot.ts +14 -1
  28. package/src/tools/session/create-session.ts +29 -3
  29. package/src/tools/session/select-device.ts +23 -2
  30. package/src/tools/test-generation/locators.ts +15 -1
  31. package/src/ui/mcp-ui-utils.ts +1630 -0
@@ -0,0 +1,1630 @@
1
+ /**
2
+ * MCP-UI Utility Functions
3
+ *
4
+ * This module provides utilities for creating UI resources using MCP-UI protocol.
5
+ * It enables interactive UI components to be returned alongside text responses.
6
+ *
7
+ * Reference: https://mcpui.dev/guide/introduction
8
+ */
9
+
10
+ /**
11
+ * Creates a UIResource object following MCP-UI protocol
12
+ * @param uri - Unique identifier using ui:// scheme (e.g., 'ui://appium-mcp/device-picker')
13
+ * @param htmlContent - HTML string to render in the iframe
14
+ * @returns UIResource object ready to be included in MCP response content
15
+ */
16
+ export function createUIResource(
17
+ uri: string,
18
+ htmlContent: string
19
+ ): {
20
+ type: 'resource';
21
+ resource: {
22
+ uri: string;
23
+ mimeType: 'text/html';
24
+ text: string;
25
+ };
26
+ } {
27
+ return {
28
+ type: 'resource',
29
+ resource: {
30
+ uri,
31
+ mimeType: 'text/html',
32
+ text: htmlContent,
33
+ },
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Creates a device picker UI component
39
+ * @param devices - Array of device objects with name, udid, state, etc.
40
+ * @param platform - 'android' or 'ios'
41
+ * @param deviceType - 'simulator' or 'real' for iOS
42
+ * @returns HTML string for device picker UI
43
+ */
44
+ export function createDevicePickerUI(
45
+ devices: Array<{
46
+ name?: string;
47
+ udid: string;
48
+ state?: string;
49
+ type?: string;
50
+ }>,
51
+ platform: 'android' | 'ios',
52
+ deviceType?: 'simulator' | 'real'
53
+ ): string {
54
+ const deviceTypeLabel =
55
+ platform === 'ios' && deviceType
56
+ ? deviceType === 'simulator'
57
+ ? 'iOS Simulators'
58
+ : 'iOS Devices'
59
+ : 'Android Devices';
60
+
61
+ const deviceCards = devices
62
+ .map(
63
+ (device, index) => `
64
+ <div class="device-card" data-udid="${device.udid}" data-index="${index}">
65
+ <div class="device-header">
66
+ <h3>${device.name || device.udid}</h3>
67
+ ${device.state ? `<span class="device-state ${device.state.toLowerCase()}">${device.state}</span>` : ''}
68
+ </div>
69
+ <div class="device-details">
70
+ <p><strong>UDID:</strong> <code>${device.udid}</code></p>
71
+ ${device.type ? `<p><strong>Type:</strong> ${device.type}</p>` : ''}
72
+ </div>
73
+ <button class="select-device-btn" onclick="selectDevice('${device.udid}')">
74
+ Select Device
75
+ </button>
76
+ </div>
77
+ `
78
+ )
79
+ .join('');
80
+
81
+ return `
82
+ <!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>Device Picker - ${deviceTypeLabel}</title>
88
+ <style>
89
+ * {
90
+ margin: 0;
91
+ padding: 0;
92
+ box-sizing: border-box;
93
+ }
94
+ body {
95
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
96
+ padding: 20px;
97
+ background: #f5f5f5;
98
+ color: #333;
99
+ }
100
+ .container {
101
+ max-width: 1200px;
102
+ margin: 0 auto;
103
+ }
104
+ .header {
105
+ margin-bottom: 24px;
106
+ }
107
+ .header h1 {
108
+ font-size: 24px;
109
+ margin-bottom: 8px;
110
+ color: #1a1a1a;
111
+ }
112
+ .header p {
113
+ color: #666;
114
+ font-size: 14px;
115
+ }
116
+ .devices-grid {
117
+ display: grid;
118
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
119
+ gap: 16px;
120
+ }
121
+ .device-card {
122
+ background: white;
123
+ border: 1px solid #e0e0e0;
124
+ border-radius: 8px;
125
+ padding: 16px;
126
+ transition: all 0.2s;
127
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
128
+ }
129
+ .device-card:hover {
130
+ border-color: #007AFF;
131
+ box-shadow: 0 4px 12px rgba(0,122,255,0.15);
132
+ transform: translateY(-2px);
133
+ }
134
+ .device-header {
135
+ display: flex;
136
+ justify-content: space-between;
137
+ align-items: center;
138
+ margin-bottom: 12px;
139
+ }
140
+ .device-header h3 {
141
+ font-size: 16px;
142
+ font-weight: 600;
143
+ color: #1a1a1a;
144
+ }
145
+ .device-state {
146
+ padding: 4px 8px;
147
+ border-radius: 4px;
148
+ font-size: 12px;
149
+ font-weight: 500;
150
+ text-transform: uppercase;
151
+ }
152
+ .device-state.booted {
153
+ background: #d4edda;
154
+ color: #155724;
155
+ }
156
+ .device-state.shutdown {
157
+ background: #f8d7da;
158
+ color: #721c24;
159
+ }
160
+ .device-details {
161
+ margin-bottom: 12px;
162
+ font-size: 13px;
163
+ }
164
+ .device-details p {
165
+ margin-bottom: 6px;
166
+ color: #666;
167
+ }
168
+ .device-details code {
169
+ background: #f5f5f5;
170
+ padding: 2px 6px;
171
+ border-radius: 3px;
172
+ font-size: 12px;
173
+ font-family: 'Monaco', 'Menlo', monospace;
174
+ }
175
+ .select-device-btn {
176
+ width: 100%;
177
+ padding: 10px 16px;
178
+ background: #007AFF;
179
+ color: white;
180
+ border: none;
181
+ border-radius: 6px;
182
+ font-size: 14px;
183
+ font-weight: 500;
184
+ cursor: pointer;
185
+ transition: background 0.2s;
186
+ }
187
+ .select-device-btn:hover {
188
+ background: #0056b3;
189
+ }
190
+ .select-device-btn:active {
191
+ transform: scale(0.98);
192
+ }
193
+ .empty-state {
194
+ text-align: center;
195
+ padding: 40px;
196
+ color: #999;
197
+ }
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <div class="container">
202
+ <div class="header">
203
+ <h1>📱 Select ${deviceTypeLabel}</h1>
204
+ <p>Found ${devices.length} device${devices.length !== 1 ? 's' : ''}. Click on a device to select it.</p>
205
+ </div>
206
+ <div class="devices-grid">
207
+ ${devices.length > 0 ? deviceCards : '<div class="empty-state">No devices found</div>'}
208
+ </div>
209
+ </div>
210
+ <script>
211
+ function selectDevice(udid) {
212
+ // Send intent message to parent window
213
+ window.parent.postMessage({
214
+ type: 'intent',
215
+ payload: {
216
+ intent: 'select-device',
217
+ params: {
218
+ platform: '${platform}',
219
+ ${deviceType ? `deviceType: '${deviceType}',` : ''}
220
+ deviceUdid: udid
221
+ }
222
+ }
223
+ }, '*');
224
+ }
225
+
226
+ // Add click handlers for better UX
227
+ document.querySelectorAll('.device-card').forEach(card => {
228
+ card.addEventListener('click', (e) => {
229
+ if (e.target.classList.contains('select-device-btn')) return;
230
+ const udid = card.dataset.udid;
231
+ selectDevice(udid);
232
+ });
233
+ });
234
+ </script>
235
+ </body>
236
+ </html>
237
+ `;
238
+ }
239
+
240
+ /**
241
+ * Creates a screenshot viewer UI component
242
+ * @param screenshotBase64 - Base64 encoded PNG image
243
+ * @param filepath - Path where screenshot was saved
244
+ * @returns HTML string for screenshot viewer
245
+ */
246
+ export function createScreenshotViewerUI(
247
+ screenshotBase64: string,
248
+ filepath: string
249
+ ): string {
250
+ return `
251
+ <!DOCTYPE html>
252
+ <html lang="en">
253
+ <head>
254
+ <meta charset="UTF-8">
255
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
256
+ <title>Screenshot Viewer</title>
257
+ <style>
258
+ * {
259
+ margin: 0;
260
+ padding: 0;
261
+ box-sizing: border-box;
262
+ }
263
+ body {
264
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
265
+ background: #1a1a1a;
266
+ color: #fff;
267
+ padding: 20px;
268
+ overflow: hidden;
269
+ }
270
+ .viewer-container {
271
+ display: flex;
272
+ flex-direction: column;
273
+ height: 100vh;
274
+ max-width: 100%;
275
+ }
276
+ .toolbar {
277
+ display: flex;
278
+ justify-content: space-between;
279
+ align-items: center;
280
+ padding: 12px 16px;
281
+ background: #2a2a2a;
282
+ border-radius: 8px 8px 0 0;
283
+ margin-bottom: 1px;
284
+ }
285
+ .toolbar-left {
286
+ display: flex;
287
+ gap: 8px;
288
+ align-items: center;
289
+ }
290
+ .toolbar-right {
291
+ display: flex;
292
+ gap: 8px;
293
+ }
294
+ .btn {
295
+ padding: 6px 12px;
296
+ background: #007AFF;
297
+ color: white;
298
+ border: none;
299
+ border-radius: 4px;
300
+ font-size: 13px;
301
+ cursor: pointer;
302
+ transition: background 0.2s;
303
+ }
304
+ .btn:hover {
305
+ background: #0056b3;
306
+ }
307
+ .btn-secondary {
308
+ background: #444;
309
+ }
310
+ .btn-secondary:hover {
311
+ background: #555;
312
+ }
313
+ .filepath {
314
+ font-size: 12px;
315
+ color: #999;
316
+ font-family: 'Monaco', 'Menlo', monospace;
317
+ }
318
+ .image-container {
319
+ flex: 1;
320
+ overflow: auto;
321
+ background: #1a1a1a;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ padding: 20px;
326
+ }
327
+ .screenshot-img {
328
+ max-width: 100%;
329
+ max-height: 100%;
330
+ object-fit: contain;
331
+ border-radius: 4px;
332
+ box-shadow: 0 4px 20px rgba(0,0,0,0.5);
333
+ cursor: zoom-in;
334
+ }
335
+ .screenshot-img.zoomed {
336
+ cursor: zoom-out;
337
+ transform: scale(2);
338
+ transition: transform 0.3s;
339
+ }
340
+ .zoom-controls {
341
+ position: absolute;
342
+ bottom: 20px;
343
+ right: 20px;
344
+ display: flex;
345
+ gap: 8px;
346
+ background: rgba(42, 42, 42, 0.9);
347
+ padding: 8px;
348
+ border-radius: 6px;
349
+ }
350
+ .zoom-btn {
351
+ width: 32px;
352
+ height: 32px;
353
+ background: #007AFF;
354
+ color: white;
355
+ border: none;
356
+ border-radius: 4px;
357
+ cursor: pointer;
358
+ font-size: 16px;
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: center;
362
+ }
363
+ .zoom-btn:hover {
364
+ background: #0056b3;
365
+ }
366
+ </style>
367
+ </head>
368
+ <body>
369
+ <div class="viewer-container">
370
+ <div class="toolbar">
371
+ <div class="toolbar-left">
372
+ <span style="font-size: 14px; font-weight: 500;">📸 Screenshot</span>
373
+ <span class="filepath">${filepath}</span>
374
+ </div>
375
+ <div class="toolbar-right">
376
+ <button class="btn btn-secondary" onclick="downloadScreenshot()">Download</button>
377
+ <button class="btn" onclick="takeNewScreenshot()">Take New</button>
378
+ </div>
379
+ </div>
380
+ <div class="image-container" id="imageContainer">
381
+ <img src="data:image/png;base64,${screenshotBase64}"
382
+ alt="Screenshot"
383
+ class="screenshot-img"
384
+ id="screenshotImg"
385
+ onclick="toggleZoom()">
386
+ </div>
387
+ <div class="zoom-controls">
388
+ <button class="zoom-btn" onclick="zoomIn()">+</button>
389
+ <button class="zoom-btn" onclick="zoomOut()">−</button>
390
+ <button class="zoom-btn" onclick="resetZoom()">⌂</button>
391
+ </div>
392
+ </div>
393
+ <script>
394
+ let currentZoom = 1;
395
+ const img = document.getElementById('screenshotImg');
396
+
397
+ function toggleZoom() {
398
+ if (currentZoom === 1) {
399
+ zoomIn();
400
+ } else {
401
+ resetZoom();
402
+ }
403
+ }
404
+
405
+ function zoomIn() {
406
+ currentZoom = Math.min(currentZoom + 0.5, 4);
407
+ img.style.transform = \`scale(\${currentZoom})\`;
408
+ }
409
+
410
+ function zoomOut() {
411
+ currentZoom = Math.max(currentZoom - 0.5, 0.5);
412
+ img.style.transform = \`scale(\${currentZoom})\`;
413
+ }
414
+
415
+ function resetZoom() {
416
+ currentZoom = 1;
417
+ img.style.transform = 'scale(1)';
418
+ }
419
+
420
+ function downloadScreenshot() {
421
+ const link = document.createElement('a');
422
+ link.href = img.src;
423
+ link.download = '${filepath.split('/').pop() || 'screenshot.png'}';
424
+ link.click();
425
+ }
426
+
427
+ function takeNewScreenshot() {
428
+ window.parent.postMessage({
429
+ type: 'tool',
430
+ payload: {
431
+ toolName: 'appium_screenshot',
432
+ params: {}
433
+ }
434
+ }, '*');
435
+ }
436
+
437
+ // Keyboard shortcuts
438
+ document.addEventListener('keydown', (e) => {
439
+ if (e.key === '+' || e.key === '=') zoomIn();
440
+ if (e.key === '-') zoomOut();
441
+ if (e.key === '0') resetZoom();
442
+ });
443
+ </script>
444
+ </body>
445
+ </html>
446
+ `;
447
+ }
448
+
449
+ /**
450
+ * Creates a session dashboard UI component
451
+ * @param sessionInfo - Session information object
452
+ * @returns HTML string for session dashboard
453
+ */
454
+ export function createSessionDashboardUI(sessionInfo: {
455
+ sessionId: string | any;
456
+ platform: string;
457
+ automationName: string;
458
+ deviceName?: string;
459
+ platformVersion?: string;
460
+ udid?: string;
461
+ }): string {
462
+ // Safely convert sessionId to string
463
+ const sessionIdStr =
464
+ typeof sessionInfo.sessionId === 'string'
465
+ ? sessionInfo.sessionId
466
+ : String(sessionInfo.sessionId || 'Unknown');
467
+
468
+ // Get first 8 characters for display, or full string if shorter
469
+ const sessionIdDisplay =
470
+ sessionIdStr.length > 8
471
+ ? `${sessionIdStr.substring(0, 8)}...`
472
+ : sessionIdStr;
473
+
474
+ return `
475
+ <!DOCTYPE html>
476
+ <html lang="en">
477
+ <head>
478
+ <meta charset="UTF-8">
479
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
480
+ <title>Session Dashboard</title>
481
+ <style>
482
+ * {
483
+ margin: 0;
484
+ padding: 0;
485
+ box-sizing: border-box;
486
+ }
487
+ body {
488
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
489
+ background: #f5f5f5;
490
+ padding: 20px;
491
+ }
492
+ .dashboard {
493
+ max-width: 800px;
494
+ margin: 0 auto;
495
+ background: white;
496
+ border-radius: 12px;
497
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
498
+ overflow: hidden;
499
+ }
500
+ .header {
501
+ background: linear-gradient(135deg, #007AFF 0%, #0056b3 100%);
502
+ color: white;
503
+ padding: 24px;
504
+ }
505
+ .header h1 {
506
+ font-size: 24px;
507
+ margin-bottom: 8px;
508
+ }
509
+ .header .status {
510
+ display: inline-block;
511
+ padding: 4px 12px;
512
+ background: rgba(255,255,255,0.2);
513
+ border-radius: 12px;
514
+ font-size: 12px;
515
+ font-weight: 500;
516
+ }
517
+ .content {
518
+ padding: 24px;
519
+ }
520
+ .info-grid {
521
+ display: grid;
522
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
523
+ gap: 16px;
524
+ margin-bottom: 24px;
525
+ }
526
+ .info-card {
527
+ padding: 16px;
528
+ background: #f8f9fa;
529
+ border-radius: 8px;
530
+ border-left: 3px solid #007AFF;
531
+ }
532
+ .info-card label {
533
+ display: block;
534
+ font-size: 12px;
535
+ color: #666;
536
+ margin-bottom: 4px;
537
+ text-transform: uppercase;
538
+ font-weight: 500;
539
+ }
540
+ .info-card value {
541
+ display: block;
542
+ font-size: 16px;
543
+ color: #1a1a1a;
544
+ font-weight: 600;
545
+ }
546
+ .actions {
547
+ display: flex;
548
+ gap: 12px;
549
+ flex-wrap: wrap;
550
+ }
551
+ .btn {
552
+ padding: 10px 20px;
553
+ border: none;
554
+ border-radius: 6px;
555
+ font-size: 14px;
556
+ font-weight: 500;
557
+ cursor: pointer;
558
+ transition: all 0.2s;
559
+ }
560
+ .btn-primary {
561
+ background: #007AFF;
562
+ color: white;
563
+ }
564
+ .btn-primary:hover {
565
+ background: #0056b3;
566
+ }
567
+ .btn-secondary {
568
+ background: #f0f0f0;
569
+ color: #333;
570
+ }
571
+ .btn-secondary:hover {
572
+ background: #e0e0e0;
573
+ }
574
+ .btn-danger {
575
+ background: #dc3545;
576
+ color: white;
577
+ }
578
+ .btn-danger:hover {
579
+ background: #c82333;
580
+ }
581
+ </style>
582
+ </head>
583
+ <body>
584
+ <div class="dashboard">
585
+ <div class="header">
586
+ <h1>📱 Appium Session Dashboard</h1>
587
+ <span class="status">● Active</span>
588
+ </div>
589
+ <div class="content">
590
+ <div class="info-grid">
591
+ <div class="info-card">
592
+ <label>Session ID</label>
593
+ <value>${sessionIdDisplay}</value>
594
+ </div>
595
+ <div class="info-card">
596
+ <label>Platform</label>
597
+ <value>${sessionInfo.platform}</value>
598
+ </div>
599
+ <div class="info-card">
600
+ <label>Automation</label>
601
+ <value>${sessionInfo.automationName}</value>
602
+ </div>
603
+ ${
604
+ sessionInfo.deviceName
605
+ ? `
606
+ <div class="info-card">
607
+ <label>Device</label>
608
+ <value>${sessionInfo.deviceName}</value>
609
+ </div>
610
+ `
611
+ : ''
612
+ }
613
+ ${
614
+ sessionInfo.platformVersion
615
+ ? `
616
+ <div class="info-card">
617
+ <label>Platform Version</label>
618
+ <value>${sessionInfo.platformVersion}</value>
619
+ </div>
620
+ `
621
+ : ''
622
+ }
623
+ ${
624
+ sessionInfo.udid
625
+ ? `
626
+ <div class="info-card">
627
+ <label>UDID</label>
628
+ <value><code style="font-size: 12px;">${sessionInfo.udid}</code></value>
629
+ </div>
630
+ `
631
+ : ''
632
+ }
633
+ </div>
634
+ <div class="actions">
635
+ <button class="btn btn-primary" onclick="takeScreenshot()">📸 Screenshot</button>
636
+ <button class="btn btn-primary" onclick="getPageSource()">📄 Page Source</button>
637
+ <button class="btn btn-primary" onclick="generateLocators()">🔍 Generate Locators</button>
638
+ <button class="btn btn-secondary" onclick="getContexts()">🌐 Contexts</button>
639
+ <button class="btn btn-danger" onclick="deleteSession()">🗑️ End Session</button>
640
+ </div>
641
+ </div>
642
+ </div>
643
+ <script>
644
+ function takeScreenshot() {
645
+ window.parent.postMessage({
646
+ type: 'tool',
647
+ payload: {
648
+ toolName: 'appium_screenshot',
649
+ params: {}
650
+ }
651
+ }, '*');
652
+ }
653
+
654
+ function getPageSource() {
655
+ window.parent.postMessage({
656
+ type: 'tool',
657
+ payload: {
658
+ toolName: 'appium_get_page_source',
659
+ params: {}
660
+ }
661
+ }, '*');
662
+ }
663
+
664
+ function generateLocators() {
665
+ window.parent.postMessage({
666
+ type: 'tool',
667
+ payload: {
668
+ toolName: 'generate_locators',
669
+ params: {}
670
+ }
671
+ }, '*');
672
+ }
673
+
674
+ function getContexts() {
675
+ window.parent.postMessage({
676
+ type: 'tool',
677
+ payload: {
678
+ toolName: 'appium_get_contexts',
679
+ params: {}
680
+ }
681
+ }, '*');
682
+ }
683
+
684
+ function deleteSession() {
685
+ if (confirm('Are you sure you want to end this session?')) {
686
+ window.parent.postMessage({
687
+ type: 'tool',
688
+ payload: {
689
+ toolName: 'delete_session',
690
+ params: {}
691
+ }
692
+ }, '*');
693
+ }
694
+ }
695
+ </script>
696
+ </body>
697
+ </html>
698
+ `;
699
+ }
700
+
701
+ /**
702
+ * Creates a locator generator UI component
703
+ * @param locators - Array of elements with locators
704
+ * @returns HTML string for locator generator UI
705
+ */
706
+ export function createLocatorGeneratorUI(
707
+ locators: Array<{
708
+ tagName: string;
709
+ locators: Record<string, string>;
710
+ text: string;
711
+ contentDesc: string;
712
+ resourceId: string;
713
+ clickable: boolean;
714
+ enabled: boolean;
715
+ displayed: boolean;
716
+ }>
717
+ ): string {
718
+ const locatorCards = locators
719
+ .map(
720
+ (element, index) => `
721
+ <div class="locator-card" data-index="${index}">
722
+ <div class="locator-header">
723
+ <h3>${element.tagName}</h3>
724
+ <div class="badges">
725
+ ${element.clickable ? '<span class="badge badge-clickable">Clickable</span>' : ''}
726
+ ${element.enabled ? '<span class="badge badge-enabled">Enabled</span>' : ''}
727
+ ${element.displayed ? '<span class="badge badge-displayed">Displayed</span>' : ''}
728
+ </div>
729
+ </div>
730
+ ${element.text ? `<p class="element-text"><strong>Text:</strong> ${element.text}</p>` : ''}
731
+ ${element.contentDesc ? `<p class="element-text"><strong>Content Desc:</strong> ${element.contentDesc}</p>` : ''}
732
+ ${element.resourceId ? `<p class="element-text"><strong>Resource ID:</strong> <code>${element.resourceId}</code></p>` : ''}
733
+ <div class="locators-list">
734
+ ${Object.entries(element.locators)
735
+ .map(
736
+ ([strategy, selector]) => `
737
+ <div class="locator-item">
738
+ <span class="strategy">${strategy}</span>
739
+ <code class="selector">${selector}</code>
740
+ <button class="test-btn" onclick="testLocator('${strategy}', \`${selector.replace(/`/g, '\\`')}\`)">Test</button>
741
+ </div>
742
+ `
743
+ )
744
+ .join('')}
745
+ </div>
746
+ </div>
747
+ `
748
+ )
749
+ .join('');
750
+
751
+ return `
752
+ <!DOCTYPE html>
753
+ <html lang="en">
754
+ <head>
755
+ <meta charset="UTF-8">
756
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
757
+ <title>Locator Generator</title>
758
+ <style>
759
+ * {
760
+ margin: 0;
761
+ padding: 0;
762
+ box-sizing: border-box;
763
+ }
764
+ body {
765
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
766
+ background: #f5f5f5;
767
+ padding: 20px;
768
+ }
769
+ .container {
770
+ max-width: 1200px;
771
+ margin: 0 auto;
772
+ }
773
+ .header {
774
+ margin-bottom: 24px;
775
+ }
776
+ .header h1 {
777
+ font-size: 24px;
778
+ margin-bottom: 8px;
779
+ }
780
+ .locators-grid {
781
+ display: grid;
782
+ gap: 16px;
783
+ }
784
+ .locator-card {
785
+ background: white;
786
+ border: 1px solid #e0e0e0;
787
+ border-radius: 8px;
788
+ padding: 16px;
789
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
790
+ }
791
+ .locator-header {
792
+ display: flex;
793
+ justify-content: space-between;
794
+ align-items: center;
795
+ margin-bottom: 12px;
796
+ }
797
+ .locator-header h3 {
798
+ font-size: 16px;
799
+ font-weight: 600;
800
+ }
801
+ .badges {
802
+ display: flex;
803
+ gap: 6px;
804
+ }
805
+ .badge {
806
+ padding: 2px 8px;
807
+ border-radius: 4px;
808
+ font-size: 11px;
809
+ font-weight: 500;
810
+ }
811
+ .badge-clickable {
812
+ background: #d4edda;
813
+ color: #155724;
814
+ }
815
+ .badge-enabled {
816
+ background: #d1ecf1;
817
+ color: #0c5460;
818
+ }
819
+ .badge-displayed {
820
+ background: #fff3cd;
821
+ color: #856404;
822
+ }
823
+ .element-text {
824
+ font-size: 13px;
825
+ color: #666;
826
+ margin-bottom: 8px;
827
+ }
828
+ .element-text code {
829
+ background: #f5f5f5;
830
+ padding: 2px 6px;
831
+ border-radius: 3px;
832
+ font-size: 12px;
833
+ }
834
+ .locators-list {
835
+ margin-top: 12px;
836
+ }
837
+ .locator-item {
838
+ display: flex;
839
+ align-items: center;
840
+ gap: 12px;
841
+ padding: 8px;
842
+ background: #f8f9fa;
843
+ border-radius: 4px;
844
+ margin-bottom: 6px;
845
+ }
846
+ .strategy {
847
+ font-size: 12px;
848
+ font-weight: 600;
849
+ color: #007AFF;
850
+ min-width: 120px;
851
+ }
852
+ .selector {
853
+ flex: 1;
854
+ font-size: 12px;
855
+ font-family: 'Monaco', 'Menlo', monospace;
856
+ background: white;
857
+ padding: 4px 8px;
858
+ border-radius: 3px;
859
+ overflow-x: auto;
860
+ }
861
+ .test-btn {
862
+ padding: 4px 12px;
863
+ background: #007AFF;
864
+ color: white;
865
+ border: none;
866
+ border-radius: 4px;
867
+ font-size: 12px;
868
+ cursor: pointer;
869
+ }
870
+ .test-btn:hover {
871
+ background: #0056b3;
872
+ }
873
+ </style>
874
+ </head>
875
+ <body>
876
+ <div class="container">
877
+ <div class="header">
878
+ <h1>🔍 Generated Locators</h1>
879
+ <p>Found ${locators.length} interactable element${locators.length !== 1 ? 's' : ''}</p>
880
+ </div>
881
+ <div class="locators-grid">
882
+ ${locators.length > 0 ? locatorCards : '<p>No locators found</p>'}
883
+ </div>
884
+ </div>
885
+ <script>
886
+ function testLocator(strategy, selector) {
887
+ window.parent.postMessage({
888
+ type: 'tool',
889
+ payload: {
890
+ toolName: 'appium_find_element',
891
+ params: {
892
+ strategy: strategy,
893
+ selector: selector
894
+ }
895
+ }
896
+ }, '*');
897
+ }
898
+ </script>
899
+ </body>
900
+ </html>
901
+ `;
902
+ }
903
+
904
+ /**
905
+ * Creates a page source inspector UI component
906
+ * @param pageSource - XML page source string
907
+ * @returns HTML string for page source inspector
908
+ */
909
+ export function createPageSourceInspectorUI(pageSource: string): string {
910
+ // Escape HTML for safe display
911
+ const escapedSource = pageSource
912
+ .replace(/&/g, '&amp;')
913
+ .replace(/</g, '&lt;')
914
+ .replace(/>/g, '&gt;')
915
+ .replace(/"/g, '&quot;')
916
+ .replace(/'/g, '&#039;');
917
+
918
+ return `
919
+ <!DOCTYPE html>
920
+ <html lang="en">
921
+ <head>
922
+ <meta charset="UTF-8">
923
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
924
+ <title>Page Source Inspector</title>
925
+ <style>
926
+ * {
927
+ margin: 0;
928
+ padding: 0;
929
+ box-sizing: border-box;
930
+ }
931
+ body {
932
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
933
+ background: #1e1e1e;
934
+ color: #d4d4d4;
935
+ padding: 0;
936
+ overflow: hidden;
937
+ }
938
+ .toolbar {
939
+ display: flex;
940
+ justify-content: space-between;
941
+ align-items: center;
942
+ padding: 12px 16px;
943
+ background: #2d2d2d;
944
+ border-bottom: 1px solid #3e3e3e;
945
+ }
946
+ .toolbar-left {
947
+ display: flex;
948
+ gap: 8px;
949
+ align-items: center;
950
+ }
951
+ .toolbar-right {
952
+ display: flex;
953
+ gap: 8px;
954
+ }
955
+ .btn {
956
+ padding: 6px 12px;
957
+ background: #007AFF;
958
+ color: white;
959
+ border: none;
960
+ border-radius: 4px;
961
+ font-size: 13px;
962
+ cursor: pointer;
963
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
964
+ }
965
+ .btn:hover {
966
+ background: #0056b3;
967
+ }
968
+ .btn-secondary {
969
+ background: #444;
970
+ }
971
+ .btn-secondary:hover {
972
+ background: #555;
973
+ }
974
+ .info {
975
+ font-size: 12px;
976
+ color: #999;
977
+ }
978
+ .viewer {
979
+ height: calc(100vh - 50px);
980
+ overflow: auto;
981
+ padding: 16px;
982
+ }
983
+ .xml-content {
984
+ background: #1e1e1e;
985
+ color: #d4d4d4;
986
+ white-space: pre;
987
+ font-size: 13px;
988
+ line-height: 1.6;
989
+ }
990
+ .xml-tag {
991
+ color: #569cd6;
992
+ }
993
+ .xml-attr {
994
+ color: #9cdcfe;
995
+ }
996
+ .xml-value {
997
+ color: #ce9178;
998
+ }
999
+ .search-box {
1000
+ padding: 6px 12px;
1001
+ background: #3e3e3e;
1002
+ border: 1px solid #555;
1003
+ border-radius: 4px;
1004
+ color: #d4d4d4;
1005
+ font-size: 13px;
1006
+ width: 200px;
1007
+ }
1008
+ .search-box:focus {
1009
+ outline: none;
1010
+ border-color: #007AFF;
1011
+ }
1012
+ </style>
1013
+ </head>
1014
+ <body>
1015
+ <div class="toolbar">
1016
+ <div class="toolbar-left">
1017
+ <span style="font-size: 14px; font-weight: 500;">📄 Page Source Inspector</span>
1018
+ <span class="info">${pageSource.length} characters</span>
1019
+ </div>
1020
+ <div class="toolbar-right">
1021
+ <input type="text" class="search-box" id="searchBox" placeholder="Search...">
1022
+ <button class="btn btn-secondary" onclick="copyToClipboard()">Copy</button>
1023
+ <button class="btn btn-secondary" onclick="formatXML()">Format</button>
1024
+ <button class="btn" onclick="generateLocators()">Generate Locators</button>
1025
+ </div>
1026
+ </div>
1027
+ <div class="viewer">
1028
+ <pre class="xml-content" id="xmlContent">${escapedSource}</pre>
1029
+ </div>
1030
+ <script>
1031
+ function copyToClipboard() {
1032
+ const text = document.getElementById('xmlContent').textContent;
1033
+ navigator.clipboard.writeText(text).then(() => {
1034
+ alert('Copied to clipboard!');
1035
+ });
1036
+ }
1037
+
1038
+ function formatXML() {
1039
+ const content = document.getElementById('xmlContent').textContent;
1040
+ try {
1041
+ const parser = new DOMParser();
1042
+ const xmlDoc = parser.parseFromString(content, 'text/xml');
1043
+ const serializer = new XMLSerializer();
1044
+ const formatted = serializer.serializeToString(xmlDoc);
1045
+ document.getElementById('xmlContent').textContent = formatted;
1046
+ } catch (e) {
1047
+ alert('Failed to format XML');
1048
+ }
1049
+ }
1050
+
1051
+ function generateLocators() {
1052
+ window.parent.postMessage({
1053
+ type: 'tool',
1054
+ payload: {
1055
+ toolName: 'generate_locators',
1056
+ params: {}
1057
+ }
1058
+ }, '*');
1059
+ }
1060
+
1061
+ // Search functionality
1062
+ document.getElementById('searchBox').addEventListener('input', (e) => {
1063
+ const searchTerm = e.target.value.toLowerCase();
1064
+ const content = document.getElementById('xmlContent');
1065
+ if (!searchTerm) {
1066
+ content.innerHTML = \`${escapedSource}\`;
1067
+ return;
1068
+ }
1069
+ const highlighted = content.textContent.replace(
1070
+ new RegExp(\`(\${searchTerm})\`, 'gi'),
1071
+ '<mark style="background: #ffd700; color: #000;">$1</mark>'
1072
+ );
1073
+ content.innerHTML = highlighted;
1074
+ });
1075
+ </script>
1076
+ </body>
1077
+ </html>
1078
+ `;
1079
+ }
1080
+
1081
+ /**
1082
+ * Creates a context switcher UI component
1083
+ * @param contexts - Array of context names
1084
+ * @param currentContext - Currently active context name
1085
+ * @returns HTML string for context switcher
1086
+ */
1087
+ export function createContextSwitcherUI(
1088
+ contexts: string[],
1089
+ currentContext: string | null
1090
+ ): string {
1091
+ const contextCards = contexts
1092
+ .map(
1093
+ context => `
1094
+ <div class="context-card ${context === currentContext ? 'active' : ''}"
1095
+ onclick="switchContext('${context}')">
1096
+ <div class="context-header">
1097
+ <h3>${context}</h3>
1098
+ ${context === currentContext ? '<span class="badge-active">Active</span>' : ''}
1099
+ </div>
1100
+ <div class="context-type">
1101
+ ${context === 'NATIVE_APP' ? '📱 Native App' : '🌐 WebView'}
1102
+ </div>
1103
+ <button class="switch-btn" onclick="event.stopPropagation(); switchContext('${context}')">
1104
+ ${context === currentContext ? 'Current' : 'Switch'}
1105
+ </button>
1106
+ </div>
1107
+ `
1108
+ )
1109
+ .join('');
1110
+
1111
+ return `
1112
+ <!DOCTYPE html>
1113
+ <html lang="en">
1114
+ <head>
1115
+ <meta charset="UTF-8">
1116
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1117
+ <title>Context Switcher</title>
1118
+ <style>
1119
+ * {
1120
+ margin: 0;
1121
+ padding: 0;
1122
+ box-sizing: border-box;
1123
+ }
1124
+ body {
1125
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
1126
+ background: #f5f5f5;
1127
+ padding: 20px;
1128
+ }
1129
+ .container {
1130
+ max-width: 800px;
1131
+ margin: 0 auto;
1132
+ }
1133
+ .header {
1134
+ margin-bottom: 24px;
1135
+ }
1136
+ .header h1 {
1137
+ font-size: 24px;
1138
+ margin-bottom: 8px;
1139
+ }
1140
+ .header p {
1141
+ color: #666;
1142
+ font-size: 14px;
1143
+ }
1144
+ .contexts-grid {
1145
+ display: grid;
1146
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
1147
+ gap: 16px;
1148
+ }
1149
+ .context-card {
1150
+ background: white;
1151
+ border: 2px solid #e0e0e0;
1152
+ border-radius: 8px;
1153
+ padding: 16px;
1154
+ cursor: pointer;
1155
+ transition: all 0.2s;
1156
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1157
+ }
1158
+ .context-card:hover {
1159
+ border-color: #007AFF;
1160
+ box-shadow: 0 4px 12px rgba(0,122,255,0.15);
1161
+ transform: translateY(-2px);
1162
+ }
1163
+ .context-card.active {
1164
+ border-color: #007AFF;
1165
+ background: #f0f7ff;
1166
+ }
1167
+ .context-header {
1168
+ display: flex;
1169
+ justify-content: space-between;
1170
+ align-items: center;
1171
+ margin-bottom: 12px;
1172
+ }
1173
+ .context-header h3 {
1174
+ font-size: 16px;
1175
+ font-weight: 600;
1176
+ color: #1a1a1a;
1177
+ font-family: 'Monaco', 'Menlo', monospace;
1178
+ }
1179
+ .badge-active {
1180
+ padding: 4px 8px;
1181
+ background: #d4edda;
1182
+ color: #155724;
1183
+ border-radius: 4px;
1184
+ font-size: 11px;
1185
+ font-weight: 500;
1186
+ }
1187
+ .context-type {
1188
+ font-size: 13px;
1189
+ color: #666;
1190
+ margin-bottom: 12px;
1191
+ }
1192
+ .switch-btn {
1193
+ width: 100%;
1194
+ padding: 8px 16px;
1195
+ background: #007AFF;
1196
+ color: white;
1197
+ border: none;
1198
+ border-radius: 6px;
1199
+ font-size: 14px;
1200
+ font-weight: 500;
1201
+ cursor: pointer;
1202
+ transition: background 0.2s;
1203
+ }
1204
+ .switch-btn:hover {
1205
+ background: #0056b3;
1206
+ }
1207
+ .context-card.active .switch-btn {
1208
+ background: #6c757d;
1209
+ cursor: default;
1210
+ }
1211
+ </style>
1212
+ </head>
1213
+ <body>
1214
+ <div class="container">
1215
+ <div class="header">
1216
+ <h1>🌐 Context Switcher</h1>
1217
+ <p>Found ${contexts.length} context${contexts.length !== 1 ? 's' : ''}. Click to switch.</p>
1218
+ </div>
1219
+ <div class="contexts-grid">
1220
+ ${contexts.length > 0 ? contextCards : '<p>No contexts available</p>'}
1221
+ </div>
1222
+ </div>
1223
+ <script>
1224
+ function switchContext(contextName) {
1225
+ window.parent.postMessage({
1226
+ type: 'tool',
1227
+ payload: {
1228
+ toolName: 'appium_switch_context',
1229
+ params: {
1230
+ context: contextName
1231
+ }
1232
+ }
1233
+ }, '*');
1234
+ }
1235
+ </script>
1236
+ </body>
1237
+ </html>
1238
+ `;
1239
+ }
1240
+
1241
+ /**
1242
+ * Creates an app list UI component
1243
+ * @param apps - Array of app objects with packageName and appName
1244
+ * @returns HTML string for app list
1245
+ */
1246
+ export function createAppListUI(
1247
+ apps: Array<{ packageName: string; appName?: string }>
1248
+ ): string {
1249
+ const appCards = apps
1250
+ .map(
1251
+ app => `
1252
+ <div class="app-card" data-package="${app.packageName}">
1253
+ <div class="app-header">
1254
+ <h3>${app.appName || app.packageName}</h3>
1255
+ </div>
1256
+ <div class="app-details">
1257
+ <p><strong>Package:</strong> <code>${app.packageName}</code></p>
1258
+ </div>
1259
+ <div class="app-actions">
1260
+ <button class="btn btn-primary" onclick="activateApp('${app.packageName}')">Activate</button>
1261
+ <button class="btn btn-secondary" onclick="terminateApp('${app.packageName}')">Terminate</button>
1262
+ <button class="btn btn-danger" onclick="uninstallApp('${app.packageName}')">Uninstall</button>
1263
+ </div>
1264
+ </div>
1265
+ `
1266
+ )
1267
+ .join('');
1268
+
1269
+ return `
1270
+ <!DOCTYPE html>
1271
+ <html lang="en">
1272
+ <head>
1273
+ <meta charset="UTF-8">
1274
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1275
+ <title>Installed Apps</title>
1276
+ <style>
1277
+ * {
1278
+ margin: 0;
1279
+ padding: 0;
1280
+ box-sizing: border-box;
1281
+ }
1282
+ body {
1283
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
1284
+ background: #f5f5f5;
1285
+ padding: 20px;
1286
+ }
1287
+ .container {
1288
+ max-width: 1200px;
1289
+ margin: 0 auto;
1290
+ }
1291
+ .header {
1292
+ margin-bottom: 24px;
1293
+ display: flex;
1294
+ justify-content: space-between;
1295
+ align-items: center;
1296
+ }
1297
+ .header h1 {
1298
+ font-size: 24px;
1299
+ }
1300
+ .header p {
1301
+ color: #666;
1302
+ font-size: 14px;
1303
+ }
1304
+ .search-box {
1305
+ padding: 8px 12px;
1306
+ border: 1px solid #ddd;
1307
+ border-radius: 6px;
1308
+ font-size: 14px;
1309
+ width: 300px;
1310
+ }
1311
+ .apps-grid {
1312
+ display: grid;
1313
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1314
+ gap: 16px;
1315
+ }
1316
+ .app-card {
1317
+ background: white;
1318
+ border: 1px solid #e0e0e0;
1319
+ border-radius: 8px;
1320
+ padding: 16px;
1321
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1322
+ transition: all 0.2s;
1323
+ }
1324
+ .app-card:hover {
1325
+ border-color: #007AFF;
1326
+ box-shadow: 0 4px 12px rgba(0,122,255,0.15);
1327
+ }
1328
+ .app-header h3 {
1329
+ font-size: 16px;
1330
+ font-weight: 600;
1331
+ margin-bottom: 12px;
1332
+ color: #1a1a1a;
1333
+ }
1334
+ .app-details {
1335
+ margin-bottom: 12px;
1336
+ font-size: 13px;
1337
+ }
1338
+ .app-details code {
1339
+ background: #f5f5f5;
1340
+ padding: 2px 6px;
1341
+ border-radius: 3px;
1342
+ font-size: 12px;
1343
+ font-family: 'Monaco', 'Menlo', monospace;
1344
+ }
1345
+ .app-actions {
1346
+ display: flex;
1347
+ gap: 8px;
1348
+ flex-wrap: wrap;
1349
+ }
1350
+ .btn {
1351
+ padding: 6px 12px;
1352
+ border: none;
1353
+ border-radius: 4px;
1354
+ font-size: 12px;
1355
+ font-weight: 500;
1356
+ cursor: pointer;
1357
+ transition: background 0.2s;
1358
+ }
1359
+ .btn-primary {
1360
+ background: #007AFF;
1361
+ color: white;
1362
+ }
1363
+ .btn-primary:hover {
1364
+ background: #0056b3;
1365
+ }
1366
+ .btn-secondary {
1367
+ background: #6c757d;
1368
+ color: white;
1369
+ }
1370
+ .btn-secondary:hover {
1371
+ background: #5a6268;
1372
+ }
1373
+ .btn-danger {
1374
+ background: #dc3545;
1375
+ color: white;
1376
+ }
1377
+ .btn-danger:hover {
1378
+ background: #c82333;
1379
+ }
1380
+ .hidden {
1381
+ display: none;
1382
+ }
1383
+ </style>
1384
+ </head>
1385
+ <body>
1386
+ <div class="container">
1387
+ <div class="header">
1388
+ <div>
1389
+ <h1>📱 Installed Apps</h1>
1390
+ <p>Found ${apps.length} app${apps.length !== 1 ? 's' : ''}</p>
1391
+ </div>
1392
+ <input type="text" class="search-box" id="searchBox" placeholder="Search apps...">
1393
+ </div>
1394
+ <div class="apps-grid" id="appsGrid">
1395
+ ${apps.length > 0 ? appCards : '<p>No apps found</p>'}
1396
+ </div>
1397
+ </div>
1398
+ <script>
1399
+ function activateApp(packageName) {
1400
+ window.parent.postMessage({
1401
+ type: 'tool',
1402
+ payload: {
1403
+ toolName: 'appium_activate_app',
1404
+ params: {
1405
+ id: packageName
1406
+ }
1407
+ }
1408
+ }, '*');
1409
+ }
1410
+
1411
+ function terminateApp(packageName) {
1412
+ if (confirm('Are you sure you want to terminate this app?')) {
1413
+ window.parent.postMessage({
1414
+ type: 'tool',
1415
+ payload: {
1416
+ toolName: 'appium_terminate_app',
1417
+ params: {
1418
+ id: packageName
1419
+ }
1420
+ }
1421
+ }, '*');
1422
+ }
1423
+ }
1424
+
1425
+ function uninstallApp(packageName) {
1426
+ if (confirm('Are you sure you want to uninstall this app? This action cannot be undone.')) {
1427
+ window.parent.postMessage({
1428
+ type: 'tool',
1429
+ payload: {
1430
+ toolName: 'appium_uninstall_app',
1431
+ params: {
1432
+ id: packageName
1433
+ }
1434
+ }
1435
+ }, '*');
1436
+ }
1437
+ }
1438
+
1439
+ // Search functionality
1440
+ document.getElementById('searchBox').addEventListener('input', (e) => {
1441
+ const searchTerm = e.target.value.toLowerCase();
1442
+ const cards = document.querySelectorAll('.app-card');
1443
+ cards.forEach(card => {
1444
+ const text = card.textContent.toLowerCase();
1445
+ if (text.includes(searchTerm)) {
1446
+ card.classList.remove('hidden');
1447
+ } else {
1448
+ card.classList.add('hidden');
1449
+ }
1450
+ });
1451
+ });
1452
+ </script>
1453
+ </body>
1454
+ </html>
1455
+ `;
1456
+ }
1457
+
1458
+ /**
1459
+ * Creates a test code viewer UI component
1460
+ * @param code - Generated test code string
1461
+ * @param language - Code language (java, javascript, etc.)
1462
+ * @returns HTML string for test code viewer
1463
+ */
1464
+ export function createTestCodeViewerUI(
1465
+ code: string,
1466
+ language: string = 'java'
1467
+ ): string {
1468
+ // Escape HTML for safe display
1469
+ const escapedCode = code
1470
+ .replace(/&/g, '&amp;')
1471
+ .replace(/</g, '&lt;')
1472
+ .replace(/>/g, '&gt;')
1473
+ .replace(/"/g, '&quot;')
1474
+ .replace(/'/g, '&#039;');
1475
+
1476
+ return `
1477
+ <!DOCTYPE html>
1478
+ <html lang="en">
1479
+ <head>
1480
+ <meta charset="UTF-8">
1481
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1482
+ <title>Test Code Viewer</title>
1483
+ <style>
1484
+ * {
1485
+ margin: 0;
1486
+ padding: 0;
1487
+ box-sizing: border-box;
1488
+ }
1489
+ body {
1490
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
1491
+ background: #1e1e1e;
1492
+ color: #d4d4d4;
1493
+ overflow: hidden;
1494
+ }
1495
+ .toolbar {
1496
+ display: flex;
1497
+ justify-content: space-between;
1498
+ align-items: center;
1499
+ padding: 12px 16px;
1500
+ background: #2d2d2d;
1501
+ border-bottom: 1px solid #3e3e3e;
1502
+ }
1503
+ .toolbar-left {
1504
+ display: flex;
1505
+ gap: 12px;
1506
+ align-items: center;
1507
+ }
1508
+ .toolbar-right {
1509
+ display: flex;
1510
+ gap: 8px;
1511
+ }
1512
+ .btn {
1513
+ padding: 6px 12px;
1514
+ background: #007AFF;
1515
+ color: white;
1516
+ border: none;
1517
+ border-radius: 4px;
1518
+ font-size: 13px;
1519
+ cursor: pointer;
1520
+ transition: background 0.2s;
1521
+ }
1522
+ .btn:hover {
1523
+ background: #0056b3;
1524
+ }
1525
+ .btn-secondary {
1526
+ background: #444;
1527
+ }
1528
+ .btn-secondary:hover {
1529
+ background: #555;
1530
+ }
1531
+ .language-badge {
1532
+ padding: 4px 8px;
1533
+ background: #007AFF;
1534
+ color: white;
1535
+ border-radius: 4px;
1536
+ font-size: 11px;
1537
+ font-weight: 500;
1538
+ text-transform: uppercase;
1539
+ }
1540
+ .viewer {
1541
+ height: calc(100vh - 50px);
1542
+ overflow: auto;
1543
+ padding: 16px;
1544
+ }
1545
+ .code-content {
1546
+ background: #1e1e1e;
1547
+ color: #d4d4d4;
1548
+ white-space: pre;
1549
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
1550
+ font-size: 13px;
1551
+ line-height: 1.6;
1552
+ margin: 0;
1553
+ }
1554
+ .line-numbers {
1555
+ display: inline-block;
1556
+ padding-right: 16px;
1557
+ color: #858585;
1558
+ user-select: none;
1559
+ text-align: right;
1560
+ min-width: 50px;
1561
+ }
1562
+ </style>
1563
+ </head>
1564
+ <body>
1565
+ <div class="toolbar">
1566
+ <div class="toolbar-left">
1567
+ <span style="font-size: 14px; font-weight: 500;">💻 Test Code Viewer</span>
1568
+ <span class="language-badge">${language}</span>
1569
+ <span style="font-size: 12px; color: #999;">${code.length} characters, ${code.split('\\n').length} lines</span>
1570
+ </div>
1571
+ <div class="toolbar-right">
1572
+ <button class="btn btn-secondary" onclick="copyToClipboard()">Copy Code</button>
1573
+ <button class="btn btn-secondary" onclick="downloadCode()">Download</button>
1574
+ <button class="btn" onclick="formatCode()">Format</button>
1575
+ </div>
1576
+ </div>
1577
+ <div class="viewer">
1578
+ <pre class="code-content" id="codeContent">${escapedCode}</pre>
1579
+ </div>
1580
+ <script>
1581
+ function copyToClipboard() {
1582
+ const text = document.getElementById('codeContent').textContent;
1583
+ navigator.clipboard.writeText(text).then(() => {
1584
+ alert('Code copied to clipboard!');
1585
+ });
1586
+ }
1587
+
1588
+ function downloadCode() {
1589
+ const text = document.getElementById('codeContent').textContent;
1590
+ const blob = new Blob([text], { type: 'text/plain' });
1591
+ const url = URL.createObjectURL(blob);
1592
+ const a = document.createElement('a');
1593
+ a.href = url;
1594
+ a.download = 'TestCode.${language === 'java' ? 'java' : 'js'}';
1595
+ a.click();
1596
+ URL.revokeObjectURL(url);
1597
+ }
1598
+
1599
+ function formatCode() {
1600
+ // Basic formatting - could be enhanced with a proper formatter
1601
+ const content = document.getElementById('codeContent');
1602
+ const text = content.textContent;
1603
+ // Add line numbers
1604
+ const lines = text.split('\\n');
1605
+ const formatted = lines.map((line, i) =>
1606
+ \`<span class="line-numbers">\${i + 1}</span>\${line}\`
1607
+ ).join('\\n');
1608
+ content.innerHTML = formatted;
1609
+ }
1610
+
1611
+ // Initial format
1612
+ formatCode();
1613
+ </script>
1614
+ </body>
1615
+ </html>
1616
+ `;
1617
+ }
1618
+
1619
+ /**
1620
+ * Helper function to add UI resource to response content
1621
+ * Returns both text and UI resource for backward compatibility
1622
+ */
1623
+ export function addUIResourceToResponse(
1624
+ response: { content: Array<{ type: string; text?: string }> },
1625
+ uiResource: ReturnType<typeof createUIResource>
1626
+ ): { content: Array<any> } {
1627
+ return {
1628
+ content: [...response.content, uiResource],
1629
+ };
1630
+ }