pinokiod 3.181.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 (46) hide show
  1. package/kernel/util.js +15 -2
  2. package/package.json +1 -1
  3. package/server/index.js +111 -12
  4. package/server/public/common.js +433 -240
  5. package/server/public/files-app/app.css +64 -0
  6. package/server/public/files-app/app.js +87 -0
  7. package/server/public/install.js +8 -1
  8. package/server/public/layout.js +11 -1
  9. package/server/public/sound/beep.mp3 +0 -0
  10. package/server/public/sound/bell.mp3 +0 -0
  11. package/server/public/sound/bright-ring.mp3 +0 -0
  12. package/server/public/sound/clap.mp3 +0 -0
  13. package/server/public/sound/deep-ring.mp3 +0 -0
  14. package/server/public/sound/gasp.mp3 +0 -0
  15. package/server/public/sound/hehe.mp3 +0 -0
  16. package/server/public/sound/levelup.mp3 +0 -0
  17. package/server/public/sound/light-pop.mp3 +0 -0
  18. package/server/public/sound/light-ring.mp3 +0 -0
  19. package/server/public/sound/meow.mp3 +0 -0
  20. package/server/public/sound/piano.mp3 +0 -0
  21. package/server/public/sound/pop.mp3 +0 -0
  22. package/server/public/sound/uhoh.mp3 +0 -0
  23. package/server/public/sound/whistle.mp3 +0 -0
  24. package/server/public/style.css +173 -2
  25. package/server/public/tab-idle-notifier.js +697 -4
  26. package/server/public/terminal-settings.js +1131 -0
  27. package/server/public/urldropdown.css +28 -1
  28. package/server/views/{terminals.ejs → agents.ejs} +97 -30
  29. package/server/views/app.ejs +112 -65
  30. package/server/views/bootstrap.ejs +8 -0
  31. package/server/views/connect.ejs +1 -1
  32. package/server/views/d.ejs +172 -18
  33. package/server/views/editor.ejs +8 -0
  34. package/server/views/file_browser.ejs +4 -0
  35. package/server/views/index.ejs +1 -1
  36. package/server/views/init/index.ejs +9 -1
  37. package/server/views/install.ejs +8 -0
  38. package/server/views/net.ejs +1 -1
  39. package/server/views/network.ejs +1 -1
  40. package/server/views/pro.ejs +8 -0
  41. package/server/views/prototype/index.ejs +8 -0
  42. package/server/views/screenshots.ejs +1 -2
  43. package/server/views/settings.ejs +1 -2
  44. package/server/views/shell.ejs +8 -0
  45. package/server/views/terminal.ejs +8 -0
  46. package/server/views/tools.ejs +1 -2
@@ -1720,8 +1720,10 @@ if (typeof hotkeys === 'function') {
1720
1720
  }
1721
1721
  };
1722
1722
 
