pinokiod 3.86.0 → 3.87.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 (67) hide show
  1. package/Dockerfile +61 -0
  2. package/docker-entrypoint.sh +75 -0
  3. package/kernel/api/hf/index.js +1 -1
  4. package/kernel/api/index.js +1 -1
  5. package/kernel/api/shell/index.js +6 -0
  6. package/kernel/api/terminal/index.js +166 -0
  7. package/kernel/bin/conda.js +3 -2
  8. package/kernel/bin/index.js +53 -2
  9. package/kernel/bin/setup.js +32 -0
  10. package/kernel/bin/vs.js +11 -2
  11. package/kernel/index.js +42 -2
  12. package/kernel/info.js +36 -0
  13. package/kernel/peer.js +42 -15
  14. package/kernel/router/index.js +23 -15
  15. package/kernel/router/localhost_static_router.js +0 -3
  16. package/kernel/router/pinokio_domain_router.js +333 -0
  17. package/kernel/shells.js +21 -1
  18. package/kernel/util.js +2 -2
  19. package/package.json +2 -1
  20. package/script/install-mode.js +33 -0
  21. package/script/pinokio.json +7 -0
  22. package/server/index.js +513 -173
  23. package/server/public/Socket.js +48 -0
  24. package/server/public/common.js +1441 -276
  25. package/server/public/fseditor.js +71 -12
  26. package/server/public/install.js +1 -1
  27. package/server/public/layout.js +740 -0
  28. package/server/public/modalinput.js +0 -1
  29. package/server/public/style.css +97 -105
  30. package/server/public/tab-idle-notifier.js +629 -0
  31. package/server/public/terminal_input_tracker.js +63 -0
  32. package/server/public/urldropdown.css +319 -53
  33. package/server/public/urldropdown.js +615 -159
  34. package/server/public/window_storage.js +97 -28
  35. package/server/socket.js +40 -9
  36. package/server/views/500.ejs +2 -2
  37. package/server/views/app.ejs +3136 -1367
  38. package/server/views/bookmarklet.ejs +1 -1
  39. package/server/views/bootstrap.ejs +1 -1
  40. package/server/views/columns.ejs +2 -13
  41. package/server/views/connect.ejs +3 -4
  42. package/server/views/container.ejs +1 -2
  43. package/server/views/d.ejs +223 -53
  44. package/server/views/editor.ejs +1 -1
  45. package/server/views/file_explorer.ejs +1 -1
  46. package/server/views/index.ejs +12 -11
  47. package/server/views/index2.ejs +4 -4
  48. package/server/views/init/index.ejs +4 -5
  49. package/server/views/install.ejs +1 -1
  50. package/server/views/layout.ejs +105 -0
  51. package/server/views/net.ejs +39 -7
  52. package/server/views/network.ejs +20 -6
  53. package/server/views/network2.ejs +1 -1
  54. package/server/views/old_network.ejs +2 -2
  55. package/server/views/partials/dynamic.ejs +3 -5
  56. package/server/views/partials/menu.ejs +3 -5
  57. package/server/views/partials/running.ejs +1 -1
  58. package/server/views/pro.ejs +1 -1
  59. package/server/views/prototype/index.ejs +1 -1
  60. package/server/views/review.ejs +11 -23
  61. package/server/views/rows.ejs +2 -13
  62. package/server/views/screenshots.ejs +293 -138
  63. package/server/views/settings.ejs +3 -4
  64. package/server/views/setup.ejs +1 -2
  65. package/server/views/shell.ejs +277 -26
  66. package/server/views/terminal.ejs +322 -49
  67. package/server/views/tools.ejs +448 -4
@@ -4,13 +4,64 @@
4
4
  */
5
5
 
