pinokiod 3.180.0 → 3.182.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 (51) hide show
  1. package/kernel/favicon.js +91 -34
  2. package/kernel/peer.js +73 -0
  3. package/kernel/util.js +28 -4
  4. package/package.json +1 -1
  5. package/server/index.js +237 -35
  6. package/server/public/common.js +677 -240
  7. package/server/public/files-app/app.css +64 -0
  8. package/server/public/files-app/app.js +87 -0
  9. package/server/public/install.js +8 -1
  10. package/server/public/layout.js +124 -0
  11. package/server/public/nav.js +227 -64
  12. package/server/public/sound/beep.mp3 +0 -0
  13. package/server/public/sound/bell.mp3 +0 -0
  14. package/server/public/sound/bright-ring.mp3 +0 -0
  15. package/server/public/sound/clap.mp3 +0 -0
  16. package/server/public/sound/deep-ring.mp3 +0 -0
  17. package/server/public/sound/gasp.mp3 +0 -0
  18. package/server/public/sound/hehe.mp3 +0 -0
  19. package/server/public/sound/levelup.mp3 +0 -0
  20. package/server/public/sound/light-pop.mp3 +0 -0
  21. package/server/public/sound/light-ring.mp3 +0 -0
  22. package/server/public/sound/meow.mp3 +0 -0
  23. package/server/public/sound/piano.mp3 +0 -0
  24. package/server/public/sound/pop.mp3 +0 -0
  25. package/server/public/sound/uhoh.mp3 +0 -0
  26. package/server/public/sound/whistle.mp3 +0 -0
  27. package/server/public/style.css +195 -4
  28. package/server/public/tab-idle-notifier.js +700 -4
  29. package/server/public/terminal-settings.js +1131 -0
  30. package/server/public/urldropdown.css +28 -1
  31. package/server/socket.js +71 -4
  32. package/server/views/{terminals.ejs → agents.ejs} +108 -32
  33. package/server/views/app.ejs +321 -104
  34. package/server/views/bootstrap.ejs +8 -0
  35. package/server/views/connect.ejs +10 -1
  36. package/server/views/d.ejs +172 -18
  37. package/server/views/editor.ejs +8 -0
  38. package/server/views/file_browser.ejs +4 -0
  39. package/server/views/index.ejs +10 -1
  40. package/server/views/init/index.ejs +18 -3
  41. package/server/views/install.ejs +8 -0
  42. package/server/views/layout.ejs +2 -0
  43. package/server/views/net.ejs +10 -1
  44. package/server/views/network.ejs +10 -1
  45. package/server/views/pro.ejs +8 -0
  46. package/server/views/prototype/index.ejs +8 -0
  47. package/server/views/screenshots.ejs +10 -2
  48. package/server/views/settings.ejs +10 -2
  49. package/server/views/shell.ejs +8 -0
  50. package/server/views/terminal.ejs +8 -0
  51. package/server/views/tools.ejs +10 -2
@@ -1462,10 +1462,56 @@ if (typeof hotkeys === 'function') {
1462
1462
  })
1463
1463
  }
1464
1464
 
