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