ui: new extensions modal (#1711)

This commit is contained in:
Lily Delalande
2025-03-17 09:11:42 -07:00
committed by GitHub
parent 3a2fb892e9
commit 27a9121c28
9 changed files with 462 additions and 210 deletions
+51 -5
View File
@@ -1,18 +1,64 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { Card } from './ui/card';
interface ModalProps {
children: React.ReactNode;
footer?: React.ReactNode; // Optional footer
onClose: () => void; // Function to call when modal should close
preventBackdropClose?: boolean; // Optional prop to prevent closing on backdrop click
}
/**
* A reusable modal component that renders content with a semi-transparent backdrop and blur effect.
* Closes when clicking outside the modal or pressing Esc key.
*/
export default function Modal({ children }: ModalProps) {
export default function Modal({
children,
footer,
onClose,
preventBackdropClose = false,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
// Handle click outside the modal content
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (preventBackdropClose) return;
// Check if the click was on the backdrop and not on the modal content
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose();
}
};
// Handle Esc key press
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
// Add event listener
document.addEventListener('keydown', handleEscKey);
// Clean up
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [onClose]);
return (
<div className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards]">
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-6">
<div className="space-y-6">{children}</div>
<div
className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards] flex items-center justify-center p-4"
onClick={handleBackdropClick}
>
<Card
ref={modalRef}
className="relative w-[500px] max-w-full bg-bgApp rounded-xl shadow-none my-10 overflow-hidden max-h-[90vh] flex flex-col"
>
<div className="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">{children}</div>
{footer && (
<div className="border-t border-borderSubtle bg-bgApp w-full mt-auto">{footer}</div>
)}
</Card>
</div>
);
@@ -3,12 +3,17 @@ import { Button } from '../../ui/button';
import { Plus } from 'lucide-react';
import { GPSIcon } from '../../ui/icons';
import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
import { ExtensionConfig } from '../../../api/types.gen';
import ExtensionList from './subcomponents/ExtensionList';
import ExtensionModal from './modal/ExtensionModal';
import {
createExtensionConfig,
ExtensionFormData,
extensionToFormData,
getDefaultFormData,
} from './utils';
export default function ExtensionsSection() {
const { toggleExtension, getExtensions, addExtension } = useConfig();
const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
@@ -74,6 +79,16 @@ export default function ExtensionsSection() {
}
};
const handleDeleteExtension = async (name: string) => {
try {
await removeExtension(name);
handleModalClose();
fetchExtensions(); // Refresh the list after deleting
} catch (error) {
console.error('Failed to delete extension:', error);
}
};
const handleModalClose = () => {
setIsModalOpen(false);
setIsAddModalOpen(false);
@@ -122,7 +137,9 @@ export default function ExtensionsSection() {
initialData={extensionToFormData(selectedExtension)}
onClose={handleModalClose}
onSubmit={handleUpdateExtension}
onDelete={handleDeleteExtension}
submitLabel="Save Changes"
modalType={'edit'}
/>
)}
@@ -134,90 +151,9 @@ export default function ExtensionsSection() {
onClose={handleModalClose}
onSubmit={handleAddExtension}
submitLabel="Add Extension"
modalType={'add'}
/>
)}
</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,
};
}
}
@@ -1,6 +1,6 @@
import React from 'react';
import { Button } from '../../../ui/button';
import { X } from 'lucide-react';
import { Plus, X } from 'lucide-react';
import { Input } from '../../../ui/input';
interface EnvVarsSectionProps {
@@ -8,6 +8,8 @@ interface EnvVarsSectionProps {
onAdd: () => void;
onRemove: (index: number) => void;
onChange: (index: number, field: 'key' | 'value', value: string) => void;
submitAttempted: boolean;
isValid: boolean;
}
export default function EnvVarsSection({
@@ -15,40 +17,73 @@ export default function EnvVarsSection({
onAdd,
onRemove,
onChange,
submitAttempted,
isValid,
}: 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 className="relative mb-2">
{' '}
{/* Added relative positioning with minimal margin */}
<label className="text-sm font-medium text-textStandard mb-2 block">
Environment Variables
</label>
{submitAttempted && !isValid && (
<div className="text-xs text-red-500 mt-1">
{' '}
{/* Removed absolute positioning */}
Environment variables must consist of sets of variable names and values
</div>
)}
</div>
<div className="space-y-2">
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
{/* Existing environment variables */}
{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"
/>
<React.Fragment key={index}>
<div className="relative">
<Input
value={envVar.key}
onChange={(e) => onChange(index, 'key', e.target.value)}
placeholder="Variable name"
className={`w-full bg-bgSubtle border-borderSubtle text-textStandard`}
/>
</div>
<div className="relative">
<Input
value={envVar.value}
onChange={(e) => onChange(index, 'value', e.target.value)}
placeholder="Value"
className={`w-full bg-bgSubtle border-borderSubtle text-textStandard`}
/>
</div>
<Button
onClick={() => onRemove(index)}
variant="ghost"
className="p-2 h-auto hover:bg-subtle"
className="group p-2 h-auto text-iconSubtle hover:bg-transparent min-w-[60px] flex justify-start"
>
<X className="h-4 w-4" />
<X className="h-3 w-3 text-gray-400 group-hover:text-white group-hover:drop-shadow-sm transition-all" />
</Button>
</div>
</React.Fragment>
))}
{/* Empty row with Add button */}
<Input
placeholder="Variable name"
className="w-full border-borderStandard text-textStandard"
disabled
/>
<Input
placeholder="Value"
className="w-full border-borderStandard text-textStandard"
disabled
/>
<Button
onClick={onAdd}
variant="ghost"
className="flex items-center justify-start gap-1 px-2 pr-4 text-s font-medium rounded-full dark:bg-slate-400 dark:text-gray-300 bg-gray-300 text-slate-400 dark:hover:bg-slate-300 hover:bg-gray-500 hover:text-white dark:hover:text-gray-900 transition-colors min-w-[60px] h-9"
>
<Plus className="h-3 w-3" /> Add
</Button>
</div>
</div>
);
@@ -3,57 +3,55 @@ import React from 'react';
interface ExtensionConfigFieldsProps {
type: 'stdio' | 'sse' | 'builtin';
cmd: string;
args: string;
full_cmd: string;
endpoint: string;
onChange: (key: string, value: any) => void;
submitAttempted?: boolean;
isValid?: boolean;
}
export default function ExtensionConfigFields({
type,
cmd,
args,
full_cmd,
endpoint,
onChange,
submitAttempted = false,
isValid,
}: 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"
/>
<label className="text-sm font-medium mb-2 block text-textStandard">Command</label>
<div className="relative">
<Input
value={full_cmd}
onChange={(e) => onChange('cmd', e.target.value)}
placeholder="e.g. npx -y @modelcontextprotocol/my-extension <filepath>"
className={`w-full ${!submitAttempted || isValid ? 'border-borderSubtle' : 'border-red-500'} text-textStandard`}
/>
{submitAttempted && !isValid && (
<div className="absolute text-xs text-red-500 mt-1">Command is required</div>
)}
</div>
</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"
/>
<label className="text-sm font-medium mb-2 block text-textStandard">Endpoint</label>
<div className="relative">
<Input
value={endpoint}
onChange={(e) => onChange('endpoint', e.target.value)}
placeholder="Enter endpoint URL..."
className={`w-full ${!submitAttempted || isValid ? 'border-borderSubtle' : 'border-red-500'} text-textStandard`}
/>
{submitAttempted && !isValid && (
<div className="absolute text-xs text-red-500 mt-1">Endpoint URL is required</div>
)}
</div>
</div>
);
}
@@ -1,20 +1,22 @@
// 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 { ExtensionFormData } from '../utils';
import EnvVarsSection from './EnvVarsSection';
import ExtensionConfigFields from './ExtensionConfigFields';
import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react';
interface ExtensionModalProps {
title: string;
initialData: ExtensionFormData;
onClose: () => void;
onSubmit: (formData: ExtensionFormData) => void;
onDelete?: (name: string) => void;
submitLabel: string;
modalType: 'add' | 'edit';
}
export default function ExtensionModal({
@@ -22,9 +24,13 @@ export default function ExtensionModal({
initialData,
onClose,
onSubmit,
onDelete,
submitLabel,
modalType,
}: ExtensionModalProps) {
const [formData, setFormData] = useState<ExtensionFormData>(initialData);
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const [submitAttempted, setSubmitAttempted] = useState(false);
const handleAddEnvVar = () => {
setFormData({
@@ -51,69 +57,192 @@ export default function ExtensionModal({
});
};
// Function to determine which icon to display with proper styling
const getModalIcon = () => {
if (showDeleteConfirmation) {
return <AlertTriangle className="text-red-500" size={24} />;
}
return modalType === 'add' ? (
<PlusIcon className="text-iconStandard" size={24} />
) : (
<Edit className="text-iconStandard" size={24} />
);
};
const isNameValid = () => {
return formData.name.trim() !== '';
};
const isConfigValid = () => {
return (
(formData.type === 'stdio' && formData.cmd && formData.cmd.trim() !== '') ||
(formData.type === 'sse' && formData.endpoint && formData.endpoint.trim() !== '')
);
};
const isEnvVarsValid = () => {
return formData.envVars.every(
({ key, value }) => (key === '' && value === '') || (key !== '' && value !== '')
);
};
// Form validation
const isFormValid = () => {
return isNameValid() && isConfigValid() && isEnvVarsValid();
};
// Handle submit with validation
const handleSubmit = () => {
setSubmitAttempted(true);
if (isFormValid()) {
onSubmit(formData);
}
};
// Create footer buttons based on current state
const footerContent = showDeleteConfirmation ? (
// Delete confirmation footer
<>
<div className="w-full px-6 py-4 bg-red-900/20 border-t border-red-500/30">
<p className="text-red-400 text-sm mb-2">
Are you sure you want to delete "{formData.name}"? This action cannot be undone.
</p>
</div>
<Button
onClick={() => onDelete && onDelete(formData.name)}
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-red-900/20 text-red-500 font-medium text-md"
>
<Trash2 className="h-4 w-4 mr-2" /> Confirm Delete
</Button>
<Button
onClick={() => setShowDeleteConfirmation(false)}
variant="ghost"
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
>
Cancel
</Button>
</>
) : (
// Normal footer
<>
{modalType === 'edit' && onDelete && (
<Button
onClick={() => setShowDeleteConfirmation(true)}
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-red-500 font-medium text-md"
>
<Trash2 className="h-4 w-4 mr-2" /> Delete Extension
</Button>
)}
<Button
onClick={handleSubmit}
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-textProminent font-medium text-md"
>
{submitLabel}
</Button>
<Button
onClick={onClose}
variant="ghost"
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
>
Cancel
</Button>
</>
);
// Update title based on current state
const modalTitle = showDeleteConfirmation ? `Delete Extension "${formData.name}"` : title;
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>
<Modal footer={footerContent} onClose={onClose}>
{/* Title and Icon */}
<div className="flex flex-col mb-6">
<div>{getModalIcon()}</div>
<div className="mt-2">
<h2 className="text-2xl font-regular text-textStandard">{modalTitle}</h2>
</div>
</div>
{showDeleteConfirmation ? (
<div className="mb-6">
<p className="text-textStandard">
This will permanently remove this extension and all of its settings.
</p>
</div>
) : (
<>
{/* Form Fields */}
{/* Name */}
<div className="flex justify-between gap-4 mb-6">
<div className="flex-1">
<label className="text-sm font-medium mb-2 block text-textStandard">
Extension Name
</label>
<div className="relative">
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter extension name..."
className={`${!submitAttempted || formData.name.trim() !== '' ? 'border-borderSubtle' : 'border-red-500'} text-textStandard focus:border-borderStandard`}
/>
{submitAttempted && !isNameValid() && (
<div className="absolute text-xs text-red-500 mt-1">Name is required</div>
)}
</div>
</div>
{/*Type Dropdown */}
<div className="w-[200px]">
<label className="text-sm font-medium mb-2 block text-textStandard">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: 'Standard IO (STDIO)' },
{ value: 'sse', label: 'Security Service Edge (SSE)' },
]}
styles={createDarkSelectStyles('200px')}
theme={darkSelectTheme}
isSearchable={false}
/>
</div>
</div>
{/* Divider */}
<hr className="border-t border-borderSubtle mb-6" />
{/* Config Fields */}
<div className="mb-6">
<ExtensionConfigFields
type={formData.type}
full_cmd={formData.cmd || ''}
endpoint={formData.endpoint || ''}
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
submitAttempted={submitAttempted}
isValid={isConfigValid()}
/>
</div>
{/* Divider */}
<hr className="border-t border-borderSubtle mb-6" />
{/* Environment Variables */}
<div className="mb-6">
<EnvVarsSection
envVars={formData.envVars}
onAdd={handleAddEnvVar}
onRemove={handleRemoveEnvVar}
onChange={Object.assign(handleEnvVarChange, { setSubmitAttempted })}
submitAttempted={submitAttempted}
isValid={isEnvVarsValid()}
/>
</div>
</>
)}
</Modal>
);
}
// ExtensionConfigFields.tsx
// EnvVarsSection.tsx
@@ -12,6 +12,15 @@ interface ExtensionItemProps {
}
export default function ExtensionItem({ extension, onToggle, onConfigure }: ExtensionItemProps) {
const renderFormattedSubtitle = () => {
const subtitle = getSubtitle(extension);
return subtitle.split('\n').map((part, index) => (
<React.Fragment key={index}>
{index === 0 ? part : <span className="font-mono text-xs">{part}</span>}
{index < subtitle.split('\n').length - 1 && <br />}
</React.Fragment>
));
};
return (
<div className="rounded-lg border border-borderSubtle p-4 mb-2">
<div className="flex items-center justify-between mb-2">
@@ -33,7 +42,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
/>
</div>
</div>
<p className="text-sm text-textSubtle">{getSubtitle(extension)}</p>
<p className="text-sm text-textSubtle">{renderFormattedSubtitle()}</p>
</div>
);
}
@@ -3,6 +3,7 @@ import { FixedExtensionEntry } from '../../../ConfigContext';
import { ExtensionConfig } from '../../../../api/types.gen';
import ExtensionItem from './ExtensionItem';
import builtInExtensionsData from '../../../../built-in-extensions.json';
import { combineCmdAndArgs } from '../utils';
interface ExtensionListProps {
extensions: FixedExtensionEntry[];
@@ -45,7 +46,8 @@ export function getSubtitle(config: ExtensionConfig): string {
return 'Built-in extension';
}
if (config.type === 'stdio') {
return `STDIO extension${config.cmd ? ` (${config.cmd})` : ''}`;
const full_command = combineCmdAndArgs(config.cmd, config.args);
return `STDIO extension${full_command ? `\n${full_command}` : ''}`;
}
if (config.type === 'sse') {
return `SSE extension${config.uri ? ` (${config.uri})` : ''}`;
@@ -0,0 +1,97 @@
import { FixedExtensionEntry } from '../../ConfigContext';
import { ExtensionConfig } from '../../../api/types.gen';
export interface ExtensionFormData {
name: string;
type: 'stdio' | 'sse' | 'builtin';
cmd?: string;
endpoint?: string;
enabled: boolean;
envVars: { key: string; value: string }[];
}
export function getDefaultFormData(): ExtensionFormData {
return {
name: '',
type: 'stdio',
cmd: '',
endpoint: '',
enabled: true,
envVars: [],
};
}
export 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' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined,
endpoint: extension.type === 'sse' ? extension.uri : undefined,
enabled: extension.enabled,
envVars,
};
}
export 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') {
// we put the cmd + args all in the form cmd field but need to split out into cmd + args
const { cmd, args } = splitCmdAndArgs(formData.cmd);
return {
type: 'stdio',
name: formData.name,
cmd: cmd,
args: 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,
};
}
}
export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } {
const words = str.trim().split(/\s+/);
const cmd = words[0] || '';
const args = words.slice(1);
return {
cmd,
args,
};
}
export function combineCmdAndArgs(cmd: string, args: string[]): string {
return [cmd, ...args].join(' ');
}
@@ -63,7 +63,7 @@ export default function ProviderConfigurationModal() {
};
return (
<Modal>
<Modal onClose={closeModal}>
<div className="space-y-1">
{/* Logo area - centered above title */}
<ProviderLogo providerName={currentProvider.name} />