170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
import { View, ViewOptions } from '../../utils/navigationUtils';
|
|
import ExtensionsSection from '../settings/extensions/ExtensionsSection';
|
|
import { ExtensionConfig } from '../../api';
|
|
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
|
import { Button } from '../ui/button';
|
|
import { Plus } from 'lucide-react';
|
|
import { GPSIcon } from '../ui/icons';
|
|
import { useState, useEffect } from 'react';
|
|
import kebabCase from 'lodash/kebabCase';
|
|
import ExtensionModal from '../settings/extensions/modal/ExtensionModal';
|
|
import {
|
|
getDefaultFormData,
|
|
ExtensionFormData,
|
|
createExtensionConfig,
|
|
} from '../settings/extensions/utils';
|
|
import { activateExtensionDefault } from '../settings/extensions';
|
|
import { useConfig } from '../ConfigContext';
|
|
import { SearchView } from '../conversation/SearchView';
|
|
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
|
|
|
|
export type ExtensionsViewOptions = {
|
|
deepLinkConfig?: ExtensionConfig;
|
|
showEnvVars?: boolean;
|
|
};
|
|
|
|
export default function ExtensionsView({
|
|
viewOptions,
|
|
}: {
|
|
onClose: () => void;
|
|
setView: (view: View, viewOptions?: ViewOptions) => void;
|
|
viewOptions: ExtensionsViewOptions;
|
|
}) {
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const { addExtension } = useConfig();
|
|
|
|
// Only trigger refresh when deep link config changes AND we don't need to show env vars
|
|
useEffect(() => {
|
|
if (viewOptions.deepLinkConfig && !viewOptions.showEnvVars) {
|
|
setRefreshKey((prevKey) => prevKey + 1);
|
|
}
|
|
}, [viewOptions.deepLinkConfig, viewOptions.showEnvVars]);
|
|
|
|
const scrollToExtension = (extensionName: string) => {
|
|
setTimeout(() => {
|
|
const element = document.getElementById(`extension-${kebabCase(extensionName)}`);
|
|
if (element) {
|
|
element.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center',
|
|
});
|
|
// Add a subtle highlight effect
|
|
element.style.boxShadow = '0 0 0 2px rgba(59, 130, 246, 0.5)';
|
|
setTimeout(() => {
|
|
element.style.boxShadow = '';
|
|
}, 2000);
|
|
}
|
|
}, 200);
|
|
};
|
|
|
|
// Scroll to extension whenever extensionId is provided (after refresh)
|
|
useEffect(() => {
|
|
if (viewOptions.deepLinkConfig?.name && refreshKey > 0) {
|
|
scrollToExtension(viewOptions.deepLinkConfig?.name);
|
|
}
|
|
}, [viewOptions.deepLinkConfig?.name, refreshKey]);
|
|
|
|
const handleModalClose = () => {
|
|
setIsAddModalOpen(false);
|
|
};
|
|
|
|
const handleAddExtension = async (formData: ExtensionFormData) => {
|
|
// Close the modal immediately
|
|
handleModalClose();
|
|
|
|
const extensionConfig = createExtensionConfig(formData);
|
|
|
|
try {
|
|
await activateExtensionDefault({
|
|
addToConfig: addExtension,
|
|
extensionConfig: extensionConfig,
|
|
});
|
|
// Trigger a refresh of the extensions list
|
|
setRefreshKey((prevKey) => prevKey + 1);
|
|
} catch (error) {
|
|
console.error('Failed to activate extension:', error);
|
|
setRefreshKey((prevKey) => prevKey + 1);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<MainPanelLayout>
|
|
<div
|
|
className="flex flex-col min-w-0 flex-1 overflow-y-auto relative"
|
|
data-search-scroll-area
|
|
>
|
|
<div className="bg-background-primary px-8 pb-4 pt-16">
|
|
<div className="flex flex-col page-transition">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<h1 className="text-4xl font-light">Extensions</h1>
|
|
</div>
|
|
<p className="text-sm text-text-secondary mb-2">
|
|
These extensions use the Model Context Protocol (MCP). They can expand Goose's
|
|
capabilities using three main components: Prompts, Resources, and Tools.{' '}
|
|
{getSearchShortcutText()} to search.
|
|
</p>
|
|
<p className="text-sm text-text-secondary mb-6">
|
|
Extensions enabled here are used as the default for new chats. You can also toggle
|
|
active extensions during chat.
|
|
</p>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex gap-4 mb-8">
|
|
<Button
|
|
className="flex items-center gap-2 justify-center"
|
|
variant="default"
|
|
onClick={() => setIsAddModalOpen(true)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Add custom extension
|
|
</Button>
|
|
<Button
|
|
className="flex items-center gap-2 justify-center"
|
|
variant="secondary"
|
|
onClick={() =>
|
|
window.open('https://block.github.io/goose/v1/extensions/', '_blank')
|
|
}
|
|
>
|
|
<GPSIcon size={12} />
|
|
Browse extensions
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-8 pb-16">
|
|
<SearchView onSearch={(term) => setSearchTerm(term)} placeholder="Search extensions...">
|
|
<ExtensionsSection
|
|
key={refreshKey}
|
|
deepLinkConfig={viewOptions.deepLinkConfig}
|
|
showEnvVars={viewOptions.showEnvVars}
|
|
hideButtons={true}
|
|
searchTerm={searchTerm}
|
|
onModalClose={(extensionName: string) => {
|
|
scrollToExtension(extensionName);
|
|
}}
|
|
/>
|
|
</SearchView>
|
|
</div>
|
|
|
|
{/* Bottom padding space - same as in hub.tsx */}
|
|
<div className="block h-8" />
|
|
</div>
|
|
|
|
{/* Modal for adding a new extension */}
|
|
{isAddModalOpen && (
|
|
<ExtensionModal
|
|
title="Add custom extension"
|
|
initialData={getDefaultFormData()}
|
|
onClose={handleModalClose}
|
|
onSubmit={handleAddExtension}
|
|
submitLabel="Add Extension"
|
|
modalType={'add'}
|
|
/>
|
|
)}
|
|
</MainPanelLayout>
|
|
);
|
|
}
|