pinokiod 3.270.0 → 3.272.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 (56) hide show
  1. package/kernel/ansi_stream_tracker.js +115 -0
  2. package/kernel/api/app/index.js +422 -0
  3. package/kernel/api/htmlmodal/index.js +94 -0
  4. package/kernel/app_launcher/index.js +115 -0
  5. package/kernel/app_launcher/platform/base.js +276 -0
  6. package/kernel/app_launcher/platform/linux.js +229 -0
  7. package/kernel/app_launcher/platform/macos.js +163 -0
  8. package/kernel/app_launcher/platform/unsupported.js +34 -0
  9. package/kernel/app_launcher/platform/windows.js +247 -0
  10. package/kernel/bin/conda-meta.js +93 -0
  11. package/kernel/bin/conda.js +2 -4
  12. package/kernel/bin/index.js +2 -4
  13. package/kernel/index.js +9 -2
  14. package/kernel/peer.js +186 -19
  15. package/kernel/shell.js +212 -1
  16. package/package.json +1 -1
  17. package/server/index.js +491 -6
  18. package/server/public/common.js +224 -741
  19. package/server/public/create-launcher.js +754 -0
  20. package/server/public/htmlmodal.js +292 -0
  21. package/server/public/logs.js +715 -0
  22. package/server/public/resizeSync.js +117 -0
  23. package/server/public/style.css +651 -6
  24. package/server/public/tab-idle-notifier.js +34 -59
  25. package/server/public/tab-link-popover.js +7 -10
  26. package/server/public/terminal-settings.js +723 -9
  27. package/server/public/terminal_input_utils.js +72 -0
  28. package/server/public/terminal_key_caption.js +187 -0
  29. package/server/public/urldropdown.css +120 -3
  30. package/server/public/xterm-inline-bridge.js +116 -0
  31. package/server/socket.js +29 -0
  32. package/server/views/agents.ejs +1 -2
  33. package/server/views/app.ejs +55 -28
  34. package/server/views/bookmarklet.ejs +1 -1
  35. package/server/views/bootstrap.ejs +1 -0
  36. package/server/views/connect.ejs +1 -2
  37. package/server/views/create.ejs +63 -0
  38. package/server/views/editor.ejs +36 -4
  39. package/server/views/index.ejs +1 -2
  40. package/server/views/index2.ejs +1 -2
  41. package/server/views/init/index.ejs +36 -28
  42. package/server/views/install.ejs +20 -22
  43. package/server/views/layout.ejs +2 -8
  44. package/server/views/logs.ejs +155 -0
  45. package/server/views/mini.ejs +0 -18
  46. package/server/views/net.ejs +2 -2
  47. package/server/views/network.ejs +1 -2
  48. package/server/views/network2.ejs +1 -2
  49. package/server/views/old_network.ejs +1 -2
  50. package/server/views/pro.ejs +26 -23
  51. package/server/views/prototype/index.ejs +30 -34
  52. package/server/views/screenshots.ejs +1 -2
  53. package/server/views/settings.ejs +1 -20
  54. package/server/views/shell.ejs +59 -66
  55. package/server/views/terminal.ejs +118 -73
  56. package/server/views/tools.ejs +1 -2
@@ -38,6 +38,22 @@ function pinokioBroadcastMessage(payload, targetOrigin = '*', contextWindow = nu
38
38
  targets = new Set();
39
39
  }
40
40
  if (targets.size === 0) {
41
+ try {
42
+ const origin = (() => {
43
+ try {
44
+ return ctx.location ? ctx.location.origin : window.location.origin;
45
+ } catch (_) {
46
+ return '*';
47
+ }
48
+ })();
49
+ const event = new MessageEvent('message', {
50
+ data: payload,
51
+ origin,
52
+ source: ctx
53
+ });
54
+ ctx.dispatchEvent(event);
55
+ dispatched = true;
56
+ } catch (_) {}
41
57
  return dispatched;
42
58
  }
