pinokiod 3.85.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 (88) 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 +8 -1
  5. package/kernel/api/shell/index.js +6 -0
  6. package/kernel/api/terminal/index.js +166 -0
  7. package/kernel/bin/caddy.js +10 -4
  8. package/kernel/bin/conda.js +3 -2
  9. package/kernel/bin/index.js +53 -2
  10. package/kernel/bin/setup.js +32 -0
  11. package/kernel/bin/vs.js +11 -2
  12. package/kernel/index.js +42 -2
  13. package/kernel/info.js +36 -0
  14. package/kernel/peer.js +42 -18
  15. package/kernel/prototype.js +1 -0
  16. package/kernel/router/index.js +23 -15
  17. package/kernel/router/localhost_static_router.js +0 -3
  18. package/kernel/router/pinokio_domain_router.js +333 -0
  19. package/kernel/shell.js +43 -2
  20. package/kernel/shells.js +21 -1
  21. package/kernel/util.js +4 -2
  22. package/package.json +2 -1
  23. package/pipe/views/login.ejs +1 -1
  24. package/script/install-mode.js +33 -0
  25. package/script/pinokio.json +7 -0
  26. package/server/index.js +636 -246
  27. package/server/public/Socket.js +48 -0
  28. package/server/public/common.js +1956 -257
  29. package/server/public/fseditor.js +71 -12
  30. package/server/public/install.js +1 -1
  31. package/server/public/layout.js +740 -0
  32. package/server/public/modalinput.js +0 -1
  33. package/server/public/opener.js +12 -11
  34. package/server/public/serve/style.css +1 -1
  35. package/server/public/style.css +122 -129
  36. package/server/public/tab-idle-notifier.js +629 -0
  37. package/server/public/terminal_input_tracker.js +63 -0
  38. package/server/public/urldropdown.css +780 -45
  39. package/server/public/urldropdown.js +806 -156
  40. package/server/public/window_storage.js +97 -28
  41. package/server/socket.js +40 -9
  42. package/server/views/404.ejs +1 -1
  43. package/server/views/500.ejs +3 -3
  44. package/server/views/app.ejs +3146 -1381
  45. package/server/views/bookmarklet.ejs +197 -0
  46. package/server/views/bootstrap.ejs +1 -1
  47. package/server/views/columns.ejs +2 -13
  48. package/server/views/connect/x.ejs +4 -4
  49. package/server/views/connect.ejs +13 -14
  50. package/server/views/container.ejs +3 -4
  51. package/server/views/d.ejs +225 -55
  52. package/server/views/download.ejs +1 -1
  53. package/server/views/editor.ejs +2 -2
  54. package/server/views/env_editor.ejs +3 -3
  55. package/server/views/explore.ejs +2 -2
  56. package/server/views/file_explorer.ejs +3 -3
  57. package/server/views/git.ejs +7 -7
  58. package/server/views/github.ejs +3 -3
  59. package/server/views/help.ejs +2 -2
  60. package/server/views/index.ejs +17 -16
  61. package/server/views/index2.ejs +7 -7
  62. package/server/views/init/index.ejs +15 -79
  63. package/server/views/install.ejs +4 -4
  64. package/server/views/keys.ejs +2 -2
  65. package/server/views/layout.ejs +105 -0
  66. package/server/views/mini.ejs +2 -2
  67. package/server/views/net.ejs +45 -13
  68. package/server/views/network.ejs +41 -27
  69. package/server/views/network2.ejs +11 -11
  70. package/server/views/old_network.ejs +10 -10
  71. package/server/views/partials/dynamic.ejs +3 -5
  72. package/server/views/partials/menu.ejs +3 -5
  73. package/server/views/partials/running.ejs +1 -1
  74. package/server/views/pro.ejs +369 -0
  75. package/server/views/prototype/index.ejs +3 -3
  76. package/server/views/required_env_editor.ejs +2 -2
  77. package/server/views/review.ejs +15 -27
  78. package/server/views/rows.ejs +2 -13
  79. package/server/views/screenshots.ejs +298 -142
  80. package/server/views/settings.ejs +6 -7
  81. package/server/views/setup.ejs +3 -4
  82. package/server/views/setup_home.ejs +2 -2
  83. package/server/views/share_editor.ejs +4 -4
  84. package/server/views/shell.ejs +280 -29
  85. package/server/views/start.ejs +2 -2
  86. package/server/views/task.ejs +2 -2
  87. package/server/views/terminal.ejs +326 -52
  88. package/server/views/tools.ejs +461 -17