1723
+ const isFalseyString = (value) => typeof value === 'string' && ['false', '0', 'no', 'off'].includes(value.trim().toLowerCase());
1724
+
1723
1725
  const enqueueSound = (url) => {
1724
- if (!url) {
1726
+ if (!url || url === false || isFalseyString(url)) {
1725
1727
  return;
1726
1728
  }
1727
1729
  pendingSounds.push(url);
@@ -2533,6 +2535,120 @@ document.addEventListener("DOMContentLoaded", () => {
2533
2535
 
2534
2536
  let createLauncherModalInstance = null;
2535
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
+ }
2536
2652
 
2537
2653
  function initCreateLauncherFlow() {
2538
2654
  const trigger = document.getElementById('create-launcher-button');
@@ -2549,295 +2665,362 @@ document.addEventListener("DOMContentLoaded", () => {
2549
2665
  requestAnimationFrame(openPendingCreateLauncherModal);
2550
2666
  }
2551
2667
 
2552
- function ensureCreateLauncherModal() {
2668
+ async function ensureCreateLauncherModal() {
2553
2669
  if (createLauncherModalInstance) {
2554
2670
  return createLauncherModalInstance;
2555
2671
  }
2672
+ if (createLauncherModalPromise) {
2673
+ return createLauncherModalPromise;
2674
+ }
2556
2675
 
2557
- const overlay = document.createElement('div');
2558
- overlay.className = 'modal-overlay create-launcher-modal-overlay';
2676
+ createLauncherModalPromise = (async () => {
2677
+ const tools = await getCreateLauncherTools();
2559
2678
 
2560
- const modal = document.createElement('div');
2561
- modal.className = 'create-launcher-modal';
2562
- modal.setAttribute('role', 'dialog');
2563
- modal.setAttribute('aria-modal', 'true');
2679
+ const overlay = document.createElement('div');
2680
+ overlay.className = 'modal-overlay create-launcher-modal-overlay';
2564
2681
 
2565
- const header = document.createElement('div');
2566
- header.className = 'create-launcher-modal-header';
2682
+ const modal = document.createElement('div');
2683
+ modal.className = 'create-launcher-modal';
2684
+ modal.setAttribute('role', 'dialog');
2685
+ modal.setAttribute('aria-modal', 'true');
2567
2686
 
2568
- const iconWrapper = document.createElement('div');
2569
- iconWrapper.className = 'create-launcher-modal-icon';
2687
+ const header = document.createElement('div');
2688
+ header.className = 'create-launcher-modal-header';
2570
2689
 
2571
- const headerIcon = document.createElement('i');
2572
- //headerIcon.className = 'fa-solid fa-magnifying-glass';
2573
- headerIcon.className = 'fa-solid fa-wand-magic-sparkles'
2574
- iconWrapper.appendChild(headerIcon);
2690
+ const iconWrapper = document.createElement('div');
2691
+ iconWrapper.className = 'create-launcher-modal-icon';
2575
2692
 
2576
- const headingStack = document.createElement('div');
2577
- headingStack.className = 'create-launcher-modal-headings';
2578
-
2579
- const title = document.createElement('h3');
2580
- title.id = 'create-launcher-modal-title';
2581
- title.textContent = 'Create';
2693
+ const headerIcon = document.createElement('i');
2694
+ headerIcon.className = 'fa-solid fa-wand-magic-sparkles'
2695
+ iconWrapper.appendChild(headerIcon);
2582
2696
 
2583
- const description = document.createElement('p');
2584
- description.className = 'create-launcher-modal-description';
2585
- description.id = 'create-launcher-modal-description';
2586
- 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';
2587
2699
 
2588
- modal.setAttribute('aria-labelledby', title.id);
2589
- modal.setAttribute('aria-describedby', description.id);
2700
+ const title = document.createElement('h3');
2701
+ title.id = 'create-launcher-modal-title';
2702
+ title.textContent = 'Create';
2590
2703
 
2591
- headingStack.appendChild(title);
2592
- headingStack.appendChild(description);
2593
- header.appendChild(iconWrapper);
2594
- 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'
2595
2708
 
2596
- const promptLabel = document.createElement('label');
2597
- promptLabel.className = 'create-launcher-modal-label';
2598
- promptLabel.textContent = 'What do you want to do?';
2709
+ modal.setAttribute('aria-labelledby', title.id);
2710
+ modal.setAttribute('aria-describedby', description.id);
2599
2711
 
2600
- const promptTextarea = document.createElement('textarea');
2601
- promptTextarea.className = 'create-launcher-modal-textarea';
2602
- 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)';
2603
- promptLabel.appendChild(promptTextarea);
2712
+ headingStack.appendChild(title);
2713
+ headingStack.appendChild(description);
2714
+ header.appendChild(iconWrapper);
2715
+ header.appendChild(headingStack);
2604
2716
 
2605
- const templateWrapper = document.createElement('div');
2606
- templateWrapper.className = 'create-launcher-modal-template';
2607
- 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?';
2608
2720
 
2609
- const templateTitle = document.createElement('div');
2610
- templateTitle.className = 'create-launcher-modal-template-title';
2611
- 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);
2612
2725
 
2613
- const templateDescription = document.createElement('p');
2614
- templateDescription.className = 'create-launcher-modal-template-description';
2615
- 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';
2616
2729
 
2617
- const templateFields = document.createElement('div');
2618
- 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';
2619
2733
 
2620
- templateWrapper.appendChild(templateTitle);
2621
- templateWrapper.appendChild(templateDescription);
2622
- 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.';
2623
2737
 
2624
- const folderLabel = document.createElement('label');
2625
- folderLabel.className = 'create-launcher-modal-label';
2626
- folderLabel.textContent = 'name';
2738
+ const templateFields = document.createElement('div');
2739
+ templateFields.className = 'create-launcher-modal-template-fields';
2627
2740
 
2628
- const folderInput = document.createElement('input');
2629
- folderInput.type = 'text';
2630
- folderInput.placeholder = 'example: my-launcher';
2631
- folderInput.className = 'create-launcher-modal-input';
2632
- folderLabel.appendChild(folderInput);
2741
+ templateWrapper.appendChild(templateTitle);
2742
+ templateWrapper.appendChild(templateDescription);
2743
+ templateWrapper.appendChild(templateFields);
2633
2744
 
2745
+ const folderLabel = document.createElement('label');
2746
+ folderLabel.className = 'create-launcher-modal-label';
2747
+ folderLabel.textContent = 'name';
2634
2748
 
2635
- const toolWrapper = document.createElement('div');
2636
- 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);
2637
2754
 
2638
- const toolTitle = document.createElement('div');
2639
- toolTitle.className = 'create-launcher-modal-tools-title';
2640
- toolTitle.textContent = 'Choose AI tool';
2755
+ const toolWrapper = document.createElement('div');
2756
+ toolWrapper.className = 'create-launcher-modal-tools';
2641
2757
 
2642
- const toolOptions = document.createElement('div');
2643
- 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';
2644
2761
 
2645
- const tools = [
2646
- { value: 'claude', label: 'Claude Code', iconSrc: '/asset/plugin/code/claude/claude.png', defaultChecked: true },
2647
- { value: 'codex', label: 'OpenAI Codex', iconSrc: '/asset/plugin/code/codex/openai.webp', defaultChecked: false },
2648
- { value: 'gemini', label: 'Google Gemini CLI', iconSrc: '/asset/plugin/code/gemini/gemini.jpeg', defaultChecked: false }
2649
- ];
2762
+ const toolOptions = document.createElement('div');
2763
+ toolOptions.className = 'create-launcher-modal-tools-options';
2650
2764
 
2651
- const toolEntries = [];
2765
+ const toolEntries = [];
2766
+ const defaultToolIndex = tools.findIndex((tool) => tool.isDefault);
2767
+ const initialSelectionIndex = defaultToolIndex >= 0 ? defaultToolIndex : (tools.length > 0 ? 0 : -1);
2652
2768
 
2653
- tools.forEach(({ value, label, iconSrc, defaultChecked }) => {
2654
- const option = document.createElement('label');
2655
- 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
+ });
2656
2789
 
2657
- const radio = document.createElement('input');
2658
- radio.type = 'radio';
2659
- radio.name = 'create-launcher-tool';
2660
- radio.value = value;
2661
- if (defaultChecked) {
2662
- radio.checked = true;
2663
- }
2790
+ orderedGroups.forEach(([category, entries]) => {
2791
+ const group = document.createElement('div');
2792
+ group.className = 'create-launcher-modal-tools-group';
2664
2793
 
2665
- const badge = document.createElement('span');
2666
- badge.className = 'create-launcher-modal-tool-label';
2667
- badge.textContent = label;
2668
-
2669
- option.appendChild(radio);
2670
- if (iconSrc) {
2671
- const icon = document.createElement('img');
2672
- icon.className = 'create-launcher-modal-tool-icon';
2673
- icon.src = iconSrc;
2674
- icon.alt = `${label} icon`;
2675
- icon.onerror = () => { icon.style.display='none'; }
2676
- option.appendChild(icon);
2677
- }
2678
- option.appendChild(badge);
2679
- toolOptions.appendChild(option);
2680
- toolEntries.push({ input: radio, container: option });
2681
- radio.addEventListener('change', () => {
2682
- updateToolSelections(toolEntries);
2683
- });
2684
- });
2794
+ const heading = document.createElement('div');
2795
+ heading.className = 'create-launcher-modal-tools-group-title';
2796
+ heading.textContent = category;
2797
+ group.appendChild(heading);
2685
2798
 
2686
- toolWrapper.appendChild(toolTitle);
2687
- toolWrapper.appendChild(toolOptions);
2799
+ const groupList = document.createElement('div');
2800
+ groupList.className = 'create-launcher-modal-tools-group-options';
2688
2801
 
2689
- const error = document.createElement('div');
2690
- 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
+ });
2691
2809
 
2692
- const actions = document.createElement('div');
2693
- actions.className = 'create-launcher-modal-actions';
2694
-
2695
- const cancelButton = document.createElement('button');
2696
- cancelButton.type = 'button';
2697
- cancelButton.className = 'create-launcher-modal-button cancel';
2698
- cancelButton.textContent = 'Cancel';
2699
-
2700
- const confirmButton = document.createElement('button');
2701
- confirmButton.type = 'button';
2702
- confirmButton.className = 'create-launcher-modal-button confirm';
2703
- confirmButton.textContent = 'Create';
2704
-
2705
- actions.appendChild(cancelButton);
2706
- actions.appendChild(confirmButton);
2707
-
2708
- const advancedLink = document.createElement('a');
2709
- advancedLink.className = 'create-launcher-modal-advanced';
2710
- advancedLink.href = '/init';
2711
- advancedLink.textContent = 'Or, try advanced options';
2712
-
2713
- const bookmarkletLink = document.createElement('a');
2714
- bookmarkletLink.className = 'create-launcher-modal-advanced secondary';
2715
- bookmarkletLink.href = '/bookmarklet';
2716
- bookmarkletLink.target = '_blank';
2717
- bookmarkletLink.setAttribute("features", "browser")
2718
- bookmarkletLink.rel = 'noopener';
2719
- bookmarkletLink.textContent = 'Add 1-click bookmarklet';
2720
-
2721
- const linkRow = document.createElement('div');
2722
- linkRow.className = 'create-launcher-modal-links';
2723
- linkRow.appendChild(advancedLink);
2724
- linkRow.appendChild(bookmarkletLink);
2725
-
2726
- modal.appendChild(header);
2727
- modal.appendChild(promptLabel);
2728
- modal.appendChild(templateWrapper);
2729
- modal.appendChild(folderLabel);
2730
- modal.appendChild(toolWrapper);
2731
- modal.appendChild(error);
2732
- modal.appendChild(actions);
2733
- modal.appendChild(linkRow);
2734
- overlay.appendChild(modal);
2735
- 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
+ }
2736
2823
 
2737
- let folderEditedByUser = false;
2738
- let templateValues = new Map();
2824
+ if (index === initialSelectionIndex) {
2825
+ radio.checked = true;
2826
+ }
2739
2827
 
2740
- function syncTemplateFields(promptText, defaults = {}) {
2741
- const variableNames = extractTemplateVariableNames(promptText);
2742
- const previousValues = templateValues;
2743
- 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
+ });
2744
2849
 
2745
- variableNames.forEach((name) => {
2746
- if (Object.prototype.hasOwnProperty.call(defaults, name) && defaults[name] !== undefined) {
2747
- newValues.set(name, defaults[name]);
2748
- } else if (previousValues.has(name)) {
2749
- newValues.set(name, previousValues.get(name));
2750
- } else {
2751
- newValues.set(name, '');
2752
- }
2850
+ group.appendChild(groupList);
2851
+ toolOptions.appendChild(group);
2753
2852
  });
2754
2853
 
2755
- templateValues = newValues;
2756
- templateFields.innerHTML = '';
2757
-
2758
- if (variableNames.length === 0) {
2759
- templateWrapper.style.display = 'none';
2760
- 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);
2761
2859
  }
2762
2860
 
2763
- 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
+ });
2764
2929
 
