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

View File

@@ -10,7 +10,7 @@
"license": {
"name": "Apache-2.0"
},
"version": "1.0.13"
"version": "1.0.14"
},
"paths": {
"/config": {

View File

@@ -4,65 +4,29 @@ import { Switch } from '../../ui/switch';
import { Plus, X } from 'lucide-react';
import { Gear } from '../../icons/Gear';
import { GPSIcon } from '../../ui/icons';
import { useConfig } from '../../ConfigContext';
import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
import Modal from '../../Modal';
import { Input } from '../../ui/input';
import Select from 'react-select';
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
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})` : ''}`;
};
import { ExtensionConfig } from '../../../api/types.gen';
export default function ExtensionsSection() {
const { config, read, updateExtension, addExtension } = useConfig();
const [extensions, setExtensions] = useState<ExtensionItem[]>([]);
const [selectedExtension, setSelectedExtension] = useState<ExtensionItem | null>(null);
const { toggleExtension, getExtensions, addExtension } = useConfig();
const [loading, setLoading] = useState<boolean>(true);
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 [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [formData, setFormData] = useState<{
name: string;
type: 'stdio' | 'sse';
type: 'stdio' | 'sse' | 'builtin';
cmd?: string;
args?: string[];
endpoint?: string;
enabled: boolean;
envVars: EnvVar[];
envVars: { key: string; value: string }[];
}>({
name: '',
type: 'stdio',
@@ -73,63 +37,82 @@ export default function ExtensionsSection() {
envVars: [],
});
useEffect(() => {
const extensions = read('extensions', false);
if (extensions) {
const extensionItems: ExtensionItem[] = Object.entries(extensions).map(([name, ext]) => {
const extensionConfig = ext as ExtensionConfig;
return {
id: name,
title: getFriendlyTitle(name),
subtitle: getSubtitle(extensionConfig),
enabled: extensionConfig.enabled,
canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs,
config: extensionConfig,
};
});
setExtensions(extensionItems);
// 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';
}
}, [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(() => {
if (selectedExtension) {
const envVars = selectedExtension.config.envs
? Object.entries(selectedExtension.config.envs).map(([key, value]) => ({
key,
value: value as string,
}))
: [];
// Type guard: Check if 'envs' property exists for this variant
const hasEnvs = selectedExtension.type === 'sse' || selectedExtension.type === 'stdio';
const envVars =
hasEnvs && selectedExtension.envs
? Object.entries(selectedExtension.envs).map(([key, value]) => ({
key,
value: value as string,
}))
: [];
setFormData({
name: selectedExtension.config.name,
type: selectedExtension.config.type as 'stdio' | 'sse',
cmd: selectedExtension.config.type === 'stdio' ? selectedExtension.config.cmd : undefined,
args: selectedExtension.config.args || [],
endpoint:
selectedExtension.config.type === 'sse' ? selectedExtension.config.cmd : undefined,
enabled: selectedExtension.config.enabled,
name: selectedExtension.name,
type: selectedExtension.type,
cmd: selectedExtension.type === 'stdio' ? selectedExtension.cmd : undefined,
args: selectedExtension.type === 'stdio' ? selectedExtension.args : [],
endpoint: selectedExtension.type === 'sse' ? selectedExtension.uri : undefined,
enabled: selectedExtension.enabled,
envVars,
});
}
}, [selectedExtension]);
const handleExtensionToggle = async (id: string) => {
const extension = extensions.find((ext) => ext.id === id);
if (extension) {
const updatedConfig = {
...extension.config,
enabled: !extension.config.enabled,
};
try {
await updateExtension(id, updatedConfig);
} catch (error) {
console.error('Failed to update extension:', error);
}
const handleExtensionToggle = async (name: string) => {
try {
await toggleExtension(name);
fetchExtensions(); // Refresh the list after toggling
} catch (error) {
console.error('Failed to toggle extension:', error);
}
};
const handleConfigureClick = (extension: ExtensionItem) => {
const handleConfigureClick = (extension: FixedExtensionEntry) => {
setSelectedExtension(extension);
setIsModalOpen(true);
};
@@ -145,24 +128,35 @@ export default function ExtensionsSection() {
{} as Record<string, string>
);
const extensionConfig = {
name: formData.name,
type: formData.type,
enabled: formData.enabled,
envs,
...(formData.type === 'stdio'
? {
cmd: formData.cmd,
args: formData.args,
}
: {
cmd: formData.endpoint,
}),
};
let extensionConfig: ExtensionConfig;
if (formData.type === 'stdio') {
extensionConfig = {
type: 'stdio',
name: formData.name,
cmd: formData.cmd,
args: formData.args,
...(Object.keys(envs).length > 0 ? { envs } : {}),
};
} else if (formData.type === 'sse') {
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 {
await addExtension(formData.name, extensionConfig);
await addExtension(formData.name, extensionConfig, formData.enabled);
handleModalClose();
fetchExtensions(); // Refresh the list after adding
} catch (error) {
console.error('Failed to add extension:', error);
}
@@ -207,7 +201,6 @@ export default function ExtensionsSection() {
envVars: newEnvVars,
});
};
const handleSaveConfig = async () => {
if (!selectedExtension) return;
@@ -221,24 +214,36 @@ export default function ExtensionsSection() {
{} as Record<string, string>
);
const updatedConfig = {
name: formData.name,
type: formData.type,
enabled: formData.enabled,
envs,
...(formData.type === 'stdio'
? {
cmd: formData.cmd,
args: formData.args,
}
: {
cmd: formData.endpoint,
}),
};
let extensionConfig: ExtensionConfig;
if (formData.type === 'stdio') {
extensionConfig = {
type: 'stdio',
name: formData.name,
cmd: formData.cmd,
args: formData.args,
...(Object.keys(envs).length > 0 ? { envs } : {}),
};
} else if (formData.type === 'sse') {
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 {
await updateExtension(selectedExtension.id, updatedConfig);
// CHANGE: Use addExtension instead of updateExtension
await addExtension(formData.name, extensionConfig, formData.enabled);
handleModalClose();
fetchExtensions(); // Refresh the list after updating
} catch (error) {
console.error('Failed to update extension configuration:', error);
}
@@ -256,14 +261,17 @@ export default function ExtensionsSection() {
</p>
<div className="space-y-2">
{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="space-y-1">
<h3 className="font-medium text-textStandard">{extension.title}</h3>
<p className="text-sm text-textSubtle">{extension.subtitle}</p>
<h3 className="font-medium text-textStandard">
{getFriendlyTitle(extension.name)}
</h3>
<p className="text-sm text-textSubtle">{getSubtitle(extension)}</p>
</div>
<div className="flex items-center gap-4">
{extension.canConfigure && (
{/* Only show config button for non-builtin extensions */}
{extension.type !== 'builtin' && (
<button
className="text-textSubtle hover:text-textStandard"
onClick={() => handleConfigureClick(extension)}
@@ -273,8 +281,8 @@ export default function ExtensionsSection() {
)}
<Switch
checked={extension.enabled}
onCheckedChange={() => handleExtensionToggle(extension.id)}
className="bg-[#393838] [&_span[data-state]]:bg-white"
onCheckedChange={() => handleExtensionToggle(extension.name)}
variant="mono"
/>
</div>
</div>
@@ -329,10 +337,10 @@ export default function ExtensionsSection() {
<label className="text-sm font-medium mb-2 block">Type</label>
<Select
value={{ value: formData.type, label: formData.type.toUpperCase() }}
onChange={(option) =>
onChange={(option: { value: string; label: string } | null) =>
setFormData({
...formData,
type: (option?.value as 'stdio' | 'sse') || 'stdio',
type: (option?.value as 'stdio' | 'sse' | 'builtin') || 'stdio',
})
}
options={[
@@ -464,7 +472,7 @@ export default function ExtensionsSection() {
<label className="text-sm font-medium mb-2 block">Type</label>
<Select
value={{ value: formData.type, label: formData.type.toUpperCase() }}
onChange={(option) =>
onChange={(option: { value: string; label: string } | null) =>
setFormData({
...formData,
type: (option?.value as 'stdio' | 'sse') || 'stdio',