43
59
  targets.forEach((target) => {
@@ -1501,17 +1517,55 @@ if (typeof hotkeys === 'function') {
1501
1517
  if (typeof window === 'undefined') {
1502
1518
  return;
1503
1519
  }
1504
- // Avoid duplicate audio playback: if this is the top-level layout page, or if the
1505
- // top window already owns notification playback, skip initialising this bridge.
1506
- try {
1507
- const isTop = window.top === window;
1508
- if (isTop && document.getElementById('layout-root')) {
1509
- return; // layout shell handles notifications
1510
- }
1511
- if (!isTop && window.top && window.top.__pinokioTopNotifyListener) {
1512
- return; // top-level listener active; avoid duplicates from iframes
1513
- }
1514
- } catch (_) {}
1520
+ const shouldDeferToTopListener = (() => {
1521
+ const isLikelyMobile = () => {
1522
+ try {
1523
+ if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
1524
+ if (navigator.userAgentData.mobile) {
1525
+ return true;
1526
+ }
1527
+ }
1528
+ } catch (_) {}
1529
+ try {
1530
+ const ua = (navigator.userAgent || '').toLowerCase();
1531
+ if (ua && /iphone|ipad|ipod|android|mobile/.test(ua)) {
1532
+ return true;
1533
+ }
1534
+ } catch (_) {}
1535
+ try {
1536
+ if (navigator.maxTouchPoints && navigator.maxTouchPoints > 1) {
1537
+ return true;
1538
+ }
1539
+ } catch (_) {}
1540
+ try {
1541
+ if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) {
1542
+ return true;
1543
+ }
1544
+ } catch (_) {}
1545
+ return false;
1546
+ };
1547
+ try {
1548
+ const topWindow = window.top;
1549
+ const isTop = topWindow === window;
1550
+ if (isTop && document.getElementById('layout-root')) {
1551
+ const topOwnsAudio = isLikelyMobile();
1552
+ try { window.__pinokioTopHandlesNotificationAudio = topOwnsAudio; } catch (_) {}
1553
+ return topOwnsAudio;
1554
+ }
1555
+ if (!isTop && topWindow) {
1556
+ if (typeof topWindow.__pinokioTopHandlesNotificationAudio === 'boolean') {
1557
+ return topWindow.__pinokioTopHandlesNotificationAudio;
1558
+ }
1559
+ if (topWindow.__pinokioTopNotifyListener) {
1560
+ return true;
1561
+ }
1562
+ }
1563
+ } catch (_) {}
1564
+ return false;
1565
+ })();
1566
+ if (shouldDeferToTopListener) {
1567
+ return;
1568
+ }
1515
1569
  if (window.__pinokioNotificationAudioInitialized) {
1516
1570
  return;
1517
1571
  }
@@ -1966,8 +2020,10 @@ if (typeof hotkeys === 'function') {
1966
2020
  const style = document.createElement('style');
1967
2021
  style.textContent = `
1968
2022
  .pinokio-connect-curtain{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;background:rgba(15,23,42,0.35);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center}
1969
- .pinokio-connect-msg{user-select:none;-webkit-user-select:none;color:#fff;background:rgba(15,23,42,0.85);padding:14px 18px;border-radius:12px;font:600 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 16px 40px rgba(0,0,0,.38)}
1970
- @media (max-width:768px){.pinokio-connect-msg{font-size:15px;padding:12px 16px}}
2023
+ .pinokio-connect-msg{user-select:none;-webkit-user-select:none;color:#fff;background:rgba(15,23,42,0.85);padding:14px 18px;border-radius:12px;font:500 15px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 16px 40px rgba(0,0,0,.38);text-align:center;max-width:200px}
2024
+ .pinokio-connect-msg-title{font-weight:600;font-size:16px;margin-bottom:4px}
2025
+ .pinokio-connect-msg-hint{font-size:13px;opacity:.72}
2026
+ @media (max-width:768px){.pinokio-connect-msg{font-size:14px;padding:12px 16px}}
1971
2027
  `;
1972
2028
  document.head.appendChild(style);
1973
2029
 
@@ -1978,31 +2034,71 @@ if (typeof hotkeys === 'function') {
1978
2034
  overlay.tabIndex = 0;
1979
2035
  const msg = document.createElement('div');
1980
2036
  msg.className = 'pinokio-connect-msg';
1981
- msg.textContent = 'Tap to connect';
2037
+ const msgTitle = document.createElement('div');
2038
+ msgTitle.className = 'pinokio-connect-msg-title';
2039
+ msgTitle.textContent = 'Tap to connect';
2040
+ const msgHint = document.createElement('div');
2041
+ msgHint.className = 'pinokio-connect-msg-hint';
2042
+ msgHint.textContent = 'To type into the terminal, use the "Input" button.';
2043
+ msg.appendChild(msgTitle);
2044
+ msg.appendChild(msgHint);
1982
2045
  overlay.appendChild(msg);
1983
2046
  window.__pinokioConnectCurtainInstalled = true;
1984
2047
  return overlay;
1985
2048
  };
1986
2049
 
2050
+ const SOUND_PREF_STORAGE_KEY = 'pinokio:idle-sound';
1987
2051
  const primeAudio = async () => {
2052
+ // Determine whether the user picked a custom `/sound/...` clip.
2053
+ // Fall back to the built-in chime if no preference exists.
2054
+ const preferCustom = (() => {
2055
+ try {
2056
+ const raw = localStorage.getItem(SOUND_PREF_STORAGE_KEY);
2057
+ if (!raw) return null;
2058
+ const parsed = JSON.parse(raw);
2059
+ const choice = typeof parsed?.choice === 'string' ? parsed.choice.trim() : '';
2060
+ if (choice && choice.startsWith('/sound/')) {
2061
+ return choice;
2062
+ }
2063
+ } catch (_) {}
2064
+ return null;
2065
+ })();
2066
+
2067
+ // Grab or create an Audio element for the chosen asset and prime it.
2068
+ const asset = preferCustom || '/chime.mp3';
2069
+ let audioEl;
2070
+ if (preferCustom) {
2071
+ audioEl = window.__pinokioCustomNotificationAudio;
2072
+ if (!audioEl || audioEl.__pinokioSrc !== preferCustom) {
2073
+ audioEl = new Audio(preferCustom);
2074
+ audioEl.preload = 'auto';
2075
+ audioEl.loop = false;
2076
+ audioEl.__pinokioSrc = preferCustom;
2077
+ window.__pinokioCustomNotificationAudio = audioEl;
2078
+ }
2079
+ } else {
2080
+ audioEl = window.__pinokioChimeAudio;
2081
+ if (!audioEl) {
2082
+ audioEl = new Audio('/chime.mp3');
2083
+ audioEl.preload = 'auto';
2084
+ audioEl.loop = false;
2085
+ audioEl.__pinokioSrc = '/chime.mp3';
2086
+ window.__pinokioChimeAudio = audioEl;
2087
+ }
2088
+ }
2089
+
2090
+ const wasMuted = audioEl.muted;
2091
+ audioEl.muted = true;
2092
+ audioEl.currentTime = 0;
1988
2093
  try {
1989
- let a = window.__pinokioChimeAudio;
1990
- if (!a) {
1991
- a = new Audio('/chime.mp3');
1992
- a.preload = 'auto';
1993
- a.loop = false;
1994
- a.muted = false;
1995
- window.__pinokioChimeAudio = a;
1996
- }
1997
- a.currentTime = 0;
1998
- await a.play(); // must be called synchronously in gesture handler
1999
- try { a.pause(); a.currentTime = 0; } catch (_) {}
2000
- try { window.__pinokioAudioArmed = true; } catch (_) {}
2001
- return true;
2002
- } catch (_) {
2003
- try { window.__pinokioAudioArmed = true; } catch (_) {}
2004
- return false;
2094
+ await audioEl.play();
2095
+ } finally {
2096
+ try { audioEl.pause(); } catch (_) {}
2097
+ audioEl.currentTime = 0;
2098
+ audioEl.muted = wasMuted;
2005
2099
  }
2100
+ try { window.__pinokioAudioArmed = true; } catch (_) {}
2101
+ return true;
2006
2102
  };
2007
2103
 
2008
2104
  const setup = () => {
@@ -2596,23 +2692,6 @@ document.addEventListener("DOMContentLoaded", () => {
2596
2692
  });
2597
2693
  }, true);
2598
2694
 
2599
- if (document.querySelector("#genlog")) {
2600
- document.querySelector("#genlog").addEventListener("click", (e) => {
2601
- e.preventDefault()
2602
- e.stopPropagation()
2603
- e.target.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i>'
2604
- fetch("/pinokio/log", {
2605
- method: "post",
2606
- }).then((res) => {
2607
- let btn = document.querySelector("#genlog")
2608
- let btn2 = document.querySelector("#downloadlogs")
2609
- btn2.classList.remove("hidden")
2610
- btn.classList.add("hidden")
2611
- btn.innerHTML = '<i class="fa-solid fa-circle-check"></i> Generated!'
2612
- //btn.classList.add("hidden")
2613
- })
2614
- })
2615
- }
2616
2695
  const closeWindowButton = document.querySelector("#close-window");
2617
2696
  if (closeWindowButton) {
2618
2697
  const isInIframe = (() => {
@@ -2717,738 +2796,142 @@ document.addEventListener("DOMContentLoaded", () => {
2717
2796
  })
2718
2797
  }
2719
2798
 
2720
- let pendingCreateLauncherDefaults = null;
2721
- let shouldCleanupCreateLauncherQuery = false;
2722
-
2723
- initCreateLauncherFlow();
2724
- handleCreateLauncherQueryParams();
2725
-
2726
- function openPendingCreateLauncherModal() {
2727
- if (!pendingCreateLauncherDefaults) return;
2728
- showCreateLauncherModal(pendingCreateLauncherDefaults);
2729
- pendingCreateLauncherDefaults = null;
2730
-
2731
- if (!shouldCleanupCreateLauncherQuery) return;
2732
- shouldCleanupCreateLauncherQuery = false;
2799
+ const createLauncherState = {
2800
+ pendingDefaults: null,
2801
+ shouldCleanupQuery: false,
2802
+ loaderPromise: null,
2803
+ };
2733
2804
 
2734
- try {
2735
- const url = new URL(window.location.href);
2736
- Array.from(url.searchParams.keys()).forEach((key) => {
2737
- if (
2738
- key === 'create' ||
2739
- key === 'prompt' ||
2740
- key === 'folder' ||
2741
- key === 'tool' ||
2742
- key.startsWith('template.') ||
2743
- key.startsWith('template_')
2744
- ) {
2745
- url.searchParams.delete(key);
2746
- }
2747
- });
2748
- window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
2749
- } catch (error) {
2750
- console.warn('Failed to update history for create launcher params', error);
2751
- }
2752
- }
2753
-
2754
- let createLauncherModalInstance = null;
2755
- let createLauncherKeydownHandler = null;
2756
- let createLauncherModalPromise = null;
2757
-
2758
- const createLauncherFallbackTools = [
2759
- {
2760
- value: 'claude',
2761
- label: 'Claude Code',
2762
- iconSrc: '/asset/plugin/code/claude/claude.png',
2763
- isDefault: true,
2764
- href: '/run/plugin/code/claude/pinokio.js',
2765
- category: 'CLI',
2766
- },
2767
- {
2768
- value: 'codex',
2769
- label: 'OpenAI Codex',
2770
- iconSrc: '/asset/plugin/code/codex/openai.webp',
2771
- isDefault: false,
2772
- href: '/run/plugin/code/codex/pinokio.js',
2773
- category: 'CLI',
2774
- },
2775
- {
2776
- value: 'gemini',
2777
- label: 'Google Gemini CLI',
2778
- iconSrc: '/asset/plugin/code/gemini/gemini.jpeg',
2779
- isDefault: false,
2780
- href: '/run/plugin/code/gemini/pinokio.js',
2781
- category: 'CLI',
2782
- },
2783
- ];
2784
-
2785
- let cachedCreateLauncherTools = null;
2786
- let loadingCreateLauncherTools = null;
2787
-
2788
- function mapPluginMenuToCreateLauncherTools(menu) {
2789
- if (!Array.isArray(menu)) return [];
2790
-
2791
- return menu
2792
- .map((plugin) => {
2793
- if (!plugin || (!plugin.href && !plugin.link)) {
2794
- return null;
2795
- }
2796
- const href = typeof plugin.href === 'string' ? plugin.href.trim() : '';
2797
- const label = plugin.title || plugin.text || plugin.name || href || '';
2798
-
2799
- let slug = '';
2800
- if (href) {
2801
- const segments = href.split('/').filter(Boolean);
2802
- if (segments.length >= 2) {
2803
- slug = segments[segments.length - 2] || '';
2804
- }
2805
- if (!slug && segments.length) {
2806
- slug = segments[segments.length - 1] || '';
2807
- }
2808
- if (slug.endsWith('.js')) {
2809
- slug = slug.replace(/\.js$/i, '');
2810
- }
2811
- }
2812
- if (!slug && label) {
2813
- slug = label
2814
- .toLowerCase()
2815
- .replace(/[^a-z0-9]+/g, '-')
2816
- .replace(/^-+|-+$/g, '');
2817
- }
2818
- const value = slug || href || (typeof plugin.link === 'string' ? plugin.link.trim() : '');
2819
- if (!value) {
2820
- return null;
2821
- }
2822
- const iconSrc = plugin.image || null;
2823
- const runs = Array.isArray(plugin.run) ? plugin.run : [];
2824
- const hasExec = runs.some((step) => step && step.method === 'exec');
2825
- const category = hasExec ? 'IDE' : 'CLI';
2826
- return {
2827
- value,
2828
- label,
2829
- iconSrc,
2830
- isDefault: Boolean(plugin.default === true),
2831
- href: href || null,
2832
- category,
2833
- };
2834
- })
2835
- .filter(Boolean);
2836
- }
2805
+ initializeCreateLauncherIntegration();
2837
2806
 
2838
- async function getCreateLauncherTools() {
2839
- if (Array.isArray(cachedCreateLauncherTools) && cachedCreateLauncherTools.length > 0) {
2840
- return cachedCreateLauncherTools;
2807
+ function initializeCreateLauncherIntegration() {
2808
+ const defaults = parseCreateLauncherDefaults();
2809
+ const triggerExists = document.getElementById('create-launcher-button');
2810
+ if (!triggerExists && !defaults) {
2811
+ return;
2841
2812
  }
2842
- if (loadingCreateLauncherTools) {
2843
- return loadingCreateLauncherTools;
2813
+ if (defaults) {
2814
+ createLauncherState.pendingDefaults = defaults;
2815
+ createLauncherState.shouldCleanupQuery = true;
2844
2816
  }
2845
2817
 
2846
- loadingCreateLauncherTools = fetch('/api/plugin/menu')
2847
- .then((res) => {
2848
- if (!res.ok) {
2849
- throw new Error(`Failed to load plugin menu: ${res.status}`);
2850
- }
2851
- return res.json();
2852
- })
2853
- .then((data) => {
2854
- const menu = data && Array.isArray(data.menu) ? data.menu : [];
2855
- const tools = mapPluginMenuToCreateLauncherTools(menu);
2856
- return tools.length > 0 ? tools : createLauncherFallbackTools.slice();
2857
- })
2858
- .catch((error) => {
2859
- console.warn('Falling back to default agents for create launcher modal', error);
2860
- return createLauncherFallbackTools.slice();
2861
- })
2862
- .finally(() => {
2863
- loadingCreateLauncherTools = null;
2864
- });
2865
-
2866
- const tools = await loadingCreateLauncherTools;
2867
- cachedCreateLauncherTools = tools;
2868
- return tools;
2869
- }
2870
-
2871
- function initCreateLauncherFlow() {
2872
- const trigger = document.getElementById('create-launcher-button');
2873
- if (!trigger) return;
2874
- if (trigger.dataset.createLauncherInit === 'true') return;
2875
- trigger.dataset.createLauncherInit = 'true';
2876
-
2877
- trigger.addEventListener('click', () => {
2878
- showCreateLauncherModal();
2818
+ ensureCreateLauncherModule().then((api) => {
2819
+ if (!api) {
2820
+ return;
2821
+ }
2822
+ initCreateLauncherTrigger(api);
2823
+ openPendingCreateLauncherModal(api);
2879
2824
  });
2880
-
2881
- // If we already captured query params that request the modal, open it now that the
2882
- // trigger has been initialised and the modal can be constructed.
2883
- requestAnimationFrame(openPendingCreateLauncherModal);
2884
2825
  }
2885
2826
 
2886
- async function ensureCreateLauncherModal() {
2887
- if (createLauncherModalInstance) {
2888
- return createLauncherModalInstance;
2827
+ function ensureCreateLauncherModule() {
2828
+ if (window.CreateLauncher) {
2829
+ return Promise.resolve(window.CreateLauncher);
2889
2830
  }
2890
- if (createLauncherModalPromise) {
2891
- return createLauncherModalPromise;
2831
+ if (createLauncherState.loaderPromise) {
2832
+ return createLauncherState.loaderPromise;
2892
2833
  }
2893
2834
 
2894
- createLauncherModalPromise = (async () => {
2895
- const tools = await getCreateLauncherTools();
2896
-
2897
- const overlay = document.createElement('div');
2898
- overlay.className = 'modal-overlay create-launcher-modal-overlay';
2899
-
2900
- const modal = document.createElement('div');
2901
- modal.className = 'create-launcher-modal';
2902
- modal.setAttribute('role', 'dialog');
2903
- modal.setAttribute('aria-modal', 'true');
2904
-
2905
- const header = document.createElement('div');
2906
- header.className = 'create-launcher-modal-header';
2907
-
2908
- const iconWrapper = document.createElement('div');
2909
- iconWrapper.className = 'create-launcher-modal-icon';
2910
-
2911
- const headerIcon = document.createElement('i');
2912
- headerIcon.className = 'fa-solid fa-wand-magic-sparkles'
2913
- iconWrapper.appendChild(headerIcon);
2914
-
2915
- const headingStack = document.createElement('div');
2916
- headingStack.className = 'create-launcher-modal-headings';
2917
-
2918
- const title = document.createElement('h3');
2919
- title.id = 'create-launcher-modal-title';
2920
- title.textContent = 'Create';
2921
-
2922
- const description = document.createElement('p');
2923
- description.className = 'create-launcher-modal-description';
2924
- description.id = 'create-launcher-modal-description';
2925
- description.textContent = 'Create a reusable and shareable launcher for any task or any app'
2926
-
2927
- modal.setAttribute('aria-labelledby', title.id);
2928
- modal.setAttribute('aria-describedby', description.id);
2929
-
2930
- headingStack.appendChild(title);
2931
- headingStack.appendChild(description);
2932
- header.appendChild(iconWrapper);
2933
- header.appendChild(headingStack);
2934
-
2935
- const closeButton = document.createElement('button');
2936
- closeButton.type = 'button';
2937
- closeButton.className = 'create-launcher-modal-close';
2938
- closeButton.setAttribute('aria-label', 'Close create launcher modal');
2939
- closeButton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
2940
- header.appendChild(closeButton);
2941
-
2942
- const promptLabel = document.createElement('label');
2943
- promptLabel.className = 'create-launcher-modal-label';
2944
- promptLabel.textContent = 'What do you want to do?';
2945
-
2946
- const promptTextarea = document.createElement('textarea');
2947
- promptTextarea.className = 'create-launcher-modal-textarea';
2948
- promptTextarea.placeholder = 'Examples: "a 1-click launcher for ComfyUI", "I want to change file format", "I want to clone a website to run locally", etc. (Leave empty to decide later)';
2949
- promptLabel.appendChild(promptTextarea);
2950
-
2951
- const templateWrapper = document.createElement('div');
2952
- templateWrapper.className = 'create-launcher-modal-template';
2953
- templateWrapper.style.display = 'none';
2954
-
2955
- const templateTitle = document.createElement('div');
2956
- templateTitle.className = 'create-launcher-modal-template-title';
2957
- templateTitle.textContent = 'Template variables';
2958
-
2959
- const templateDescription = document.createElement('p');
2960
- templateDescription.className = 'create-launcher-modal-template-description';
2961
- templateDescription.textContent = 'Fill in each variable below before creating your launcher.';
2962
-
2963
- const templateFields = document.createElement('div');
2964
- templateFields.className = 'create-launcher-modal-template-fields';
2965
-
2966
- templateWrapper.appendChild(templateTitle);
2967
- templateWrapper.appendChild(templateDescription);
2968
- templateWrapper.appendChild(templateFields);
2969
-
2970
- const folderLabel = document.createElement('label');
2971
- folderLabel.className = 'create-launcher-modal-label';
2972
- folderLabel.textContent = 'name';
2973
-
2974
- const folderInput = document.createElement('input');
2975
- folderInput.type = 'text';
2976
- folderInput.placeholder = 'example: my-launcher';
2977
- folderInput.className = 'create-launcher-modal-input';
2978
- folderLabel.appendChild(folderInput);
2979
-
2980
- const toolWrapper = document.createElement('div');
2981
- toolWrapper.className = 'create-launcher-modal-tools';
2982
-
2983
- const toolTitle = document.createElement('div');
2984
- toolTitle.className = 'create-launcher-modal-tools-title';
2985
- toolTitle.textContent = 'Select Agent';
2986
-
2987
- const toolOptions = document.createElement('div');
2988
- toolOptions.className = 'create-launcher-modal-tools-options';
2989
-
2990
- const toolEntries = [];
2991
- const defaultToolIndex = tools.findIndex((tool) => tool.isDefault);
2992
- const initialSelectionIndex = defaultToolIndex >= 0 ? defaultToolIndex : (tools.length > 0 ? 0 : -1);
2993
-
2994
- const groupedTools = tools.reduce((acc, tool, index) => {
2995
- const category = tool.category || 'CLI';
2996
- if (!acc.has(category)) {
2997
- acc.set(category, []);
2998
- }
2999
- acc.get(category).push({ tool, index });
3000
- return acc;
3001
- }, new Map());
3002
-
3003
- const categoryOrder = ['CLI', 'IDE'];
3004
- const orderedGroups = [];
3005
- categoryOrder.forEach((cat) => {
3006
- if (groupedTools.has(cat)) {
3007
- orderedGroups.push([cat, groupedTools.get(cat)]);
3008
- groupedTools.delete(cat);
3009
- }
3010
- });
3011
- groupedTools.forEach((value, key) => {
3012
- orderedGroups.push([key, value]);
3013
- });
3014
-
3015
- orderedGroups.forEach(([category, entries]) => {
3016
- const group = document.createElement('div');
3017
- group.className = 'create-launcher-modal-tools-group';
3018
-
3019
- const heading = document.createElement('div');
3020
- heading.className = 'create-launcher-modal-tools-group-title';
3021
- heading.textContent = category;
3022
- group.appendChild(heading);
3023
-
3024
- const groupList = document.createElement('div');
3025
- groupList.className = 'create-launcher-modal-tools-group-options';
3026
-
3027
- const sortedEntries = entries.slice().sort((a, b) => {
3028
- const nameA = (a.tool && a.tool.label ? a.tool.label : '').toLowerCase();
3029
- const nameB = (b.tool && b.tool.label ? b.tool.label : '').toLowerCase();
3030
- if (nameA < nameB) return -1;
3031
- if (nameA > nameB) return 1;
3032
- return 0;
3033
- });
3034
-
3035
- sortedEntries.forEach(({ tool, index }) => {
3036
- const option = document.createElement('label');
3037
- option.className = 'create-launcher-modal-tool';
3038
-
3039
- const radio = document.createElement('input');
3040
- radio.type = 'radio';
3041
- radio.name = 'create-launcher-tool';
3042
- radio.value = tool.value;
3043
- radio.dataset.agentLabel = tool.label;
3044
- radio.dataset.agentCategory = category;
3045
- if (tool.href) {
3046
- radio.dataset.agentHref = tool.href;
3047
- }
3048
-
3049
- if (index === initialSelectionIndex) {
3050
- radio.checked = true;
3051
- }
3052
-
3053
- const badge = document.createElement('span');
3054
- badge.className = 'create-launcher-modal-tool-label';
3055
- badge.textContent = tool.label;
3056
-
3057
- option.appendChild(radio);
3058
- if (tool.iconSrc) {
3059
- const icon = document.createElement('img');
3060
- icon.className = 'create-launcher-modal-tool-icon';
3061
- icon.src = tool.iconSrc;
3062
- icon.alt = `${tool.label} icon`;
3063
- icon.onerror = () => { icon.style.display = 'none'; };
3064
- option.appendChild(icon);
3065
- }
3066
- option.appendChild(badge);
3067
- groupList.appendChild(option);
3068
- const entry = { input: radio, container: option, meta: tool };
3069
- toolEntries.push(entry);
3070
- radio.addEventListener('change', () => {
3071
- updateToolSelections(toolEntries);
3072
- });
3073
- });
3074
-
3075
- group.appendChild(groupList);
3076
- toolOptions.appendChild(group);
3077
- });
3078
-
3079
- if (!toolEntries.length) {
3080
- const emptyState = document.createElement('div');
3081
- emptyState.className = 'create-launcher-modal-tools-empty';
3082
- emptyState.textContent = 'No agents available.';
3083
- toolOptions.appendChild(emptyState);
3084
- }
3085
-
3086
- toolWrapper.appendChild(toolTitle);
3087
- toolWrapper.appendChild(toolOptions);
3088
-
3089
- const error = document.createElement('div');
3090
- error.className = 'create-launcher-modal-error';
3091
-
3092
- const actions = document.createElement('div');
3093
- actions.className = 'create-launcher-modal-actions';
3094
-
3095
- const cancelButton = document.createElement('button');
3096
- cancelButton.type = 'button';
3097
- cancelButton.className = 'create-launcher-modal-button cancel';
3098
- cancelButton.textContent = 'Cancel';
3099
-
3100
- const confirmButton = document.createElement('button');
3101
- confirmButton.type = 'button';
3102
- confirmButton.className = 'create-launcher-modal-button confirm';
3103
- confirmButton.textContent = 'Create';
3104
-
3105
- actions.appendChild(cancelButton);
3106
- actions.appendChild(confirmButton);
3107
-
3108
- const advancedLink = document.createElement('a');
3109
- advancedLink.className = 'create-launcher-modal-advanced';
3110
- advancedLink.href = '/init';
3111
- advancedLink.textContent = 'Or, try advanced options';
3112
-
3113
- const bookmarkletLink = document.createElement('a');
3114
- bookmarkletLink.className = 'create-launcher-modal-advanced secondary';
3115
- bookmarkletLink.href = '/bookmarklet';
3116
- bookmarkletLink.target = '_blank';
3117
- bookmarkletLink.setAttribute('features', 'browser');
3118
- bookmarkletLink.rel = 'noopener';
3119
- bookmarkletLink.textContent = 'Add 1-click bookmarklet';
3120
-
3121
- const linkRow = document.createElement('div');
3122
- linkRow.className = 'create-launcher-modal-links';
3123
- linkRow.appendChild(advancedLink);
3124
- linkRow.appendChild(bookmarkletLink);
3125
-
3126
- modal.appendChild(header);
3127
- modal.appendChild(promptLabel);
3128
- modal.appendChild(templateWrapper);
3129
- modal.appendChild(folderLabel);
3130
- modal.appendChild(toolWrapper);
3131
- modal.appendChild(error);
3132
- modal.appendChild(actions);
3133
- modal.appendChild(linkRow);
3134
- overlay.appendChild(modal);
3135
- document.body.appendChild(overlay);
3136
-
3137
- let folderEditedByUser = false;
3138
- let templateValues = new Map();
3139
-
3140
- function syncTemplateFields(promptText, defaults = {}) {
3141
- const variableNames = extractTemplateVariableNames(promptText);
3142
- const previousValues = templateValues;
3143
- const newValues = new Map();
3144
-
3145
- variableNames.forEach((name) => {
3146
- if (Object.prototype.hasOwnProperty.call(defaults, name) && defaults[name] !== undefined) {
3147
- newValues.set(name, defaults[name]);
3148
- } else if (previousValues.has(name)) {
3149
- newValues.set(name, previousValues.get(name));
3150
- } else {
3151
- newValues.set(name, '');
3152
- }
3153
- });
3154
-
3155
- templateValues = newValues;
3156
- templateFields.innerHTML = '';
3157
-
3158
- if (variableNames.length === 0) {
3159
- templateWrapper.style.display = 'none';
3160
- return;
3161
- }
3162
-
3163
- templateWrapper.style.display = 'flex';
3164
-
3165
- variableNames.forEach((name) => {
3166
- const field = document.createElement('label');
3167
- field.className = 'create-launcher-modal-template-field';
3168
-
3169
- const labelText = document.createElement('span');
3170
- labelText.className = 'create-launcher-modal-template-field-label';
3171
- labelText.textContent = name;
3172
-
3173
- const input = document.createElement('input');
3174
- input.type = 'text';
3175
- input.className = 'create-launcher-modal-template-input';
3176
- input.placeholder = `Enter ${name}`;
3177
- input.value = templateValues.get(name) || '';
3178
- input.dataset.templateInput = name;
3179
- input.addEventListener('input', () => {
3180
- templateValues.set(name, input.value);
3181
- });
3182
-
3183
- field.appendChild(labelText);
3184
- field.appendChild(input);
3185
- templateFields.appendChild(field);
3186
- });
3187
- }
3188
-
3189
- folderInput.addEventListener('input', () => {
3190
- folderEditedByUser = true;
3191
- });
3192
-
3193
- promptTextarea.addEventListener('input', () => {
3194
- syncTemplateFields(promptTextarea.value);
3195
- if (folderEditedByUser) return;
3196
- folderInput.value = generateFolderSuggestion(promptTextarea.value);
3197
- });
3198
-
3199
- cancelButton.addEventListener('click', hideCreateLauncherModal);
3200
- closeButton.addEventListener('click', hideCreateLauncherModal);
3201
- confirmButton.addEventListener('click', submitCreateLauncherModal);
3202
-
3203
- advancedLink.addEventListener('click', () => {
3204
- hideCreateLauncherModal();
3205
- });
3206
-
3207
- bookmarkletLink.addEventListener('click', () => {
3208
- hideCreateLauncherModal();
3209
- });
3210
-
3211
- createLauncherModalInstance = {
3212
- overlay,
3213
- modal,
3214
- folderInput,
3215
- promptTextarea,
3216
- cancelButton,
3217
- confirmButton,
3218
- error,
3219
- toolEntries,
3220
- toolOptions,
3221
- toolWrapper,
3222
- resetFolderTracking() {
3223
- folderEditedByUser = false;
3224
- },
3225
- syncTemplateFields,
3226
- getTemplateValues() {
3227
- return new Map(templateValues);
3228
- },
3229
- templateFields,
3230
- markFolderEdited() {
3231
- folderEditedByUser = true;
3232
- }
2835
+ createLauncherState.loaderPromise = new Promise((resolve) => {
2836
+ const script = document.createElement('script');
2837
+ script.src = '/create-launcher.js';
2838
+ script.async = true;
2839
+ script.onload = () => resolve(window.CreateLauncher || null);
2840
+ script.onerror = (error) => {
2841
+ console.warn('Failed to load create launcher module', error);
2842
+ resolve(null);
3233
2843
  };
3234
-
3235
- updateToolSelections(toolEntries);
3236
-
3237
- return createLauncherModalInstance;
3238
- })();
3239
-
3240
- try {
3241
- return await createLauncherModalPromise;
3242
- } finally {
3243
- createLauncherModalPromise = null;
3244
- }
3245
- }
3246
-
3247
- async function showCreateLauncherModal(defaults = {}) {
3248
-
3249
- let response = await fetch("/bundle/dev").then((res) => {
3250
- return res.json()
3251
- })
3252
- if (response.available) {
3253
- } else {
3254
- location.href = "/setup/dev?callback=/"
3255
- return
3256
- }
3257
-
3258
- const modal = await ensureCreateLauncherModal();
3259
-
3260
- modal.error.textContent = '';
3261
- modal.resetFolderTracking();
3262
- const { prompt = '', folder = '', tool = '' } = defaults;
3263
-
3264
- modal.promptTextarea.value = prompt;
3265
- if (folder) {
3266
- modal.folderInput.value = folder;
3267
- if (typeof modal.markFolderEdited === 'function') {
3268
- modal.markFolderEdited();
3269
- }
3270
- } else if (prompt) {
3271
- modal.folderInput.value = generateFolderSuggestion(prompt);
3272
- } else {
3273
- modal.folderInput.value = '';
3274
- }
3275
-
3276
- const matchingToolEntry = modal.toolEntries.find((entry) => entry.input.value === tool);
3277
- const defaultToolEntryIndex = modal.toolEntries.findIndex((entry) => entry.meta && entry.meta.isDefault);
3278
- const fallbackToolIndex = defaultToolEntryIndex >= 0 ? defaultToolEntryIndex : 0;
3279
- modal.toolEntries.forEach((entry, index) => {
3280
- entry.input.checked = matchingToolEntry ? entry === matchingToolEntry : index === fallbackToolIndex;
3281
- });
3282
- updateToolSelections(modal.toolEntries);
3283
-
3284
- modal.syncTemplateFields(modal.promptTextarea.value, defaults.templateValues || {});
3285
-
3286
- requestAnimationFrame(() => {
3287
- modal.overlay.classList.add('is-visible');
3288
- requestAnimationFrame(() => {
3289
- modal.folderInput.select();
3290
- modal.promptTextarea.focus();
3291
- });
2844
+ const target = document.head || document.body || document.documentElement;
2845
+ target.appendChild(script);
3292
2846
  });
3293
2847
 
3294
- createLauncherKeydownHandler = (event) => {
3295
- if (event.key === 'Escape') {
3296
- event.preventDefault();
3297
- hideCreateLauncherModal();
3298
- } else if (event.key === 'Enter' && event.target === modal.folderInput) {
3299
- event.preventDefault();
3300
- submitCreateLauncherModal();
3301
- }
3302
- };
3303
-
3304
- document.addEventListener('keydown', createLauncherKeydownHandler, true);
2848
+ return createLauncherState.loaderPromise;
3305
2849
  }
3306
2850
 
3307
- function hideCreateLauncherModal() {
3308
- if (!createLauncherModalInstance) return;
3309
- createLauncherModalInstance.overlay.classList.remove('is-visible');
3310
- if (createLauncherKeydownHandler) {
3311
- document.removeEventListener('keydown', createLauncherKeydownHandler, true);
3312
- createLauncherKeydownHandler = null;
3313
- }
3314
- }
3315
-
3316
- async function submitCreateLauncherModal() {
3317
- const modal = await ensureCreateLauncherModal();
3318
- modal.error.textContent = '';
3319
-
3320
- const folderName = modal.folderInput.value.trim();
3321
- const rawPrompt = modal.promptTextarea.value;
3322
- const templateValues = modal.getTemplateValues ? modal.getTemplateValues() : new Map();
3323
- const selectedEntry = modal.toolEntries.find((entry) => entry.input.checked);
3324
- const defaultToolEntryIndex = modal.toolEntries.findIndex((entry) => entry.meta && entry.meta.isDefault);
3325
- const fallbackEntry = defaultToolEntryIndex >= 0 ? modal.toolEntries[defaultToolEntryIndex] : modal.toolEntries[0];
3326
- const selectedTool = (selectedEntry || fallbackEntry)?.input.value || '';
3327
-
3328
- if (!selectedTool) {
3329
- modal.error.textContent = 'Please select an agent.';
2851
+ function initCreateLauncherTrigger(api) {
2852
+ const trigger = document.getElementById('create-launcher-button');
2853
+ if (!trigger) {
3330
2854
  return;
3331
2855
  }
3332
-
3333
- if (!folderName) {
3334
- modal.error.textContent = 'Please enter a folder name.';
3335
- modal.folderInput.focus();
2856
+ if (trigger.dataset.createLauncherInit === 'true') {
3336
2857
  return;
3337
2858
  }
2859
+ trigger.dataset.createLauncherInit = 'true';
2860
+ trigger.addEventListener('click', () => {
2861
+ api.showModal();
2862
+ });
2863
+ }
3338
2864
 
3339
- if (folderName.includes(' ')) {
3340
- modal.error.textContent = 'Folder names cannot contain spaces.';
3341
- modal.folderInput.focus();
2865
+ function openPendingCreateLauncherModal(api) {
2866
+ if (!api || !createLauncherState.pendingDefaults) {
3342
2867
  return;
3343
2868
  }
3344
-
3345
- let finalPrompt = rawPrompt;
3346
- if (templateValues.size > 0) {
3347
- const missingVariables = [];
3348
- templateValues.forEach((value, name) => {
3349
- if (!value || value.trim() === '') {
3350
- missingVariables.push(name);
3351
- }
3352
- });
3353
-
3354
- if (missingVariables.length > 0) {
3355
- modal.error.textContent = `Please fill in values for: ${missingVariables.join(', ')}`;
3356
- const targetInput = modal.templateFields?.querySelector(`[data-template-input="${missingVariables[0]}"]`);
3357
- if (targetInput) {
3358
- targetInput.focus();
3359
- } else {
3360
- modal.promptTextarea.focus();
3361
- }
3362
- return;
3363
- }
3364
-
3365
- finalPrompt = applyTemplateValues(rawPrompt, templateValues);
2869
+ api.showModal(createLauncherState.pendingDefaults);
2870
+ createLauncherState.pendingDefaults = null;
2871
+ if (createLauncherState.shouldCleanupQuery) {
2872
+ cleanupCreateLauncherParams();
2873
+ createLauncherState.shouldCleanupQuery = false;
3366
2874
  }
3367
-
3368
- const prompt = finalPrompt.trim();
3369
-
3370
- const url = `/pro?name=${encodeURIComponent(folderName)}&message=${encodeURIComponent(prompt)}&tool=${encodeURIComponent(selectedTool)}`;
3371
- hideCreateLauncherModal();
3372
- window.location.href = url;
3373
2875
  }
3374
2876
 
3375
- function handleCreateLauncherQueryParams() {
3376
- const params = new URLSearchParams(window.location.search);
3377
- if (!params.has('create')) return;
2877
+ function parseCreateLauncherDefaults() {
2878
+ try {
2879
+ const params = new URLSearchParams(window.location.search);
2880
+ if (!params.has('create')) {
2881
+ return null;
2882
+ }
3378
2883
 
3379
- const defaults = {};
3380
- const templateDefaults = {};
2884
+ const defaults = {};
2885
+ const templateDefaults = {};
3381
2886
 
3382
- const promptParam = params.get('prompt');
3383
- if (promptParam) defaults.prompt = promptParam.trim();
2887
+ const promptParam = params.get('prompt');
2888
+ if (promptParam) defaults.prompt = promptParam.trim();
3384
2889
 
3385
- const folderParam = params.get('folder');
3386
- if (folderParam) defaults.folder = folderParam.trim();
2890
+ const folderParam = params.get('folder');
2891
+ if (folderParam) defaults.folder = folderParam.trim();
3387
2892
 
3388
- const toolParam = params.get('tool');
3389
- if (toolParam) defaults.tool = toolParam.trim();
2893
+ const toolParam = params.get('tool');
2894
+ if (toolParam) defaults.tool = toolParam.trim();
3390
2895
 
3391
- params.forEach((value, key) => {
3392
- if (key.startsWith('template.') || key.startsWith('template_')) {
3393
- const name = key.replace(/^template[._]/, '');
3394
- if (name) {
3395
- templateDefaults[name] = value ? value.trim() : '';
2896
+ params.forEach((value, key) => {
2897
+ if (key.startsWith('template.') || key.startsWith('template_')) {
2898
+ const name = key.replace(/^template[._]/, '');
2899
+ if (name) {
2900
+ templateDefaults[name] = value ? value.trim() : '';
2901
+ }
3396
2902
  }
3397
- }
3398
- });
3399
-
3400
- if (Object.keys(templateDefaults).length > 0) {
3401
- defaults.templateValues = templateDefaults;
3402
- }
3403
-
3404
- pendingCreateLauncherDefaults = defaults;
3405
- shouldCleanupCreateLauncherQuery = true;
3406
-
3407
- requestAnimationFrame(openPendingCreateLauncherModal);
3408
- }
3409
-
3410
- function generateFolderSuggestion(prompt) {
3411
- if (!prompt) return '';
3412
- return prompt
3413
- .toLowerCase()
3414
- .replace(/[^a-z0-9\-\s_]/g, '')
3415
- .replace(/[\s_]+/g, '-')
3416
- .replace(/^-+|-+$/g, '')
3417
- .slice(0, 50);
3418
- }
2903
+ });
3419
2904
 
3420
- function updateToolSelections(entries) {
3421
- entries.forEach(({ input, container }) => {
3422
- if (input.checked) {
3423
- container.classList.add('selected');
3424
- } else {
3425
- container.classList.remove('selected');
2905
+ if (Object.keys(templateDefaults).length > 0) {
2906
+ defaults.templateValues = templateDefaults;
3426
2907
  }
3427
- });
3428
- }
3429
2908
 
3430
- function extractTemplateVariableNames(template) {
3431
- const regex = /{{\s*([a-zA-Z0-9_][a-zA-Z0-9_\-.]*)\s*}}/g;
3432
- const names = new Set();
3433
- if (!template) return [];
3434
- let match;
3435
- while ((match = regex.exec(template)) !== null) {
3436
- names.add(match[1]);
2909
+ return defaults;
2910
+ } catch (error) {
2911
+ console.warn('Failed to parse create launcher params', error);
2912
+ return null;
3437
2913
  }
3438
- return Array.from(names);
3439
2914
  }
3440
2915
 
3441
- function escapeRegExp(str) {
3442
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2916
+ function cleanupCreateLauncherParams() {
2917
+ try {
2918
+ const url = new URL(window.location.href);
2919
+ Array.from(url.searchParams.keys()).forEach((key) => {
2920
+ if (
2921
+ key === 'create' ||
2922
+ key === 'prompt' ||
2923
+ key === 'folder' ||
2924
+ key === 'tool' ||
2925
+ key.startsWith('template.') ||
2926
+ key.startsWith('template_')
2927
+ ) {
2928
+ url.searchParams.delete(key);
2929
+ }
2930
+ });
2931
+ window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
2932
+ } catch (error) {
2933
+ console.warn('Failed to clean up create launcher params', error);
2934
+ }
3443
2935
  }
3444
2936
 
3445
- function applyTemplateValues(template, values) {
3446
- if (!template) return '';
3447
- let result = template;
3448
- values.forEach((value, name) => {
3449
- const pattern = new RegExp(`{{\\s*${escapeRegExp(name)}\\s*}}`, 'g');
3450
- result = result.replace(pattern, value);
3451
- });
3452
- return result;
3453
- }
3454
2937
  })