2765
- variableNames.forEach((name) => {
2766
- const field = document.createElement('label');
2767
- field.className = 'create-launcher-modal-template-field';
2930
+ templateValues = newValues;
2931
+ templateFields.innerHTML = '';
2768
2932
 
2769
- const labelText = document.createElement('span');
2770
- labelText.className = 'create-launcher-modal-template-field-label';
2771
- labelText.textContent = name;
2933
+ if (variableNames.length === 0) {
2934
+ templateWrapper.style.display = 'none';
2935
+ return;
2936
+ }
2772
2937
 
2773
- const input = document.createElement('input');
2774
- input.type = 'text';
2775
- input.className = 'create-launcher-modal-template-input';
2776
- input.placeholder = `Enter ${name}`;
2777
- input.value = templateValues.get(name) || '';
2778
- input.dataset.templateInput = name;
2779
- input.addEventListener('input', () => {
2780
- 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);
2781
2961
  });
2962
+ }
2782
2963
 
2783
- field.appendChild(labelText);
2784
- field.appendChild(input);
2785
- templateFields.appendChild(field);
2964
+ folderInput.addEventListener('input', () => {
2965
+ folderEditedByUser = true;
2786
2966
  });
2787
- }
2788
2967
 
2789
- folderInput.addEventListener('input', () => {
2790
- folderEditedByUser = true;
2791
- });
2968
+ promptTextarea.addEventListener('input', () => {
2969
+ syncTemplateFields(promptTextarea.value);
2970
+ if (folderEditedByUser) return;
2971
+ folderInput.value = generateFolderSuggestion(promptTextarea.value);
2972
+ });
2792
2973
 
