diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 5383a170ea..53e47db513 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -57,7 +57,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const { prompt, setPrompt } = usePrompt() const { currentThreadId } = useThreads() const { t } = useTranslation() - const { spellCheckChatInput, experimentalFeatures } = useGeneralSetting() + const { spellCheckChatInput } = useGeneralSetting() const maxRows = 10 @@ -586,8 +586,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { )} - {experimentalFeatures && - selectedModel?.capabilities?.includes('tools') && + {selectedModel?.capabilities?.includes('tools') && hasActiveMCPServers && ( { const matches = useMatches() const navigate = useNavigate() - const { experimentalFeatures } = useGeneralSetting() const { providers } = useModelProvider() // Filter providers that have active API keys (or are llama.cpp which doesn't need one) @@ -79,15 +77,10 @@ const SettingsMenu = () => { title: 'common:hardware', route: route.settings.hardware, }, - // Only show MCP Servers when experimental features are enabled - ...(experimentalFeatures - ? [ - { - title: 'common:mcp-servers', - route: route.settings.mcp_servers, - }, - ] - : []), + { + title: 'common:mcp-servers', + route: route.settings.mcp_servers, + }, { title: 'common:local_api_server', route: route.settings.local_api_server, diff --git a/web-app/src/containers/__tests__/SettingsMenu.test.tsx b/web-app/src/containers/__tests__/SettingsMenu.test.tsx index 14b7bfca7e..56a73fbb8f 100644 --- a/web-app/src/containers/__tests__/SettingsMenu.test.tsx +++ b/web-app/src/containers/__tests__/SettingsMenu.test.tsx @@ -5,7 +5,6 @@ import SettingsMenu from '../SettingsMenu' import { useNavigate, useMatches } from '@tanstack/react-router' import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { useModelProvider } from '@/hooks/useModelProvider' -import { useAppState } from '@/hooks/useAppState' // Mock dependencies vi.mock('@tanstack/react-router', () => ({ @@ -25,9 +24,7 @@ vi.mock('@/i18n/react-i18next-compat', () => ({ })) vi.mock('@/hooks/useGeneralSetting', () => ({ - useGeneralSetting: vi.fn(() => ({ - experimentalFeatures: false, - })), + useGeneralSetting: vi.fn(() => ({})), })) vi.mock('@/hooks/useModelProvider', () => ({ @@ -71,14 +68,14 @@ describe('SettingsMenu', () => { beforeEach(() => { vi.clearAllMocks() - + vi.mocked(useNavigate).mockReturnValue(mockNavigate) vi.mocked(useMatches).mockReturnValue(mockMatches) }) it('renders all menu items', () => { render() - + expect(screen.getByText('common:general')).toBeInTheDocument() expect(screen.getByText('common:appearance')).toBeInTheDocument() expect(screen.getByText('common:privacy')).toBeInTheDocument() @@ -88,29 +85,14 @@ describe('SettingsMenu', () => { expect(screen.getByText('common:local_api_server')).toBeInTheDocument() expect(screen.getByText('common:https_proxy')).toBeInTheDocument() expect(screen.getByText('common:extensions')).toBeInTheDocument() - }) - - it('does not show MCP Servers when experimental features disabled', () => { - render() - - expect(screen.queryByText('common:mcp-servers')).not.toBeInTheDocument() - }) - - it('shows MCP Servers when experimental features enabled', () => { - vi.mocked(useGeneralSetting).mockReturnValue({ - experimentalFeatures: true, - }) - - render() - expect(screen.getByText('common:mcp-servers')).toBeInTheDocument() }) it('shows provider expansion chevron when providers are active', () => { render() - + const chevronButtons = screen.getAllByRole('button') - const chevron = chevronButtons.find(button => + const chevron = chevronButtons.find((button) => button.querySelector('svg.tabler-icon-chevron-right') ) expect(chevron).toBeInTheDocument() @@ -119,14 +101,14 @@ describe('SettingsMenu', () => { it('expands providers submenu when chevron is clicked', async () => { const user = userEvent.setup() render() - + const chevronButtons = screen.getAllByRole('button') - const chevron = chevronButtons.find(button => + const chevron = chevronButtons.find((button) => button.querySelector('svg.tabler-icon-chevron-right') ) if (!chevron) throw new Error('Chevron button not found') await user.click(chevron) - + expect(screen.getByTestId('provider-avatar-openai')).toBeInTheDocument() expect(screen.getByTestId('provider-avatar-llama.cpp')).toBeInTheDocument() }) @@ -138,52 +120,56 @@ describe('SettingsMenu', () => { params: { providerName: 'openai' }, }, ]) - + render() - + expect(screen.getByTestId('provider-avatar-openai')).toBeInTheDocument() expect(screen.getByTestId('provider-avatar-llama.cpp')).toBeInTheDocument() }) it('highlights active provider in submenu', async () => { const user = userEvent.setup() - + vi.mocked(useMatches).mockReturnValue([ { routeId: '/settings/providers/$providerName', params: { providerName: 'openai' }, }, ]) - + render() - + // First expand the providers submenu const chevronButtons = screen.getAllByRole('button') - const chevron = chevronButtons.find(button => + const chevron = chevronButtons.find((button) => button.querySelector('svg.tabler-icon-chevron-right') ) if (chevron) await user.click(chevron) - - const openaiProvider = screen.getByTestId('provider-avatar-openai').closest('div') + + const openaiProvider = screen + .getByTestId('provider-avatar-openai') + .closest('div') expect(openaiProvider).toBeInTheDocument() }) it('navigates to provider when provider is clicked', async () => { const user = userEvent.setup() render() - + // First expand the providers const chevronButtons = screen.getAllByRole('button') - const chevron = chevronButtons.find(button => + const chevron = chevronButtons.find((button) => button.querySelector('svg.tabler-icon-chevron-right') ) if (!chevron) throw new Error('Chevron button not found') await user.click(chevron) - + // Then click on a provider - const openaiProvider = screen.getByTestId('provider-avatar-openai').closest('div') + const openaiProvider = screen + .getByTestId('provider-avatar-openai') + .closest('div') await user.click(openaiProvider!) - + expect(mockNavigate).toHaveBeenCalledWith({ to: '/settings/providers/$providerName', params: { providerName: 'openai' }, @@ -192,18 +178,22 @@ describe('SettingsMenu', () => { it('shows mobile menu toggle button', () => { render() - - const menuToggle = screen.getByRole('button', { name: 'Toggle settings menu' }) + + const menuToggle = screen.getByRole('button', { + name: 'Toggle settings menu', + }) expect(menuToggle).toBeInTheDocument() }) it('opens mobile menu when toggle is clicked', async () => { const user = userEvent.setup() render() - - const menuToggle = screen.getByRole('button', { name: 'Toggle settings menu' }) + + const menuToggle = screen.getByRole('button', { + name: 'Toggle settings menu', + }) await user.click(menuToggle) - + // Menu should now be visible const menu = screen.getByText('common:general').closest('div') expect(menu).toHaveClass('flex') @@ -212,21 +202,23 @@ describe('SettingsMenu', () => { it('closes mobile menu when X is clicked', async () => { const user = userEvent.setup() render() - + // Open menu first - const menuToggle = screen.getByRole('button', { name: 'Toggle settings menu' }) + const menuToggle = screen.getByRole('button', { + name: 'Toggle settings menu', + }) await user.click(menuToggle) - + // Then close it await user.click(menuToggle) - + // Just verify the toggle button is still there after clicking twice expect(menuToggle).toBeInTheDocument() }) it('hides llamacpp provider during setup remote provider step', async () => { const user = userEvent.setup() - + vi.mocked(useMatches).mockReturnValue([ { routeId: '/settings/providers/', @@ -234,16 +226,16 @@ describe('SettingsMenu', () => { search: { step: 'setup_remote_provider' }, }, ]) - + render() - + // First expand the providers submenu const chevronButtons = screen.getAllByRole('button') - const chevron = chevronButtons.find(button => + const chevron = chevronButtons.find((button) => button.querySelector('svg.tabler-icon-chevron-right') ) if (chevron) await user.click(chevron) - + // llamacpp provider div should have hidden class const llamacppElement = screen.getByTestId('provider-avatar-llama.cpp') expect(llamacppElement.parentElement).toHaveClass('hidden') @@ -253,7 +245,7 @@ describe('SettingsMenu', () => { it('filters out inactive providers from submenu', async () => { const user = userEvent.setup() - + vi.mocked(useModelProvider).mockReturnValue({ providers: [ { @@ -268,17 +260,19 @@ describe('SettingsMenu', () => { }, ], }) - + render() - + // Expand providers const chevronButtons = screen.getAllByRole('button') - const chevron = chevronButtons.find(button => + const chevron = chevronButtons.find((button) => button.querySelector('svg.tabler-icon-chevron-right') ) if (chevron) await user.click(chevron) - + expect(screen.getByTestId('provider-avatar-openai')).toBeInTheDocument() - expect(screen.queryByTestId('provider-avatar-anthropic')).not.toBeInTheDocument() + expect( + screen.queryByTestId('provider-avatar-anthropic') + ).not.toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useGeneralSetting.test.ts b/web-app/src/hooks/__tests__/useGeneralSetting.test.ts index 19e71c4faf..f55835c0f7 100644 --- a/web-app/src/hooks/__tests__/useGeneralSetting.test.ts +++ b/web-app/src/hooks/__tests__/useGeneralSetting.test.ts @@ -31,16 +31,15 @@ describe('useGeneralSetting', () => { beforeEach(async () => { vi.clearAllMocks() - + // Get the mocked ExtensionManager const { ExtensionManager } = await import('@/lib/extension') mockExtensionManager = ExtensionManager - + // Reset store state to defaults useGeneralSetting.setState({ currentLanguage: 'en', spellCheckChatInput: true, - experimentalFeatures: false, huggingfaceToken: undefined, }) @@ -49,7 +48,7 @@ describe('useGeneralSetting', () => { getSettings: vi.fn().mockResolvedValue(null), updateSettings: vi.fn(), }) - + mockExtensionManager.getInstance.mockReturnValue({ getByName: mockGetByName, }) @@ -60,11 +59,9 @@ describe('useGeneralSetting', () => { expect(result.current.currentLanguage).toBe('en') expect(result.current.spellCheckChatInput).toBe(true) - expect(result.current.experimentalFeatures).toBe(false) expect(result.current.huggingfaceToken).toBeUndefined() expect(typeof result.current.setCurrentLanguage).toBe('function') expect(typeof result.current.setSpellCheckChatInput).toBe('function') - expect(typeof result.current.setExperimentalFeatures).toBe('function') expect(typeof result.current.setHuggingfaceToken).toBe('function') }) @@ -155,42 +152,6 @@ describe('useGeneralSetting', () => { }) }) - describe('setExperimentalFeatures', () => { - it('should enable experimental features', () => { - const { result } = renderHook(() => useGeneralSetting()) - - act(() => { - result.current.setExperimentalFeatures(true) - }) - - expect(result.current.experimentalFeatures).toBe(true) - }) - - it('should disable experimental features', () => { - const { result } = renderHook(() => useGeneralSetting()) - - act(() => { - result.current.setExperimentalFeatures(false) - }) - - expect(result.current.experimentalFeatures).toBe(false) - }) - - it('should toggle experimental features multiple times', () => { - const { result } = renderHook(() => useGeneralSetting()) - - act(() => { - result.current.setExperimentalFeatures(true) - }) - expect(result.current.experimentalFeatures).toBe(true) - - act(() => { - result.current.setExperimentalFeatures(false) - }) - expect(result.current.experimentalFeatures).toBe(false) - }) - }) - describe('setHuggingfaceToken', () => { it('should set huggingface token', () => { const { result } = renderHook(() => useGeneralSetting()) @@ -235,7 +196,7 @@ describe('useGeneralSetting', () => { const mockGetByName = vi.fn() const mockGetSettings = vi.fn().mockResolvedValue(mockSettings) const mockUpdateSettings = vi.fn() - + mockExtensionManager.getInstance.mockReturnValue({ getByName: mockGetByName, }) @@ -252,9 +213,9 @@ describe('useGeneralSetting', () => { expect(mockExtensionManager.getInstance).toHaveBeenCalled() expect(mockGetByName).toHaveBeenCalledWith('@janhq/download-extension') - + // Wait for async operations - await new Promise(resolve => setTimeout(resolve, 0)) + await new Promise((resolve) => setTimeout(resolve, 0)) expect(mockGetSettings).toHaveBeenCalled() expect(mockUpdateSettings).toHaveBeenCalledWith([ @@ -272,13 +233,11 @@ describe('useGeneralSetting', () => { act(() => { result1.current.setCurrentLanguage('id') result1.current.setSpellCheckChatInput(false) - result1.current.setExperimentalFeatures(true) result1.current.setHuggingfaceToken('shared-token') }) expect(result2.current.currentLanguage).toBe('id') expect(result2.current.spellCheckChatInput).toBe(false) - expect(result2.current.experimentalFeatures).toBe(true) expect(result2.current.huggingfaceToken).toBe('shared-token') }) }) @@ -290,13 +249,11 @@ describe('useGeneralSetting', () => { act(() => { result.current.setCurrentLanguage('vn') result.current.setSpellCheckChatInput(false) - result.current.setExperimentalFeatures(true) result.current.setHuggingfaceToken('complex-token-123') }) expect(result.current.currentLanguage).toBe('vn') expect(result.current.spellCheckChatInput).toBe(false) - expect(result.current.experimentalFeatures).toBe(true) expect(result.current.huggingfaceToken).toBe('complex-token-123') }) @@ -314,11 +271,9 @@ describe('useGeneralSetting', () => { // Second update act(() => { - result.current.setExperimentalFeatures(true) result.current.setHuggingfaceToken('sequential-token') }) - expect(result.current.experimentalFeatures).toBe(true) expect(result.current.huggingfaceToken).toBe('sequential-token') // Third update @@ -331,4 +286,4 @@ describe('useGeneralSetting', () => { expect(result.current.spellCheckChatInput).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index e1511ee51e..134dc1ae1b 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -29,7 +29,6 @@ import { OUT_OF_CONTEXT_SIZE } from '@/utils/error' import { updateSettings } from '@/services/providers' import { useContextSizeApproval } from './useModelContextApproval' import { useModelLoad } from './useModelLoad' -import { useGeneralSetting } from './useGeneralSetting' import { ReasoningProcessor, extractReasoningFromMessage, @@ -37,7 +36,6 @@ import { export const useChat = () => { const { prompt, setPrompt } = usePrompt() - const { experimentalFeatures } = useGeneralSetting() const { tools, updateTokenSpeed, @@ -247,13 +245,12 @@ export const useChat = () => { let isCompleted = false // Filter tools based on model capabilities and available tools for this thread - let availableTools = - experimentalFeatures && selectedModel?.capabilities?.includes('tools') - ? tools.filter((tool) => { - const disabledTools = getDisabledToolsForThread(activeThread.id) - return !disabledTools.includes(tool.name) - }) - : [] + let availableTools = selectedModel?.capabilities?.includes('tools') + ? tools.filter((tool) => { + const disabledTools = getDisabledToolsForThread(activeThread.id) + return !disabledTools.includes(tool.name) + }) + : [] let assistantLoopSteps = 0 @@ -543,7 +540,6 @@ export const useChat = () => { setPrompt, selectedModel, currentAssistant, - experimentalFeatures, tools, updateLoadingModel, getDisabledToolsForThread, diff --git a/web-app/src/hooks/useGeneralSetting.ts b/web-app/src/hooks/useGeneralSetting.ts index 6d8a9e22ec..b356ca8a35 100644 --- a/web-app/src/hooks/useGeneralSetting.ts +++ b/web-app/src/hooks/useGeneralSetting.ts @@ -6,10 +6,8 @@ import { ExtensionManager } from '@/lib/extension' type LeftPanelStoreState = { currentLanguage: Language spellCheckChatInput: boolean - experimentalFeatures: boolean huggingfaceToken?: string setHuggingfaceToken: (token: string) => void - setExperimentalFeatures: (value: boolean) => void setSpellCheckChatInput: (value: boolean) => void setCurrentLanguage: (value: Language) => void } @@ -19,9 +17,7 @@ export const useGeneralSetting = create()( (set) => ({ currentLanguage: 'en', spellCheckChatInput: true, - experimentalFeatures: false, huggingfaceToken: undefined, - setExperimentalFeatures: (value) => set({ experimentalFeatures: value }), setSpellCheckChatInput: (value) => set({ spellCheckChatInput: value }), setCurrentLanguage: (value) => set({ currentLanguage: value }), setHuggingfaceToken: (token) => { diff --git a/web-app/src/routes/settings/__tests__/general.test.tsx b/web-app/src/routes/settings/__tests__/general.test.tsx index 96388b0fbe..e21a28dcfd 100644 --- a/web-app/src/routes/settings/__tests__/general.test.tsx +++ b/web-app/src/routes/settings/__tests__/general.test.tsx @@ -61,8 +61,6 @@ vi.mock('@/hooks/useGeneralSetting', () => ({ useGeneralSetting: () => ({ spellCheckChatInput: true, setSpellCheckChatInput: vi.fn(), - experimentalFeatures: false, - setExperimentalFeatures: vi.fn(), huggingfaceToken: 'test-token', setHuggingfaceToken: vi.fn(), }), @@ -188,12 +186,14 @@ vi.mock('@tauri-apps/plugin-opener', () => ({ })) vi.mock('@tauri-apps/api/webviewWindow', () => { - const MockWebviewWindow = vi.fn().mockImplementation((label: string, options: any) => ({ - once: vi.fn(), - setFocus: vi.fn(), - })) + const MockWebviewWindow = vi + .fn() + .mockImplementation((label: string, options: any) => ({ + once: vi.fn(), + setFocus: vi.fn(), + })) MockWebviewWindow.getByLabel = vi.fn().mockReturnValue(null) - + return { WebviewWindow: MockWebviewWindow, } @@ -299,16 +299,6 @@ describe('General Settings Route', () => { // expect(screen.getByTestId('language-switcher')).toBeInTheDocument() // }) - it('should render switches for experimental features and spell check', async () => { - const Component = GeneralRoute.component as React.ComponentType - await act(async () => { - render() - }) - - const switches = screen.getAllByTestId('switch') - expect(switches.length).toBeGreaterThanOrEqual(2) - }) - it('should render huggingface token input', async () => { const Component = GeneralRoute.component as React.ComponentType await act(async () => { @@ -336,24 +326,6 @@ describe('General Settings Route', () => { expect(switches[0]).toBeInTheDocument() }) - it('should handle experimental features toggle', async () => { - const Component = GeneralRoute.component as React.ComponentType - await act(async () => { - render() - }) - - const switches = screen.getAllByTestId('switch') - expect(switches.length).toBeGreaterThan(0) - - // Test that switches are interactive - if (switches.length > 1) { - await act(async () => { - fireEvent.click(switches[1]) - }) - expect(switches[1]).toBeInTheDocument() - } - }) - it('should handle huggingface token change', async () => { const Component = GeneralRoute.component as React.ComponentType await act(async () => { @@ -514,16 +486,16 @@ describe('General Settings Route', () => { act(() => { fireEvent.click(checkUpdateButton) }) - + // Now the button should be disabled while checking expect(checkUpdateButton).toBeDisabled() - + // Resolve the promise to finish the update check await act(async () => { resolveUpdate!(null) await updatePromise }) - + // Button should be enabled again expect(checkUpdateButton).not.toBeDisabled() } diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index 3ee558ae7b..6c44b57a54 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -46,9 +46,6 @@ import { stopAllModels } from '@/services/models' import { SystemEvent } from '@/types/events' import { Input } from '@/components/ui/input' import { useHardware } from '@/hooks/useHardware' -import { getConnectedServers } from '@/services/mcp' -import { invoke } from '@tauri-apps/api/core' -import { useMCPServers } from '@/hooks/useMCPServers' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.general as any)({ @@ -60,8 +57,6 @@ function General() { const { spellCheckChatInput, setSpellCheckChatInput, - experimentalFeatures, - setExperimentalFeatures, huggingfaceToken, setHuggingfaceToken, } = useGeneralSetting() @@ -209,38 +204,6 @@ function General() { } }, [t, checkForUpdate]) - const handleStopAllMCPServers = async () => { - try { - const connectedServers = await getConnectedServers() - - // Stop each connected server - const stopPromises = connectedServers.map((serverName) => - invoke('deactivate_mcp_server', { name: serverName }).catch((error) => { - console.error(`Error stopping MCP server ${serverName}:`, error) - return Promise.resolve() // Continue with other servers even if one fails - }) - ) - - await Promise.all(stopPromises) - - // Update server configs to set active: false for stopped servers - const { mcpServers, editServer } = useMCPServers.getState() - connectedServers.forEach((serverName) => { - const serverConfig = mcpServers[serverName] - if (serverConfig) { - editServer(serverName, { ...serverConfig, active: false }) - } - }) - - if (connectedServers.length > 0) { - toast.success(`Stopped ${connectedServers.length} MCP server(s)`) - } - } catch (error) { - console.error('Error stopping MCP servers:', error) - toast.error('Failed to stop MCP servers') - } - } - return (
@@ -430,19 +393,6 @@ function General() { {/* Advanced */} - { - await handleStopAllMCPServers() - setExperimentalFeatures(e) - }} - /> - } - />