fix: handle tool availability states (#5183)

* fix: handle tool availability states

* Update web-app/src/hooks/useToolAvailable.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update web-app/src/hooks/useToolAvailable.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update web-app/src/hooks/useToolAvailable.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: hub refresh

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Louis 2025-06-04 00:17:39 +07:00 committed by GitHub
parent 9c825956e8
commit 171b1e8c60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 84 additions and 107 deletions

View File

@ -401,15 +401,17 @@ export default class JanModelExtension extends ModelExtension {
api api
.get('v1/models/hub?author=cortexso&tag=cortex.cpp') .get('v1/models/hub?author=cortexso&tag=cortex.cpp')
.json<Data<string>>() .json<Data<string>>()
.then((e) => { .then(async (e) => {
e.data?.forEach((model) => { await Promise.all(
e.data?.map((model) => {
if ( if (
!models.some( !models.some(
(e) => 'modelSource' in e && e.modelSource === model (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)) .catch((e) => console.debug(e))

View File

@ -8,13 +8,12 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { getTools } from '@/services/mcp'
import { MCPTool } from '@/types/completion'
import { useThreads } from '@/hooks/useThreads' import { useThreads } from '@/hooks/useThreads'
import { useToolAvailable } from '@/hooks/useToolAvailable' import { useToolAvailable } from '@/hooks/useToolAvailable'
import React from 'react' import React from 'react'
import { useAppState } from '@/hooks/useAppState'
interface DropdownToolsAvailableProps { interface DropdownToolsAvailableProps {
children: (isOpen: boolean, toolsCount: number) => React.ReactNode children: (isOpen: boolean, toolsCount: number) => React.ReactNode
@ -25,45 +24,20 @@ export default function DropdownToolsAvailable({
children, children,
initialMessage = false, initialMessage = false,
}: DropdownToolsAvailableProps) { }: DropdownToolsAvailableProps) {
const [tools, setTools] = useState<MCPTool[]>([]) const { tools } = useAppState()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const { getCurrentThread } = useThreads() const { getCurrentThread } = useThreads()
const { const {
isToolAvailable, isToolDisabled,
setToolAvailableForThread, setToolDisabledForThread,
setDefaultAvailableTools, setDefaultDisabledTools,
initializeThreadTools, initializeThreadTools,
getAvailableToolsForThread, getDisabledToolsForThread,
getDefaultAvailableTools, getDefaultDisabledTools,
} = useToolAvailable() } = useToolAvailable()
const currentThread = getCurrentThread() 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 // Separate effect for thread initialization - only when we have tools and a new thread
useEffect(() => { useEffect(() => {
if (tools.length > 0 && currentThread?.id) { if (tools.length > 0 && currentThread?.id) {
@ -74,40 +48,38 @@ export default function DropdownToolsAvailable({
const handleToolToggle = (toolName: string, checked: boolean) => { const handleToolToggle = (toolName: string, checked: boolean) => {
if (initialMessage) { if (initialMessage) {
// Update default tools for new threads/index page // Update default tools for new threads/index page
const currentDefaults = getDefaultAvailableTools() const currentDefaults = getDefaultDisabledTools()
if (checked) { if (checked) {
if (!currentDefaults.includes(toolName)) { setDefaultDisabledTools(
setDefaultAvailableTools([...currentDefaults, toolName])
}
} else {
setDefaultAvailableTools(
currentDefaults.filter((name) => name !== toolName) currentDefaults.filter((name) => name !== toolName)
) )
} else {
setDefaultDisabledTools([...currentDefaults, toolName])
} }
} else if (currentThread?.id) { } else if (currentThread?.id) {
// Update tools for specific thread // Update tools for specific thread
setToolAvailableForThread(currentThread.id, toolName, checked) setToolDisabledForThread(currentThread.id, toolName, checked)
} }
} }
const isToolChecked = (toolName: string): boolean => { const isToolChecked = (toolName: string): boolean => {
if (initialMessage) { if (initialMessage) {
// Use default tools for index page // Use default tools for index page
return getDefaultAvailableTools().includes(toolName) return !getDefaultDisabledTools().includes(toolName)
} else if (currentThread?.id) { } else if (currentThread?.id) {
// Use thread-specific tools // Use thread-specific tools
return isToolAvailable(currentThread.id, toolName) return !isToolDisabled(currentThread.id, toolName)
} }
return false return false
} }
const getEnabledToolsCount = (): number => { const getEnabledToolsCount = (): number => {
if (initialMessage) { const disabledTools = initialMessage
return getDefaultAvailableTools().length ? getDefaultDisabledTools()
} else if (currentThread?.id) { : currentThread?.id
return getAvailableToolsForThread(currentThread.id).length ? getDisabledToolsForThread(currentThread.id)
} : []
return 0 return tools.filter((tool) => !disabledTools.includes(tool.name)).length
} }
const renderTrigger = () => children(isOpen, getEnabledToolsCount()) const renderTrigger = () => children(isOpen, getEnabledToolsCount())

View File

@ -44,12 +44,16 @@ export const useChat = () => {
const { approvedTools, showApprovalModal, allowAllMCPPermissions } = const { approvedTools, showApprovalModal, allowAllMCPPermissions } =
useToolApproval() useToolApproval()
const { getAvailableToolsForThread } = useToolAvailable() const { getDisabledToolsForThread } = useToolAvailable()
const { getProviderByName, selectedModel, selectedProvider } = const { getProviderByName, selectedModel, selectedProvider } =
useModelProvider() useModelProvider()
const { getCurrentThread: retrieveThread, createThread, updateThreadTimestamp } = useThreads() const {
getCurrentThread: retrieveThread,
createThread,
updateThreadTimestamp,
} = useThreads()
const { getMessages, addMessage } = useMessages() const { getMessages, addMessage } = useMessages()
const router = useRouter() const router = useRouter()
@ -134,10 +138,8 @@ export const useChat = () => {
// Filter tools based on model capabilities and available tools for this thread // Filter tools based on model capabilities and available tools for this thread
let availableTools = selectedModel?.capabilities?.includes('tools') let availableTools = selectedModel?.capabilities?.includes('tools')
? tools.filter((tool) => { ? tools.filter((tool) => {
const availableToolNames = getAvailableToolsForThread( const disabledTools = getDisabledToolsForThread(activeThread.id)
activeThread.id return !disabledTools.includes(tool.name)
)
return availableToolNames.includes(tool.name)
}) })
: [] : []
@ -247,7 +249,7 @@ export const useChat = () => {
updateTokenSpeed, updateTokenSpeed,
approvedTools, approvedTools,
showApprovalModal, showApprovalModal,
getAvailableToolsForThread, getDisabledToolsForThread,
allowAllMCPPermissions, allowAllMCPPermissions,
] ]
) )

View File

@ -3,102 +3,100 @@ import { persist, createJSONStorage } from 'zustand/middleware'
import { localStorageKey } from '@/constants/localStorage' import { localStorageKey } from '@/constants/localStorage'
import { MCPTool } from '@/types/completion' import { MCPTool } from '@/types/completion'
type ToolAvailableState = { type ToolDisabledState = {
// Track available tools per thread // Track disabled tools per thread
availableTools: Record<string, string[]> // threadId -> toolNames[] disabledTools: Record<string, string[]> // threadId -> toolNames[]
// Global default available tools (for new threads/index page) // Global default disabled tools (for new threads/index page)
defaultAvailableTools: string[] defaultDisabledTools: string[]
// Actions // Actions
setToolAvailableForThread: ( setToolDisabledForThread: (
threadId: string, threadId: string,
toolName: string, toolName: string,
available: boolean available: boolean
) => void ) => void
isToolAvailable: (threadId: string, toolName: string) => boolean isToolDisabled: (threadId: string, toolName: string) => boolean
getAvailableToolsForThread: (threadId: string) => string[] getDisabledToolsForThread: (threadId: string) => string[]
setDefaultAvailableTools: (toolNames: string[]) => void setDefaultDisabledTools: (toolNames: string[]) => void
getDefaultAvailableTools: () => string[] getDefaultDisabledTools: () => string[]
// Initialize thread tools from default or existing thread settings // Initialize thread tools from default or existing thread settings
initializeThreadTools: (threadId: string, allTools: MCPTool[]) => void initializeThreadTools: (threadId: string, allTools: MCPTool[]) => void
} }
export const useToolAvailable = create<ToolAvailableState>()( export const useToolAvailable = create<ToolDisabledState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
availableTools: {}, disabledTools: {},
defaultAvailableTools: [], defaultDisabledTools: [],
setToolAvailableForThread: ( setToolDisabledForThread: (
threadId: string, threadId: string,
toolName: string, toolName: string,
available: boolean available: boolean
) => { ) => {
set((state) => { set((state) => {
const currentTools = state.availableTools[threadId] || [] const currentTools = state.disabledTools[threadId] || []
let updatedTools: string[] let updatedTools: string[]
if (available) { if (available) {
// Add tool if not already present // Remove disabled tool
updatedTools = currentTools.includes(toolName) updatedTools = [...currentTools.filter((tool) => tool !== toolName)]
? currentTools
: [...currentTools, toolName]
} else { } else {
// Remove tool // Disable tool
updatedTools = currentTools.filter((tool) => tool !== toolName) updatedTools = [...currentTools, toolName]
} }
return { return {
availableTools: { disabledTools: {
...state.availableTools, ...state.disabledTools,
[threadId]: updatedTools, [threadId]: updatedTools,
}, },
} }
}) })
}, },
isToolAvailable: (threadId: string, toolName: string) => { isToolDisabled: (threadId: string, toolName: string) => {
const state = get() const state = get()
// If no thread-specific settings, use default // If no thread-specific settings, use default
if (!state.availableTools[threadId]) { if (!state.disabledTools[threadId]) {
return state.defaultAvailableTools.includes(toolName) 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() const state = get()
// If no thread-specific settings, use default // If no thread-specific settings, use default
if (!state.availableTools[threadId]) { if (!state.disabledTools[threadId]) {
return state.defaultAvailableTools return state.defaultDisabledTools
} }
return state.availableTools[threadId] || [] return state.disabledTools[threadId] || []
}, },
setDefaultAvailableTools: (toolNames: string[]) => { setDefaultDisabledTools: (toolNames: string[]) => {
set({ defaultAvailableTools: toolNames }) set({ defaultDisabledTools: toolNames })
}, },
getDefaultAvailableTools: () => { getDefaultDisabledTools: () => {
return get().defaultAvailableTools return get().defaultDisabledTools
}, },
initializeThreadTools: (threadId: string, allTools: MCPTool[]) => { initializeThreadTools: (threadId: string, allTools: MCPTool[]) => {
const state = get() const state = get()
// If thread already has settings, don't override // If thread already has settings, don't override
if (state.availableTools[threadId]) { if (state.disabledTools[threadId]) {
return return
} }
// Initialize with default tools only // Initialize with default tools only
// Don't auto-enable all tools if defaults are explicitly empty // 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) allTools.some((tool) => tool.name === toolName)
) )
set((currentState) => ({ set((currentState) => ({
availableTools: { disabledTools: {
...currentState.availableTools, ...currentState.disabledTools,
[threadId]: initialTools, [threadId]: initialTools,
}, },
})) }))
@ -109,8 +107,8 @@ export const useToolAvailable = create<ToolAvailableState>()(
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
// Persist all state // Persist all state
partialize: (state) => ({ partialize: (state) => ({
availableTools: state.availableTools, disabledTools: state.disabledTools,
defaultAvailableTools: state.defaultAvailableTools, defaultDisabledTools: state.defaultDisabledTools,
}), }),
} }
) )

View File

@ -29,7 +29,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { addModelSource, downloadModel } from '@/services/models' import { addModelSource, downloadModel, fetchModelHub } from '@/services/models'
import { useDownloadStore } from '@/hooks/useDownloadStore' import { useDownloadStore } from '@/hooks/useDownloadStore'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import HeaderPage from '@/containers/HeaderPage' import HeaderPage from '@/containers/HeaderPage'
@ -82,6 +82,9 @@ function Hub() {
[modelId]: !prev[modelId], [modelId]: !prev[modelId],
})) }))
} }
useEffect(() => {
fetchModelHub().then(fetchSources)
}, [fetchSources])
useEffect(() => { useEffect(() => {
if (search.repo) { if (search.repo) {