2793
- promptTextarea.addEventListener('input', () => {
2794
- syncTemplateFields(promptTextarea.value);
2795
- if (folderEditedByUser) return;
2796
- folderInput.value = generateFolderSuggestion(promptTextarea.value);
2797
- });
2974
+ cancelButton.addEventListener('click', hideCreateLauncherModal);
2975
+ confirmButton.addEventListener('click', submitCreateLauncherModal);
2976
+ overlay.addEventListener('click', (event) => {
2977
+ if (event.target === overlay) {
2978
+ hideCreateLauncherModal();
2979
+ }
2980
+ });
2798
2981
 
2799
- cancelButton.addEventListener('click', hideCreateLauncherModal);
2800
- confirmButton.addEventListener('click', submitCreateLauncherModal);
2801
- overlay.addEventListener('click', (event) => {
2802
- if (event.target === overlay) {
2982
+ advancedLink.addEventListener('click', () => {
2803
2983
  hideCreateLauncherModal();
2804
- }
2805
- });
2984
+ });
2806
2985
 
2807
- advancedLink.addEventListener('click', () => {
2808
- hideCreateLauncherModal();
2809
- });
2986
+ bookmarkletLink.addEventListener('click', () => {
2987
+ hideCreateLauncherModal();
2988
+ });
2810
2989
 
