diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 62c446b62645..9a284f112a33 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -11,6 +11,7 @@ import { LocalMessageStorage } from '../utils/localMessageStorage'; import { DirSwitcher } from './bottom_menu/DirSwitcher'; import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar'; import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection'; +import { BottomMenuExtensionSelection } from './bottom_menu/BottomMenuExtensionSelection'; import { AlertType, useAlerts } from './alerts'; import { useConfig } from './ConfigContext'; import { useModelAndProvider } from './ModelAndProviderContext'; @@ -1590,7 +1591,12 @@ export default function ChatInput({
-
+ {process.env.ALPHA && sessionId && ( + <> +
+ + + )}
diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx new file mode 100644 index 000000000000..05b0b11b782f --- /dev/null +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -0,0 +1,147 @@ +import { useCallback, useMemo, useState } from 'react'; +import { Puzzle } from 'lucide-react'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu'; +import { Input } from '../ui/input'; +import { Switch } from '../ui/switch'; +import { FixedExtensionEntry, useConfig } from '../ConfigContext'; +import { toggleExtension } from '../settings/extensions/extension-manager'; +import { toastService } from '../../toasts'; +import { getFriendlyTitle } from '../settings/extensions/subcomponents/ExtensionList'; + +interface BottomMenuExtensionSelectionProps { + sessionId: string; +} + +export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionSelectionProps) => { + const [searchQuery, setSearchQuery] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const { extensionsList, addExtension } = useConfig(); + + const handleToggle = useCallback( + async (extensionConfig: FixedExtensionEntry) => { + if (!sessionId) { + toastService.error({ + title: 'Extension Toggle Error', + msg: 'No active session found. Please start a chat session first.', + traceback: 'No session ID available', + }); + return; + } + + try { + const toggleDirection = extensionConfig.enabled ? 'toggleOff' : 'toggleOn'; + + await toggleExtension({ + toggle: toggleDirection, + extensionConfig: extensionConfig, + addToConfig: addExtension, + toastOptions: { silent: false }, + sessionId: sessionId, + }); + } catch (error) { + toastService.error({ + title: 'Extension Error', + msg: `Failed to ${extensionConfig.enabled ? 'disable' : 'enable'} ${extensionConfig.name}`, + traceback: error instanceof Error ? error.message : String(error), + }); + } + }, + [sessionId, addExtension] + ); + + const filteredExtensions = useMemo(() => { + return extensionsList.filter((ext) => { + const query = searchQuery.toLowerCase(); + return ( + ext.name.toLowerCase().includes(query) || + (ext.description && ext.description.toLowerCase().includes(query)) + ); + }); + }, [extensionsList, searchQuery]); + + const sortedExtensions = useMemo(() => { + const getTypePriority = (type: string): number => { + const priorities: Record = { + builtin: 0, + platform: 1, + frontend: 2, + }; + return priorities[type] ?? Number.MAX_SAFE_INTEGER; + }; + + return [...filteredExtensions].sort((a, b) => { + // First sort by priority type + const typeDiff = getTypePriority(a.type) - getTypePriority(b.type); + if (typeDiff !== 0) return typeDiff; + + // Then sort by enabled status (enabled first) + if (a.enabled !== b.enabled) return a.enabled ? -1 : 1; + + // Finally sort alphabetically + return a.name.localeCompare(b.name); + }); + }, [filteredExtensions]); + + const activeCount = useMemo(() => { + return extensionsList.filter((ext) => ext.enabled).length; + }, [extensionsList]); + + return ( + { + setIsOpen(open); + if (!open) { + setSearchQuery(''); // Reset search when closing + } + }} + > + + + + +
+ setSearchQuery(e.target.value)} + className="h-8 text-sm" + autoFocus + /> +
+
+ {sortedExtensions.length === 0 ? ( +
+ {searchQuery ? 'no extensions found' : 'no extensions available'} +
+ ) : ( + sortedExtensions.map((ext) => ( +
handleToggle(ext)} + title={ext.description || ext.name} + > +
{getFriendlyTitle(ext)}
+
e.stopPropagation()}> + handleToggle(ext)} + variant="mono" + /> +
+
+ )) + )} +
+
+
+ ); +};