1465
+ // Stable per-browser device identifier
1466
+ (function initPinokioDeviceId() {
1467
+ if (typeof window === 'undefined') {
1468
+ return;
1469
+ }
1470
+ try {
1471
+ const KEY = 'pinokio:device-id';
1472
+ const gen = () => `${Date.now()}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
1473
+ const get = () => {
1474
+ try {
1475
+ let id = localStorage.getItem(KEY);
1476
+ if (typeof id !== 'string' || id.length < 8) {
1477
+ id = gen();
1478
+ localStorage.setItem(KEY, id);
1479
+ }
1480
+ return id;
1481
+ } catch (_) {
1482
+ // Fallback when localStorage is unavailable
1483
+ if (!window.__pinokioVolatileDeviceId) {
1484
+ window.__pinokioVolatileDeviceId = gen();
1485
+ }
1486
+ return window.__pinokioVolatileDeviceId;
1487
+ }
1488
+ };
1489
+ // Expose helpers
1490
+ if (!window.PinokioGetDeviceId) {
1491
+ window.PinokioGetDeviceId = get;
1492
+ }
1493
+ // Convenience alias
1494
+ window.PinokioDeviceId = get();
1495
+ } catch (_) {
1496
+ // ignore
1497
+ }
1498
+ })();
1499
+
1465
1500
  (function initNotificationAudioBridge() {
1466
1501
  if (typeof window === 'undefined') {
1467
1502
  return;
1468
1503
  }
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 (_) {}
1469
1515
  if (window.__pinokioNotificationAudioInitialized) {
1470
1516
  return;
1471
1517
  }
@@ -1478,6 +1524,64 @@ if (typeof hotkeys === 'function') {
1478
1524
  let reconnectTimeout = null;
1479
1525
  let activeAudio = null;
1480
1526
 
1527
+ // Lightweight visual indicator to confirm notification receipt (mobile-friendly)
1528
+ let notifyIndicatorEl = null;
1529
+ let notifyIndicatorStyleInjected = false;
1530
+ const ensureNotifyIndicator = () => {
1531
+ if (!notifyIndicatorStyleInjected) {
1532
+ try {
1533
+ const style = document.createElement('style');
1534
+ style.textContent = `
1535
+ .pinokio-notify-indicator{position:fixed;top:12px;right:12px;z-index:2147483647;display:none;align-items:center;gap:8px;padding:8px 10px;border-radius:999px;background:rgba(15,23,42,0.92);color:#fff;font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 10px 30px rgba(0,0,0,0.35)}
1536
+ .pinokio-notify-indicator .bell{font-size:14px}
1537
+ .pinokio-notify-indicator.show{display:inline-flex;animation:pinokioNotifyPop 160ms ease-out, pinokioNotifyFade 1600ms ease-in 700ms forwards}
1538
+ @keyframes pinokioNotifyPop{from{transform:translateY(-6px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
1539
+ @keyframes pinokioNotifyFade{to{opacity:0;transform:translateY(-4px)}}
1540
+ @media (max-width: 768px){.pinokio-notify-indicator{top:10px;right:10px;padding:7px 9px;font-size:12px}}
1541
+ `;
1542
+ document.head.appendChild(style);
1543
+ notifyIndicatorStyleInjected = true;
1544
+ } catch (_) {}
1545
+ }
1546
+ if (!notifyIndicatorEl) {
1547
+ try {
1548
+ const el = document.createElement('div');
1549
+ el.className = 'pinokio-notify-indicator';
1550
+ const icon = document.createElement('span');
1551
+ icon.className = 'bell';
1552
+ icon.textContent = '🔔';
1553
+ const text = document.createElement('span');
1554
+ text.className = 'text';
1555
+ text.textContent = 'Notification received';
1556
+ el.appendChild(icon);
1557
+ el.appendChild(text);
1558
+ document.body.appendChild(el);
1559
+ notifyIndicatorEl = el;
1560
+ } catch (_) {}
1561
+ }
1562
+ };
1563
+ const flashNotifyIndicator = (payload) => {
1564
+ try {
1565
+ ensureNotifyIndicator();
1566
+ if (!notifyIndicatorEl) return;
1567
+ const text = notifyIndicatorEl.querySelector('.text');
1568
+ if (text) {
1569
+ const msg = (payload && typeof payload.message === 'string' && payload.message.trim()) ? payload.message.trim() : 'Notification received';
1570
+ // Keep it short on mobile
1571
+ text.textContent = msg.length > 80 ? (msg.slice(0, 77) + '…') : msg;
1572
+ }
1573
+ // retrigger animation
1574
+ notifyIndicatorEl.classList.remove('show');
1575
+ // force reflow
1576
+ void notifyIndicatorEl.offsetWidth;
1577
+ notifyIndicatorEl.classList.add('show');
1578
+ // Auto-hide handled by CSS animation; keep element for reuse
1579
+ window.setTimeout(() => {
1580
+ if (notifyIndicatorEl) notifyIndicatorEl.classList.remove('show');
1581
+ }, 2600);
1582
+ } catch (_) {}
1583
+ };
1584
+
1481
1585
  const leaderStorageKey = 'pinokio.notification.leader';
1482
1586
  const leaderHeartbeatMs = 5000;
1483
1587
  const leaderStaleMs = 15000;
@@ -1616,8 +1720,10 @@ if (typeof hotkeys === 'function') {
1616
1720
  }
1617
1721
  };
1618
1722
 
1723
+ const isFalseyString = (value) => typeof value === 'string' && ['false', '0', 'no', 'off'].includes(value.trim().toLowerCase());
1724
+
1619
1725
  const enqueueSound = (url) => {
1620
- if (!url) {
1726
+ if (!url || url === false || isFalseyString(url)) {
1621
1727
  return;
1622
1728
  }
1623
1729
  pendingSounds.push(url);
@@ -1629,6 +1735,18 @@ if (typeof hotkeys === 'function') {
1629
1735
  return;
1630
1736
  }
1631
1737
  const payload = packet.data || {};
1738
+ // If targeted to a specific device, ignore only when our id exists and mismatches
1739
+ try {
1740
+ const targetId = (typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null;
1741
+ if (targetId) {
1742
+ const myId = (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : null;
1743
+ if (myId && myId !== targetId) {
1744
+ return;
1745
+ }
1746
+ }
1747
+ } catch (_) {}
1748
+ // Visual confirmation regardless of audio outcome (useful on mobile)
1749
+ flashNotifyIndicator(payload);
1632
1750
  if (typeof payload.sound === 'string' && payload.sound) {
1633
1751
  enqueueSound(payload.sound);
1634
1752
  }
@@ -1696,6 +1814,7 @@ if (typeof hotkeys === 'function') {
1696
1814
  {
1697
1815
  method: CHANNEL_ID,
1698
1816
  mode: 'listen',
1817
+ device_id: (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : undefined,
1699
1818
  },
1700
1819
  handlePacket
1701
1820
  );
@@ -1768,6 +1887,127 @@ if (typeof hotkeys === 'function') {
1768
1887
  // Attempt to become leader immediately on load.
1769
1888
  attemptLeadership();
1770
1889
  })();
1890
+
1891
+ // Mobile "Tap to connect" curtain to prime audio on the top-level page
1892
+ (function initMobileConnectCurtain() {
1893
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
1894
+ return;
1895
+ }
1896
+ try {
1897
+ if (window.__pinokioConnectCurtainInstalled || window.__pinokioConnectCurtainInstalling) {
1898
+ return;
1899
+ }
1900
+ } catch (_) {}
1901
+ try {
1902
+ if (window.top && window.top !== window) {
1903
+ return; // only top-level
1904
+ }
1905
+ } catch (_) {
1906
+ // cross-origin parent; just bail
1907
+ return;
1908
+ }
1909
+ if (window.__pinokioConnectCurtainInstalled) {
1910
+ return;
1911
+ }
1912
+
1913
+ const isLikelyMobile = () => {
1914
+ try {
1915
+ if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
1916
+ if (navigator.userAgentData.mobile) return true;
1917
+ }
1918
+ } catch (_) {}
1919
+ try {
1920
+ const ua = (navigator.userAgent || '').toLowerCase();
1921
+ if (/iphone|ipad|ipod|android|mobile/.test(ua)) return true;
1922
+ } catch (_) {}
1923
+ try {
1924
+ if (navigator.maxTouchPoints && navigator.maxTouchPoints > 1) return true;
1925
+ } catch (_) {}
1926
+ try {
1927
+ if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) return true;
1928
+ } catch (_) {}
1929
+ try {
1930
+ if (window.matchMedia && window.matchMedia('(max-width: 900px)').matches) return true;
1931
+ } catch (_) {}
1932
+ return false;
1933
+ };
1934
+
1935
+ const createCurtain = () => {
1936
+ const style = document.createElement('style');
1937
+ style.textContent = `
1938
+ .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}
1939
+ .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)}
1940
+ @media (max-width:768px){.pinokio-connect-msg{font-size:15px;padding:12px 16px}}
1941
+ `;
1942
+ document.head.appendChild(style);
1943
+
1944
+ const overlay = document.createElement('div');
1945
+ overlay.className = 'pinokio-connect-curtain';
1946
+ overlay.setAttribute('role', 'button');
1947
+ overlay.setAttribute('aria-label', 'Tap to connect');
1948
+ overlay.tabIndex = 0;
1949
+ const msg = document.createElement('div');
1950
+ msg.className = 'pinokio-connect-msg';
1951
+ msg.textContent = 'Tap to connect';
1952
+ overlay.appendChild(msg);
1953
+ window.__pinokioConnectCurtainInstalled = true;
1954
+ return overlay;
1955
+ };
1956
+
1957
+ const primeAudio = async () => {
1958
+ try {
1959
+ let a = window.__pinokioChimeAudio;
1960
+ if (!a) {
1961
+ a = new Audio('/chime.mp3');
1962
+ a.preload = 'auto';
1963
+ a.loop = false;
1964
+ a.muted = false;
1965
+ window.__pinokioChimeAudio = a;
1966
+ }
1967
+ a.currentTime = 0;
1968
+ await a.play(); // must be called synchronously in gesture handler
1969
+ try { a.pause(); a.currentTime = 0; } catch (_) {}
1970
+ try { window.__pinokioAudioArmed = true; } catch (_) {}
1971
+ return true;
1972
+ } catch (_) {
1973
+ try { window.__pinokioAudioArmed = true; } catch (_) {}
1974
+ return false;
1975
+ }
1976
+ };
1977
+
1978
+ const setup = () => {
1979
+ let forceParam = false;
1980
+ try {
1981
+ const usp = new URLSearchParams(window.location.search);
1982
+ forceParam = usp.has('connect') || usp.get('connect') === '1';
1983
+ } catch (_) {}
1984
+ if (!(forceParam || isLikelyMobile())) {
1985
+ return;
1986
+ }
1987
+ if (window.__pinokioConnectCurtainInstalled || window.__pinokioConnectCurtainInstalling) {
1988
+ return;
1989
+ }
1990
+ try { window.__pinokioConnectCurtainInstalling = true; } catch (_) {}
1991
+ const overlay = createCurtain();
1992
+ let handled = false;
1993
+ const onTap = async (e) => {
1994
+ if (handled) return;
1995
+ handled = true;
1996
+ try { e.preventDefault(); e.stopPropagation(); } catch (_) {}
1997
+ try { await primeAudio(); } catch (_) {}
1998
+ try { overlay.remove(); } catch (_) {}
1999
+ try { window.__pinokioConnectCurtainInstalled = true; window.__pinokioConnectCurtainInstalling = false; } catch (_) {}
2000
+ };
2001
+ overlay.addEventListener('pointerdown', onTap, { once: true, capture: true });
2002
+ document.body.appendChild(overlay);
2003
+ };
2004
+
2005
+ if (document.readyState === 'loading') {
2006
+ document.addEventListener('DOMContentLoaded', setup, { once: true });
2007
+ } else {
2008
+ setup();
2009
+ }
2010
+ })();
1771
2011
  const refreshParent = (e) => {
1772
2012
  let dispatched = false;
1773
2013
  if (typeof window !== 'undefined' && typeof window.PinokioBroadcastMessage === 'function') {
@@ -1967,6 +2207,12 @@ document.addEventListener("DOMContentLoaded", () => {
1967
2207
  }
1968
2208
  if (document.querySelector("#refresh-page")) {
1969
2209
  document.querySelector("#refresh-page").addEventListener("click", (e) => {
2210
+ try {
2211
+ const headerEl = document.querySelector("header.navheader");
2212
+ const isMinimized = !!(headerEl && headerEl.classList.contains("minimized"));
2213
+ const key = `pinokio:header-restore-once:${location.pathname}`;
2214
+ sessionStorage.setItem(key, isMinimized ? "1" : "0");
2215
+ } catch (_) {}
1970
2216
  location.reload()
1971
2217
  /*
1972
2218
  let browserview = document.querySelector(".browserview")
@@ -2289,6 +2535,120 @@ document.addEventListener("DOMContentLoaded", () => {
2289
2535
 
2290
2536
  let createLauncherModalInstance = null;
2291
2537
  let createLauncherKeydownHandler = null;
2538
+ let createLauncherModalPromise = null;
2539
+
2540
+ const createLauncherFallbackTools = [
2541
+ {
2542
+ value: 'claude',
2543
+ label: 'Claude Code',
2544
+ iconSrc: '/asset/plugin/code/claude/claude.png',
2545
+ isDefault: true,
2546
+ href: '/run/plugin/code/claude/pinokio.js',
2547
+ category: 'CLI',
2548
+ },
2549
+ {
2550
+ value: 'codex',
2551
+ label: 'OpenAI Codex',
2552
+ iconSrc: '/asset/plugin/code/codex/openai.webp',
2553
+ isDefault: false,
2554
+ href: '/run/plugin/code/codex/pinokio.js',
2555
+ category: 'CLI',
2556
+ },
2557
+ {
2558
+ value: 'gemini',
2559
+ label: 'Google Gemini CLI',
2560
+ iconSrc: '/asset/plugin/code/gemini/gemini.jpeg',
2561
+ isDefault: false,
2562
+ href: '/run/plugin/code/gemini/pinokio.js',
2563
+ category: 'CLI',
2564
+ },
2565
+ ];
2566
+
2567
+ let cachedCreateLauncherTools = null;
2568
+ let loadingCreateLauncherTools = null;
2569
+
2570
+ function mapPluginMenuToCreateLauncherTools(menu) {
2571
+ if (!Array.isArray(menu)) return [];
2572
+
2573
+ return menu
2574
+ .map((plugin) => {
2575
+ if (!plugin || (!plugin.href && !plugin.link)) {
2576
+ return null;
2577
+ }
2578
+ const href = typeof plugin.href === 'string' ? plugin.href.trim() : '';
2579
+ const label = plugin.title || plugin.text || plugin.name || href || '';
2580
+
2581
+ let slug = '';
2582
+ if (href) {
2583
+ const segments = href.split('/').filter(Boolean);
2584
+ if (segments.length >= 2) {
2585
+ slug = segments[segments.length - 2] || '';
2586
+ }
2587
+ if (!slug && segments.length) {
2588
+ slug = segments[segments.length - 1] || '';
2589
+ }
2590
+ if (slug.endsWith('.js')) {
2591
+ slug = slug.replace(/\.js$/i, '');
2592
+ }
2593
+ }
2594
+ if (!slug && label) {
2595
+ slug = label
2596
+ .toLowerCase()
2597
+ .replace(/[^a-z0-9]+/g, '-')
2598
+ .replace(/^-+|-+$/g, '');
2599
+ }
2600
+ const value = slug || href || (typeof plugin.link === 'string' ? plugin.link.trim() : '');
2601
+ if (!value) {
2602
+ return null;
2603
+ }
2604
+ const iconSrc = plugin.image || null;
2605
+ const runs = Array.isArray(plugin.run) ? plugin.run : [];
2606
+ const hasExec = runs.some((step) => step && step.method === 'exec');
2607
+ const category = hasExec ? 'IDE' : 'CLI';
2608
+ return {
2609
+ value,
2610
+ label,
2611
+ iconSrc,
2612
+ isDefault: Boolean(plugin.default === true),
2613
+ href: href || null,
2614
+ category,
2615
+ };
2616
+ })
2617
+ .filter(Boolean);
2618
+ }
2619
+
2620
+ async function getCreateLauncherTools() {
2621
+ if (Array.isArray(cachedCreateLauncherTools) && cachedCreateLauncherTools.length > 0) {
2622
+ return cachedCreateLauncherTools;
2623
+ }
2624
+ if (loadingCreateLauncherTools) {
2625
+ return loadingCreateLauncherTools;
2626
+ }
2627
+
2628
+ loadingCreateLauncherTools = fetch('/api/plugin/menu')
2629
+ .then((res) => {
2630
+ if (!res.ok) {
2631
+ throw new Error(`Failed to load plugin menu: ${res.status}`);
2632
+ }
2633
+ return res.json();
2634
+ })
2635
+ .then((data) => {
2636
+ const menu = data && Array.isArray(data.menu) ? data.menu : [];
2637
+ const tools = mapPluginMenuToCreateLauncherTools(menu);
2638
+ return tools.length > 0 ? tools : createLauncherFallbackTools.slice();
2639
+ })
2640
+ .catch((error) => {
2641
+ console.warn('Falling back to default agents for create launcher modal', error);
2642
+ return createLauncherFallbackTools.slice();
2643
+ })
2644
+ .finally(() => {
2645
+ loadingCreateLauncherTools = null;
2646
+ });
2647
+
2648
+ const tools = await loadingCreateLauncherTools;
2649
+ cachedCreateLauncherTools = tools;
2650
+ return tools;
2651
+ }
2292
2652
 
2293
2653
  function initCreateLauncherFlow() {
2294
2654
  const trigger = document.getElementById('create-launcher-button');
@@ -2305,295 +2665,362 @@ document.addEventListener("DOMContentLoaded", () => {
2305
2665
  requestAnimationFrame(openPendingCreateLauncherModal);
2306
2666
  }
2307
2667
 
2308
- function ensureCreateLauncherModal() {
2668
+ async function ensureCreateLauncherModal() {
2309
2669
  if (createLauncherModalInstance) {
2310
2670
  return createLauncherModalInstance;
2311
2671
  }
2672
+ if (createLauncherModalPromise) {
2673
+ return createLauncherModalPromise;
2674
+ }
2312
2675
 
2313
- const overlay = document.createElement('div');
2314
- overlay.className = 'modal-overlay create-launcher-modal-overlay';
2315
-
2316
- const modal = document.createElement('div');
2317
- modal.className = 'create-launcher-modal';
2318
- modal.setAttribute('role', 'dialog');
2319
- modal.setAttribute('aria-modal', 'true');
2676
+ createLauncherModalPromise = (async () => {
2677
+ const tools = await getCreateLauncherTools();
2320
2678
 
2321
- const header = document.createElement('div');
2322
- header.className = 'create-launcher-modal-header';
2679
+ const overlay = document.createElement('div');
2680
+ overlay.className = 'modal-overlay create-launcher-modal-overlay';
2323
2681
 
2324
- const iconWrapper = document.createElement('div');
2325
- iconWrapper.className = 'create-launcher-modal-icon';
2682
+ const modal = document.createElement('div');
2683
+ modal.className = 'create-launcher-modal';
2684
+ modal.setAttribute('role', 'dialog');
2685
+ modal.setAttribute('aria-modal', 'true');
2326
2686
 
2327
- const headerIcon = document.createElement('i');
2328
- //headerIcon.className = 'fa-solid fa-magnifying-glass';
2329
- headerIcon.className = 'fa-solid fa-wand-magic-sparkles'
2330
- iconWrapper.appendChild(headerIcon);
2687
+ const header = document.createElement('div');
2688
+ header.className = 'create-launcher-modal-header';
2331
2689
 
2332
- const headingStack = document.createElement('div');
2333
- headingStack.className = 'create-launcher-modal-headings';
2690
+ const iconWrapper = document.createElement('div');
2691
+ iconWrapper.className = 'create-launcher-modal-icon';
2334
2692
 
2335
- const title = document.createElement('h3');
2336
- title.id = 'create-launcher-modal-title';
2337
- title.textContent = 'Create';
2693
+ const headerIcon = document.createElement('i');
2694
+ headerIcon.className = 'fa-solid fa-wand-magic-sparkles'
2695
+ iconWrapper.appendChild(headerIcon);
2338
2696
 
2339
- const description = document.createElement('p');
2340
- description.className = 'create-launcher-modal-description';
2341
- description.id = 'create-launcher-modal-description';
2342
- description.textContent = 'Create a reusable and shareable launcher for any task or any app'
2697
+ const headingStack = document.createElement('div');
2698
+ headingStack.className = 'create-launcher-modal-headings';
2343
2699
 
2344
- modal.setAttribute('aria-labelledby', title.id);
2345
- modal.setAttribute('aria-describedby', description.id);
2700
+ const title = document.createElement('h3');
2701
+ title.id = 'create-launcher-modal-title';
2702
+ title.textContent = 'Create';
2346
2703
 
2347
- headingStack.appendChild(title);
2348
- headingStack.appendChild(description);
2349
- header.appendChild(iconWrapper);
2350
- header.appendChild(headingStack);
2704
+ const description = document.createElement('p');
2705
+ description.className = 'create-launcher-modal-description';
2706
+ description.id = 'create-launcher-modal-description';
2707
+ description.textContent = 'Create a reusable and shareable launcher for any task or any app'
2351
2708
 
2352
- const promptLabel = document.createElement('label');
2353
- promptLabel.className = 'create-launcher-modal-label';
2354
- promptLabel.textContent = 'What do you want to do?';
2709
+ modal.setAttribute('aria-labelledby', title.id);
2710
+ modal.setAttribute('aria-describedby', description.id);
2355
2711
 
2356
- const promptTextarea = document.createElement('textarea');
2357
- promptTextarea.className = 'create-launcher-modal-textarea';
2358
- 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)';
2359
- promptLabel.appendChild(promptTextarea);
2712
+ headingStack.appendChild(title);
2713
+ headingStack.appendChild(description);
2714
+ header.appendChild(iconWrapper);
2715
+ header.appendChild(headingStack);
2360
2716
 
2361
- const templateWrapper = document.createElement('div');
2362
- templateWrapper.className = 'create-launcher-modal-template';
2363
- templateWrapper.style.display = 'none';
2717
+ const promptLabel = document.createElement('label');
2718
+ promptLabel.className = 'create-launcher-modal-label';
2719
+ promptLabel.textContent = 'What do you want to do?';
2364
2720
 
2365
- const templateTitle = document.createElement('div');
2366
- templateTitle.className = 'create-launcher-modal-template-title';
2367
- templateTitle.textContent = 'Template variables';
2721
+ const promptTextarea = document.createElement('textarea');
2722
+ promptTextarea.className = 'create-launcher-modal-textarea';
2723
+ 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)';
2724
+ promptLabel.appendChild(promptTextarea);
2368
2725
 
2369
- const templateDescription = document.createElement('p');
2370
- templateDescription.className = 'create-launcher-modal-template-description';
2371
- templateDescription.textContent = 'Fill in each variable below before creating your launcher.';
2726
+ const templateWrapper = document.createElement('div');
2727
+ templateWrapper.className = 'create-launcher-modal-template';
2728
+ templateWrapper.style.display = 'none';
2372
2729
 
2373
- const templateFields = document.createElement('div');
2374
- templateFields.className = 'create-launcher-modal-template-fields';
2730
+ const templateTitle = document.createElement('div');
2731
+ templateTitle.className = 'create-launcher-modal-template-title';
2732
+ templateTitle.textContent = 'Template variables';
2375
2733
 
2376
- templateWrapper.appendChild(templateTitle);
2377
- templateWrapper.appendChild(templateDescription);
2378
- templateWrapper.appendChild(templateFields);
2734
+ const templateDescription = document.createElement('p');
2735
+ templateDescription.className = 'create-launcher-modal-template-description';
2736
+ templateDescription.textContent = 'Fill in each variable below before creating your launcher.';
2379
2737
 
2380
- const folderLabel = document.createElement('label');
2381
- folderLabel.className = 'create-launcher-modal-label';
2382
- folderLabel.textContent = 'name';
2738
+ const templateFields = document.createElement('div');
2739
+ templateFields.className = 'create-launcher-modal-template-fields';
2383
2740
 
2384
- const folderInput = document.createElement('input');
2385
- folderInput.type = 'text';
2386
- folderInput.placeholder = 'example: my-launcher';
2387
- folderInput.className = 'create-launcher-modal-input';
2388
- folderLabel.appendChild(folderInput);
2741
+ templateWrapper.appendChild(templateTitle);
2742
+ templateWrapper.appendChild(templateDescription);
2743
+ templateWrapper.appendChild(templateFields);
2389
2744
 
2745
+ const folderLabel = document.createElement('label');
2746
+ folderLabel.className = 'create-launcher-modal-label';
2747
+ folderLabel.textContent = 'name';
2390
2748
 
2391
- const toolWrapper = document.createElement('div');
2392
- toolWrapper.className = 'create-launcher-modal-tools';
2749
+ const folderInput = document.createElement('input');
2750
+ folderInput.type = 'text';
2751
+ folderInput.placeholder = 'example: my-launcher';
2752
+ folderInput.className = 'create-launcher-modal-input';
2753
+ folderLabel.appendChild(folderInput);
2393
2754
 
2394
- const toolTitle = document.createElement('div');
2395
- toolTitle.className = 'create-launcher-modal-tools-title';
2396
- toolTitle.textContent = 'Choose AI tool';
2755
+ const toolWrapper = document.createElement('div');
2756
+ toolWrapper.className = 'create-launcher-modal-tools';
2397
2757
 
2398
- const toolOptions = document.createElement('div');
2399
- toolOptions.className = 'create-launcher-modal-tools-options';
2758
+ const toolTitle = document.createElement('div');
2759
+ toolTitle.className = 'create-launcher-modal-tools-title';
2760
+ toolTitle.textContent = 'Select Agent';
2400
2761
 
2401
- const tools = [
2402
- { value: 'claude', label: 'Claude Code', iconSrc: '/asset/plugin/code/claude/claude.png', defaultChecked: true },
2403
- { value: 'codex', label: 'OpenAI Codex', iconSrc: '/asset/plugin/code/codex/openai.webp', defaultChecked: false },
2404
- { value: 'gemini', label: 'Google Gemini CLI', iconSrc: '/asset/plugin/code/gemini/gemini.jpeg', defaultChecked: false }
2405
- ];
2762
+ const toolOptions = document.createElement('div');
2763
+ toolOptions.className = 'create-launcher-modal-tools-options';
2406
2764
 
2407
- const toolEntries = [];
2765
+ const toolEntries = [];
2766
+ const defaultToolIndex = tools.findIndex((tool) => tool.isDefault);
2767
+ const initialSelectionIndex = defaultToolIndex >= 0 ? defaultToolIndex : (tools.length > 0 ? 0 : -1);
2408
2768
 
2409
- tools.forEach(({ value, label, iconSrc, defaultChecked }) => {
2410
- const option = document.createElement('label');
2411
- option.className = 'create-launcher-modal-tool';
2769
+ const groupedTools = tools.reduce((acc, tool, index) => {
2770
+ const category = tool.category || 'CLI';
2771
+ if (!acc.has(category)) {
2772
+ acc.set(category, []);
2773
+ }
2774
+ acc.get(category).push({ tool, index });
2775
+ return acc;
2776
+ }, new Map());
2777
+
2778
+ const categoryOrder = ['CLI', 'IDE'];
2779
+ const orderedGroups = [];
2780
+ categoryOrder.forEach((cat) => {
2781
+ if (groupedTools.has(cat)) {
2782
+ orderedGroups.push([cat, groupedTools.get(cat)]);
2783
+ groupedTools.delete(cat);
2784
+ }
2785
+ });
2786
+ groupedTools.forEach((value, key) => {
2787
+ orderedGroups.push([key, value]);
2788
+ });
2412
2789
 
2413
- const radio = document.createElement('input');
2414
- radio.type = 'radio';
2415
- radio.name = 'create-launcher-tool';
2416
- radio.value = value;
2417
- if (defaultChecked) {
2418
- radio.checked = true;
2419
- }
2790
+ orderedGroups.forEach(([category, entries]) => {
2791
+ const group = document.createElement('div');
2792
+ group.className = 'create-launcher-modal-tools-group';
2420
2793
 
2421
- const badge = document.createElement('span');
2422
- badge.className = 'create-launcher-modal-tool-label';
2423
- badge.textContent = label;
2424
-
2425
- option.appendChild(radio);
2426
- if (iconSrc) {
2427
- const icon = document.createElement('img');
2428
- icon.className = 'create-launcher-modal-tool-icon';
2429
- icon.src = iconSrc;
2430
- icon.alt = `${label} icon`;
2431
- icon.onerror = () => { icon.style.display='none'; }
2432
- option.appendChild(icon);
2433
- }
2434
- option.appendChild(badge);
2435
- toolOptions.appendChild(option);
2436
- toolEntries.push({ input: radio, container: option });
2437
- radio.addEventListener('change', () => {
2438
- updateToolSelections(toolEntries);
2439
- });
2440
- });
2794
+ const heading = document.createElement('div');
2795
+ heading.className = 'create-launcher-modal-tools-group-title';
2796
+ heading.textContent = category;
2797
+ group.appendChild(heading);
2441
2798
 
2442
- toolWrapper.appendChild(toolTitle);
2443
- toolWrapper.appendChild(toolOptions);
2799
+ const groupList = document.createElement('div');
2800
+ groupList.className = 'create-launcher-modal-tools-group-options';
2444
2801
 
2445
- const error = document.createElement('div');
2446
- error.className = 'create-launcher-modal-error';
2802
+ const sortedEntries = entries.slice().sort((a, b) => {
2803
+ const nameA = (a.tool && a.tool.label ? a.tool.label : '').toLowerCase();
2804
+ const nameB = (b.tool && b.tool.label ? b.tool.label : '').toLowerCase();
2805
+ if (nameA < nameB) return -1;
2806
+ if (nameA > nameB) return 1;
2807
+ return 0;
2808
+ });
2447
2809
 
2448
- const actions = document.createElement('div');
2449
- actions.className = 'create-launcher-modal-actions';
2450
-
2451
- const cancelButton = document.createElement('button');
2452
- cancelButton.type = 'button';
2453
- cancelButton.className = 'create-launcher-modal-button cancel';
2454
- cancelButton.textContent = 'Cancel';
2455
-
2456
- const confirmButton = document.createElement('button');
2457
- confirmButton.type = 'button';
2458
- confirmButton.className = 'create-launcher-modal-button confirm';
2459
- confirmButton.textContent = 'Create';
2460
-
2461
- actions.appendChild(cancelButton);
2462
- actions.appendChild(confirmButton);
2463
-
2464
- const advancedLink = document.createElement('a');
2465
- advancedLink.className = 'create-launcher-modal-advanced';
2466
- advancedLink.href = '/init';
2467
- advancedLink.textContent = 'Or, try advanced options';
2468
-
2469
- const bookmarkletLink = document.createElement('a');
2470
- bookmarkletLink.className = 'create-launcher-modal-advanced secondary';
2471
- bookmarkletLink.href = '/bookmarklet';
2472
- bookmarkletLink.target = '_blank';
2473
- bookmarkletLink.setAttribute("features", "browser")
2474
- bookmarkletLink.rel = 'noopener';
2475
- bookmarkletLink.textContent = 'Add 1-click bookmarklet';
2476
-
2477
- const linkRow = document.createElement('div');
2478
- linkRow.className = 'create-launcher-modal-links';
2479
- linkRow.appendChild(advancedLink);
2480
- linkRow.appendChild(bookmarkletLink);
2481
-
2482
- modal.appendChild(header);
2483
- modal.appendChild(promptLabel);
2484
- modal.appendChild(templateWrapper);
2485
- modal.appendChild(folderLabel);
2486
- modal.appendChild(toolWrapper);
2487
- modal.appendChild(error);
2488
- modal.appendChild(actions);
2489
- modal.appendChild(linkRow);
2490
- overlay.appendChild(modal);
2491
- document.body.appendChild(overlay);
2810
+ sortedEntries.forEach(({ tool, index }) => {
2811
+ const option = document.createElement('label');
2812
+ option.className = 'create-launcher-modal-tool';
2813
+
2814
+ const radio = document.createElement('input');
2815
+ radio.type = 'radio';
2816
+ radio.name = 'create-launcher-tool';
2817
+ radio.value = tool.value;
2818
+ radio.dataset.agentLabel = tool.label;
2819
+ radio.dataset.agentCategory = category;
2820
+ if (tool.href) {
2821
+ radio.dataset.agentHref = tool.href;
2822
+ }
2492
2823
 
2493
- let folderEditedByUser = false;
2494
- let templateValues = new Map();
2824
+ if (index === initialSelectionIndex) {
2825
+ radio.checked = true;
2826
+ }
2495
2827
 
2496
- function syncTemplateFields(promptText, defaults = {}) {
2497
- const variableNames = extractTemplateVariableNames(promptText);
2498
- const previousValues = templateValues;
2499
- const newValues = new Map();
2828
+ const badge = document.createElement('span');
2829
+ badge.className = 'create-launcher-modal-tool-label';
2830
+ badge.textContent = tool.label;
2831
+
2832
+ option.appendChild(radio);
2833
+ if (tool.iconSrc) {
2834
+ const icon = document.createElement('img');
2835
+ icon.className = 'create-launcher-modal-tool-icon';
2836
+ icon.src = tool.iconSrc;
2837
+ icon.alt = `${tool.label} icon`;
2838
+ icon.onerror = () => { icon.style.display = 'none'; };
2839
+ option.appendChild(icon);
2840
+ }
2841
+ option.appendChild(badge);
2842
+ groupList.appendChild(option);
2843
+ const entry = { input: radio, container: option, meta: tool };
2844
+ toolEntries.push(entry);
2845
+ radio.addEventListener('change', () => {
2846
+ updateToolSelections(toolEntries);
2847
+ });
2848
+ });
2500
2849
 
2501
- variableNames.forEach((name) => {
2502
- if (Object.prototype.hasOwnProperty.call(defaults, name) && defaults[name] !== undefined) {
2503
- newValues.set(name, defaults[name]);
2504
- } else if (previousValues.has(name)) {
2505
- newValues.set(name, previousValues.get(name));
2506
- } else {
2507
- newValues.set(name, '');
2508
- }
2850
+ group.appendChild(groupList);
2851
+ toolOptions.appendChild(group);
2509
2852
  });
2510
2853
 
2511
- templateValues = newValues;
2512
- templateFields.innerHTML = '';
2513
-
2514
- if (variableNames.length === 0) {
2515
- templateWrapper.style.display = 'none';
2516
- return;
2854
+ if (!toolEntries.length) {
2855
+ const emptyState = document.createElement('div');
2856
+ emptyState.className = 'create-launcher-modal-tools-empty';
2857
+ emptyState.textContent = 'No agents available.';
2858
+ toolOptions.appendChild(emptyState);
2517
2859
  }
2518
2860
 
2519
- templateWrapper.style.display = 'flex';
2861
+ toolWrapper.appendChild(toolTitle);
2862
+ toolWrapper.appendChild(toolOptions);
2863
+
2864
+ const error = document.createElement('div');
2865
+ error.className = 'create-launcher-modal-error';
2866
+
2867
+ const actions = document.createElement('div');
2868
+ actions.className = 'create-launcher-modal-actions';
2869
+
2870
+ const cancelButton = document.createElement('button');
2871
+ cancelButton.type = 'button';
2872
+ cancelButton.className = 'create-launcher-modal-button cancel';
2873
+ cancelButton.textContent = 'Cancel';
2874
+
2875
+ const confirmButton = document.createElement('button');
2876
+ confirmButton.type = 'button';
2877
+ confirmButton.className = 'create-launcher-modal-button confirm';
2878
+ confirmButton.textContent = 'Create';
2879
+
2880
+ actions.appendChild(cancelButton);
2881
+ actions.appendChild(confirmButton);
2882
+
2883
+ const advancedLink = document.createElement('a');
2884
+ advancedLink.className = 'create-launcher-modal-advanced';
2885
+ advancedLink.href = '/init';
2886
+ advancedLink.textContent = 'Or, try advanced options';
2887
+
2888
+ const bookmarkletLink = document.createElement('a');
2889
+ bookmarkletLink.className = 'create-launcher-modal-advanced secondary';
2890
+ bookmarkletLink.href = '/bookmarklet';
2891
+ bookmarkletLink.target = '_blank';
2892
+ bookmarkletLink.setAttribute('features', 'browser');
2893
+ bookmarkletLink.rel = 'noopener';
2894
+ bookmarkletLink.textContent = 'Add 1-click bookmarklet';
2895
+
2896
+ const linkRow = document.createElement('div');
2897
+ linkRow.className = 'create-launcher-modal-links';
2898
+ linkRow.appendChild(advancedLink);
2899
+ linkRow.appendChild(bookmarkletLink);
2900
+
2901
+ modal.appendChild(header);
2902
+ modal.appendChild(promptLabel);
2903
+ modal.appendChild(templateWrapper);
2904
+ modal.appendChild(folderLabel);
2905
+ modal.appendChild(toolWrapper);
2906
+ modal.appendChild(error);
2907
+ modal.appendChild(actions);
2908
+ modal.appendChild(linkRow);
2909
+ overlay.appendChild(modal);
2910
+ document.body.appendChild(overlay);
2911
+
2912
+ let folderEditedByUser = false;
2913
+ let templateValues = new Map();
2914
+
2915
+ function syncTemplateFields(promptText, defaults = {}) {
2916
+ const variableNames = extractTemplateVariableNames(promptText);
2917
+ const previousValues = templateValues;
2918
+ const newValues = new Map();
2919
+
2920
+ variableNames.forEach((name) => {
2921
+ if (Object.prototype.hasOwnProperty.call(defaults, name) && defaults[name] !== undefined) {
2922
+ newValues.set(name, defaults[name]);
2923
+ } else if (previousValues.has(name)) {
2924
+ newValues.set(name, previousValues.get(name));
2925
+ } else {
2926
+ newValues.set(name, '');
2927
+ }
2928
+ });
2520
2929
 
2521
- variableNames.forEach((name) => {
2522
- const field = document.createElement('label');
2523
- field.className = 'create-launcher-modal-template-field';
2930
+ templateValues = newValues;
2931
+ templateFields.innerHTML = '';
2524
2932
 
2525
- const labelText = document.createElement('span');
2526
- labelText.className = 'create-launcher-modal-template-field-label';
2527
- labelText.textContent = name;
2933
+ if (variableNames.length === 0) {
2934
+ templateWrapper.style.display = 'none';
2935
+ return;
2936
+ }
2528
2937
 
2529
- const input = document.createElement('input');
2530
- input.type = 'text';
2531
- input.className = 'create-launcher-modal-template-input';
2532
- input.placeholder = `Enter ${name}`;
2533
- input.value = templateValues.get(name) || '';
2534
- input.dataset.templateInput = name;
2535
- input.addEventListener('input', () => {
2536
- templateValues.set(name, input.value);
2938
+ templateWrapper.style.display = 'flex';
2939
+
2940
+ variableNames.forEach((name) => {
2941
+ const field = document.createElement('label');
2942
+ field.className = 'create-launcher-modal-template-field';
2943
+
2944
+ const labelText = document.createElement('span');
2945
+ labelText.className = 'create-launcher-modal-template-field-label';
2946
+ labelText.textContent = name;
2947
+
2948
+ const input = document.createElement('input');
2949
+ input.type = 'text';
2950
+ input.className = 'create-launcher-modal-template-input';
2951
+ input.placeholder = `Enter ${name}`;
2952
+ input.value = templateValues.get(name) || '';
2953
+ input.dataset.templateInput = name;
2954
+ input.addEventListener('input', () => {
2955
+ templateValues.set(name, input.value);
2956
+ });
2957
+
2958
+ field.appendChild(labelText);
2959
+ field.appendChild(input);
2960
+ templateFields.appendChild(field);
2537
2961
  });
2962
+ }
2538
2963
 
2539
- field.appendChild(labelText);
2540
- field.appendChild(input);
2541
- templateFields.appendChild(field);
2964
+ folderInput.addEventListener('input', () => {
2965
+ folderEditedByUser = true;
2542
2966
  });
2543
- }
2544
2967
 
2545
- folderInput.addEventListener('input', () => {
2546
- folderEditedByUser = true;
2547
- });
2968
+ promptTextarea.addEventListener('input', () => {
2969
+ syncTemplateFields(promptTextarea.value);
2970
+ if (folderEditedByUser) return;
2971
+ folderInput.value = generateFolderSuggestion(promptTextarea.value);
2972
+ });
2548
2973
 
2549
- promptTextarea.addEventListener('input', () => {
2550
- syncTemplateFields(promptTextarea.value);
2551
- if (folderEditedByUser) return;
2552
- folderInput.value = generateFolderSuggestion(promptTextarea.value);
2553
- });
2974
+ cancelButton.addEventListener('click', hideCreateLauncherModal);
2975
+ confirmButton.addEventListener('click', submitCreateLauncherModal);
2976
+ overlay.addEventListener('click', (event) => {
2977
+ if (event.target === overlay) {
2978
+ hideCreateLauncherModal();
2979
+ }
2980
+ });
2554
2981
 
2555
- cancelButton.addEventListener('click', hideCreateLauncherModal);
2556
- confirmButton.addEventListener('click', submitCreateLauncherModal);
2557
- overlay.addEventListener('click', (event) => {
2558
- if (event.target === overlay) {
2982
+ advancedLink.addEventListener('click', () => {
2559
2983
  hideCreateLauncherModal();
2560
- }
2561
- });
2984
+ });
2562
2985
 
2563
- advancedLink.addEventListener('click', () => {
2564
- hideCreateLauncherModal();
2565
- });
2986
+ bookmarkletLink.addEventListener('click', () => {
2987
+ hideCreateLauncherModal();
2988
+ });
2566
2989
 
2567
- bookmarkletLink.addEventListener('click', () => {
2568
- hideCreateLauncherModal();
2569
- });
2990
+ createLauncherModalInstance = {
2991
+ overlay,
2992
+ modal,
2993
+ folderInput,
2994
+ promptTextarea,
2995
+ cancelButton,
2996
+ confirmButton,
2997
+ error,
2998
+ toolEntries,
2999
+ toolOptions,
3000
+ toolWrapper,
3001
+ resetFolderTracking() {
3002
+ folderEditedByUser = false;
3003
+ },
3004
+ syncTemplateFields,
3005
+ getTemplateValues() {
3006
+ return new Map(templateValues);
3007
+ },
3008
+ templateFields,
3009
+ markFolderEdited() {
3010
+ folderEditedByUser = true;
3011
+ }
3012
+ };
2570
3013
 
2571
- createLauncherModalInstance = {
2572
- overlay,
2573
- modal,
2574
- folderInput,
2575
- promptTextarea,
2576
- cancelButton,
2577
- confirmButton,
2578
- error,
2579
- toolEntries,
2580
- // description,
2581
- resetFolderTracking() {
2582
- folderEditedByUser = false;
2583
- },
2584
- syncTemplateFields,
2585
- getTemplateValues() {
2586
- return new Map(templateValues);
2587
- },
2588
- templateFields,
2589
- markFolderEdited() {
2590
- folderEditedByUser = true;
2591
- }
2592
- };
3014
+ updateToolSelections(toolEntries);
2593
3015
 
2594
- updateToolSelections(toolEntries);
3016
+ return createLauncherModalInstance;
3017
+ })();
2595
3018
 
2596
- return createLauncherModalInstance;
3019
+ try {
3020
+ return await createLauncherModalPromise;
3021
+ } finally {
3022
+ createLauncherModalPromise = null;
3023
+ }
2597
3024
  }
2598
3025
 
2599
3026
  async function showCreateLauncherModal(defaults = {}) {
@@ -2607,7 +3034,7 @@ document.addEventListener("DOMContentLoaded", () => {
2607
3034
  return
2608
3035
  }
2609
3036
 
2610
- const modal = ensureCreateLauncherModal();
3037
+ const modal = await ensureCreateLauncherModal();
2611
3038
 
2612
3039
  modal.error.textContent = '';
2613
3040
  modal.resetFolderTracking();
@@ -2626,8 +3053,10 @@ document.addEventListener("DOMContentLoaded", () => {
2626
3053
  }
2627
3054
 
2628
3055
  const matchingToolEntry = modal.toolEntries.find((entry) => entry.input.value === tool);
3056
+ const defaultToolEntryIndex = modal.toolEntries.findIndex((entry) => entry.meta && entry.meta.isDefault);
3057
+ const fallbackToolIndex = defaultToolEntryIndex >= 0 ? defaultToolEntryIndex : 0;
2629
3058
  modal.toolEntries.forEach((entry, index) => {
2630
- entry.input.checked = matchingToolEntry ? entry === matchingToolEntry : index === 0;
3059
+ entry.input.checked = matchingToolEntry ? entry === matchingToolEntry : index === fallbackToolIndex;
2631
3060
  });
2632
3061
  updateToolSelections(modal.toolEntries);
2633
3062
 
@@ -2663,14 +3092,22 @@ document.addEventListener("DOMContentLoaded", () => {
2663
3092
  }
2664
3093
  }
2665
3094
 
2666
- function submitCreateLauncherModal() {
2667
- const modal = ensureCreateLauncherModal();
3095
+ async function submitCreateLauncherModal() {
3096
+ const modal = await ensureCreateLauncherModal();
2668
3097
  modal.error.textContent = '';
2669
3098
 
2670
3099
  const folderName = modal.folderInput.value.trim();
2671
3100
  const rawPrompt = modal.promptTextarea.value;
2672
3101
  const templateValues = modal.getTemplateValues ? modal.getTemplateValues() : new Map();
2673
- const selectedTool = modal.toolEntries.find((entry) => entry.input.checked)?.input.value || 'claude';
3102
+ const selectedEntry = modal.toolEntries.find((entry) => entry.input.checked);
3103
+ const defaultToolEntryIndex = modal.toolEntries.findIndex((entry) => entry.meta && entry.meta.isDefault);
3104
+ const fallbackEntry = defaultToolEntryIndex >= 0 ? modal.toolEntries[defaultToolEntryIndex] : modal.toolEntries[0];
3105
+ const selectedTool = (selectedEntry || fallbackEntry)?.input.value || '';
3106
+
3107
+ if (!selectedTool) {
3108
+ modal.error.textContent = 'Please select an agent.';
3109
+ return;
3110
+ }
2674
3111
 
2675
3112
  if (!folderName) {
2676
3113
  modal.error.textContent = 'Please enter a folder name.';