mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-22 23:14:30 +01:00
chore: update extensions section to work with new endpoints (#1696)
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"license": {
|
||||
"name": "Apache-2.0"
|
||||
},
|
||||
"version": "1.0.13"
|
||||
"version": "1.0.14"
|
||||
},
|
||||
"paths": {
|
||||
"/config": {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user