6
6
  function initUrlDropdown(config = {}) {
7
- const urlInput = document.querySelector('.urlbar input[type="url"]');
8
- const dropdown = document.getElementById('url-dropdown');
7
+ if (window.PinokioUrlDropdown && typeof window.PinokioUrlDropdown.destroy === 'function') {
8
+ try {
9
+ window.PinokioUrlDropdown.destroy();
10
+ } catch (error) {
11
+ console.error('Failed to dispose existing URL dropdown', error);
12
+ }
13
+ }
14
+
15
+ let urlInput = document.querySelector('.urlbar input[type="url"]');
16
+ let dropdown = document.getElementById('url-dropdown');
9
17
  const mobileButton = document.getElementById('mobile-link-button');
10
-
18
+ const mobileButtonHandler = () => showMobileModal();
19
+
20
+ const fallbackElements = {
21
+ form: null,
22
+ dropdown: null
23
+ };
24
+
25
+ const ensureFallbackInput = () => {
26
+ if (fallbackElements.form) {
27
+ const existingInput = fallbackElements.form.querySelector('input[type="url"]');
28
+ if (existingInput) return existingInput;
29
+ }
30
+ if (!document.body) return null;
31
+ const form = document.createElement('form');
32
+ form.className = 'urlbar pinokio-url-fallback';
33
+ form.id = 'pinokio-url-fallback-form';
34
+ form.style.display = 'none';
35
+ const input = document.createElement('input');
36
+ input.type = 'url';
37
+ form.appendChild(input);
38
+ document.body.appendChild(form);
39
+ fallbackElements.form = form;
40
+ return input;
41
+ };
42
+
43
+ const ensureFallbackDropdown = () => {
44
+ if (fallbackElements.dropdown) return fallbackElements.dropdown;
45
+ if (!document.body) return null;
46
+ const el = document.createElement('div');
47
+ el.id = 'url-dropdown';
48
+ el.className = 'url-dropdown';
49
+ el.style.display = 'none';
50
+ document.body.appendChild(el);
51
+ fallbackElements.dropdown = el;
52
+ return el;
53
+ };
54
+
55
+ if (!urlInput) {
56
+ urlInput = ensureFallbackInput();
57
+ }
58
+
59
+ if (!dropdown) {
60
+ dropdown = ensureFallbackDropdown();
61
+ }
62
+
11
63
  if (!urlInput || !dropdown) {
12
- console.warn('URL dropdown elements not found');
13
- return;
64
+ console.warn('URL dropdown elements not found; process picker modal will be limited.');
14
65
  }
15
66
 
16
67
  // Configuration options
@@ -21,11 +72,62 @@ function initUrlDropdown(config = {}) {
21
72
  ...config
22
73
  };
23
74
 
75
+ const toArray = (value) => {
76
+ if (!value) return [];
77
+ if (Array.isArray(value)) {
78
+ return value.filter(Boolean);
79
+ }
80
+ return [value];
81
+ };
82
+
83
+ const getProcessUrls = (process) => {
84
+ const urls = (process && process.urls) || {};
85
+ const httpUrl = urls.http || (process && process.ip ? `http://${process.ip}` : null);
86
+ const httpsUrls = toArray(urls.https || (process && process.protocol === 'https' && process.url ? process.url : null))
87
+ .map((value) => {
88
+ if (typeof value !== 'string') return null;
89
+ const trimmed = value.trim();
90
+ if (!trimmed) return null;
91
+ if (/^https?:\/\//i.test(trimmed)) {
92
+ return trimmed.replace(/^http:/i, 'https:');
93
+ }
94
+ return `https://${trimmed}`;
95
+ })
96
+ .filter(Boolean);
97
+ return { httpUrl, httpsUrls };
98
+ };
99
+
100
+ const getProcessDisplayUrl = (process) => {
101
+ if (process && typeof process.url === 'string' && process.url.trim().length > 0) {
102
+ return process.url;
103
+ }
104
+ const { httpUrl, httpsUrls } = getProcessUrls(process);
105
+ if (httpsUrls.length > 0) {
106
+ return httpsUrls[0];
107
+ }
108
+ return httpUrl;
109
+ };
110
+
111
+ const getProcessFilterValues = (process) => {
112
+ const urls = new Set();
113
+ const display = getProcessDisplayUrl(process);
114
+ if (display) {
115
+ urls.add(display);
116
+ }
117
+ const { httpUrl, httpsUrls } = getProcessUrls(process);
118
+ if (httpUrl) {
119
+ urls.add(httpUrl);
120
+ }
121
+ httpsUrls.forEach((httpsUrl) => urls.add(httpsUrl));
122
+ return Array.from(urls);
123
+ };
124
+
24
125
  let isDropdownVisible = false;
25
126
  let allProcesses = []; // Store all processes for filtering
26
127
  let filteredProcesses = []; // Store currently filtered processes
27
128
  let createLauncherModal = null;
28
129
  let pendingCreateDetail = null;
130
+ let mobileModalKeydownHandler = null;
29
131
  const EMPTY_STATE_DESCRIPTION = 'enter a prompt to create a launcher';
30
132
 
31
133
  // Initialize input field state based on clear behavior
@@ -39,17 +141,19 @@ function initUrlDropdown(config = {}) {
39
141
  });
40
142
 
41
143
  // Event listeners
42
- urlInput.addEventListener('focus', function() {
43
- // Auto-select text for restore behavior to make filtering easier
44
- if (options.clearBehavior === 'restore' && urlInput.value) {
45
- // Use setTimeout to ensure the focus event completes first
46
- setTimeout(() => {
47
- urlInput.select();
48
- }, 0);
49
- }
50
- showDropdown();
51
- });
52
- urlInput.addEventListener('input', handleInputChange);
144
+ if (urlInput) {
145
+ urlInput.addEventListener('focus', function() {
146
+ // Auto-select text for restore behavior to make filtering easier
147
+ if (options.clearBehavior === 'restore' && urlInput.value) {
148
+ // Use setTimeout to ensure the focus event completes first
149
+ setTimeout(() => {
150
+ urlInput.select();
151
+ }, 0);
152
+ }
153
+ showDropdown();
154
+ });
155
+ urlInput.addEventListener('input', handleInputChange);
156
+ }
53
157
 
54
158
  // Hide dropdown when clicking outside
