Mini agent extension config (#2209)
Co-authored-by: Zaki Ali <zaki@squareup.com>
This commit is contained in:
@@ -7,9 +7,10 @@ import Back from './icons/Back';
|
|||||||
import { Bars } from './icons/Bars';
|
import { Bars } from './icons/Bars';
|
||||||
import { Geese } from './icons/Geese';
|
import { Geese } from './icons/Geese';
|
||||||
import Copy from './icons/Copy';
|
import Copy from './icons/Copy';
|
||||||
import Check from './icons/Check';
|
import { useConfig } from './ConfigContext';
|
||||||
import { useConfig } from '../components/ConfigContext';
|
import { FixedExtensionEntry } from './ConfigContext';
|
||||||
import { settingsV2Enabled } from '../flags';
|
import ExtensionList from './settings_v2/extensions/subcomponents/ExtensionList';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
interface RecipeEditorProps {
|
interface RecipeEditorProps {
|
||||||
config?: Recipe;
|
config?: Recipe;
|
||||||
@@ -28,47 +29,83 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
const [description, setDescription] = useState(config?.description || '');
|
const [description, setDescription] = useState(config?.description || '');
|
||||||
const [instructions, setInstructions] = useState(config?.instructions || '');
|
const [instructions, setInstructions] = useState(config?.instructions || '');
|
||||||
const [activities, setActivities] = useState<string[]>(config?.activities || []);
|
const [activities, setActivities] = useState<string[]>(config?.activities || []);
|
||||||
const [availableExtensions, setAvailableExtensions] = useState<FullExtensionConfig[]>([]);
|
const [extensionOptions, setExtensionOptions] = useState<FixedExtensionEntry[]>([]);
|
||||||
const [selectedExtensions, setSelectedExtensions] = useState<string[]>(
|
|
||||||
config?.extensions?.map((e) => e.id) || []
|
|
||||||
);
|
|
||||||
const [newActivity, setNewActivity] = useState('');
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [extensionsLoaded, setExtensionsLoaded] = useState(false);
|
||||||
|
|
||||||
|
// Initialize selected extensions for the recipe from config or localStorage
|
||||||
|
const [recipeExtensions, setRecipeExtensions] = useState<string[]>(() => {
|
||||||
|
// First try to get from localStorage
|
||||||
|
const stored = localStorage.getItem('recipe_editor_extensions');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse localStorage recipe extensions:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to config if available, using extension names
|
||||||
|
const exts = [];
|
||||||
|
return exts;
|
||||||
|
});
|
||||||
|
const [newActivity, setNewActivity] = useState('');
|
||||||
|
|
||||||
// Section visibility state
|
// Section visibility state
|
||||||
const [activeSection, setActiveSection] = useState<
|
const [activeSection, setActiveSection] = useState<
|
||||||
'none' | 'activities' | 'instructions' | 'extensions'
|
'none' | 'activities' | 'instructions' | 'extensions'
|
||||||
>('none');
|
>('none');
|
||||||
|
|
||||||
// Load extensions
|
// Load extensions when component mounts and when switching to extensions section
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadExtensions = async () => {
|
if (activeSection === 'extensions' && !extensionsLoaded) {
|
||||||
if (settingsV2Enabled) {
|
const loadExtensions = async () => {
|
||||||
try {
|
try {
|
||||||
const extensions = await getExtensions(false); // force refresh to get latest
|
const extensions = await getExtensions(false); // force refresh to get latest
|
||||||
console.log('extensions {}', extensions);
|
console.log('Loading extensions for recipe editor');
|
||||||
setAvailableExtensions(extensions || []);
|
|
||||||
|
if (extensions && extensions.length > 0) {
|
||||||
|
// Map the extensions with the current selection state from recipeExtensions
|
||||||
|
const initializedExtensions = extensions.map((ext) => ({
|
||||||
|
...ext,
|
||||||
|
enabled: recipeExtensions.includes(ext.name),
|
||||||
|
}));
|
||||||
|
|
||||||
|
setExtensionOptions(initializedExtensions);
|
||||||
|
setExtensionsLoaded(true);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load extensions:', error);
|
console.error('Failed to load extensions:', error);
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
const userSettingsStr = localStorage.getItem('user_settings');
|
loadExtensions();
|
||||||
if (userSettingsStr) {
|
}
|
||||||
const userSettings = JSON.parse(userSettingsStr);
|
}, [activeSection, getExtensions, recipeExtensions, extensionsLoaded]);
|
||||||
setAvailableExtensions(userSettings.extensions || []);
|
|
||||||
}
|
// Effect for updating extension options when recipeExtensions change
|
||||||
}
|
useEffect(() => {
|
||||||
};
|
if (extensionsLoaded && extensionOptions.length > 0) {
|
||||||
loadExtensions();
|
const updatedOptions = extensionOptions.map((ext) => ({
|
||||||
// Intentionally omitting getExtensions from deps to avoid refresh loops
|
...ext,
|
||||||
// eslint-disable-next-line
|
enabled: recipeExtensions.includes(ext.name),
|
||||||
}, []);
|
}));
|
||||||
|
setExtensionOptions(updatedOptions);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [recipeExtensions, extensionsLoaded]);
|
||||||
|
|
||||||
|
const handleExtensionToggle = (extension: FixedExtensionEntry) => {
|
||||||
|
console.log('Toggling extension:', extension.name);
|
||||||
|
setRecipeExtensions((prev) => {
|
||||||
|
const isSelected = prev.includes(extension.name);
|
||||||
|
const newState = isSelected
|
||||||
|
? prev.filter((extName) => extName !== extension.name)
|
||||||
|
: [...prev, extension.name];
|
||||||
|
|
||||||
|
// Persist to localStorage
|
||||||
|
localStorage.setItem('recipe_editor_extensions', JSON.stringify(newState));
|
||||||
|
|
||||||
const handleExtensionToggle = (id: string) => {
|
|
||||||
console.log('Toggling extension:', id);
|
|
||||||
setSelectedExtensions((prev) => {
|
|
||||||
const isSelected = prev.includes(id);
|
|
||||||
const newState = isSelected ? prev.filter((extId) => extId !== id) : [...prev, id];
|
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -86,8 +123,8 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
|
|
||||||
const getCurrentConfig = (): Recipe => {
|
const getCurrentConfig = (): Recipe => {
|
||||||
console.log('Creating config with:', {
|
console.log('Creating config with:', {
|
||||||
selectedExtensions,
|
selectedExtensions: recipeExtensions,
|
||||||
availableExtensions,
|
availableExtensions: extensionOptions,
|
||||||
recipeConfig,
|
recipeConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,9 +134,9 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
description,
|
description,
|
||||||
instructions,
|
instructions,
|
||||||
activities,
|
activities,
|
||||||
extensions: selectedExtensions
|
extensions: recipeExtensions
|
||||||
.map((name) => {
|
.map((name) => {
|
||||||
const extension = availableExtensions.find((e) => e.name === name);
|
const extension = extensionOptions.find((e) => e.name === name);
|
||||||
console.log('Looking for extension:', name, 'Found:', extension);
|
console.log('Looking for extension:', name, 'Found:', extension);
|
||||||
if (!extension) return null;
|
if (!extension) return null;
|
||||||
|
|
||||||
@@ -139,6 +176,8 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
const handleOpenAgent = () => {
|
const handleOpenAgent = () => {
|
||||||
if (validateForm()) {
|
if (validateForm()) {
|
||||||
const updatedConfig = getCurrentConfig();
|
const updatedConfig = getCurrentConfig();
|
||||||
|
// Clear stored extensions when submitting
|
||||||
|
localStorage.removeItem('recipe_editor_extensions');
|
||||||
window.electron.createChatWindow(
|
window.electron.createChatWindow(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -166,6 +205,13 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset extensionsLoaded when section changes away from extensions
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeSection !== 'extensions') {
|
||||||
|
setExtensionsLoaded(false);
|
||||||
|
}
|
||||||
|
}, [activeSection]);
|
||||||
|
|
||||||
// Render expanded section content
|
// Render expanded section content
|
||||||
const renderSectionContent = () => {
|
const renderSectionContent = () => {
|
||||||
switch (activeSection) {
|
switch (activeSection) {
|
||||||
@@ -258,49 +304,23 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mb-8 mt-6">
|
<div className="mb-8 mt-6">
|
||||||
<h2 className="text-2xl font-medium mb-2 text-textProminent">Extensions</h2>
|
<h2 className="text-2xl font-medium mb-2 text-textProminent">Extensions</h2>
|
||||||
<p className="text-textSubtle">
|
<p className="text-textSubtle">Select extensions to bundle in the recipe</p>
|
||||||
Choose which extensions will be available to your agent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{availableExtensions.map((extension) => (
|
|
||||||
<button
|
|
||||||
key={extension.name}
|
|
||||||
className="p-4 border border-borderSubtle rounded-lg flex justify-between items-center w-full text-left hover:bg-bgSubtle bg-bgApp"
|
|
||||||
onClick={() => handleExtensionToggle(extension.name)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-textProminent">{extension.name}</h3>
|
|
||||||
<p className="text-sm text-textSubtle">
|
|
||||||
{extension.description || 'No description available'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="relative inline-block w-10 align-middle select-none">
|
|
||||||
<div
|
|
||||||
className={`w-10 h-6 rounded-full transition-colors duration-200 ease-in-out ${
|
|
||||||
selectedExtensions.includes(extension.name)
|
|
||||||
? 'bg-bgAppInverse'
|
|
||||||
: 'bg-borderSubtle'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-6 h-6 rounded-full bg-bgApp border-2 transform transition-transform duration-200 ease-in-out ${
|
|
||||||
selectedExtensions.includes(extension.name)
|
|
||||||
? 'translate-x-4 border-bgAppInverse'
|
|
||||||
: 'translate-x-0 border-borderSubtle'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
{extensionsLoaded ? (
|
||||||
|
<ExtensionList
|
||||||
|
extensions={extensionOptions}
|
||||||
|
onToggle={handleExtensionToggle}
|
||||||
|
isStatic={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-textSubtle">Loading extensions...</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-2 py-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-medium mb-2 text-textProminent">Agent</h2>
|
<h2 className="text-lg font-medium mb-2 text-textProminent">Agent</h2>
|
||||||
<input
|
<input
|
||||||
@@ -315,7 +335,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
className={`w-full p-3 border rounded-lg bg-bgApp text-textStandard ${
|
className={`w-full p-3 border rounded-lg bg-bgApp text-textStandard ${
|
||||||
errors.title ? 'border-red-500' : 'border-borderSubtle'
|
errors.title ? 'border-red-500' : 'border-borderSubtle'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Agent Name (required)"
|
placeholder="Agent Recipe Name (required)"
|
||||||
/>
|
/>
|
||||||
{errors.title && <div className="text-red-500 text-sm mt-1">{errors.title}</div>}
|
{errors.title && <div className="text-red-500 text-sm mt-1">{errors.title}</div>}
|
||||||
</div>
|
</div>
|
||||||
@@ -348,7 +368,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="font-medium text-textProminent">Activities</h3>
|
<h3 className="font-medium text-textProminent">Activities</h3>
|
||||||
<p className="text-textSubtle text-sm">
|
<p className="text-textSubtle text-sm">
|
||||||
Starting activities present in the home panel on a fresh goose session
|
Starting activities present in the home panel on a fresh session
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
|
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
|
||||||
@@ -360,9 +380,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
>
|
>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="font-medium text-textProminent">Instructions</h3>
|
<h3 className="font-medium text-textProminent">Instructions</h3>
|
||||||
<p className="text-textSubtle text-sm">
|
<p className="text-textSubtle text-sm">Recipe instructions sent to the model</p>
|
||||||
Starting activities present in the home panel on a fresh goose session
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
|
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
|
||||||
</button>
|
</button>
|
||||||
@@ -374,7 +392,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="font-medium text-textProminent">Extensions</h3>
|
<h3 className="font-medium text-textProminent">Extensions</h3>
|
||||||
<p className="text-textSubtle text-sm">
|
<p className="text-textSubtle text-sm">
|
||||||
Starting activities present in the home panel on a fresh goose session
|
Extensions to be enabled by default with this recipe
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
|
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
|
||||||
@@ -397,7 +415,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-col space-y-2 pt-4">
|
<div className="flex flex-col space-y-2 pt-1">
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenAgent}
|
onClick={handleOpenAgent}
|
||||||
className="w-full p-3 bg-bgAppInverse text-textProminentInverse rounded-lg hover:bg-bgStandardInverse disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full p-3 bg-bgAppInverse text-textProminentInverse rounded-lg hover:bg-bgStandardInverse disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
@@ -406,7 +424,10 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
Open agent
|
Open agent
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.close()}
|
onClick={() => {
|
||||||
|
localStorage.removeItem('recipe_editor_extensions');
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
className="w-full p-3 text-textSubtle rounded-lg hover:bg-bgSubtle"
|
className="w-full p-3 text-textSubtle rounded-lg hover:bg-bgSubtle"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -425,10 +446,11 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
|||||||
<Geese className="w-12 h-12 text-iconProminent" />
|
<Geese className="w-12 h-12 text-iconProminent" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-medium text-center text-textProminent">
|
<h1 className="text-2xl font-medium text-center text-textProminent">
|
||||||
Create custom agent
|
Create an agent recipe
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-textSubtle text-center mt-2 text-sm">
|
<p className="text-textSubtle text-center mt-2 text-sm">
|
||||||
Your custom agent can be shared with others
|
Your custom agent recipe can be shared with others. Fill in the sections below to
|
||||||
|
create!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
import BackButton from '../ui/BackButton';
|
import BackButton from '../ui/BackButton';
|
||||||
import type { View } from '../../App';
|
import type { View } from '../../App';
|
||||||
|
|||||||
@@ -19,9 +19,22 @@ import { ExtensionConfig } from '../../../api/types.gen';
|
|||||||
interface ExtensionSectionProps {
|
interface ExtensionSectionProps {
|
||||||
deepLinkConfig?: ExtensionConfig;
|
deepLinkConfig?: ExtensionConfig;
|
||||||
showEnvVars?: boolean;
|
showEnvVars?: boolean;
|
||||||
|
hideButtons?: boolean;
|
||||||
|
hideHeader?: boolean;
|
||||||
|
disableConfiguration?: boolean;
|
||||||
|
customToggle?: (extension: FixedExtensionEntry) => Promise<boolean | void>;
|
||||||
|
selectedExtensions?: string[]; // Add controlled state
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionsSection({ deepLinkConfig, showEnvVars }: ExtensionSectionProps) {
|
export default function ExtensionsSection({
|
||||||
|
deepLinkConfig,
|
||||||
|
showEnvVars,
|
||||||
|
hideButtons,
|
||||||
|
hideHeader,
|
||||||
|
disableConfiguration,
|
||||||
|
customToggle,
|
||||||
|
selectedExtensions = [],
|
||||||
|
}: ExtensionSectionProps) {
|
||||||
const { getExtensions, addExtension, removeExtension } = useConfig();
|
const { getExtensions, addExtension, removeExtension } = useConfig();
|
||||||
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
|
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
|
||||||
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
||||||
@@ -37,22 +50,35 @@ export default function ExtensionsSection({ deepLinkConfig, showEnvVars }: Exten
|
|||||||
const fetchExtensions = useCallback(async () => {
|
const fetchExtensions = useCallback(async () => {
|
||||||
const extensionsList = await getExtensions(true); // Force refresh
|
const extensionsList = await getExtensions(true); // Force refresh
|
||||||
// Sort extensions by name to maintain consistent order
|
// Sort extensions by name to maintain consistent order
|
||||||
const sortedExtensions = [...extensionsList].sort((a, b) => {
|
const sortedExtensions = [...extensionsList]
|
||||||
// First sort by builtin
|
.sort((a, b) => {
|
||||||
if (a.type === 'builtin' && b.type !== 'builtin') return -1;
|
// First sort by builtin
|
||||||
if (a.type !== 'builtin' && b.type === 'builtin') return 1;
|
if (a.type === 'builtin' && b.type !== 'builtin') return -1;
|
||||||
|
if (a.type !== 'builtin' && b.type === 'builtin') return 1;
|
||||||
|
|
||||||
// Then sort by bundled (handle null/undefined cases)
|
// Then sort by bundled (handle null/undefined cases)
|
||||||
const aBundled = a.bundled === true;
|
const aBundled = a.bundled === true;
|
||||||
const bBundled = b.bundled === true;
|
const bBundled = b.bundled === true;
|
||||||
if (aBundled && !bBundled) return -1;
|
if (aBundled && !bBundled) return -1;
|
||||||
if (!aBundled && bBundled) return 1;
|
if (!aBundled && bBundled) return 1;
|
||||||
|
|
||||||
// Finally sort alphabetically within each group
|
// Finally sort alphabetically within each group
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
})
|
||||||
|
.map((ext) => ({
|
||||||
|
...ext,
|
||||||
|
// Use selectedExtensions to determine enabled state in recipe editor
|
||||||
|
enabled: disableConfiguration ? selectedExtensions.includes(ext.name) : ext.enabled,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Setting extensions with selectedExtensions:',
|
||||||
|
selectedExtensions,
|
||||||
|
'Extensions:',
|
||||||
|
sortedExtensions
|
||||||
|
);
|
||||||
setExtensions(sortedExtensions);
|
setExtensions(sortedExtensions);
|
||||||
}, [getExtensions]);
|
}, [getExtensions, disableConfiguration, selectedExtensions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchExtensions();
|
fetchExtensions();
|
||||||
@@ -60,6 +86,17 @@ export default function ExtensionsSection({ deepLinkConfig, showEnvVars }: Exten
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleExtensionToggle = async (extension: FixedExtensionEntry) => {
|
const handleExtensionToggle = async (extension: FixedExtensionEntry) => {
|
||||||
|
if (customToggle) {
|
||||||
|
await customToggle(extension);
|
||||||
|
// After custom toggle, update the local state to reflect the change
|
||||||
|
setExtensions((prevExtensions) =>
|
||||||
|
prevExtensions.map((ext) =>
|
||||||
|
ext.name === extension.name ? { ...ext, enabled: !ext.enabled } : ext
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// If extension is enabled, we are trying to toggle if off, otherwise on
|
// If extension is enabled, we are trying to toggle if off, otherwise on
|
||||||
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
|
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
|
||||||
const extensionConfig = extractExtensionConfig(extension);
|
const extensionConfig = extractExtensionConfig(extension);
|
||||||
@@ -149,37 +186,46 @@ export default function ExtensionsSection({ deepLinkConfig, showEnvVars }: Exten
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="extensions" className="px-8">
|
<section id="extensions" className="px-8">
|
||||||
<div className="flex justify-between items-center mb-2">
|
{!hideHeader && (
|
||||||
<h2 className="text-xl font-medium text-textStandard">Extensions</h2>
|
<>
|
||||||
</div>
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="border-b border-borderSubtle pb-8">
|
<h2 className="text-xl font-medium text-textStandard">Extensions</h2>
|
||||||
<p className="text-sm text-textStandard mb-6">
|
</div>
|
||||||
These extensions use the Model Context Protocol (MCP). They can expand Goose's
|
<div className="border-b border-borderSubtle">
|
||||||
capabilities using three main components: Prompts, Resources, and Tools.
|
<p className="text-sm text-textStandard mb-6">
|
||||||
</p>
|
These extensions use the Model Context Protocol (MCP). They can expand Goose's
|
||||||
|
capabilities using three main components: Prompts, Resources, and Tools.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`${!hideHeader ? '' : 'border-b border-borderSubtle'} pb-8`}>
|
||||||
<ExtensionList
|
<ExtensionList
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
onToggle={handleExtensionToggle}
|
onToggle={handleExtensionToggle}
|
||||||
onConfigure={handleConfigureClick}
|
onConfigure={handleConfigureClick}
|
||||||
|
disableConfiguration={disableConfiguration}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-4 pt-4 w-full">
|
{!hideButtons && (
|
||||||
<Button
|
<div className="flex gap-4 pt-4 w-full">
|
||||||
className="flex items-center gap-2 justify-center text-white dark:text-black bg-bgAppInverse hover:bg-bgStandardInverse [&>svg]:!size-4"
|
<Button
|
||||||
onClick={() => setIsAddModalOpen(true)}
|
className="flex items-center gap-2 justify-center text-white dark:text-black bg-bgAppInverse hover:bg-bgStandardInverse [&>svg]:!size-4"
|
||||||
>
|
onClick={() => setIsAddModalOpen(true)}
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
Add custom extension
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
Add custom extension
|
||||||
<Button
|
</Button>
|
||||||
className="flex items-center gap-2 justify-center text-textStandard bg-bgApp border border-borderSubtle hover:border-borderProminent hover:bg-bgApp [&>svg]:!size-4"
|
<Button
|
||||||
onClick={() => window.open('https://block.github.io/goose/v1/extensions/', '_blank')}
|
className="flex items-center gap-2 justify-center text-textStandard bg-bgApp border border-borderSubtle hover:border-borderProminent hover:bg-bgApp [&>svg]:!size-4"
|
||||||
>
|
onClick={() => window.open('https://block.github.io/goose/v1/extensions/', '_blank')}
|
||||||
<GPSIcon size={12} />
|
>
|
||||||
Browse extensions
|
<GPSIcon size={12} />
|
||||||
</Button>
|
Browse extensions
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Modal for updating an existing extension */}
|
{/* Modal for updating an existing extension */}
|
||||||
{isModalOpen && selectedExtension && (
|
{isModalOpen && selectedExtension && (
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ import { getSubtitle, getFriendlyTitle } from './ExtensionList';
|
|||||||
|
|
||||||
interface ExtensionItemProps {
|
interface ExtensionItemProps {
|
||||||
extension: FixedExtensionEntry;
|
extension: FixedExtensionEntry;
|
||||||
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void>;
|
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
|
||||||
onConfigure: (extension: FixedExtensionEntry) => void;
|
onConfigure?: (extension: FixedExtensionEntry) => void;
|
||||||
|
isStatic?: boolean; // to not allow users to edit configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionItem({ extension, onToggle, onConfigure }: ExtensionItemProps) {
|
export default function ExtensionItem({
|
||||||
|
extension,
|
||||||
|
onToggle,
|
||||||
|
onConfigure,
|
||||||
|
isStatic,
|
||||||
|
}: ExtensionItemProps) {
|
||||||
// Add local state to track the visual toggle state
|
// Add local state to track the visual toggle state
|
||||||
const [visuallyEnabled, setVisuallyEnabled] = useState(extension.enabled);
|
const [visuallyEnabled, setVisuallyEnabled] = useState(extension.enabled);
|
||||||
// Track if we're in the process of toggling
|
// Track if we're in the process of toggling
|
||||||
@@ -59,7 +65,9 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
|
|||||||
|
|
||||||
// Bundled extensions and builtins are not editable
|
// Bundled extensions and builtins are not editable
|
||||||
// Over time we can take the first part of the conditional away as people have bundled: true in their config.yaml entries
|
// Over time we can take the first part of the conditional away as people have bundled: true in their config.yaml entries
|
||||||
const editable = !(extension.type === 'builtin' || extension.bundled);
|
|
||||||
|
// allow configuration editing if extension is not a builtin/bundled extension AND isStatic = false
|
||||||
|
const editable = !(extension.type === 'builtin' || extension.bundled) && !isStatic;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -78,7 +86,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
|
|||||||
{editable && (
|
{editable && (
|
||||||
<button
|
<button
|
||||||
className="text-textSubtle hover:text-textStandard"
|
className="text-textSubtle hover:text-textStandard"
|
||||||
onClick={() => onConfigure(extension)}
|
onClick={() => (onConfigure ? onConfigure(extension) : () => {})}
|
||||||
>
|
>
|
||||||
<Gear className="h-4 w-4" />
|
<Gear className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ import { combineCmdAndArgs, removeShims } from '../utils';
|
|||||||
|
|
||||||
interface ExtensionListProps {
|
interface ExtensionListProps {
|
||||||
extensions: FixedExtensionEntry[];
|
extensions: FixedExtensionEntry[];
|
||||||
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void>;
|
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
|
||||||
onConfigure: (extension: FixedExtensionEntry) => void;
|
onConfigure?: (extension: FixedExtensionEntry) => void;
|
||||||
|
isStatic?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionList({ extensions, onToggle, onConfigure }: ExtensionListProps) {
|
export default function ExtensionList({
|
||||||
|
extensions,
|
||||||
|
onToggle,
|
||||||
|
onConfigure,
|
||||||
|
isStatic,
|
||||||
|
}: ExtensionListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||||
{extensions.map((extension) => (
|
{extensions.map((extension) => (
|
||||||
@@ -20,6 +26,7 @@ export default function ExtensionList({ extensions, onToggle, onConfigure }: Ext
|
|||||||
extension={extension}
|
extension={extension}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
onConfigure={onConfigure}
|
onConfigure={onConfigure}
|
||||||
|
isStatic={isStatic}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ export default function ProviderConfigurationModal() {
|
|||||||
|
|
||||||
// go through the keys are remove them
|
// go through the keys are remove them
|
||||||
for (const param of params) {
|
for (const param of params) {
|
||||||
console.log('param', param.name, 'secret', param.secret);
|
|
||||||
await remove(param.name, param.secret);
|
await remove(param.name, param.secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user