2811
- bookmarkletLink.addEventListener('click', () => {
2812
- hideCreateLauncherModal();
2813
- });
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
+ };
2814
3013
 
2815
- createLauncherModalInstance = {
2816
- overlay,
2817
- modal,
2818
- folderInput,
2819
- promptTextarea,
2820
- cancelButton,
2821
- confirmButton,
2822
- error,
2823
- toolEntries,
2824
- // description,
2825
- resetFolderTracking() {
2826
- folderEditedByUser = false;
2827
- },
2828
- syncTemplateFields,
2829
- getTemplateValues() {
2830
- return new Map(templateValues);
2831
- },
2832
- templateFields,
2833
- markFolderEdited() {
2834
- folderEditedByUser = true;
2835
- }
2836
- };
3014
+ updateToolSelections(toolEntries);
2837
3015
 
2838
- updateToolSelections(toolEntries);
3016
+ return createLauncherModalInstance;
3017
+ })();
2839
3018
 
2840
- return createLauncherModalInstance;
3019
+ try {
3020
+ return await createLauncherModalPromise;
3021
+ } finally {
3022
+ createLauncherModalPromise = null;
3023
+ }
2841
3024
  }
2842
3025
 
2843
3026
  async function showCreateLauncherModal(defaults = {}) {
@@ -2851,7 +3034,7 @@ document.addEventListener("DOMContentLoaded", () => {
2851
3034
  return
2852
3035
  }
2853
3036
 
2854
- const modal = ensureCreateLauncherModal();
3037
+ const modal = await ensureCreateLauncherModal();
2855
3038
 
2856
3039
  modal.error.textContent = '';
2857
3040
  modal.resetFolderTracking();
@@ -2870,8 +3053,10 @@ document.addEventListener("DOMContentLoaded", () => {
2870
3053
  }
2871
3054
 
2872
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;
2873
3058
  modal.toolEntries.forEach((entry, index) => {
2874
- entry.input.checked = matchingToolEntry ? entry === matchingToolEntry : index === 0;
3059
+ entry.input.checked = matchingToolEntry ? entry === matchingToolEntry : index === fallbackToolIndex;
2875
3060
  });
2876
3061
  updateToolSelections(modal.toolEntries);
2877
3062
 
@@ -2907,14 +3092,22 @@ document.addEventListener("DOMContentLoaded", () => {
2907
3092
  }
2908
3093
  }
2909
3094
 
2910
- function submitCreateLauncherModal() {
2911
- const modal = ensureCreateLauncherModal();
3095
+ async function submitCreateLauncherModal() {
3096
+ const modal = await ensureCreateLauncherModal();
2912
3097
  modal.error.textContent = '';
2913
3098
 
2914
3099
  const folderName = modal.folderInput.value.trim();
2915
3100
  const rawPrompt = modal.promptTextarea.value;
2916
3101
  const templateValues = modal.getTemplateValues ? modal.getTemplateValues() : new Map();
2917
- 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
+ }
2918
3111
 
2919
3112
  if (!folderName) {
2920
3113
  modal.error.textContent = 'Please enter a folder name.';