55
159
  document.addEventListener('click', function(e) {
@@ -84,6 +188,7 @@ function initUrlDropdown(config = {}) {
84
188
 
85
189
 
86
190
  function initializeInputValue() {
191
+ if (!urlInput) return;
87
192
  if (options.clearBehavior === 'empty') {
88
193
  urlInput.value = '';
89
194
  } else if (options.clearBehavior === 'restore') {
@@ -95,6 +200,7 @@ function initUrlDropdown(config = {}) {
95
200
  }
96
201
 
97
202
  function showDropdown() {
203
+ if (!dropdown || !urlInput) return;
98
204
  if (isDropdownVisible && allProcesses.length > 0) {
99
205
  // If dropdown is already visible and we have data, show all initially
100
206
  showAllProcesses();
@@ -138,6 +244,7 @@ function initUrlDropdown(config = {}) {
138
244
  }
139
245
 
140
246
  function handleInputChange() {
247
+ if (!urlInput) return;
141
248
  if (!isDropdownVisible) return;
142
249
 
143
250
  const query = urlInput.value.toLowerCase().trim();
@@ -152,11 +259,12 @@ function initUrlDropdown(config = {}) {
152
259
  } else {
153
260
  // Filter processes based on name and URL
154
261
  filteredProcesses = allProcesses.filter(process => {
155
- const url = `http://${process.ip}`;
156
- const name = process.name.toLowerCase();
157
- const urlLower = url.toLowerCase();
158
-
159
- return name.includes(query) || urlLower.includes(query);
262
+ const name = (process.name || '').toLowerCase();
263
+ if (name.includes(query)) {
264
+ return true;
265
+ }
266
+ const urls = getProcessFilterValues(process);
267
+ return urls.some((value) => (value || '').toLowerCase().includes(query));
160
268
  });
161
269
  }
162
270
 
@@ -165,7 +273,9 @@ function initUrlDropdown(config = {}) {
165
273
 
166
274
  function hideDropdown() {
167
275
  isDropdownVisible = false;
168
- dropdown.style.display = 'none';
276
+ if (dropdown) {
277
+ dropdown.style.display = 'none';
278
+ }
169
279
  }
170
280
 
171
281
  function createHostBadge(host) {
@@ -258,8 +368,6 @@ function initUrlDropdown(config = {}) {
258
368
  platformIcon = 'fa-solid fa-desktop';
259
369
  break;
260
370
  }
261
-
262
- console.log({ isLocal, host })
263
371
  const hostName = isLocal ? `${host.name} (This Machine)` : `${host.name} (Peer)`;
264
372
 
265
373
  return `
@@ -274,15 +382,63 @@ function initUrlDropdown(config = {}) {
274
382
  }
275
383
 
276
384
  function populateDropdown(processes) {
385
+ const currentUrl = window.location.href;
386
+ const currentTitle = document.title || 'Current tab';
387
+
388
+ let html = '';
389
+ if (currentUrl) {
390
+ html += `
391
+ <div class="url-dropdown-host-header current-tab">
392
+ <span class="host-name">Current tab</span>
393
+ </div>
394
+ <div class="url-dropdown-item" data-url="${escapeHtml(currentUrl)}" data-host-type="current">
395
+ <div class="url-dropdown-name">
396
+ <span>
397
+ <i class="fa-solid fa-clone"></i>
398
+ ${escapeHtml(currentTitle)}
399
+ </span>
400
+ </div>
401
+ <div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
402
+ </div>
403
+ `;
404
+ }
405
+
277
406
  if (processes.length === 0) {
278
- showEmptyState(dropdown, urlInput);
407
+ html += createEmptyStateHtml(getEmptyStateMessage(urlInput));
408
+ dropdown.innerHTML = html;
409
+ attachCreateButtonHandler(dropdown, urlInput);
410
+ dropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
411
+ item.addEventListener('click', function() {
412
+ const url = this.getAttribute('data-url');
413
+ const type = this.getAttribute('data-host-type');
414
+ urlInput.value = url;
415
+ urlInput.setAttribute("data-host-type", type || 'current');
416
+ hideDropdown();
417
+
418
+ if (type === "local") {
419
+ let redirect_uri = "/container?url=" + url;
420
+ location.href = redirect_uri;
421
+ } else {
422
+ if (!type || type === 'current') {
423
+ location.href = url;
424
+ return;
425
+ }
426
+ let u = new URL(url);
427
+ if (String(u.port) === "42000") {
428
+ window.open(url, "_blank", 'self');
429
+ } else {
430
+ let redirect_uri = "/container?url=" + url;
431
+ location.href = redirect_uri;
432
+ }
433
+ }
434
+ });
435
+ });
279
436
  return;
280
437
  }
281
438
 
282
439
  // Group processes by host
283
440
  const groupedProcesses = groupProcessesByHost(processes);
284
-
285
- let html = '';
441
+
286
442
  Object.keys(groupedProcesses).forEach(hostKey => {
287
443
  const hostData = groupedProcesses[hostKey];
288
444
  const hostInfo = hostData.host;
@@ -312,7 +468,10 @@ function initUrlDropdown(config = {}) {
312
468
  `;
313
469
  } else {
314
470
  // Normal selectable item
315
- const url = `http://${process.ip}`;
471
+ const url = getProcessDisplayUrl(process);
472
+ if (!url) {
473
+ return;
474
+ }
316
475
  html += `
317
476
  <div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
318
477
  <div class="url-dropdown-name">
@@ -334,13 +493,15 @@ function initUrlDropdown(config = {}) {
334
493
  const url = this.getAttribute('data-url');
335
494
  const type = this.getAttribute('data-host-type');
336
495
  urlInput.value = url;
337
- urlInput.setAttribute("data-host-type", type);
496
+ urlInput.setAttribute("data-host-type", type || 'remote');
338
497
  hideDropdown();
339
498
 
340
499
  // Navigate directly instead of dispatching submit event
341
500
  if (type === "local") {
342
501
  let redirect_uri = "/container?url=" + url;
343
502
  location.href = redirect_uri;
503
+ } else if (type === 'current') {
504
+ location.href = url;
344
505
  } else {
345
506
  let u = new URL(url);
346
507
  if (String(u.port) === "42000") {
@@ -370,98 +531,345 @@ function initUrlDropdown(config = {}) {
370
531
  return div.innerHTML;
371
532
  }
372
533
 
534
+ function getModalOverlay() {
535
+ return document.getElementById('url-modal-overlay');
536
+ }
537
+
538
+ function getModalRefs() {
539
+ const overlay = getModalOverlay();
540
+ return overlay ? overlay._modalRefs : null;
541
+ }
542
+
543
+ function resolveModal(refs, value) {
544
+ if (!refs || typeof refs.resolve !== 'function') {
545
+ return;
546
+ }
547
+ const resolver = refs.resolve;
548
+ refs.resolve = null;
549
+ refs.returnSelection = false;
550
+ try {
551
+ resolver(value);
552
+ } catch (err) {
553
+ console.error('Failed to resolve URL modal selection', err);
554
+ }
555
+ }
556
+
557
+ function buildPaneUrl(url, type) {
558
+ if (!url || typeof url !== 'string') return url;
559
+
560
+ const ensureContainer = () => {
561
+ if (url.startsWith('/container?url=')) return url;
562
+ return `/container?url=${encodeURIComponent(url)}`;
563
+ };
564
+
565
+ switch (type) {
566
+ case 'current':
567
+ return url;
568
+ case 'local':
569
+ return ensureContainer();
570
+ case 'remote':
571
+ try {
572
+ const parsed = new URL(url);
573
+ if (String(parsed.port) === '42000') {
574
+ return url;
575
+ }
576
+ } catch (_) {
577
+ // If URL constructor fails, fall back to container redirect
578
+ }
579
+ return ensureContainer();
580
+ default:
581
+ return ensureContainer();
582
+ }
583
+ }
584
+
585
+ function handleModalSelection(url, type) {
586
+ const refs = getModalRefs();
587
+ if (!refs) return;
588
+
589
+ const paneUrl = buildPaneUrl(url, type);
590
+
591
+ if (refs.input) {
592
+ refs.input.value = paneUrl;
593
+ if (typeof refs.updateConfirmState === 'function') {
594
+ refs.updateConfirmState();
595
+ }
596
+ }
597
+
598
+ if (!refs.returnSelection && urlInput) {
599
+ urlInput.value = paneUrl;
600
+ urlInput.setAttribute('data-host-type', type || 'remote');
601
+ }
602
+
603
+ if (refs.returnSelection) {
604
+ resolveModal(refs, paneUrl);
605
+ closeMobileModal({ suppressResolve: true });
606
+ return;
607
+ }
608
+
609
+ closeMobileModal();
610
+
611
+ if (!type || type === 'current') {
612
+ location.href = paneUrl;
613
+ return;
614
+ }
615
+
616
+ if (type === 'local' || type === 'remote') {
617
+ if (paneUrl.startsWith('/container?url=')) {
618
+ location.href = paneUrl;
619
+ return;
620
+ }
621
+ try {
622
+ const parsed = new URL(paneUrl);
623
+ if (String(parsed.port) === '42000') {
624
+ window.open(paneUrl, '_blank', 'self');
625
+ } else {
626
+ location.href = `/container?url=${encodeURIComponent(paneUrl)}`;
627
+ }
628
+ } catch (error) {
629
+ console.error('Failed to open URL, redirecting directly', error);
630
+ location.href = paneUrl;
631
+ }
632
+ return;
633
+ }
634
+
635
+ location.href = paneUrl;
636
+ }
637
+
373
638
  // Mobile modal functionality
374
639
  function createMobileModal() {
375
640
  const overlay = document.createElement('div');
376
- overlay.className = 'url-modal-overlay';
641
+ overlay.className = 'modal-overlay url-modal-overlay';
377
642
  overlay.id = 'url-modal-overlay';
378
-
643
+
379
644
  const content = document.createElement('div');
380
645
  content.className = 'url-modal-content';
381
-
382
- const closeButton = document.createElement('span');
646
+ content.setAttribute('role', 'dialog');
647
+ content.setAttribute('aria-modal', 'true');
648
+
649
+ const closeButton = document.createElement('button');
650
+ closeButton.type = 'button';
383
651
  closeButton.className = 'url-modal-close';
652
+ closeButton.setAttribute('aria-label', 'Close');
384
653
  closeButton.innerHTML = '&times;';
385
- closeButton.onclick = closeMobileModal;
386
-
654
+
655
+ const heading = document.createElement('h3');
656
+ heading.textContent = 'Open a URL';
657
+ heading.id = 'url-modal-title';
658
+
659
+ const description = document.createElement('p');
660
+ description.className = 'url-modal-description';
661
+ description.id = 'url-modal-description';
662
+ description.textContent = 'Enter a local URL or choose from running processes.';
663
+
664
+ content.setAttribute('aria-labelledby', heading.id);
665
+ content.setAttribute('aria-describedby', description.id);
666
+
387
667
  const modalInput = document.createElement('input');
388
668
  modalInput.type = 'url';
389
669
  modalInput.className = 'url-modal-input';
390
- modalInput.placeholder = 'enter a local url';
391
-
670
+ modalInput.placeholder = 'Example: http://localhost:7860';
671
+
392
672
  const modalDropdown = document.createElement('div');
393
673
  modalDropdown.className = 'url-dropdown';
394
674
  modalDropdown.id = 'url-modal-dropdown';
395
- modalDropdown.style.position = 'relative';
396
- modalDropdown.style.top = '0';
397
- modalDropdown.style.left = '0';
398
- modalDropdown.style.right = '0';
399
- modalDropdown.style.marginTop = '10px';
400
-
401
- content.appendChild(closeButton);
402
- content.appendChild(modalInput);
403
- content.appendChild(modalDropdown);
404
- overlay.appendChild(content);
405
-
406
- return { overlay, input: modalInput, dropdown: modalDropdown };
675
+
676
+ const actions = document.createElement('div');
677
+ actions.className = 'url-modal-actions';
678
+
679
+ const cancelButton = document.createElement('button');
680
+ cancelButton.type = 'button';
681
+ cancelButton.className = 'url-modal-button cancel';
682
+ cancelButton.textContent = 'Cancel';
683
+
684
+ const confirmButton = document.createElement('button');
685
+ confirmButton.type = 'button';
686
+ confirmButton.className = 'url-modal-button confirm';
687
+ confirmButton.textContent = 'Open';
688
+ confirmButton.disabled = true;
689
+
690
+ actions.append(cancelButton, confirmButton);
691
+
692
+ content.append(closeButton, heading, description, modalInput, modalDropdown, actions);
693
+ overlay.append(content);
694
+
695
+ const updateConfirmState = () => {
696
+ confirmButton.disabled = !modalInput.value.trim();
697
+ };
698
+
699
+ modalInput.addEventListener('focus', () => {
700
+ if (options.clearBehavior === 'restore' && modalInput.value) {
701
+ setTimeout(() => modalInput.select(), 0);
702
+ }
703
+ updateConfirmState();
704
+ showModalDropdown(modalDropdown);
705
+ });
706
+
707
+ modalInput.addEventListener('input', () => {
708
+ handleModalInputChange(modalInput, modalDropdown);
709
+ updateConfirmState();
710
+ });
711
+
712
+ modalInput.addEventListener('keydown', (event) => {
713
+ if (event.key === 'Enter') {
714
+ event.preventDefault();
715
+ submitMobileModal();
716
+ }
717
+ });
718
+
719
+ cancelButton.addEventListener('click', closeMobileModal);
720
+ confirmButton.addEventListener('click', submitMobileModal);
721
+ closeButton.addEventListener('click', closeMobileModal);
722
+
723
+ overlay.addEventListener('click', (event) => {
724
+ if (event.target === overlay) {
725
+ closeMobileModal();
726
+ }
727
+ });
728
+
729
+ overlay._modalRefs = {
730
+ input: modalInput,
731
+ dropdown: modalDropdown,
732
+ confirmButton,
733
+ cancelButton,
734
+ closeButton,
735
+ heading,
736
+ description,
737
+ updateConfirmState,
738
+ defaults: {
739
+ title: heading.textContent,
740
+ description: description.textContent,
741
+ confirmLabel: confirmButton.textContent
742
+ },
743
+ context: 'default',
744
+ returnSelection: false,
745
+ includeCurrent: true,
746
+ resolve: null
747
+ };
748
+
749
+ return overlay;
407
750
  }
408
-
409
- function showMobileModal() {
410
- let modal = document.getElementById('url-modal-overlay');
411
- if (!modal) {
412
- const { overlay, input: modalInput, dropdown: modalDropdown } = createMobileModal();
413
- modal = overlay;
414
- document.body.appendChild(modal);
415
-
416
- // Initialize dropdown functionality for modal
417
- modalInput.addEventListener('focus', function() {
418
- if (options.clearBehavior === 'restore' && modalInput.value) {
419
- setTimeout(() => modalInput.select(), 0);
420
- }
421
- showModalDropdown(modalDropdown);
422
- });
423
- modalInput.addEventListener('input', function() {
424
- handleModalInputChange(modalInput, modalDropdown);
425
- });
426
-
427
- // Close modal when clicking outside content
428
- modal.addEventListener('click', function(e) {
429
- if (e.target === modal) {
751
+
752
+ function showMobileModal(customOptions = {}) {
753
+ let overlay = document.getElementById('url-modal-overlay');
754
+ if (!overlay) {
755
+ overlay = createMobileModal();
756
+ document.body.appendChild(overlay);
757
+ }
758
+
759
+ const refs = overlay._modalRefs || {};
760
+ const modalInput = refs.input;
761
+ const updateConfirmState = refs.updateConfirmState;
762
+ if (!modalInput || !updateConfirmState) return undefined;
763
+
764
+ const defaults = refs.defaults || {};
765
+ const title = customOptions.title || defaults.title || 'Open a URL';
766
+ const descriptionText = customOptions.description || defaults.description || 'Enter a local URL or choose from running processes.';
767
+ const confirmLabel = customOptions.confirmLabel || defaults.confirmLabel || 'Open';
768
+ const includeCurrent = customOptions.includeCurrent !== false;
769
+ const initialValue = customOptions.initialValue !== undefined
770
+ ? customOptions.initialValue
771
+ : (options.clearBehavior === 'restore'
772
+ ? ((urlInput && urlInput.value) || options.defaultValue || '')
773
+ : '');
774
+
775
+ refs.heading.textContent = title;
776
+ refs.description.textContent = descriptionText;
777
+ refs.confirmButton.textContent = confirmLabel;
778
+ refs.includeCurrent = includeCurrent;
779
+ refs.context = customOptions.context || 'default';
780
+ refs.returnSelection = Boolean(customOptions.awaitSelection);
781
+ refs.resolve = null;
782
+
783
+ modalInput.value = initialValue;
784
+ updateConfirmState();
785
+
786
+ requestAnimationFrame(() => {
787
+ overlay.classList.add('is-visible');
788
+ requestAnimationFrame(() => modalInput.focus());
789
+ });
790
+
791
+ if (!mobileModalKeydownHandler) {
792
+ mobileModalKeydownHandler = (event) => {
793
+ if (event.key === 'Escape') {
794
+ event.preventDefault();
430
795
  closeMobileModal();
431
796
  }
432
- });
433
-
434
- // Handle form submission
435
- modalInput.addEventListener('keypress', function(e) {
436
- if (e.key === 'Enter') {
437
- e.preventDefault();
438
- if (modalInput.value) {
439
- urlInput.value = modalInput.value;
440
- urlInput.closest('form').dispatchEvent(new Event('submit'));
441
- closeMobileModal();
442
- }
443
- }
797
+ };
798
+ }
799
+
800
+ document.addEventListener('keydown', mobileModalKeydownHandler, true);
801
+
802
+ if (refs.returnSelection) {
803
+ return new Promise((resolve) => {
804
+ refs.resolve = resolve;
444
805
  });
445
806
  }
446
-
447
- modal.style.display = 'flex';
448
- const modalInput = modal.querySelector('.url-modal-input');
449
-
450
- // Set initial value based on config
451
- if (options.clearBehavior === 'restore') {
452
- modalInput.value = urlInput.value || options.defaultValue || '';
453
- } else {
454
- modalInput.value = '';
807
+
808
+ return undefined;
809
+ }
810
+
811
+ function closeMobileModal(options = {}) {
812
+ const overlay = getModalOverlay();
813
+ if (!overlay) return;
814
+ overlay.classList.remove('is-visible');
815
+ const refs = overlay._modalRefs;
816
+
817
+ if (refs?.dropdown) {
818
+ refs.dropdown.style.display = 'none';
819
+ }
820
+ if (refs?.confirmButton) {
821
+ refs.confirmButton.disabled = true;
822
+ }
823
+
824
+ if (refs) {
825
+ if (options.resolveValue !== undefined) {
826
+ resolveModal(refs, options.resolveValue);
827
+ } else if (refs.returnSelection && options.suppressResolve !== true) {
828
+ resolveModal(refs, null);
829
+ } else if (!refs.returnSelection || options.keepMode) {
830
+ // Preserve resolver when explicitly requested
831
+ } else {
832
+ refs.resolve = null;
833
+ refs.returnSelection = false;
834
+ }
835
+
836
+ if (!options.keepMode) {
837
+ refs.context = 'default';
838
+ refs.includeCurrent = true;
839
+ }
840
+ }
841
+
842
+ if (mobileModalKeydownHandler) {
843
+ document.removeEventListener('keydown', mobileModalKeydownHandler, true);
844
+ mobileModalKeydownHandler = null;
455
845
  }
456
-
457
- setTimeout(() => modalInput.focus(), 100);
458
846
  }
459
-
460
- function closeMobileModal() {
461
- const modal = document.getElementById('url-modal-overlay');
462
- if (modal) {
463
- modal.style.display = 'none';
847
+
848
+ function submitMobileModal() {
849
+ const refs = getModalRefs();
850
+ if (!refs || !refs.input) return;
851
+ const input = refs.input;
852
+ const value = input.value.trim();
853
+ if (!value) return;
854
+
855
+ if (refs.returnSelection) {
856
+ const paneUrl = buildPaneUrl(value, 'remote');
857
+ resolveModal(refs, paneUrl);
858
+ closeMobileModal({ suppressResolve: true });
859
+ return;
860
+ }
861
+
862
+ if (urlInput) {
863
+ const paneUrl = buildPaneUrl(value, 'remote');
864
+ urlInput.value = paneUrl;
865
+ const form = urlInput.closest('form');
866
+ if (form) {
867
+ form.dispatchEvent(new Event('submit'));
868
+ } else {
869
+ location.href = paneUrl;
870
+ }
464
871
  }
872
+ closeMobileModal();
465
873
  }
466
874
 
467
875
  function showModalDropdown(modalDropdown) {
@@ -499,10 +907,12 @@ function initUrlDropdown(config = {}) {
499
907
  filtered = allProcesses;
500
908
  } else if (query) {
501
909
  filtered = allProcesses.filter(process => {
502
- const url = `http://${process.ip}`;
503
- const name = process.name.toLowerCase();
504
- const urlLower = url.toLowerCase();
505
- return name.includes(query) || urlLower.includes(query);
910
+ const name = (process.name || '').toLowerCase();
911
+ if (name.includes(query)) {
912
+ return true;
913
+ }
914
+ const urls = getProcessFilterValues(process);
915
+ return urls.some((value) => (value || '').toLowerCase().includes(query));
506
916
  });
507
917
  }
508
918
 
@@ -511,33 +921,58 @@ function initUrlDropdown(config = {}) {
511
921
 
512
922
  function populateModalDropdown(processes, modalDropdown) {
513
923
  const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
514
-
924
+ const currentUrl = window.location.href;
925
+ const currentTitle = document.title || 'Current tab';
926
+ const overlayRefs = getModalRefs();
927
+ const includeCurrent = overlayRefs?.includeCurrent !== false;
928
+
929
+ let html = '';
930
+
931
+ if (includeCurrent && currentUrl) {
932
+ html += `
933
+ <div class="url-dropdown-host-header current-tab">
934
+ <span class="host-name">Current tab</span>
935
+ </div>
936
+ <div class="url-dropdown-item" data-url="${escapeHtml(currentUrl)}" data-host-type="current">
937
+ <div class="url-dropdown-name">
938
+ <i class="fa-solid fa-clone"></i>
939
+ <span>${escapeHtml(currentTitle)}</span>
940
+ </div>
941
+ <div class="url-dropdown-url">${escapeHtml(currentUrl)}</div>
942
+ </div>
943
+ `;
944
+ }
945
+
515
946
  if (processes.length === 0) {
516
- showEmptyState(modalDropdown, modalInput);
947
+ html += createEmptyStateHtml(getEmptyStateMessage(modalInput));
948
+ modalDropdown.innerHTML = html;
949
+ attachCreateButtonHandler(modalDropdown, modalInput);
950
+
951
+ modalDropdown.querySelectorAll('.url-dropdown-item:not(.non-selectable)').forEach(item => {
952
+ item.addEventListener('click', function() {
953
+ const url = this.getAttribute('data-url');
954
+ const type = this.getAttribute('data-host-type');
955
+ handleModalSelection(url, type);
956
+ });
957
+ });
517
958
  return;
518
959
  }
519
960
 
520
- // Group processes by host
521
961
  const groupedProcesses = groupProcessesByHost(processes);
522
-
523
- let html = '';
524
962
  Object.keys(groupedProcesses).forEach(hostKey => {
525
963
  const hostData = groupedProcesses[hostKey];
526
964
  const hostInfo = hostData.host;
527
- const processes = hostData.processes;
965
+ const hostProcesses = hostData.processes;
528
966
  const isLocal = hostData.isLocal;
529
-
530
- // Add host header
967
+
531
968
  html += createHostHeader(hostInfo, isLocal);
532
-
533
- // Add processes for this host
534
- processes.forEach(process => {
535
- const onlineIndicator = process.online ?
536
- '<div class="status-circle online"></div>' :
969
+
970
+ hostProcesses.forEach(process => {
971
+ const onlineIndicator = process.online ?
972
+ '<div class="status-circle online"></div>' :
537
973
  '<div class="status-circle offline"></div>';
538
-
974
+
539
975
  if (process.ip === null || process.ip === undefined) {
540
- // Non-selectable item with "turn on peer network" button
541
976
  const networkUrl = `http://${process.host.ip}:42000/network`;
542
977
  html += `
543
978
  <div class="url-dropdown-item non-selectable">
@@ -549,12 +984,16 @@ function initUrlDropdown(config = {}) {
549
984
  </div>
550
985
  `;
551
986
  } else {
552
- // Normal selectable item
553
- const url = `http://${process.ip}`;
987
+ const url = getProcessDisplayUrl(process);
988
+ if (!url) {
989
+ return;
990
+ }
554
991
  html += `
555
992
  <div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
556
- ${onlineIndicator}
557
- <div class="url-dropdown-name">${escapeHtml(process.name)}</div>
993
+ <div class="url-dropdown-name">
994
+ ${onlineIndicator}
995
+ ${escapeHtml(process.name)}
996
+ </div>
558
997
  <div class="url-dropdown-url">${escapeHtml(url)}</div>
559
998
  </div>
560
999
  `;
@@ -568,28 +1007,10 @@ function initUrlDropdown(config = {}) {
568
1007
  item.addEventListener('click', function() {
569
1008
  const url = this.getAttribute('data-url');
570
1009
  const type = this.getAttribute('data-host-type');
571
- modalInput.value = url;
572
- urlInput.value = url;
573
- urlInput.setAttribute("data-host-type", type);
574
- closeMobileModal();
575
-
576
- // Navigate directly instead of dispatching submit event
577
- if (type === "local") {
578
- let redirect_uri = "/container?url=" + url;
579
- location.href = redirect_uri;
580
- } else {
581
- let u = new URL(url);
582
- if (String(u.port) === "42000") {
583
- window.open(url, "_blank", 'self');
584
- } else {
585
- let redirect_uri = "/container?url=" + url;
586
- location.href = redirect_uri;
587
- }
588
- }
1010
+ handleModalSelection(url, type);
589
1011
  });
590
1012
  });
591
1013
 
592
- // Add click handlers to peer network buttons in modal
593
1014
  modalDropdown.querySelectorAll('.peer-network-button').forEach(button => {
594
1015
  button.addEventListener('click', function(e) {
595
1016
  e.stopPropagation();
@@ -601,16 +1022,15 @@ function initUrlDropdown(config = {}) {
601
1022
 
602
1023
  // Set up mobile button click handler
603
1024
  if (mobileButton) {
604
- mobileButton.addEventListener('click', showMobileModal);
1025
+ mobileButton.addEventListener('click', mobileButtonHandler);
605
1026
  }
606
1027
 
607
- // Public API
608
- return {
1028
+ const api = {
609
1029
  show: showDropdown,
610
1030
  hide: hideDropdown,
611
1031
  showAll: showAllProcesses,
612
- showMobileModal: showMobileModal,
613
- closeMobileModal: closeMobileModal,
1032
+ showMobileModal,
1033
+ closeMobileModal,
614
1034
  refresh: function() {
615
1035
  allProcesses = []; // Clear cache to force refetch
616
1036
  if (isDropdownVisible) {
@@ -618,18 +1038,43 @@ function initUrlDropdown(config = {}) {
618
1038
  }
619
1039
  },
620
1040
  filter: handleInputChange,
1041
+ openSplitModal: function(modalOptions = {}) {
1042
+ return showMobileModal({
1043
+ title: modalOptions.title || 'Split View',
1044
+ description: modalOptions.description || 'Choose a running process or use the current tab URL for the new pane.',
1045
+ confirmLabel: modalOptions.confirmLabel || 'Split',
1046
+ includeCurrent: modalOptions.includeCurrent !== false,
1047
+ awaitSelection: true,
1048
+ context: 'split'
1049
+ });
1050
+ },
621
1051
  destroy: function() {
622
- // Remove the focus event listener (need to store reference)
623
- urlInput.removeEventListener('input', handleInputChange);
1052
+ if (urlInput) {
1053
+ urlInput.removeEventListener('input', handleInputChange);
1054
+ }
624
1055
  if (mobileButton) {
625
- mobileButton.removeEventListener('click', showMobileModal);
1056
+ mobileButton.removeEventListener('click', mobileButtonHandler);
626
1057
  }
627
1058
  hideDropdown();
628
- closeMobileModal();
1059
+ closeMobileModal({ suppressResolve: true });
629
1060
  allProcesses = [];
630
1061
  filteredProcesses = [];
1062
+ if (fallbackElements.form && fallbackElements.form.parentElement) {
1063
+ fallbackElements.form.parentElement.removeChild(fallbackElements.form);
1064
+ }
1065
+ if (fallbackElements.dropdown && fallbackElements.dropdown.parentElement) {
1066
+ fallbackElements.dropdown.parentElement.removeChild(fallbackElements.dropdown);
1067
+ }
1068
+ fallbackElements.form = null;
1069
+ fallbackElements.dropdown = null;
1070
+ if (window.PinokioUrlDropdown === api) {
1071
+ window.PinokioUrlDropdown = null;
1072
+ }
631
1073
  }
632
1074
  };
1075
+
1076
+ window.PinokioUrlDropdown = api;
1077
+ return api;
633
1078
  function showEmptyState(container, inputElement) {
634
1079
  container.innerHTML = createEmptyStateHtml(getEmptyStateMessage(inputElement));
635
1080
  attachCreateButtonHandler(container, inputElement);
@@ -691,22 +1136,27 @@ function initUrlDropdown(config = {}) {
691
1136
  pendingCreateDetail = detail;
692
1137
 
693
1138
  modal.error.textContent = '';
694
- modal.overlay.style.display = 'flex';
695
-
696
1139
  // const defaultName = generateFolderName(detail.prompt);
697
- modal.input.value = "";
1140
+ modal.input.value = '';
698
1141
  modal.description.textContent = detail.prompt
699
1142
  ? `Prompt: ${detail.prompt}`
700
1143
  : 'Enter a prompt in the search bar to describe your launcher.';
701
- modal.input.focus();
702
- modal.input.select();
1144
+
1145
+ requestAnimationFrame(() => {
1146
+ modal.overlay.classList.add('is-visible');
1147
+ requestAnimationFrame(() => {
1148
+ modal.input.focus();
1149
+ modal.input.select();
1150
+ });
1151
+ });
1152
+
703
1153
  document.addEventListener('keydown', handleCreateModalEscape, true);
704
1154
  }
705
1155
 
706
1156
  function hideCreateLauncherModal() {
707
1157
  const modal = createLauncherModal;
708
1158
  if (!modal) return;
709
- modal.overlay.style.display = 'none';
1159
+ modal.overlay.classList.remove('is-visible');
710
1160
  pendingCreateDetail = null;
711
1161
  document.removeEventListener('keydown', handleCreateModalEscape, true);
712
1162
  }
@@ -746,19 +1196,25 @@ function initUrlDropdown(config = {}) {
746
1196
  }
747
1197
 
748
1198
  const overlay = document.createElement('div');
749
- overlay.className = 'create-launcher-modal-overlay';
750
- overlay.style.display = 'none';
1199
+ overlay.className = 'modal-overlay create-launcher-modal-overlay';
751
1200
 
752
1201
  const modalContent = document.createElement('div');
753
1202
  modalContent.className = 'create-launcher-modal';
1203
+ modalContent.setAttribute('role', 'dialog');
1204
+ modalContent.setAttribute('aria-modal', 'true');
754
1205
 
755
1206
  const title = document.createElement('h3');
1207
+ title.id = 'quick-create-launcher-title';
756
1208
  title.textContent = 'Create';
757
1209
 
758
1210
  const description = document.createElement('p');
759
1211
  description.className = 'create-launcher-modal-description';
1212
+ description.id = 'quick-create-launcher-description';
760
1213
  description.textContent = 'Enter a prompt in the search bar to describe your launcher.';
761
1214
 
1215
+ modalContent.setAttribute('aria-labelledby', title.id);
1216
+ modalContent.setAttribute('aria-describedby', description.id);
1217
+
762
1218
  const label = document.createElement('label');
763
1219
  label.className = 'create-launcher-modal-label';
764
1220
  label.textContent = 'Folder name';
@@ -782,7 +1238,7 @@ function initUrlDropdown(config = {}) {
782
1238
  const confirmButton = document.createElement('button');
783
1239
  confirmButton.type = 'button';
784
1240
  confirmButton.className = 'create-launcher-modal-button confirm';
785
- confirmButton.textContent = 'OK';
1241
+ confirmButton.textContent = 'Create';
786
1242
 
787
1243
  actions.appendChild(cancelButton);
788
1244
  actions.appendChild(confirmButton);