chore: update extensions section to work with new endpoints (#1696)

This commit is contained in:
Lily Delalande
2025-03-14 10:07:41 -07:00
committed by GitHub
parent 92a9ee2026
commit 805e359acf
2 changed files with 137 additions and 129 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"license": { "license": {
"name": "Apache-2.0" "name": "Apache-2.0"
}, },
"version": "1.0.13" "version": "1.0.14"
}, },
"paths": { "paths": {
"/config": { "/config": {
@@ -4,65 +4,29 @@ import { Switch } from '../../ui/switch';
import { Plus, X } from 'lucide-react'; import { Plus, X } from 'lucide-react';
import { Gear } from '../../icons/Gear'; import { Gear } from '../../icons/Gear';
import { GPSIcon } from '../../ui/icons'; import { GPSIcon } from '../../ui/icons';
import { useConfig } from '../../ConfigContext'; import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
import Modal from '../../Modal'; import Modal from '../../Modal';
import { Input } from '../../ui/input'; import { Input } from '../../ui/input';
import Select from 'react-select'; import Select from 'react-select';
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles'; import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
import { ExtensionConfig } from '../../../api/types.gen';
interface ExtensionConfig {
args?: string[];
cmd?: string;
enabled: boolean;
envs?: Record<string, string>;
name: string;
type: 'stdio' | 'sse' | 'builtin';
}
interface ExtensionItem {
id: string;
title: string;
subtitle: string;
enabled: boolean;
canConfigure: boolean;
config: ExtensionConfig;
}
interface EnvVar {
key: string;
value: string;
}
// Helper function to get a friendly title from extension name
const getFriendlyTitle = (name: string): string => {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Helper function to get a subtitle based on extension type and configuration
const getSubtitle = (config: ExtensionConfig): string => {
if (config.type === 'builtin') {
return 'Built-in extension';
}
return `${config.type.toUpperCase()} extension${config.cmd ? ` (${config.cmd})` : ''}`;
};
export default function ExtensionsSection() { export default function ExtensionsSection() {
const { config, read, updateExtension, addExtension } = useConfig(); const { toggleExtension, getExtensions, addExtension } = useConfig();
const [extensions, setExtensions] = useState<ExtensionItem[]>([]); const [loading, setLoading] = useState<boolean>(true);
const [selectedExtension, setSelectedExtension] = useState<ExtensionItem | null>(null); const [error, setError] = useState<string | null>(null);
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [formData, setFormData] = useState<{ const [formData, setFormData] = useState<{
name: string; name: string;
type: 'stdio' | 'sse'; type: 'stdio' | 'sse' | 'builtin';
cmd?: string; cmd?: string;
args?: string[]; args?: string[];
endpoint?: string; endpoint?: string;
enabled: boolean; enabled: boolean;
envVars: EnvVar[]; envVars: { key: string; value: string }[];
}>({ }>({
name: '', name: '',
type: 'stdio', type: 'stdio',
@@ -73,63 +37,82 @@ export default function ExtensionsSection() {
envVars: [], envVars: [],
}); });
useEffect(() => { // Helper function to get a friendly title from extension name
const extensions = read('extensions', false); const getFriendlyTitle = (name: string): string => {
if (extensions) { return name
const extensionItems: ExtensionItem[] = Object.entries(extensions).map(([name, ext]) => { .split('-')
const extensionConfig = ext as ExtensionConfig; .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
return { .join(' ');
id: name, };
title: getFriendlyTitle(name),
subtitle: getSubtitle(extensionConfig), // Helper function to get a subtitle based on extension type and configuration
enabled: extensionConfig.enabled, const getSubtitle = (config: ExtensionConfig): string => {
canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs, if (config.type === 'builtin') {
config: extensionConfig, return 'Built-in extension';
};
});
setExtensions(extensionItems);
} }
}, [read]); if (config.type === 'stdio') {
return `STDIO extension${config.cmd ? ` (${config.cmd})` : ''}`;
}
if (config.type === 'sse') {
return `SSE extension${config.uri ? ` (${config.uri})` : ''}`;
}
return `Unknown type of extension`;
};
const fetchExtensions = async () => {
setLoading(true);
try {
const extensionsList = await getExtensions(true); // Force refresh
// Sort extensions by name to maintain consistent order
const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name));
setExtensions(sortedExtensions);
setError(null);
} catch (err) {
setError('Failed to load extensions');
console.error('Error loading extensions:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchExtensions();
}, []);
useEffect(() => { useEffect(() => {
if (selectedExtension) { if (selectedExtension) {
const envVars = selectedExtension.config.envs // Type guard: Check if 'envs' property exists for this variant
? Object.entries(selectedExtension.config.envs).map(([key, value]) => ({ const hasEnvs = selectedExtension.type === 'sse' || selectedExtension.type === 'stdio';
key,
value: value as string, const envVars =
})) hasEnvs && selectedExtension.envs
: []; ? Object.entries(selectedExtension.envs).map(([key, value]) => ({
key,
value: value as string,
}))
: [];
setFormData({ setFormData({
name: selectedExtension.config.name, name: selectedExtension.name,
type: selectedExtension.config.type as 'stdio' | 'sse', type: selectedExtension.type,
cmd: selectedExtension.config.type === 'stdio' ? selectedExtension.config.cmd : undefined, cmd: selectedExtension.type === 'stdio' ? selectedExtension.cmd : undefined,
args: selectedExtension.config.args || [], args: selectedExtension.type === 'stdio' ? selectedExtension.args : [],
endpoint: endpoint: selectedExtension.type === 'sse' ? selectedExtension.uri : undefined,
selectedExtension.config.type === 'sse' ? selectedExtension.config.cmd : undefined, enabled: selectedExtension.enabled,
enabled: selectedExtension.config.enabled,
envVars, envVars,
}); });
} }
}, [selectedExtension]); }, [selectedExtension]);
const handleExtensionToggle = async (id: string) => { const handleExtensionToggle = async (name: string) => {
const extension = extensions.find((ext) => ext.id === id); try {
if (extension) { await toggleExtension(name);
const updatedConfig = { fetchExtensions(); // Refresh the list after toggling
...extension.config, } catch (error) {
enabled: !extension.config.enabled, console.error('Failed to toggle extension:', error);
};
try {
await updateExtension(id, updatedConfig);
} catch (error) {
console.error('Failed to update extension:', error);
}
} }
}; };
const handleConfigureClick = (extension: ExtensionItem) => { const handleConfigureClick = (extension: FixedExtensionEntry) => {
setSelectedExtension(extension); setSelectedExtension(extension);
setIsModalOpen(true); setIsModalOpen(true);
}; };
@@ -145,24 +128,35 @@ export default function ExtensionsSection() {
{} as Record<string, string> {} as Record<string, string>
); );
const extensionConfig = { let extensionConfig: ExtensionConfig;
name: formData.name,
type: formData.type, if (formData.type === 'stdio') {
enabled: formData.enabled, extensionConfig = {
envs, type: 'stdio',
...(formData.type === 'stdio' name: formData.name,
? { cmd: formData.cmd,
cmd: formData.cmd, args: formData.args,
args: formData.args, ...(Object.keys(envs).length > 0 ? { envs } : {}),
} };
: { } else if (formData.type === 'sse') {
cmd: formData.endpoint, extensionConfig = {
}), type: 'sse',
}; name: formData.name,
uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type
...(Object.keys(envs).length > 0 ? { envs } : {}),
};
} else {
// For other types
extensionConfig = {
type: formData.type,
name: formData.name,
};
}
try { try {
await addExtension(formData.name, extensionConfig); await addExtension(formData.name, extensionConfig, formData.enabled);
handleModalClose(); handleModalClose();
fetchExtensions(); // Refresh the list after adding
} catch (error) { } catch (error) {
console.error('Failed to add extension:', error); console.error('Failed to add extension:', error);
} }
@@ -207,7 +201,6 @@ export default function ExtensionsSection() {
envVars: newEnvVars, envVars: newEnvVars,
}); });
}; };
const handleSaveConfig = async () => { const handleSaveConfig = async () => {
if (!selectedExtension) return; if (!selectedExtension) return;
@@ -221,24 +214,36 @@ export default function ExtensionsSection() {
{} as Record<string, string> {} as Record<string, string>
); );
const updatedConfig = { let extensionConfig: ExtensionConfig;
name: formData.name,
type: formData.type, if (formData.type === 'stdio') {
enabled: formData.enabled, extensionConfig = {
envs, type: 'stdio',
...(formData.type === 'stdio' name: formData.name,
? { cmd: formData.cmd,
cmd: formData.cmd, args: formData.args,
args: formData.args, ...(Object.keys(envs).length > 0 ? { envs } : {}),
} };
: { } else if (formData.type === 'sse') {
cmd: formData.endpoint, extensionConfig = {
}), type: 'sse',
}; name: formData.name,
uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type
...(Object.keys(envs).length > 0 ? { envs } : {}),
};
} else {
// For other types
extensionConfig = {
type: formData.type,
name: formData.name,
};
}
try { try {
await updateExtension(selectedExtension.id, updatedConfig); // CHANGE: Use addExtension instead of updateExtension
await addExtension(formData.name, extensionConfig, formData.enabled);
handleModalClose(); handleModalClose();
fetchExtensions(); // Refresh the list after updating
} catch (error) { } catch (error) {
console.error('Failed to update extension configuration:', error); console.error('Failed to update extension configuration:', error);
} }
@@ -256,14 +261,17 @@ export default function ExtensionsSection() {
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
{extensions.map((extension, index) => ( {extensions.map((extension, index) => (
<React.Fragment key={extension.id}> <React.Fragment key={extension.name}>
<div className="flex items-center justify-between py-3"> <div className="flex items-center justify-between py-3">
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-medium text-textStandard">{extension.title}</h3> <h3 className="font-medium text-textStandard">
<p className="text-sm text-textSubtle">{extension.subtitle}</p> {getFriendlyTitle(extension.name)}
</h3>
<p className="text-sm text-textSubtle">{getSubtitle(extension)}</p>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{extension.canConfigure && ( {/* Only show config button for non-builtin extensions */}
{extension.type !== 'builtin' && (
<button <button
className="text-textSubtle hover:text-textStandard" className="text-textSubtle hover:text-textStandard"
onClick={() => handleConfigureClick(extension)} onClick={() => handleConfigureClick(extension)}
@@ -273,8 +281,8 @@ export default function ExtensionsSection() {
)} )}
<Switch <Switch
checked={extension.enabled} checked={extension.enabled}
onCheckedChange={() => handleExtensionToggle(extension.id)} onCheckedChange={() => handleExtensionToggle(extension.name)}
className="bg-[#393838] [&_span[data-state]]:bg-white" variant="mono"
/> />
</div> </div>
</div> </div>
@@ -329,10 +337,10 @@ export default function ExtensionsSection() {
<label className="text-sm font-medium mb-2 block">Type</label> <label className="text-sm font-medium mb-2 block">Type</label>
<Select <Select
value={{ value: formData.type, label: formData.type.toUpperCase() }} value={{ value: formData.type, label: formData.type.toUpperCase() }}
onChange={(option) => onChange={(option: { value: string; label: string } | null) =>
setFormData({ setFormData({
...formData, ...formData,
type: (option?.value as 'stdio' | 'sse') || 'stdio', type: (option?.value as 'stdio' | 'sse' | 'builtin') || 'stdio',
}) })
} }
options={[ options={[
@@ -464,7 +472,7 @@ export default function ExtensionsSection() {
<label className="text-sm font-medium mb-2 block">Type</label> <label className="text-sm font-medium mb-2 block">Type</label>
<Select <Select
value={{ value: formData.type, label: formData.type.toUpperCase() }} value={{ value: formData.type, label: formData.type.toUpperCase() }}
onChange={(option) => onChange={(option: { value: string; label: string } | null) =>
setFormData({ setFormData({
...formData, ...formData,
type: (option?.value as 'stdio' | 'sse') || 'stdio', type: (option?.value as 'stdio' | 'sse') || 'stdio',