diff --git a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx
index 587670ec07f7..6cca95eb6044 100644
--- a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx
+++ b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx
@@ -4,6 +4,7 @@ import { useToolCount } from '../alerts/useToolCount';
import BottomMenuAlertPopover from './BottomMenuAlertPopover';
import type { View, ViewOptions } from '../../App';
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
+import { BottomMenuExtensions } from './BottomMenuExtensions';
import ModelsBottomBar from '../settings/models/bottom_bar/ModelsBottomBar';
import { useConfig } from '../ConfigContext';
import { useModelAndProvider } from '../ModelAndProviderContext';
@@ -207,6 +208,12 @@ export default function BottomMenu({
{/* Tool and Token count */}
{}
+ {/* Extensions Menu */}
+
+
+ {/* Separator */}
+
+
{/* Model Selector Dropdown */}
diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensions.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensions.tsx
new file mode 100644
index 000000000000..ef6cc05880cd
--- /dev/null
+++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensions.tsx
@@ -0,0 +1,193 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+import { useConfig, FixedExtensionEntry } from '../ConfigContext';
+import { View, ViewOptions } from '../../App';
+import { Puzzle } from 'lucide-react';
+import { Switch } from '../ui/switch';
+import { toggleExtension } from '../settings/extensions';
+
+interface BottomMenuExtensionsProps {
+ setView: (view: View, viewOptions?: ViewOptions) => void;
+}
+
+// Helper function to get display name from extension
+function getDisplayName(extension: FixedExtensionEntry): string {
+ if (extension.type === 'builtin' && extension.display_name) {
+ return extension.display_name;
+ }
+
+ // Format the name to be more readable
+ return extension.name
+ .split(/[-_]/) // Split on hyphens and underscores
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+}
+
+// Helper function to get description from extension
+function getDescription(extension: FixedExtensionEntry): string | null {
+ if (extension.type === 'sse' || extension.type === 'stdio') {
+ return extension.description || null;
+ }
+ return null;
+}
+
+export const BottomMenuExtensions = ({ setView }: BottomMenuExtensionsProps) => {
+ const { getExtensions, addExtension } = useConfig();
+ const [extensions, setExtensions] = useState([]);
+ const [isExtensionsMenuOpen, setIsExtensionsMenuOpen] = useState(false);
+ const [isToggling, setIsToggling] = useState(null);
+ const extensionsDropdownRef = useRef(null);
+
+ const fetchExtensions = useCallback(async () => {
+ try {
+ const extensionsList = await getExtensions(true);
+ // Sort extensions by name to maintain consistent order
+ const sortedExtensions = [...extensionsList].sort((a, b) => {
+ // First sort by builtin
+ if (a.type === 'builtin' && b.type !== 'builtin') return -1;
+ if (a.type !== 'builtin' && b.type === 'builtin') return 1;
+
+ // Then sort by bundled (handle null/undefined cases)
+ const aBundled = a.bundled === true;
+ const bBundled = b.bundled === true;
+ if (aBundled && !bBundled) return -1;
+ if (!aBundled && bBundled) return 1;
+
+ // Finally sort alphabetically within each group
+ return a.name.localeCompare(b.name);
+ });
+ setExtensions(sortedExtensions);
+ } catch (error) {
+ console.error('Failed to fetch extensions:', error);
+ }
+ }, [getExtensions]);
+
+ useEffect(() => {
+ fetchExtensions();
+ }, [fetchExtensions]);
+
+ // Add click outside handler
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ extensionsDropdownRef.current &&
+ !extensionsDropdownRef.current.contains(event.target as Node)
+ ) {
+ setIsExtensionsMenuOpen(false);
+ }
+ }
+
+ // Add the event listener when the menu is open
+ if (isExtensionsMenuOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ // Clean up the event listener
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isExtensionsMenuOpen]);
+
+ // Add effect to handle Escape key
+ useEffect(() => {
+ const handleEsc = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsExtensionsMenuOpen(false);
+ }
+ };
+
+ if (isExtensionsMenuOpen) {
+ window.addEventListener('keydown', handleEsc);
+ }
+
+ return () => {
+ window.removeEventListener('keydown', handleEsc);
+ };
+ }, [isExtensionsMenuOpen]);
+
+ const handleExtensionToggle = async (extension: FixedExtensionEntry) => {
+ if (isToggling === extension.name) return;
+
+ setIsToggling(extension.name);
+ try {
+ await toggleExtension({
+ toggle: extension.enabled ? 'toggleOff' : 'toggleOn',
+ extensionConfig: extension,
+ addToConfig: addExtension,
+ toastOptions: { silent: false }, // Show toast notifications
+ });
+ await fetchExtensions(); // Refresh the list after successful toggle
+ } catch (error) {
+ console.error('Failed to toggle extension:', error);
+ } finally {
+ setIsToggling(null);
+ }
+ };
+
+ const enabledCount = extensions.filter((ext) => ext.enabled).length;
+
+ return (
+
+
+
setIsExtensionsMenuOpen(!isExtensionsMenuOpen)}
+ >
+
+ {enabledCount} extension{enabledCount !== 1 ? 's' : ''} enabled
+
+
+
+
+ {/* Dropdown Menu */}
+ {isExtensionsMenuOpen && (
+
+
+
+ {extensions.map((extension) => (
+
+
+
+ {getDisplayName(extension)}
+
+ {getDescription(extension) && (
+
+ {getDescription(extension)}
+
+ )}
+
+
+ handleExtensionToggle(extension)}
+ disabled={isToggling === extension.name}
+ variant="mono"
+ />
+
+
+ ))}
+ {extensions.length === 0 && (
+
No extensions configured
+ )}
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx
index 0a3ad2a0d7a8..b3c1da05f40e 100644
--- a/ui/desktop/src/components/settings/SettingsView.tsx
+++ b/ui/desktop/src/components/settings/SettingsView.tsx
@@ -11,6 +11,7 @@ import AppSettingsSection from './app/AppSettingsSection';
import SchedulerSection from './scheduler/SchedulerSection';
import { ExtensionConfig } from '../../api';
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
+import { useEffect, useRef } from 'react';
export type SettingsViewOptions = {
deepLinkConfig?: ExtensionConfig;
@@ -27,6 +28,42 @@ export default function SettingsView({
setView: (view: View, viewOptions?: ViewOptions) => void;
viewOptions: SettingsViewOptions;
}) {
+ const extensionsSectionRef = useRef(null);
+
+ // Handle scrolling to extensions section
+ useEffect(() => {
+ if (viewOptions.section === 'extensions' && extensionsSectionRef.current) {
+ // Use requestAnimationFrame for better timing and DOM readiness
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ if (extensionsSectionRef.current) {
+ const element = extensionsSectionRef.current;
+ const scrollContainer = element.closest('[data-radix-scroll-area-viewport]');
+
+ if (scrollContainer) {
+ // Scroll within the ScrollArea component
+ const elementTop = element.offsetTop;
+
+ // Calculate the target scroll position with a small offset for the header
+ const targetScroll = elementTop - 20; // 20px offset from top
+
+ scrollContainer.scrollTo({
+ top: targetScroll,
+ behavior: 'smooth',
+ });
+ } else {
+ // Fallback to scrollIntoView if ScrollArea not found
+ element.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start',
+ });
+ }
+ }
+ }, 50);
+ });
+ }
+ }, [viewOptions.section]);
+
return (
@@ -44,10 +81,12 @@ export default function SettingsView({
{/* Models Section */}
{/* Extensions Section */}
-
+
+
+
{/* Scheduler Section */}
{/* Goose Modes */}