ui: reorganize extensions settings (#1702)
This commit is contained in:
@@ -1,15 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from '../../ui/button';
|
||||||
import { Switch } from '../../ui/switch';
|
import { Plus } from 'lucide-react';
|
||||||
import { Plus, X } from 'lucide-react';
|
|
||||||
import { Gear } from '../../icons/Gear';
|
|
||||||
import { GPSIcon } from '../../ui/icons';
|
import { GPSIcon } from '../../ui/icons';
|
||||||
import { useConfig, FixedExtensionEntry } 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';
|
|
||||||
import { ExtensionConfig } from '../../../api/types.gen';
|
import { ExtensionConfig } from '../../../api/types.gen';
|
||||||
|
import ExtensionList from './subcomponents/ExtensionList';
|
||||||
|
import ExtensionModal from './modal/ExtensionModal';
|
||||||
|
|
||||||
export default function ExtensionsSection() {
|
export default function ExtensionsSection() {
|
||||||
const { toggleExtension, getExtensions, addExtension } = useConfig();
|
const { toggleExtension, getExtensions, addExtension } = useConfig();
|
||||||
@@ -19,45 +15,7 @@ export default function ExtensionsSection() {
|
|||||||
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
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<{
|
|
||||||
name: string;
|
|
||||||
type: 'stdio' | 'sse' | 'builtin';
|
|
||||||
cmd?: string;
|
|
||||||
args?: string[];
|
|
||||||
endpoint?: string;
|
|
||||||
enabled: boolean;
|
|
||||||
envVars: { key: string; value: string }[];
|
|
||||||
}>({
|
|
||||||
name: '',
|
|
||||||
type: 'stdio',
|
|
||||||
cmd: '',
|
|
||||||
args: [],
|
|
||||||
endpoint: '',
|
|
||||||
enabled: true,
|
|
||||||
envVars: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
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 () => {
|
const fetchExtensions = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -78,31 +36,6 @@ export default function ExtensionsSection() {
|
|||||||
fetchExtensions();
|
fetchExtensions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedExtension) {
|
|
||||||
// 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.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 (name: string) => {
|
const handleExtensionToggle = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
await toggleExtension(name);
|
await toggleExtension(name);
|
||||||
@@ -117,41 +50,8 @@ export default function ExtensionsSection() {
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddExtension = async () => {
|
const handleAddExtension = async (formData: ExtensionFormData) => {
|
||||||
const envs = formData.envVars.reduce(
|
const extensionConfig = createExtensionConfig(formData);
|
||||||
(acc, { key, value }) => {
|
|
||||||
if (key) {
|
|
||||||
acc[key] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
await addExtension(formData.name, extensionConfig, formData.enabled);
|
await addExtension(formData.name, extensionConfig, formData.enabled);
|
||||||
@@ -162,85 +62,10 @@ export default function ExtensionsSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleUpdateExtension = async (formData: ExtensionFormData) => {
|
||||||
setIsModalOpen(false);
|
const extensionConfig = createExtensionConfig(formData);
|
||||||
setIsAddModalOpen(false);
|
|
||||||
setSelectedExtension(null);
|
|
||||||
setFormData({
|
|
||||||
name: '',
|
|
||||||
type: 'stdio',
|
|
||||||
cmd: '',
|
|
||||||
args: [],
|
|
||||||
endpoint: '',
|
|
||||||
enabled: true,
|
|
||||||
envVars: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddEnvVar = () => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
envVars: [...formData.envVars, { key: '', value: '' }],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveEnvVar = (index: number) => {
|
|
||||||
const newEnvVars = [...formData.envVars];
|
|
||||||
newEnvVars.splice(index, 1);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
envVars: newEnvVars,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
|
||||||
const newEnvVars = [...formData.envVars];
|
|
||||||
newEnvVars[index][field] = value;
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
envVars: newEnvVars,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleSaveConfig = async () => {
|
|
||||||
if (!selectedExtension) return;
|
|
||||||
|
|
||||||
const envs = formData.envVars.reduce(
|
|
||||||
(acc, { key, value }) => {
|
|
||||||
if (key) {
|
|
||||||
acc[key] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
// CHANGE: Use addExtension instead of updateExtension
|
|
||||||
await addExtension(formData.name, extensionConfig, formData.enabled);
|
await addExtension(formData.name, extensionConfig, formData.enabled);
|
||||||
handleModalClose();
|
handleModalClose();
|
||||||
fetchExtensions(); // Refresh the list after updating
|
fetchExtensions(); // Refresh the list after updating
|
||||||
@@ -249,6 +74,12 @@ export default function ExtensionsSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setIsAddModalOpen(false);
|
||||||
|
setSelectedExtension(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="extensions">
|
<section id="extensions">
|
||||||
<div className="flex justify-between items-center mb-6 px-8">
|
<div className="flex justify-between items-center mb-6 px-8">
|
||||||
@@ -259,55 +90,20 @@ export default function ExtensionsSection() {
|
|||||||
These extensions use the Model Context Protocol (MCP). They can expand Goose's
|
These extensions use the Model Context Protocol (MCP). They can expand Goose's
|
||||||
capabilities using three main components: Prompts, Resources, and Tools.
|
capabilities using three main components: Prompts, Resources, and Tools.
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
|
||||||
{extensions.map((extension, index) => (
|
<ExtensionList
|
||||||
<React.Fragment key={extension.name}>
|
extensions={extensions}
|
||||||
<div className="flex items-center justify-between py-3">
|
onToggle={handleExtensionToggle}
|
||||||
<div className="space-y-1">
|
onConfigure={handleConfigureClick}
|
||||||
<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">
|
|
||||||
{/* Only show config button for non-builtin extensions */}
|
|
||||||
{extension.type !== 'builtin' && (
|
|
||||||
<button
|
|
||||||
className="text-textSubtle hover:text-textStandard"
|
|
||||||
onClick={() => handleConfigureClick(extension)}
|
|
||||||
>
|
|
||||||
<Gear className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Switch
|
|
||||||
checked={extension.enabled}
|
|
||||||
onCheckedChange={() => handleExtensionToggle(extension.name)}
|
|
||||||
variant="mono"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{index < extensions.length - 1 && <div className="h-px bg-borderSubtle" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 pt-4 w-full">
|
<div className="flex gap-4 pt-4 w-full">
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2 flex-1 justify-center bg-[#393838] hover:bg-subtle"
|
className="flex items-center gap-2 flex-1 justify-center bg-[#393838] hover:bg-subtle"
|
||||||
onClick={() => {
|
onClick={() => setIsAddModalOpen(true)}
|
||||||
setFormData({
|
|
||||||
name: '',
|
|
||||||
type: 'stdio',
|
|
||||||
cmd: '',
|
|
||||||
args: [],
|
|
||||||
endpoint: '',
|
|
||||||
enabled: true,
|
|
||||||
envVars: [],
|
|
||||||
});
|
|
||||||
setIsAddModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Manually Add
|
Add custom extension
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2 flex-1 justify-center text-textSubtle border-standard bg-grey-60 hover:bg-subtle"
|
className="flex items-center gap-2 flex-1 justify-center text-textSubtle border-standard bg-grey-60 hover:bg-subtle"
|
||||||
@@ -319,275 +115,109 @@ export default function ExtensionsSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal for updating an existing extension */}
|
||||||
{isModalOpen && selectedExtension && (
|
{isModalOpen && selectedExtension && (
|
||||||
<Modal>
|
<ExtensionModal
|
||||||
<div className="space-y-6">
|
title="Update Extension"
|
||||||
<h2 className="text-xl font-medium">Update Extension</h2>
|
initialData={extensionToFormData(selectedExtension)}
|
||||||
|
onClose={handleModalClose}
|
||||||
<div className="flex justify-between gap-4">
|
onSubmit={handleUpdateExtension}
|
||||||
<div className="flex-1">
|
submitLabel="Save Changes"
|
||||||
<label className="text-sm font-medium mb-2 block">Extension Name</label>
|
/>
|
||||||
<Input
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
placeholder="Enter extension name..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-[200px]">
|
|
||||||
<label className="text-sm font-medium mb-2 block">Type</label>
|
|
||||||
<Select
|
|
||||||
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
|
||||||
onChange={(option: { value: string; label: string } | null) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
type: (option?.value as 'stdio' | 'sse' | 'builtin') || 'stdio',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
options={[
|
|
||||||
{ value: 'stdio', label: 'STDIO' },
|
|
||||||
{ value: 'sse', label: 'SSE' },
|
|
||||||
]}
|
|
||||||
styles={createDarkSelectStyles('200px')}
|
|
||||||
theme={darkSelectTheme}
|
|
||||||
isSearchable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{formData.type === 'stdio' ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium mb-2 block">Command</label>
|
|
||||||
<Input
|
|
||||||
value={formData.cmd || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, cmd: e.target.value, endpoint: undefined })
|
|
||||||
}
|
|
||||||
placeholder="Enter command..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium mb-2 block">Arguments</label>
|
|
||||||
<Input
|
|
||||||
value={formData.args?.join(' ') || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
args: e.target.value.split(' ').filter((arg) => arg.length > 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Enter arguments..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium mb-2 block">Endpoint</label>
|
|
||||||
<Input
|
|
||||||
value={formData.endpoint || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
endpoint: e.target.value,
|
|
||||||
cmd: undefined,
|
|
||||||
args: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Enter endpoint URL..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label className="text-sm font-medium">Environment Variables</label>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddEnvVar}
|
|
||||||
variant="ghost"
|
|
||||||
className="text-sm hover:bg-subtle"
|
|
||||||
>
|
|
||||||
Add Variable
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData.envVars.map((envVar, index) => (
|
|
||||||
<div key={index} className="flex gap-2 items-start">
|
|
||||||
<Input
|
|
||||||
value={envVar.key}
|
|
||||||
onChange={(e) => handleEnvVarChange(index, 'key', e.target.value)}
|
|
||||||
placeholder="Key"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={envVar.value}
|
|
||||||
onChange={(e) => handleEnvVarChange(index, 'value', e.target.value)}
|
|
||||||
placeholder="Value"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleRemoveEnvVar(index)}
|
|
||||||
variant="ghost"
|
|
||||||
className="p-2 h-auto hover:bg-subtle"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4">
|
|
||||||
<Button onClick={handleModalClose} variant="ghost" className="hover:bg-subtle">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSaveConfig} className="bg-[#393838] hover:bg-subtle">
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal for adding a new extension */}
|
||||||
{isAddModalOpen && (
|
{isAddModalOpen && (
|
||||||
<Modal>
|
<ExtensionModal
|
||||||
<div className="space-y-6">
|
title="Add New Extension"
|
||||||
<h2 className="text-xl font-medium">Add New Extension</h2>
|
initialData={getDefaultFormData()}
|
||||||
|
onClose={handleModalClose}
|
||||||
<div className="flex justify-between gap-4">
|
onSubmit={handleAddExtension}
|
||||||
<div className="flex-1">
|
submitLabel="Add Extension"
|
||||||
<label className="text-sm font-medium mb-2 block">Extension Name</label>
|
/>
|
||||||
<Input
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
placeholder="Enter extension name..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-[200px]">
|
|
||||||
<label className="text-sm font-medium mb-2 block">Type</label>
|
|
||||||
<Select
|
|
||||||
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
|
||||||
onChange={(option: { value: string; label: string } | null) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
type: (option?.value as 'stdio' | 'sse') || 'stdio',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
options={[
|
|
||||||
{ value: 'stdio', label: 'STDIO' },
|
|
||||||
{ value: 'sse', label: 'SSE' },
|
|
||||||
]}
|
|
||||||
styles={createDarkSelectStyles('200px')}
|
|
||||||
theme={darkSelectTheme}
|
|
||||||
isSearchable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{formData.type === 'stdio' ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium mb-2 block">Command</label>
|
|
||||||
<Input
|
|
||||||
value={formData.cmd || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, cmd: e.target.value, endpoint: undefined })
|
|
||||||
}
|
|
||||||
placeholder="Enter command..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium mb-2 block">Arguments</label>
|
|
||||||
<Input
|
|
||||||
value={formData.args?.join(' ') || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
args: e.target.value.split(' ').filter((arg) => arg.length > 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Enter arguments..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium mb-2 block">Endpoint</label>
|
|
||||||
<Input
|
|
||||||
value={formData.endpoint || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
endpoint: e.target.value,
|
|
||||||
cmd: undefined,
|
|
||||||
args: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Enter endpoint URL..."
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<label className="text-sm font-medium">Environment Variables</label>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddEnvVar}
|
|
||||||
variant="ghost"
|
|
||||||
className="text-sm hover:bg-subtle"
|
|
||||||
>
|
|
||||||
Add Variable
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData.envVars.map((envVar, index) => (
|
|
||||||
<div key={index} className="flex gap-2 items-start">
|
|
||||||
<Input
|
|
||||||
value={envVar.key}
|
|
||||||
onChange={(e) => handleEnvVarChange(index, 'key', e.target.value)}
|
|
||||||
placeholder="Key"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={envVar.value}
|
|
||||||
onChange={(e) => handleEnvVarChange(index, 'value', e.target.value)}
|
|
||||||
placeholder="Value"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleRemoveEnvVar(index)}
|
|
||||||
variant="ghost"
|
|
||||||
className="p-2 h-auto hover:bg-subtle"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4">
|
|
||||||
<Button onClick={handleModalClose} variant="ghost" className="hover:bg-subtle">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleAddExtension} className="bg-[#393838] hover:bg-subtle">
|
|
||||||
Add Extension
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
export interface ExtensionFormData {
|
||||||
|
name: string;
|
||||||
|
type: 'stdio' | 'sse' | 'builtin';
|
||||||
|
cmd?: string;
|
||||||
|
args?: string[];
|
||||||
|
endpoint?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
envVars: { key: string; value: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultFormData(): ExtensionFormData {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
type: 'stdio',
|
||||||
|
cmd: '',
|
||||||
|
args: [],
|
||||||
|
endpoint: '',
|
||||||
|
enabled: true,
|
||||||
|
envVars: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionToFormData(extension: FixedExtensionEntry): ExtensionFormData {
|
||||||
|
// Type guard: Check if 'envs' property exists for this variant
|
||||||
|
const hasEnvs = extension.type === 'sse' || extension.type === 'stdio';
|
||||||
|
|
||||||
|
const envVars =
|
||||||
|
hasEnvs && extension.envs
|
||||||
|
? Object.entries(extension.envs).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: value as string,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: extension.name,
|
||||||
|
type: extension.type,
|
||||||
|
cmd: extension.type === 'stdio' ? extension.cmd : undefined,
|
||||||
|
args: extension.type === 'stdio' ? extension.args : [],
|
||||||
|
endpoint: extension.type === 'sse' ? extension.uri : undefined,
|
||||||
|
enabled: extension.enabled,
|
||||||
|
envVars,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExtensionConfig(formData: ExtensionFormData): ExtensionConfig {
|
||||||
|
const envs = formData.envVars.reduce(
|
||||||
|
(acc, { key, value }) => {
|
||||||
|
if (key) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (formData.type === 'stdio') {
|
||||||
|
return {
|
||||||
|
type: 'stdio',
|
||||||
|
name: formData.name,
|
||||||
|
cmd: formData.cmd,
|
||||||
|
args: formData.args,
|
||||||
|
...(Object.keys(envs).length > 0 ? { envs } : {}),
|
||||||
|
};
|
||||||
|
} else if (formData.type === 'sse') {
|
||||||
|
return {
|
||||||
|
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
|
||||||
|
return {
|
||||||
|
type: formData.type,
|
||||||
|
name: formData.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '../../../ui/button';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { Input } from '../../../ui/input';
|
||||||
|
|
||||||
|
interface EnvVarsSectionProps {
|
||||||
|
envVars: { key: string; value: string }[];
|
||||||
|
onAdd: () => void;
|
||||||
|
onRemove: (index: number) => void;
|
||||||
|
onChange: (index: number, field: 'key' | 'value', value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EnvVarsSection({
|
||||||
|
envVars,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
onChange,
|
||||||
|
}: EnvVarsSectionProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<label className="text-sm font-medium">Environment Variables</label>
|
||||||
|
<Button onClick={onAdd} variant="ghost" className="text-sm hover:bg-subtle">
|
||||||
|
Add Variable
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{envVars.map((envVar, index) => (
|
||||||
|
<div key={index} className="flex gap-2 items-start">
|
||||||
|
<Input
|
||||||
|
value={envVar.key}
|
||||||
|
onChange={(e) => onChange(index, 'key', e.target.value)}
|
||||||
|
placeholder="Key"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={envVar.value}
|
||||||
|
onChange={(e) => onChange(index, 'value', e.target.value)}
|
||||||
|
placeholder="Value"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
variant="ghost"
|
||||||
|
className="p-2 h-auto hover:bg-subtle"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Input } from '../../../ui/input';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ExtensionConfigFieldsProps {
|
||||||
|
type: 'stdio' | 'sse' | 'builtin';
|
||||||
|
cmd: string;
|
||||||
|
args: string;
|
||||||
|
endpoint: string;
|
||||||
|
onChange: (key: string, value: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExtensionConfigFields({
|
||||||
|
type,
|
||||||
|
cmd,
|
||||||
|
args,
|
||||||
|
endpoint,
|
||||||
|
onChange,
|
||||||
|
}: ExtensionConfigFieldsProps) {
|
||||||
|
if (type === 'stdio') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Command</label>
|
||||||
|
<Input
|
||||||
|
value={cmd}
|
||||||
|
onChange={(e) => onChange('cmd', e.target.value)}
|
||||||
|
placeholder="Enter command..."
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Arguments</label>
|
||||||
|
<Input
|
||||||
|
value={args}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange(
|
||||||
|
'args',
|
||||||
|
e.target.value.split(' ').filter((arg) => arg.length > 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Enter arguments..."
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Endpoint</label>
|
||||||
|
<Input
|
||||||
|
value={endpoint}
|
||||||
|
onChange={(e) => onChange('endpoint', e.target.value)}
|
||||||
|
placeholder="Enter endpoint URL..."
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// ExtensionModal.tsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from '../../../ui/button';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import { Input } from '../../../ui/input';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import { createDarkSelectStyles, darkSelectTheme } from '../../../ui/select-styles';
|
||||||
|
import { ExtensionFormData } from '../ExtensionsSection';
|
||||||
|
import EnvVarsSection from './EnvVarsSection';
|
||||||
|
import ExtensionConfigFields from './ExtensionConfigFields';
|
||||||
|
|
||||||
|
interface ExtensionModalProps {
|
||||||
|
title: string;
|
||||||
|
initialData: ExtensionFormData;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (formData: ExtensionFormData) => void;
|
||||||
|
submitLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExtensionModal({
|
||||||
|
title,
|
||||||
|
initialData,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
submitLabel,
|
||||||
|
}: ExtensionModalProps) {
|
||||||
|
const [formData, setFormData] = useState<ExtensionFormData>(initialData);
|
||||||
|
|
||||||
|
const handleAddEnvVar = () => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
envVars: [...formData.envVars, { key: '', value: '' }],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveEnvVar = (index: number) => {
|
||||||
|
const newEnvVars = [...formData.envVars];
|
||||||
|
newEnvVars.splice(index, 1);
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
envVars: newEnvVars,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||||
|
const newEnvVars = [...formData.envVars];
|
||||||
|
newEnvVars[index][field] = value;
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
envVars: newEnvVars,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-xl font-medium">{title}</h2>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm font-medium mb-2 block">Extension Name</label>
|
||||||
|
<Input
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="Enter extension name..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-[200px]">
|
||||||
|
<label className="text-sm font-medium mb-2 block">Type</label>
|
||||||
|
<Select
|
||||||
|
value={{ value: formData.type, label: formData.type.toUpperCase() }}
|
||||||
|
onChange={(option: { value: string; label: string } | null) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
type: (option?.value as 'stdio' | 'sse' | 'builtin') || 'stdio',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options={[
|
||||||
|
{ value: 'stdio', label: 'STDIO' },
|
||||||
|
{ value: 'sse', label: 'SSE' },
|
||||||
|
]}
|
||||||
|
styles={createDarkSelectStyles('200px')}
|
||||||
|
theme={darkSelectTheme}
|
||||||
|
isSearchable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ExtensionConfigFields
|
||||||
|
type={formData.type}
|
||||||
|
cmd={formData.cmd || ''}
|
||||||
|
args={formData.args?.join(' ') || ''}
|
||||||
|
endpoint={formData.endpoint || ''}
|
||||||
|
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EnvVarsSection
|
||||||
|
envVars={formData.envVars}
|
||||||
|
onAdd={handleAddEnvVar}
|
||||||
|
onRemove={handleRemoveEnvVar}
|
||||||
|
onChange={handleEnvVarChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
<Button onClick={onClose} variant="ghost" className="hover:bg-subtle">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => onSubmit(formData)} className="bg-[#393838] hover:bg-subtle">
|
||||||
|
{submitLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionConfigFields.tsx
|
||||||
|
|
||||||
|
// EnvVarsSection.tsx
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// ExtensionItem.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Switch } from '../../../ui/switch';
|
||||||
|
import { Gear } from '../../../icons/Gear';
|
||||||
|
import { FixedExtensionEntry } from '../../../ConfigContext';
|
||||||
|
import { getSubtitle, getFriendlyTitle } from './ExtensionList';
|
||||||
|
|
||||||
|
interface ExtensionItemProps {
|
||||||
|
extension: FixedExtensionEntry;
|
||||||
|
onToggle: (name: string) => void;
|
||||||
|
onConfigure: (extension: FixedExtensionEntry) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExtensionItem({ extension, onToggle, onConfigure }: ExtensionItemProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-borderSubtle p-4 mb-2">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="font-medium text-textStandard">{getFriendlyTitle(extension.name)}</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Only show config button for non-builtin extensions */}
|
||||||
|
{extension.type !== 'builtin' && (
|
||||||
|
<button
|
||||||
|
className="text-textSubtle hover:text-textStandard"
|
||||||
|
onClick={() => onConfigure(extension)}
|
||||||
|
>
|
||||||
|
<Gear className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Switch
|
||||||
|
checked={extension.enabled}
|
||||||
|
onCheckedChange={() => onToggle(extension.name)}
|
||||||
|
variant="mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-textSubtle">{getSubtitle(extension)}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FixedExtensionEntry } from '../../../ConfigContext';
|
||||||
|
import { ExtensionConfig } from '../../../../api/types.gen';
|
||||||
|
import ExtensionItem from './ExtensionItem';
|
||||||
|
import builtInExtensionsData from '../../../../built-in-extensions.json';
|
||||||
|
|
||||||
|
interface ExtensionListProps {
|
||||||
|
extensions: FixedExtensionEntry[];
|
||||||
|
onToggle: (name: string) => void;
|
||||||
|
onConfigure: (extension: FixedExtensionEntry) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExtensionList({ extensions, onToggle, onConfigure }: ExtensionListProps) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{extensions.map((extension) => (
|
||||||
|
<ExtensionItem
|
||||||
|
key={extension.name}
|
||||||
|
extension={extension}
|
||||||
|
onToggle={onToggle}
|
||||||
|
onConfigure={onConfigure}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
// Helper function to get a friendly title from extension name
|
||||||
|
export function 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
|
||||||
|
export function getSubtitle(config: ExtensionConfig): string {
|
||||||
|
if (config.type === 'builtin') {
|
||||||
|
// Find matching extension in the data
|
||||||
|
const extensionData = builtInExtensionsData.find((ext) => ext.name === config.name);
|
||||||
|
if (extensionData?.description) {
|
||||||
|
return extensionData.description;
|
||||||
|
}
|
||||||
|
return 'Built-in extension';
|
||||||
|
}
|
||||||
|
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`;
|
||||||
|
}
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// TODO: copied this from old code
|
|
||||||
|
|
||||||
// import {View} from '../../../App'
|
|
||||||
// import {SettingsViewOptions} from "../SettingsView";
|
|
||||||
// import {toast} from "react-toastify";
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// export async function addExtensionFromDeepLink(
|
|
||||||
// url: string,
|
|
||||||
// setView: (view: View, options: SettingsViewOptions) => void
|
|
||||||
// ) {
|
|
||||||
// if (!url.startsWith('goose://extension')) {
|
|
||||||
// handleError(
|
|
||||||
// 'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const parsedUrl = new URL(url);
|
|
||||||
//
|
|
||||||
// if (parsedUrl.protocol !== 'goose:') {
|
|
||||||
// handleError(
|
|
||||||
// 'Failed to install extension: Invalid protocol: URL must use the goose:// scheme',
|
|
||||||
// true
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Check that all required fields are present and not empty
|
|
||||||
// const requiredFields = ['name', 'description'];
|
|
||||||
//
|
|
||||||
// for (const field of requiredFields) {
|
|
||||||
// const value = parsedUrl.searchParams.get(field);
|
|
||||||
// if (!value || value.trim() === '') {
|
|
||||||
// handleError(
|
|
||||||
// `Failed to install extension: The link is missing required field '${field}'`,
|
|
||||||
// true
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const cmd = parsedUrl.searchParams.get('cmd');
|
|
||||||
// if (!cmd) {
|
|
||||||
// handleError("Failed to install extension: Missing required 'cmd' parameter in the URL", true);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Validate that the command is one of the allowed commands
|
|
||||||
// const allowedCommands = ['npx', 'uvx', 'goosed'];
|
|
||||||
// if (!allowedCommands.includes(cmd)) {
|
|
||||||
// handleError(
|
|
||||||
// `Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,
|
|
||||||
// true
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Check for security risk with npx -c command
|
|
||||||
// const args = parsedUrl.searchParams.getAll('arg');
|
|
||||||
// if (cmd === 'npx' && args.includes('-c')) {
|
|
||||||
// handleError(
|
|
||||||
// 'Failed to install extension: npx with -c argument can lead to code injection',
|
|
||||||
// true
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const envList = parsedUrl.searchParams.getAll('env');
|
|
||||||
// const id = parsedUrl.searchParams.get('id');
|
|
||||||
// const name = parsedUrl.searchParams.get('name');
|
|
||||||
// const description = parsedUrl.searchParams.get('description');
|
|
||||||
// const timeout = parsedUrl.searchParams.get('timeout');
|
|
||||||
//
|
|
||||||
// // split env based on delimiter to a map
|
|
||||||
// const envs = envList.reduce(
|
|
||||||
// (acc, env) => {
|
|
||||||
// const [key, value] = env.split('=');
|
|
||||||
// acc[key] = value;
|
|
||||||
// return acc;
|
|
||||||
// },
|
|
||||||
// {} as Record<string, string>
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// // Create a ExtensionConfig from the URL parameters
|
|
||||||
// // Parse timeout if provided, otherwise use default
|
|
||||||
// const parsedTimeout = timeout ? parseInt(timeout, 10) : null;
|
|
||||||
//
|
|
||||||
// const extensionConfig: ExtensionConfig = {
|
|
||||||
// id,
|
|
||||||
// name,
|
|
||||||
// type: 'stdio',
|
|
||||||
// cmd,
|
|
||||||
// args,
|
|
||||||
// description,
|
|
||||||
// enabled: true,
|
|
||||||
// env_keys: Object.keys(envs).length > 0 ? Object.keys(envs) : [],
|
|
||||||
// timeout:
|
|
||||||
// parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout)
|
|
||||||
// ? parsedTimeout
|
|
||||||
// : DEFAULT_EXTENSION_TIMEOUT,
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// // Store the extension config regardless of env vars status
|
|
||||||
// storeExtensionConfig(extensionConfig);
|
|
||||||
//
|
|
||||||
// // Check if extension requires env vars and go to settings if so
|
|
||||||
// if (envVarsRequired(extensionConfig)) {
|
|
||||||
// console.log('Environment variables required, redirecting to settings');
|
|
||||||
// setView('settings', { extensionId: extensionConfig.id, showEnvVars: true });
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // If no env vars are required, proceed with extending Goosed
|
|
||||||
// await addExtension(extensionConfig);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function handleError(message: string, shouldThrow = false): void {
|
|
||||||
// toast.error(message);
|
|
||||||
// console.error(message);
|
|
||||||
// if (shouldThrow) {
|
|
||||||
// throw new Error(message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: when rust app starts, add built-in extensions to config.yaml if they aren't there already
|
|
||||||
Reference in New Issue
Block a user