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
.get('v1/models/hub?author=cortexso&tag=cortex.cpp')
.json<Data<string>>()
.then((e) => {
e.data?.forEach((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))

View File

@ -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<MCPTool[]>([])
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())

View File

@ -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,10 +138,8 @@ 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)
})
: []
@ -247,7 +249,7 @@ export const useChat = () => {
updateTokenSpeed,
approvedTools,
showApprovalModal,
getAvailableToolsForThread,
getDisabledToolsForThread,
allowAllMCPPermissions,
]
)

View File

@ -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<string, string[]> // threadId -> toolNames[]
// Global default available tools (for new threads/index page)
defaultAvailableTools: string[]
type ToolDisabledState = {
// Track disabled tools per thread
disabledTools: Record<string, string[]> // 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<ToolAvailableState>()(
export const useToolAvailable = create<ToolDisabledState>()(
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<ToolAvailableState>()(
storage: createJSONStorage(() => localStorage),
// Persist all state
partialize: (state) => ({
availableTools: state.availableTools,
defaultAvailableTools: state.defaultAvailableTools,
disabledTools: state.disabledTools,
defaultDisabledTools: state.defaultDisabledTools,
}),
}
)

View File

@ -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) {