@@ -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,9 +72,63 @@ 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
128
+ let createLauncherModal = null;
129
+ let pendingCreateDetail = null;
130
+ let mobileModalKeydownHandler = null;
131
+ const EMPTY_STATE_DESCRIPTION = 'enter a prompt to create a launcher';
27
132
 
28
133
  // Initialize input field state based on clear behavior
29
134
  initializeInputValue();
@@ -36,17 +141,19 @@ function initUrlDropdown(config = {}) {
36
141
  });
37
142
 
38
143
  // Event listeners
39
- urlInput.addEventListener('focus', function() {
40
- // Auto-select text for restore behavior to make filtering easier
41
- if (options.clearBehavior === 'restore' && urlInput.value) {
42
- // Use setTimeout to ensure the focus event completes first
43
- setTimeout(() => {
44
- urlInput.select();
45
- }, 0);
46
- }
47
- showDropdown();
48
- });
49
- 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
+ }
50
157
 
51
158
  // Hide dropdown when clicking outside
52
159
  document.addEventListener('click', function(e) {
@@ -81,6 +188,7 @@ function initUrlDropdown(config = {}) {
81
188
 
82
189
 
83
190
  function initializeInputValue() {
191
+ if (!urlInput) return;
84
192
  if (options.clearBehavior === 'empty') {
85
193
  urlInput.value = '';
86
194
  } else if (options.clearBehavior === 'restore') {
@@ -92,6 +200,7 @@ function initUrlDropdown(config = {}) {
92
200
  }
93
201
 
94
202
  function showDropdown() {
203
+ if (!dropdown || !urlInput) return;
95
204
  if (isDropdownVisible && allProcesses.length > 0) {
96
205
  // If dropdown is already visible and we have data, show all initially
97
206
  showAllProcesses();
@@ -135,6 +244,7 @@ function initUrlDropdown(config = {}) {
135
244
  }
136
245
 
137
246
  function handleInputChange() {
247
+ if (!urlInput) return;
138
248
  if (!isDropdownVisible) return;
139
249
 
140
250
  const query = urlInput.value.toLowerCase().trim();
@@ -149,11 +259,12 @@ function initUrlDropdown(config = {}) {
149
259
  } else {
150
260
  // Filter processes based on name and URL
151
261
  filteredProcesses = allProcesses.filter(process => {
152
- const url = `http://${process.ip}`;
153
- const name = process.name.toLowerCase();
154
- const urlLower = url.toLowerCase();
155
-
156
- 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));
157
268
  });
158
269
  }
159
270
 
@@ -162,7 +273,9 @@ function initUrlDropdown(config = {}) {
162
273
 
163
274
  function hideDropdown() {
164
275
  isDropdownVisible = false;
165
- dropdown.style.display = 'none';
276
+ if (dropdown) {
277
+ dropdown.style.display = 'none';
278
+ }
166
279
  }
167
280
 
168
281
  function createHostBadge(host) {
@@ -255,8 +368,6 @@ function initUrlDropdown(config = {}) {
255
368
  platformIcon = 'fa-solid fa-desktop';
256
369
  break;
257
370
  }
258
-
259
- console.log({ isLocal, host })
260
371
  const hostName = isLocal ? `${host.name} (This Machine)` : `${host.name} (Peer)`;
261
372
 
262
373
  return `
@@ -271,19 +382,63 @@ function initUrlDropdown(config = {}) {
271
382
  }
272
383
 
273
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
+
274
406
  if (processes.length === 0) {
275
- const query = urlInput.value.toLowerCase().trim();
276
- const message = query
277
- ? `No processes match "${query}"`
278
- : 'No running processes found';
279
- dropdown.innerHTML = `<div class="url-dropdown-empty">${message}</div>`;
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
+ });
280
436
  return;
281
437
  }
282
438
 
283
439
  // Group processes by host
284
440
  const groupedProcesses = groupProcessesByHost(processes);
285
-
286
- let html = '';
441
+
287
442
  Object.keys(groupedProcesses).forEach(hostKey => {
288
443
  const hostData = groupedProcesses[hostKey];
289
444
  const hostInfo = hostData.host;
@@ -313,7 +468,10 @@ function initUrlDropdown(config = {}) {
313
468
  `;
314
469
  } else {
315
470
  // Normal selectable item
316
- const url = `http://${process.ip}`;
471
+ const url = getProcessDisplayUrl(process);
472
+ if (!url) {
473
+ return;
474
+ }
317
475
  html += `
318
476
  <div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
319
477
  <div class="url-dropdown-name">
@@ -335,13 +493,15 @@ function initUrlDropdown(config = {}) {
335
493
  const url = this.getAttribute('data-url');
336
494
  const type = this.getAttribute('data-host-type');
337
495
  urlInput.value = url;
338
- urlInput.setAttribute("data-host-type", type);
496
+ urlInput.setAttribute("data-host-type", type || 'remote');
339
497
  hideDropdown();
340
498
 
341
499
  // Navigate directly instead of dispatching submit event
342
500
  if (type === "local") {
343
501
  let redirect_uri = "/container?url=" + url;
344
502
  location.href = redirect_uri;
503
+ } else if (type === 'current') {
504
+ location.href = url;
345
505
  } else {
346
506
  let u = new URL(url);
347
507
  if (String(u.port) === "42000") {
@@ -371,98 +531,345 @@ function initUrlDropdown(config = {}) {
371
531
  return div.innerHTML;
372
532
  }
373
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
+
374
638
  // Mobile modal functionality
375
639
  function createMobileModal() {
376
640
  const overlay = document.createElement('div');
377
- overlay.className = 'url-modal-overlay';
641
+ overlay.className = 'modal-overlay url-modal-overlay';
378
642
  overlay.id = 'url-modal-overlay';
379
-
643
+
380
644
  const content = document.createElement('div');
381
645
  content.className = 'url-modal-content';
382
-
383
- 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';
384
651
  closeButton.className = 'url-modal-close';
652
+ closeButton.setAttribute('aria-label', 'Close');
385
653
  closeButton.innerHTML = '&times;';
386
- closeButton.onclick = closeMobileModal;
387
-
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
+
388
667
  const modalInput = document.createElement('input');
389
668
  modalInput.type = 'url';
390
669
  modalInput.className = 'url-modal-input';
391
- modalInput.placeholder = 'enter a local url';
392
-
670
+ modalInput.placeholder = 'Example: http://localhost:7860';
671
+
393
672
  const modalDropdown = document.createElement('div');
394
673
  modalDropdown.className = 'url-dropdown';
395
674
  modalDropdown.id = 'url-modal-dropdown';
396
- modalDropdown.style.position = 'relative';
397
- modalDropdown.style.top = '0';
398
- modalDropdown.style.left = '0';
399
- modalDropdown.style.right = '0';
400
- modalDropdown.style.marginTop = '10px';
401
-
402
- content.appendChild(closeButton);
403
- content.appendChild(modalInput);
404
- content.appendChild(modalDropdown);
405
- overlay.appendChild(content);
406
-
407
- 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;
408
750
  }
409
-
410
- function showMobileModal() {
411
- let modal = document.getElementById('url-modal-overlay');
412
- if (!modal) {
413
- const { overlay, input: modalInput, dropdown: modalDropdown } = createMobileModal();
414
- modal = overlay;
415
- document.body.appendChild(modal);
416
-
417
- // Initialize dropdown functionality for modal
418
- modalInput.addEventListener('focus', function() {
419
- if (options.clearBehavior === 'restore' && modalInput.value) {
420
- setTimeout(() => modalInput.select(), 0);
421
- }
422
- showModalDropdown(modalDropdown);
423
- });
424
- modalInput.addEventListener('input', function() {
425
- handleModalInputChange(modalInput, modalDropdown);
426
- });
427
-
428
- // Close modal when clicking outside content
429
- modal.addEventListener('click', function(e) {
430
- 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();
431
795
  closeMobileModal();
432
796
  }
433
- });
434
-
435
- // Handle form submission
436
- modalInput.addEventListener('keypress', function(e) {
437
- if (e.key === 'Enter') {
438
- e.preventDefault();
439
- if (modalInput.value) {
440
- urlInput.value = modalInput.value;
441
- urlInput.closest('form').dispatchEvent(new Event('submit'));
442
- closeMobileModal();
443
- }
444
- }
797
+ };
798
+ }
799
+
800
+ document.addEventListener('keydown', mobileModalKeydownHandler, true);
801
+
802
+ if (refs.returnSelection) {
803
+ return new Promise((resolve) => {
804
+ refs.resolve = resolve;
445
805
  });
446
806
  }
447
-
448
- modal.style.display = 'flex';
449
- const modalInput = modal.querySelector('.url-modal-input');
450
-
451
- // Set initial value based on config
452
- if (options.clearBehavior === 'restore') {
453
- modalInput.value = urlInput.value || options.defaultValue || '';
454
- } else {
455
- 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;
456
845
  }
457
-
458
- setTimeout(() => modalInput.focus(), 100);
459
846
  }
460
-
461
- function closeMobileModal() {
462
- const modal = document.getElementById('url-modal-overlay');
463
- if (modal) {
464
- 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;
465
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
+ }
871
+ }
872
+ closeMobileModal();
466
873
  }
467
874
 
468
875
  function showModalDropdown(modalDropdown) {
@@ -500,10 +907,12 @@ function initUrlDropdown(config = {}) {
500
907
  filtered = allProcesses;
501
908
  } else if (query) {
502
909
  filtered = allProcesses.filter(process => {
503
- const url = `http://${process.ip}`;
504
- const name = process.name.toLowerCase();
505
- const urlLower = url.toLowerCase();
506
- 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));
507
916
  });
508
917
  }
509
918
 
@@ -512,35 +921,58 @@ function initUrlDropdown(config = {}) {
512
921
 
513
922
  function populateModalDropdown(processes, modalDropdown) {
514
923
  const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
515
-
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
+
516
946
  if (processes.length === 0) {
517
- const query = modalInput.value.toLowerCase().trim();
518
- const message = query ? `No processes match "${query}"` : 'No running processes found';
519
- modalDropdown.innerHTML = `<div class="url-dropdown-empty">${message}</div>`;
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
+ });
520
958
  return;
521
959
  }
522
960
 
523
- // Group processes by host
524
961
  const groupedProcesses = groupProcessesByHost(processes);
525
-
526
- let html = '';
527
962
  Object.keys(groupedProcesses).forEach(hostKey => {
528
963
  const hostData = groupedProcesses[hostKey];
529
964
  const hostInfo = hostData.host;
530
- const processes = hostData.processes;
965
+ const hostProcesses = hostData.processes;
531
966
  const isLocal = hostData.isLocal;
532
-
533
- // Add host header
967
+
534
968
  html += createHostHeader(hostInfo, isLocal);
535
-
536
- // Add processes for this host
537
- processes.forEach(process => {
538
- const onlineIndicator = process.online ?
539
- '<div class="status-circle online"></div>' :
969
+
970
+ hostProcesses.forEach(process => {
971
+ const onlineIndicator = process.online ?
972
+ '<div class="status-circle online"></div>' :
540
973
  '<div class="status-circle offline"></div>';
541
-
974
+
542
975
  if (process.ip === null || process.ip === undefined) {
543
- // Non-selectable item with "turn on peer network" button
544
976
  const networkUrl = `http://${process.host.ip}:42000/network`;
545
977
  html += `
546
978
  <div class="url-dropdown-item non-selectable">
@@ -552,12 +984,16 @@ function initUrlDropdown(config = {}) {
552
984
  </div>
553
985
  `;
554
986
  } else {
555
- // Normal selectable item
556
- const url = `http://${process.ip}`;
987
+ const url = getProcessDisplayUrl(process);
988
+ if (!url) {
989
+ return;
990
+ }
557
991
  html += `
558
992
  <div class="url-dropdown-item" data-url="${url}" data-host-type="${process.host.local ? "local" : "remote"}">
559
- ${onlineIndicator}
560
- <div class="url-dropdown-name">${escapeHtml(process.name)}</div>
993
+ <div class="url-dropdown-name">
994
+ ${onlineIndicator}
995
+ ${escapeHtml(process.name)}
996
+ </div>
561
997
  <div class="url-dropdown-url">${escapeHtml(url)}</div>
562
998
  </div>
563
999
  `;
@@ -571,28 +1007,10 @@ function initUrlDropdown(config = {}) {
571
1007
  item.addEventListener('click', function() {
572
1008
  const url = this.getAttribute('data-url');
573
1009
  const type = this.getAttribute('data-host-type');
574
- modalInput.value = url;
575
- urlInput.value = url;
576
- urlInput.setAttribute("data-host-type", type);
577
- closeMobileModal();
578
-
579
- // Navigate directly instead of dispatching submit event
580
- if (type === "local") {
581
- let redirect_uri = "/container?url=" + url;
582
- location.href = redirect_uri;
583
- } else {
584
- let u = new URL(url);
585
- if (String(u.port) === "42000") {
586
- window.open(url, "_blank", 'self');
587
- } else {
588
- let redirect_uri = "/container?url=" + url;
589
- location.href = redirect_uri;
590
- }
591
- }
1010
+ handleModalSelection(url, type);
592
1011
  });
593
1012
  });
594
1013
 
595
- // Add click handlers to peer network buttons in modal
596
1014
  modalDropdown.querySelectorAll('.peer-network-button').forEach(button => {
597
1015
  button.addEventListener('click', function(e) {
598
1016
  e.stopPropagation();
@@ -604,16 +1022,15 @@ function initUrlDropdown(config = {}) {
604
1022
 
605
1023
  // Set up mobile button click handler
606
1024
  if (mobileButton) {
607
- mobileButton.addEventListener('click', showMobileModal);
1025
+ mobileButton.addEventListener('click', mobileButtonHandler);
608
1026
  }
609
1027
 
610
- // Public API
611
- return {
1028
+ const api = {
612
1029
  show: showDropdown,
613
1030
  hide: hideDropdown,
614
1031
  showAll: showAllProcesses,
615
- showMobileModal: showMobileModal,
616
- closeMobileModal: closeMobileModal,
1032
+ showMobileModal,
1033
+ closeMobileModal,
617
1034
  refresh: function() {
618
1035
  allProcesses = []; // Clear cache to force refetch
619
1036
  if (isDropdownVisible) {
@@ -621,18 +1038,251 @@ function initUrlDropdown(config = {}) {
621
1038
  }
622
1039
  },
623
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
+ },
624
1051
  destroy: function() {
625
- // Remove the focus event listener (need to store reference)
626
- urlInput.removeEventListener('input', handleInputChange);
1052
+ if (urlInput) {
1053
+ urlInput.removeEventListener('input', handleInputChange);
1054
+ }
627
1055
  if (mobileButton) {
628
- mobileButton.removeEventListener('click', showMobileModal);
1056
+ mobileButton.removeEventListener('click', mobileButtonHandler);
629
1057
  }
630
1058
  hideDropdown();
631
- closeMobileModal();
1059
+ closeMobileModal({ suppressResolve: true });
632
1060
  allProcesses = [];
633
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
+ }
634
1073
  }
635
1074
  };
1075
+
1076
+ window.PinokioUrlDropdown = api;
1077
+ return api;
1078
+ function showEmptyState(container, inputElement) {
1079
+ container.innerHTML = createEmptyStateHtml(getEmptyStateMessage(inputElement));
1080
+ attachCreateButtonHandler(container, inputElement);
1081
+ }
1082
+
1083
+ function getEmptyStateMessage(inputElement) {
1084
+ const rawValue = inputElement.value.trim();
1085
+ return rawValue ? `No processes match "${rawValue}"` : 'No running processes found';
1086
+ }
1087
+
1088
+ function createEmptyStateHtml(message) {
1089
+ return `
1090
+ <div class="url-dropdown-empty">
1091
+ <div class="url-dropdown-empty-message">${escapeHtml(message)}</div>
1092
+ <div class="url-dropdown-empty-actions">
1093
+ <button type="button" class="url-dropdown-create-button">Create</button>
1094
+ <div class="url-dropdown-empty-description">${escapeHtml(EMPTY_STATE_DESCRIPTION)}</div>
1095
+ </div>
1096
+ </div>
1097
+ `;
1098
+ }
1099
+
1100
+ function attachCreateButtonHandler(container, inputElement) {
1101
+ const createButton = container.querySelector('.url-dropdown-create-button');
1102
+ if (!createButton) return;
1103
+
1104
+ createButton.addEventListener('click', function(event) {
1105
+ event.preventDefault();
1106
+ event.stopPropagation();
1107
+ const prompt = inputElement.value.trim();
1108
+ const detail = {
1109
+ query: prompt,
1110
+ prompt,
1111
+ input: inputElement,
1112
+ dropdown: container,
1113
+ context: inputElement === urlInput ? 'dropdown' : 'modal'
1114
+ };
1115
+
1116
+ if (detail.context === 'dropdown') {
1117
+ hideDropdown();
1118
+ } else {
1119
+ closeMobileModal();
1120
+ }
1121
+
1122
+ showCreateLauncherModal(detail);
1123
+
1124
+ if (typeof options.onCreate === 'function') {
1125
+ options.onCreate(detail);
1126
+ }
1127
+
1128
+ if (typeof CustomEvent === 'function') {
1129
+ container.dispatchEvent(new CustomEvent('urlDropdownCreate', { detail }));
1130
+ }
1131
+ });
1132
+ }
1133
+
1134
+ function showCreateLauncherModal(detail) {
1135
+ const modal = getCreateLauncherModal();
1136
+ pendingCreateDetail = detail;
1137
+
1138
+ modal.error.textContent = '';
1139
+ // const defaultName = generateFolderName(detail.prompt);
1140
+ modal.input.value = '';
1141
+ modal.description.textContent = detail.prompt
1142
+ ? `Prompt: ${detail.prompt}`
1143
+ : 'Enter a prompt in the search bar to describe your launcher.';
1144
+
1145
+ requestAnimationFrame(() => {
1146
+ modal.overlay.classList.add('is-visible');
1147
+ requestAnimationFrame(() => {
1148
+ modal.input.focus();
1149
+ modal.input.select();
1150
+ });
1151
+ });
1152
+
1153
+ document.addEventListener('keydown', handleCreateModalEscape, true);
1154
+ }
1155
+
1156
+ function hideCreateLauncherModal() {
1157
+ const modal = createLauncherModal;
1158
+ if (!modal) return;
1159
+ modal.overlay.classList.remove('is-visible');
1160
+ pendingCreateDetail = null;
1161
+ document.removeEventListener('keydown', handleCreateModalEscape, true);
1162
+ }
1163
+
1164
+ function confirmCreateLauncherModal() {
1165
+ if (!createLauncherModal || !pendingCreateDetail) return;
1166
+ const folderName = createLauncherModal.input.value.trim();
1167
+ if (!folderName) {
1168
+ createLauncherModal.error.textContent = 'Please enter a folder name.';
1169
+ createLauncherModal.input.focus();
1170
+ return;
1171
+ }
1172
+
1173
+ const prompt = pendingCreateDetail.prompt || '';
1174
+ const redirectUrl = `/pro?name=${encodeURIComponent(folderName)}&message=${encodeURIComponent(prompt)}`;
1175
+ hideCreateLauncherModal();
1176
+ window.location.href = redirectUrl;
1177
+ }
1178
+
1179
+ function handleCreateModalKeydown(event) {
1180
+ if (event.key === 'Enter') {
1181
+ event.preventDefault();
1182
+ confirmCreateLauncherModal();
1183
+ }
1184
+ }
1185
+
1186
+ function handleCreateModalEscape(event) {
1187
+ if (event.key === 'Escape' && pendingCreateDetail) {
1188
+ event.preventDefault();
1189
+ hideCreateLauncherModal();
1190
+ }
1191
+ }
1192
+
1193
+ function getCreateLauncherModal() {
1194
+ if (createLauncherModal) {
1195
+ return createLauncherModal;
1196
+ }
1197
+
1198
+ const overlay = document.createElement('div');
1199
+ overlay.className = 'modal-overlay create-launcher-modal-overlay';
1200
+
1201
+ const modalContent = document.createElement('div');
1202
+ modalContent.className = 'create-launcher-modal';
1203
+ modalContent.setAttribute('role', 'dialog');
1204
+ modalContent.setAttribute('aria-modal', 'true');
1205
+
1206
+ const title = document.createElement('h3');
1207
+ title.id = 'quick-create-launcher-title';
1208
+ title.textContent = 'Create';
1209
+
1210
+ const description = document.createElement('p');
1211
+ description.className = 'create-launcher-modal-description';
1212
+ description.id = 'quick-create-launcher-description';
1213
+ description.textContent = 'Enter a prompt in the search bar to describe your launcher.';
1214
+
1215
+ modalContent.setAttribute('aria-labelledby', title.id);
1216
+ modalContent.setAttribute('aria-describedby', description.id);
1217
+
1218
+ const label = document.createElement('label');
1219
+ label.className = 'create-launcher-modal-label';
1220
+ label.textContent = 'Folder name';
1221
+
1222
+ const input = document.createElement('input');
1223
+ input.type = 'text';
1224
+ input.className = 'create-launcher-modal-input';
1225
+ input.placeholder = 'example: my-launcher';
1226
+
1227
+ const error = document.createElement('div');
1228
+ error.className = 'create-launcher-modal-error';
1229
+
1230
+ const actions = document.createElement('div');
1231
+ actions.className = 'create-launcher-modal-actions';
1232
+
1233
+ const cancelButton = document.createElement('button');
1234
+ cancelButton.type = 'button';
1235
+ cancelButton.className = 'create-launcher-modal-button cancel';
1236
+ cancelButton.textContent = 'Cancel';
1237
+
1238
+ const confirmButton = document.createElement('button');
1239
+ confirmButton.type = 'button';
1240
+ confirmButton.className = 'create-launcher-modal-button confirm';
1241
+ confirmButton.textContent = 'Create';
1242
+
1243
+ actions.appendChild(cancelButton);
1244
+ actions.appendChild(confirmButton);
1245
+
1246
+ label.appendChild(input);
1247
+ modalContent.appendChild(title);
1248
+ modalContent.appendChild(description);
1249
+ modalContent.appendChild(label);
1250
+ modalContent.appendChild(error);
1251
+ modalContent.appendChild(actions);
1252
+ overlay.appendChild(modalContent);
1253
+ document.body.appendChild(overlay);
1254
+
1255
+ overlay.addEventListener('click', function(event) {
1256
+ if (event.target === overlay) {
1257
+ hideCreateLauncherModal();
1258
+ }
1259
+ });
1260
+
1261
+ cancelButton.addEventListener('click', hideCreateLauncherModal);
1262
+ confirmButton.addEventListener('click', confirmCreateLauncherModal);
1263
+ input.addEventListener('keydown', handleCreateModalKeydown);
1264
+
1265
+ createLauncherModal = {
1266
+ overlay,
1267
+ modal: modalContent,
1268
+ input,
1269
+ cancelButton,
1270
+ confirmButton,
1271
+ error,
1272
+ description
1273
+ };
1274
+
1275
+ return createLauncherModal;
1276
+ }
1277
+
1278
+ function generateFolderName(prompt) {
1279
+ if (!prompt) return '';
1280
+ const normalized = prompt
1281
+ .toLowerCase()
1282
+ .replace(/[^a-z0-9\-\s_]/g, '')
1283
+ .replace(/[\s_]+/g, '-');
1284
+ return normalized.replace(/^-+|-+$/g, '').slice(0, 50);
1285
+ }
636
1286
  }
637
1287
 
638
1288
  // Auto-initialize if DOM is already loaded, otherwise wait for it