From 8b3e3a835713b78c3988ea390ca81467ba5c33a6 Mon Sep 17 00:00:00 2001
From: Zane <75694352+zanesq@users.noreply.github.com>
Date: Fri, 24 Oct 2025 14:03:01 -0700
Subject: [PATCH] Add bottom menu extension selection (#5352)
---
ui/desktop/src/components/ChatInput.tsx | 8 +-
.../BottomMenuExtensionSelection.tsx | 147 ++++++++++++++++++
2 files changed, 154 insertions(+), 1 deletion(-)
create mode 100644 ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx
diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx
index 62c446b6..9a284f11 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 00000000..05b0b11b
--- /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"
+ />
+
+
+ ))
+ )}
+
+
+
+ );
+};