diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index bd674125a..09d1252ce 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -401,15 +401,17 @@ export default class JanModelExtension extends ModelExtension { api .get('v1/models/hub?author=cortexso&tag=cortex.cpp') .json>() - .then((e) => { - e.data?.forEach((model) => { - if ( - !models.some( - (e) => 'modelSource' in e && e.modelSource === model + .then(async (e) => { + await Promise.all( + e.data?.map((model) => { + if ( + !models.some( + (e) => 'modelSource' in e && e.modelSource === model + ) ) - ) - this.addSource(model).catch((e) => console.debug(e)) - }) + return this.addSource(model).catch((e) => console.debug(e)) + }) + ) }) ) .catch((e) => console.debug(e)) diff --git a/web-app/src/containers/DropdownToolsAvailable.tsx b/web-app/src/containers/DropdownToolsAvailable.tsx index da9b05c88..4fb5f34a0 100644 --- a/web-app/src/containers/DropdownToolsAvailable.tsx +++ b/web-app/src/containers/DropdownToolsAvailable.tsx @@ -8,13 +8,12 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Switch } from '@/components/ui/switch' -import { getTools } from '@/services/mcp' -import { MCPTool } from '@/types/completion' import { useThreads } from '@/hooks/useThreads' import { useToolAvailable } from '@/hooks/useToolAvailable' import React from 'react' +import { useAppState } from '@/hooks/useAppState' interface DropdownToolsAvailableProps { children: (isOpen: boolean, toolsCount: number) => React.ReactNode @@ -25,45 +24,20 @@ export default function DropdownToolsAvailable({ children, initialMessage = false, }: DropdownToolsAvailableProps) { - const [tools, setTools] = useState([]) + const { tools } = useAppState() const [isOpen, setIsOpen] = useState(false) const { getCurrentThread } = useThreads() const { - isToolAvailable, - setToolAvailableForThread, - setDefaultAvailableTools, + isToolDisabled, + setToolDisabledForThread, + setDefaultDisabledTools, initializeThreadTools, - getAvailableToolsForThread, - getDefaultAvailableTools, + getDisabledToolsForThread, + getDefaultDisabledTools, } = useToolAvailable() const currentThread = getCurrentThread() - useEffect(() => { - const fetchTools = async () => { - try { - const availableTools = await getTools() - setTools(availableTools) - - // If this is for the initial message (index page) and no defaults are set, - // initialize with all tools as default - if ( - initialMessage && - getDefaultAvailableTools().length === 0 && - availableTools.length > 0 - ) { - setDefaultAvailableTools(availableTools.map((tool) => tool.name)) - } - } catch (error) { - console.error('Failed to fetch tools:', error) - setTools([]) - } - } - - // Only fetch tools once when component mounts - fetchTools() - }, [initialMessage, setDefaultAvailableTools, getDefaultAvailableTools]) - // Separate effect for thread initialization - only when we have tools and a new thread useEffect(() => { if (tools.length > 0 && currentThread?.id) { @@ -74,40 +48,38 @@ export default function DropdownToolsAvailable({ const handleToolToggle = (toolName: string, checked: boolean) => { if (initialMessage) { // Update default tools for new threads/index page - const currentDefaults = getDefaultAvailableTools() + const currentDefaults = getDefaultDisabledTools() if (checked) { - if (!currentDefaults.includes(toolName)) { - setDefaultAvailableTools([...currentDefaults, toolName]) - } - } else { - setDefaultAvailableTools( + setDefaultDisabledTools( currentDefaults.filter((name) => name !== toolName) ) + } else { + setDefaultDisabledTools([...currentDefaults, toolName]) } } else if (currentThread?.id) { // Update tools for specific thread - setToolAvailableForThread(currentThread.id, toolName, checked) + setToolDisabledForThread(currentThread.id, toolName, checked) } } const isToolChecked = (toolName: string): boolean => { if (initialMessage) { // Use default tools for index page - return getDefaultAvailableTools().includes(toolName) + return !getDefaultDisabledTools().includes(toolName) } else if (currentThread?.id) { // Use thread-specific tools - return isToolAvailable(currentThread.id, toolName) + return !isToolDisabled(currentThread.id, toolName) } return false } const getEnabledToolsCount = (): number => { - if (initialMessage) { - return getDefaultAvailableTools().length - } else if (currentThread?.id) { - return getAvailableToolsForThread(currentThread.id).length - } - return 0 + const disabledTools = initialMessage + ? getDefaultDisabledTools() + : currentThread?.id + ? getDisabledToolsForThread(currentThread.id) + : [] + return tools.filter((tool) => !disabledTools.includes(tool.name)).length } const renderTrigger = () => children(isOpen, getEnabledToolsCount()) diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 36c5119e6..6d747f5e5 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -44,12 +44,16 @@ export const useChat = () => { const { approvedTools, showApprovalModal, allowAllMCPPermissions } = useToolApproval() - const { getAvailableToolsForThread } = useToolAvailable() + const { getDisabledToolsForThread } = useToolAvailable() const { getProviderByName, selectedModel, selectedProvider } = useModelProvider() - const { getCurrentThread: retrieveThread, createThread, updateThreadTimestamp } = useThreads() + const { + getCurrentThread: retrieveThread, + createThread, + updateThreadTimestamp, + } = useThreads() const { getMessages, addMessage } = useMessages() const router = useRouter() @@ -65,7 +69,7 @@ export const useChat = () => { } setTools() - let unsubscribe = () => { } + let unsubscribe = () => {} listen(SystemEvent.MCP_UPDATE, setTools).then((unsub) => { // Unsubscribe from the event when the component unmounts unsubscribe = unsub @@ -134,11 +138,9 @@ export const useChat = () => { // Filter tools based on model capabilities and available tools for this thread let availableTools = selectedModel?.capabilities?.includes('tools') ? tools.filter((tool) => { - const availableToolNames = getAvailableToolsForThread( - activeThread.id - ) - return availableToolNames.includes(tool.name) - }) + const disabledTools = getDisabledToolsForThread(activeThread.id) + return !disabledTools.includes(tool.name) + }) : [] // TODO: Later replaced by Agent setup? @@ -247,7 +249,7 @@ export const useChat = () => { updateTokenSpeed, approvedTools, showApprovalModal, - getAvailableToolsForThread, + getDisabledToolsForThread, allowAllMCPPermissions, ] ) diff --git a/web-app/src/hooks/useToolAvailable.ts b/web-app/src/hooks/useToolAvailable.ts index e7acaf32c..17f576fff 100644 --- a/web-app/src/hooks/useToolAvailable.ts +++ b/web-app/src/hooks/useToolAvailable.ts @@ -3,102 +3,100 @@ import { persist, createJSONStorage } from 'zustand/middleware' import { localStorageKey } from '@/constants/localStorage' import { MCPTool } from '@/types/completion' -type ToolAvailableState = { - // Track available tools per thread - availableTools: Record // threadId -> toolNames[] - // Global default available tools (for new threads/index page) - defaultAvailableTools: string[] +type ToolDisabledState = { + // Track disabled tools per thread + disabledTools: Record // threadId -> toolNames[] + // Global default disabled tools (for new threads/index page) + defaultDisabledTools: string[] // Actions - setToolAvailableForThread: ( + setToolDisabledForThread: ( threadId: string, toolName: string, available: boolean ) => void - isToolAvailable: (threadId: string, toolName: string) => boolean - getAvailableToolsForThread: (threadId: string) => string[] - setDefaultAvailableTools: (toolNames: string[]) => void - getDefaultAvailableTools: () => string[] + isToolDisabled: (threadId: string, toolName: string) => boolean + getDisabledToolsForThread: (threadId: string) => string[] + setDefaultDisabledTools: (toolNames: string[]) => void + getDefaultDisabledTools: () => string[] // Initialize thread tools from default or existing thread settings initializeThreadTools: (threadId: string, allTools: MCPTool[]) => void } -export const useToolAvailable = create()( +export const useToolAvailable = create()( persist( (set, get) => ({ - availableTools: {}, - defaultAvailableTools: [], + disabledTools: {}, + defaultDisabledTools: [], - setToolAvailableForThread: ( + setToolDisabledForThread: ( threadId: string, toolName: string, available: boolean ) => { set((state) => { - const currentTools = state.availableTools[threadId] || [] + const currentTools = state.disabledTools[threadId] || [] let updatedTools: string[] if (available) { - // Add tool if not already present - updatedTools = currentTools.includes(toolName) - ? currentTools - : [...currentTools, toolName] + // Remove disabled tool + updatedTools = [...currentTools.filter((tool) => tool !== toolName)] } else { - // Remove tool - updatedTools = currentTools.filter((tool) => tool !== toolName) + // Disable tool + updatedTools = [...currentTools, toolName] } return { - availableTools: { - ...state.availableTools, + disabledTools: { + ...state.disabledTools, [threadId]: updatedTools, }, } }) }, - isToolAvailable: (threadId: string, toolName: string) => { + isToolDisabled: (threadId: string, toolName: string) => { const state = get() // If no thread-specific settings, use default - if (!state.availableTools[threadId]) { - return state.defaultAvailableTools.includes(toolName) + if (!state.disabledTools[threadId]) { + return state.defaultDisabledTools.includes(toolName) } - return state.availableTools[threadId]?.includes(toolName) || false + return state.disabledTools[threadId]?.includes(toolName) || false }, - getAvailableToolsForThread: (threadId: string) => { + getDisabledToolsForThread: (threadId: string) => { const state = get() // If no thread-specific settings, use default - if (!state.availableTools[threadId]) { - return state.defaultAvailableTools + if (!state.disabledTools[threadId]) { + return state.defaultDisabledTools } - return state.availableTools[threadId] || [] + return state.disabledTools[threadId] || [] }, - setDefaultAvailableTools: (toolNames: string[]) => { - set({ defaultAvailableTools: toolNames }) + setDefaultDisabledTools: (toolNames: string[]) => { + set({ defaultDisabledTools: toolNames }) }, - getDefaultAvailableTools: () => { - return get().defaultAvailableTools + getDefaultDisabledTools: () => { + return get().defaultDisabledTools }, initializeThreadTools: (threadId: string, allTools: MCPTool[]) => { const state = get() // If thread already has settings, don't override - if (state.availableTools[threadId]) { + if (state.disabledTools[threadId]) { return } // Initialize with default tools only // Don't auto-enable all tools if defaults are explicitly empty - const initialTools = state.defaultAvailableTools.filter((toolName) => + const initialTools = state.defaultDisabledTools.filter((toolName) => allTools.some((tool) => tool.name === toolName) ) set((currentState) => ({ - availableTools: { - ...currentState.availableTools, + disabledTools: { + ...currentState.disabledTools, [threadId]: initialTools, }, })) @@ -109,8 +107,8 @@ export const useToolAvailable = create()( storage: createJSONStorage(() => localStorage), // Persist all state partialize: (state) => ({ - availableTools: state.availableTools, - defaultAvailableTools: state.defaultAvailableTools, + disabledTools: state.disabledTools, + defaultDisabledTools: state.defaultDisabledTools, }), } ) diff --git a/web-app/src/routes/hub.tsx b/web-app/src/routes/hub.tsx index ecff4276d..63d719648 100644 --- a/web-app/src/routes/hub.tsx +++ b/web-app/src/routes/hub.tsx @@ -29,7 +29,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { addModelSource, downloadModel } from '@/services/models' +import { addModelSource, downloadModel, fetchModelHub } from '@/services/models' import { useDownloadStore } from '@/hooks/useDownloadStore' import { Progress } from '@/components/ui/progress' import HeaderPage from '@/containers/HeaderPage' @@ -82,6 +82,9 @@ function Hub() { [modelId]: !prev[modelId], })) } + useEffect(() => { + fetchModelHub().then(fetchSources) + }, [fetchSources]) useEffect(() => { if (search.repo) {