From e720f9744425361f85db4e12627e838dddbff079 Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 05:43:20 -0700 Subject: [PATCH] [hAIk]: rewire toolsSlice to use tools.ts backend-bridge thunks, remove inline fetch logic, update useToolsState/useModes/useCommands/useCommandPickerItems/OpenSwarmMentionAdapter imports, replace raw OAuth disconnect fetch with OAUTH_DISCONNECT thunk --- .../app/components/useCommandPickerItems.tsx | 6 +- .../composer/OpenSwarmMentionAdapter.ts | 6 +- .../app/pages/Commands/hooks/useCommands.tsx | 6 +- .../src/app/pages/Modes/hooks/useModes.ts | 6 +- .../app/pages/Tools/hooks/useToolsState.ts | 90 ++++---- frontend/src/shared/state/toolsSlice.ts | 197 ++++++------------ 6 files changed, 121 insertions(+), 190 deletions(-) diff --git a/frontend/src/app/components/useCommandPickerItems.tsx b/frontend/src/app/components/useCommandPickerItems.tsx index 55070bfd..48735f0a 100644 --- a/frontend/src/app/components/useCommandPickerItems.tsx +++ b/frontend/src/app/components/useCommandPickerItems.tsx @@ -6,7 +6,7 @@ import LanguageIcon from '@mui/icons-material/Language'; import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined'; import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; -import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; +import { LIST_BUILTIN_TOOLS, LIST_TOOLS } from '@/shared/backend-bridge/apps/tools'; import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; import { CommandPickerItem, MODE_ICON_MAP } from './commandPickerTypes'; import { getToolGroupIcon } from './CommandPickerIcons'; @@ -24,8 +24,8 @@ export function useCommandPickerItems(trigger: '/' | '@', filter: string) { const outputsLoaded = useAppSelector((s) => s.apps.loaded); useEffect(() => { - if (!builtinLoaded) dispatch(fetchBuiltinTools()); - if (!toolsLoaded) dispatch(fetchTools()); + if (!builtinLoaded) dispatch(LIST_BUILTIN_TOOLS()); + if (!toolsLoaded) dispatch(LIST_TOOLS()); if (!outputsLoaded) dispatch(LIST_APPS()); }, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded]); diff --git a/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts b/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts index 53a87ecc..3d63fba7 100644 --- a/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts +++ b/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts @@ -5,7 +5,7 @@ import type { Unstable_MentionItem, } from '@assistant-ui/core'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; -import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; +import { LIST_BUILTIN_TOOLS, LIST_TOOLS } from '@/shared/backend-bridge/apps/tools'; import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; export interface MentionItemMetadata { @@ -51,8 +51,8 @@ export function useOpenSwarmMentionAdapter(): Unstable_MentionAdapter { const outputsLoaded = useAppSelector((s) => s.apps.loaded); useEffect(() => { - if (!builtinLoaded) dispatch(fetchBuiltinTools()); - if (!toolsLoaded) dispatch(fetchTools()); + if (!builtinLoaded) dispatch(LIST_BUILTIN_TOOLS()); + if (!toolsLoaded) dispatch(LIST_TOOLS()); if (!outputsLoaded) dispatch(LIST_APPS()); }, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded]); diff --git a/frontend/src/app/pages/Commands/hooks/useCommands.tsx b/frontend/src/app/pages/Commands/hooks/useCommands.tsx index 64270e48..f9e1365d 100644 --- a/frontend/src/app/pages/Commands/hooks/useCommands.tsx +++ b/frontend/src/app/pages/Commands/hooks/useCommands.tsx @@ -4,7 +4,7 @@ import LanguageIcon from '@mui/icons-material/Language'; import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined'; import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; -import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; +import { LIST_BUILTIN_TOOLS, LIST_TOOLS } from '@/shared/backend-bridge/apps/tools'; import { getToolGroupIcon } from '@/app/components/CommandPicker'; import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; import { LIST_SKILLS } from '@/shared/backend-bridge/apps/skills'; @@ -28,8 +28,8 @@ export function useCommands() { useEffect(() => { if (!skillsLoaded) dispatch(LIST_SKILLS()); if (!modesLoaded) dispatch(LIST_MODES()); - if (!builtinLoaded) dispatch(fetchBuiltinTools()); - if (!toolsLoaded) dispatch(fetchTools()); + if (!builtinLoaded) dispatch(LIST_BUILTIN_TOOLS()); + if (!toolsLoaded) dispatch(LIST_TOOLS()); if (!outputsLoaded) dispatch(LIST_APPS()); }, [dispatch, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]); diff --git a/frontend/src/app/pages/Modes/hooks/useModes.ts b/frontend/src/app/pages/Modes/hooks/useModes.ts index 5a000c98..491dd900 100644 --- a/frontend/src/app/pages/Modes/hooks/useModes.ts +++ b/frontend/src/app/pages/Modes/hooks/useModes.ts @@ -8,7 +8,7 @@ import { RESET_MODE, Mode, } from '@/shared/state/modesSlice'; -import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; +import { LIST_BUILTIN_TOOLS, LIST_TOOLS } from '@/shared/backend-bridge/apps/tools'; import { LIST_SKILLS } from '@/shared/backend-bridge/apps/skills'; import { ModeForm, emptyForm } from '../modesConstants'; @@ -31,8 +31,8 @@ export function useModes() { useEffect(() => { dispatch(LIST_MODES()); - dispatch(fetchBuiltinTools()); - dispatch(fetchTools()); + dispatch(LIST_BUILTIN_TOOLS()); + dispatch(LIST_TOOLS()); dispatch(LIST_SKILLS()); }, [dispatch]); diff --git a/frontend/src/app/pages/Tools/hooks/useToolsState.ts b/frontend/src/app/pages/Tools/hooks/useToolsState.ts index a2e07474..17c700e0 100644 --- a/frontend/src/app/pages/Tools/hooks/useToolsState.ts +++ b/frontend/src/app/pages/Tools/hooks/useToolsState.ts @@ -1,10 +1,11 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; +import type { ToolDefinition, BuiltinTool } from '@/shared/state/toolsSlice'; import { - fetchTools, fetchBuiltinTools, fetchBuiltinPermissions, updateBuiltinPermissions, - createTool, updateTool, deleteTool, startOAuth, fetchToolStatus, discoverTools, - ToolDefinition, BuiltinTool, -} from '@/shared/state/toolsSlice'; + LIST_TOOLS, LIST_BUILTIN_TOOLS, GET_BUILTIN_PERMISSIONS, UPDATE_BUILTIN_PERMISSIONS, + CREATE_TOOL, UPDATE_TOOL, DELETE_TOOL, OAUTH_START, GET_TOOL, DISCOVER_TOOL, + OAUTH_DISCONNECT, +} from '@/shared/backend-bridge/apps/tools'; import { searchRegistry, fetchRegistryStats, fetchServerDetail, clearDetail, McpServer } from '@/shared/state/mcpRegistrySlice'; import { LIST_APPS, UPDATE_APP } from '@/shared/backend-bridge/apps/app_builder'; import { INTEGRATIONS, Integration, CATEGORY_ORDER } from '../integrations'; @@ -70,35 +71,35 @@ export function useToolsState() { const coreSectionEnabled = useMemo(() => !coreTools.every((t) => builtinPermissions[t.name] === 'deny'), [coreTools, builtinPermissions]); const deferredSectionEnabled = useMemo(() => !deferredTools.every((t) => builtinPermissions[t.name] === 'deny'), [deferredTools, builtinPermissions]); const viewsSectionEnabled = useMemo(() => !outputs.every((o) => o.permission === 'deny'), [outputs]); const browserSectionEnabled = useMemo(() => browserTools.length > 0 && !browserTools.every((t) => builtinPermissions[t.name] === 'deny'), [browserTools, builtinPermissions]); - useEffect(() => { dispatch(fetchTools()); dispatch(fetchBuiltinTools()); dispatch(fetchBuiltinPermissions()); dispatch(LIST_APPS()); }, [dispatch]); + useEffect(() => { dispatch(LIST_TOOLS()); dispatch(LIST_BUILTIN_TOOLS()); dispatch(GET_BUILTIN_PERMISSIONS()); dispatch(LIST_APPS()); }, [dispatch]); const handleIntegrationToggle = async (integration: Integration) => { const existing = getInstalledIntegration(integration); setIntegrationLoading((p) => ({ ...p, [integration.id]: true })); try { if (existing && existing.enabled !== false) { - await dispatch(updateTool({ id: existing.id, enabled: false })); + await dispatch(UPDATE_TOOL({ toolId: existing.id, enabled: false })); setSnackbar({ open: true, message: `Disabled ${integration.name}` }); } else if (existing && existing.enabled === false) { - await dispatch(updateTool({ id: existing.id, enabled: true })); + await dispatch(UPDATE_TOOL({ toolId: existing.id, enabled: true })); if (integration.authType === 'oauth2' && existing.auth_status !== 'connected') { setSnackbar({ open: true, message: `Enabled ${integration.name} — connect your account to discover actions` }); } else { setSnackbar({ open: true, message: `Enabled ${integration.name} — re-discovering actions…` }); - const r = await dispatch(discoverTools(existing.id)); - if (discoverTools.fulfilled.match(r)) setSnackbar({ open: true, message: `${integration.name} ready — actions discovered` }); + const r = await dispatch(DISCOVER_TOOL(existing.id)); + if (DISCOVER_TOOL.fulfilled.match(r)) setSnackbar({ open: true, message: `${integration.name} ready — actions discovered` }); else setSnackbar({ open: true, message: `${integration.name} enabled but discovery failed`, severity: 'error' }); } } else { - const result = await dispatch(createTool({ name: integration.name, description: integration.description, command: '', mcp_config: integration.mcp_config, credentials: {}, auth_type: integration.authType || 'none', auth_status: 'configured', ...(integration.oauthProvider ? { oauth_provider: integration.oauthProvider } : {}) })); - if (createTool.fulfilled.match(result)) { - const newTool = result.payload; + const result = await dispatch(CREATE_TOOL({ name: integration.name, description: integration.description, command: '', mcp_config: integration.mcp_config, credentials: {}, auth_type: integration.authType || 'none', auth_status: 'configured', ...(integration.oauthProvider ? { oauth_provider: integration.oauthProvider } : {}) })); + if (CREATE_TOOL.fulfilled.match(result)) { + const newTool = result.payload.tool as unknown as ToolDefinition; if (integration.authType === 'oauth2') { setSnackbar({ open: true, message: `Enabled ${integration.name} — connect your account to discover actions` }); } else { setSnackbar({ open: true, message: `Enabled ${integration.name} — discovering actions…` }); - const r = await dispatch(discoverTools(newTool.id)); - if (discoverTools.fulfilled.match(r)) setSnackbar({ open: true, message: `${integration.name} ready — actions discovered` }); + const r = await dispatch(DISCOVER_TOOL(newTool.id)); + if (DISCOVER_TOOL.fulfilled.match(r)) setSnackbar({ open: true, message: `${integration.name} ready — actions discovered` }); else setSnackbar({ open: true, message: `${integration.name} enabled but discovery failed — is ${integration.mcp_config.command || 'the server'} installed?`, severity: 'error' }); } } @@ -109,24 +110,24 @@ export function useToolsState() { const handleDirectConnect = async (integration: Integration) => { setIntegrationLoading((p) => ({ ...p, [integration.id]: true })); try { - const result = await dispatch(createTool({ name: integration.name, description: integration.description, command: '', mcp_config: integration.mcp_config, credentials: {}, auth_type: integration.authType || 'none', auth_status: 'configured', ...(integration.oauthProvider ? { oauth_provider: integration.oauthProvider } : {}) })); - if (!createTool.fulfilled.match(result)) return; - const newTool = result.payload; + const result = await dispatch(CREATE_TOOL({ name: integration.name, description: integration.description, command: '', mcp_config: integration.mcp_config, credentials: {}, auth_type: integration.authType || 'none', auth_status: 'configured', ...(integration.oauthProvider ? { oauth_provider: integration.oauthProvider } : {}) })); + if (!CREATE_TOOL.fulfilled.match(result)) return; + const newTool = result.payload.tool as unknown as ToolDefinition; if (integration.authType === 'oauth2') handleOAuthConnect(newTool.id); else if (integration.credentialFields) openCredentialsDialog(newTool.id, integration); } finally { setIntegrationLoading((p) => ({ ...p, [integration.id]: false })); } }; const handleOAuthConnect = async (toolId: string) => { - const result = await dispatch(startOAuth(toolId)); - if (startOAuth.fulfilled.match(result)) { + const result = await dispatch(OAUTH_START(toolId)); + if (OAUTH_START.fulfilled.match(result)) { const { auth_url } = result.payload; const popup = window.open(auth_url, 'oauth', 'width=500,height=700,left=200,top=100'); const afterConnect = async () => { - const statusResult = await dispatch(fetchToolStatus(toolId)); - if (fetchToolStatus.fulfilled.match(statusResult) && statusResult.payload.auth_status === 'connected') { + const statusResult = await dispatch(GET_TOOL(toolId)); + if (GET_TOOL.fulfilled.match(statusResult) && (statusResult.payload as unknown as ToolDefinition).auth_status === 'connected') { setSnackbar({ open: true, message: `${allTools.find(t => t.id === toolId)?.name || 'Account'} connected! Discovering actions…` }); - setExpandedToolId(toolId); dispatch(discoverTools(toolId)); + setExpandedToolId(toolId); dispatch(DISCOVER_TOOL(toolId)); } else { setSnackbar({ open: true, message: `${allTools.find(t => t.id === toolId)?.name || 'Account'} connected!` }); } }; const onMessage = (event: MessageEvent) => { @@ -151,20 +152,19 @@ export function useToolsState() { if ((credDialogIntegration.credentialFields || []).some((f) => !f.optional && !credDialogValues[f.key]?.trim())) return; setCredDialogSaving(true); try { - const result = await dispatch(updateTool({ id: credDialogToolId, credentials: credDialogValues, auth_type: 'env_vars', auth_status: 'connected' })); - if (updateTool.fulfilled.match(result)) { setCredDialogOpen(false); setSnackbar({ open: true, message: `${credDialogIntegration.name} connected! Re-discovering actions…` }); dispatch(discoverTools(credDialogToolId)); } + const result = await dispatch(UPDATE_TOOL({ toolId: credDialogToolId, credentials: credDialogValues, auth_type: 'env_vars', auth_status: 'connected' })); + if (UPDATE_TOOL.fulfilled.match(result)) { setCredDialogOpen(false); setSnackbar({ open: true, message: `${credDialogIntegration.name} connected! Re-discovering actions…` }); dispatch(DISCOVER_TOOL(credDialogToolId)); } else setSnackbar({ open: true, message: 'Failed to save credentials', severity: 'error' }); } finally { setCredDialogSaving(false); } }; const handleDisconnectIntegration = async (toolId: string, integration: Integration) => { if (integration.authType === 'oauth2') { - fetch(`${API_BASE}/tools/${toolId}/oauth/disconnect`, { method: 'POST' }).catch(() => {}); - const result = await dispatch(updateTool({ id: toolId, oauth_tokens: {}, auth_status: 'configured', connected_account_email: '' })); - if (updateTool.fulfilled.match(result)) setSnackbar({ open: true, message: `${integration.name} disconnected. You can now connect a different account.` }); + const result = await dispatch(OAUTH_DISCONNECT(toolId)); + if (OAUTH_DISCONNECT.fulfilled.match(result)) setSnackbar({ open: true, message: `${integration.name} disconnected. You can now connect a different account.` }); else setSnackbar({ open: true, message: `Failed to disconnect ${integration.name}`, severity: 'error' }); } else { - await dispatch(updateTool({ id: toolId, credentials: {}, auth_type: 'none', auth_status: 'configured' })); + await dispatch(UPDATE_TOOL({ toolId, credentials: {}, auth_type: 'none', auth_status: 'configured' })); setSnackbar({ open: true, message: `${integration.name} disconnected` }); } }; @@ -172,23 +172,23 @@ export function useToolsState() { const handleDiscover = async (toolId: string) => { setDiscovering(true); try { - const result = await dispatch(discoverTools(toolId)); - if (discoverTools.fulfilled.match(result)) setSnackbar({ open: true, message: 'Actions discovered successfully' }); + const result = await dispatch(DISCOVER_TOOL(toolId)); + if (DISCOVER_TOOL.fulfilled.match(result)) setSnackbar({ open: true, message: 'Actions discovered successfully' }); else setSnackbar({ open: true, message: (result as any).error?.message || 'Discovery failed — is the MCP server running?', severity: 'error' }); } finally { setDiscovering(false); } }; - const handlePermissionChange = async (toolId: string, toolName: string, policy: string) => { const tool = items[toolId]; if (!tool) return; await dispatch(updateTool({ id: toolId, tool_permissions: { ...tool.tool_permissions, [toolName]: policy } })); }; - const handleGroupPermissionChange = async (toolId: string, names: string[], policy: string) => { const tool = items[toolId]; if (!tool) return; const updated = { ...tool.tool_permissions }; for (const name of names) updated[name] = policy; await dispatch(updateTool({ id: toolId, tool_permissions: updated })); }; - const handleBulkReadOnly = async (toolId: string) => { const tool = items[toolId]; if (!tool?.tool_permissions?._categories) return; const readNames: string[] = tool.tool_permissions._categories.read || []; const updated = { ...tool.tool_permissions }; for (const name of readNames) updated[name] = 'always_allow'; await dispatch(updateTool({ id: toolId, tool_permissions: updated })); }; - const handleResetPermissions = async (toolId: string) => { const tool = items[toolId]; if (!tool?.tool_permissions) return; const updated = { ...tool.tool_permissions }; for (const key of Object.keys(updated)) { if (!key.startsWith('_')) updated[key] = 'ask'; } await dispatch(updateTool({ id: toolId, tool_permissions: updated })); }; + const handlePermissionChange = async (toolId: string, toolName: string, policy: string) => { const tool = items[toolId]; if (!tool) return; await dispatch(UPDATE_TOOL({ toolId, tool_permissions: { ...tool.tool_permissions, [toolName]: policy } })); }; + const handleGroupPermissionChange = async (toolId: string, names: string[], policy: string) => { const tool = items[toolId]; if (!tool) return; const updated = { ...tool.tool_permissions }; for (const name of names) updated[name] = policy; await dispatch(UPDATE_TOOL({ toolId, tool_permissions: updated })); }; + const handleBulkReadOnly = async (toolId: string) => { const tool = items[toolId]; if (!tool?.tool_permissions?._categories) return; const readNames: string[] = tool.tool_permissions._categories.read || []; const updated = { ...tool.tool_permissions }; for (const name of readNames) updated[name] = 'always_allow'; await dispatch(UPDATE_TOOL({ toolId, tool_permissions: updated })); }; + const handleResetPermissions = async (toolId: string) => { const tool = items[toolId]; if (!tool?.tool_permissions) return; const updated = { ...tool.tool_permissions }; for (const key of Object.keys(updated)) { if (!key.startsWith('_')) updated[key] = 'ask'; } await dispatch(UPDATE_TOOL({ toolId, tool_permissions: updated })); }; const handleSave = async () => { const payload = { name: form.name, description: form.description, command: form.command }; - if (editingId) await dispatch(updateTool({ id: editingId, ...payload })); else await dispatch(createTool(payload)); + if (editingId) await dispatch(UPDATE_TOOL({ toolId: editingId, ...payload })); else await dispatch(CREATE_TOOL(payload)); setDialogOpen(false); }; - const handleDelete = async (id: string) => { await dispatch(deleteTool(id)); }; + const handleDelete = async (id: string) => { await dispatch(DELETE_TOOL(id)); }; const openEdit = (tool: ToolDefinition) => { setEditingId(tool.id); setForm({ name: tool.name, description: tool.description, command: tool.command }); setDialogOpen(true); }; const handleMenuOpen = (e: React.MouseEvent) => setMenuAnchor(e.currentTarget); const handleMenuClose = () => setMenuAnchor(null); @@ -202,30 +202,30 @@ export function useToolsState() { const handleExpandServer = (name: string | null) => { setExpandedServer(name); if (name && devMode) { dispatch(clearDetail()); dispatch(fetchServerDetail(name)); } }; const openMcpConfigDialog = (srv: McpServer) => { setMcpConfigServer(srv); setMcpAuthType('none'); setMcpCredentials({}); const dc = serverToMcpConfig(srv); setMcpConfigJson(JSON.stringify(Object.keys(dc).length > 0 ? dc : {}, null, 2)); setMcpConfigError(''); setMcpConfigOpen(true); }; - const handleMcpConfigSave = async () => { if (!mcpConfigServer) return; let parsedConfig: Record = {}; try { parsedConfig = JSON.parse(mcpConfigJson); } catch { setMcpConfigError('Invalid JSON'); return; } const f = serverToToolForm(mcpConfigServer); await dispatch(createTool({ name: f.name, description: f.description, command: '', mcp_config: parsedConfig, credentials: mcpCredentials, auth_type: mcpAuthType, auth_status: 'configured' })); setMcpConfigOpen(false); setSnackbar({ open: true, message: `Installed "${f.name}" as MCP tool` }); }; + const handleMcpConfigSave = async () => { if (!mcpConfigServer) return; let parsedConfig: Record = {}; try { parsedConfig = JSON.parse(mcpConfigJson); } catch { setMcpConfigError('Invalid JSON'); return; } const f = serverToToolForm(mcpConfigServer); await dispatch(CREATE_TOOL({ name: f.name, description: f.description, command: '', mcp_config: parsedConfig, credentials: mcpCredentials, auth_type: mcpAuthType, auth_status: 'configured' })); setMcpConfigOpen(false); setSnackbar({ open: true, message: `Installed "${f.name}" as MCP tool` }); }; const handleInstall = async (srv: McpServer) => { const f = serverToToolForm(srv); const mcpConfig = serverToMcpConfig(srv); const hasConfig = Object.keys(mcpConfig).length > 0; if (srv.source === 'google' && srv.remoteUrl && hasConfig) { - await dispatch(createTool({ name: f.name, description: f.description, command: '', mcp_config: mcpConfig, credentials: {}, auth_type: 'oauth2', auth_status: 'configured' })); + await dispatch(CREATE_TOOL({ name: f.name, description: f.description, command: '', mcp_config: mcpConfig, credentials: {}, auth_type: 'oauth2', auth_status: 'configured' })); setSnackbar({ open: true, message: `Installed "${f.name}" — click "Connect" to authorize` }); } else if (hasConfig && mcpConfig.type === 'stdio') { - const result = await dispatch(createTool({ name: f.name, description: f.description, command: '', mcp_config: mcpConfig, credentials: {}, auth_type: 'none', auth_status: 'configured' })); - if (createTool.fulfilled.match(result)) { + const result = await dispatch(CREATE_TOOL({ name: f.name, description: f.description, command: '', mcp_config: mcpConfig, credentials: {}, auth_type: 'none', auth_status: 'configured' })); + if (CREATE_TOOL.fulfilled.match(result)) { setSnackbar({ open: true, message: `Installed "${f.name}" — discovering actions…` }); - const r = await dispatch(discoverTools(result.payload.id)); - if (discoverTools.fulfilled.match(r)) setSnackbar({ open: true, message: `${f.name} ready — actions discovered` }); + const r = await dispatch(DISCOVER_TOOL((result.payload.tool as unknown as ToolDefinition).id)); + if (DISCOVER_TOOL.fulfilled.match(r)) setSnackbar({ open: true, message: `${f.name} ready — actions discovered` }); else setSnackbar({ open: true, message: `${f.name} installed but discovery failed — the MCP server may need setup first`, severity: 'error' }); } } else { openMcpConfigDialog(srv); } }; const handleEditInstall = (srv: McpServer) => { setRegistryOpen(false); setEditingId(null); setForm(serverToToolForm(srv)); setDialogOpen(true); }; - const handleSectionEnabledChange = async (tls: BuiltinTool[], enabled: boolean) => { const perms: Record = {}; for (const t of tls) perms[t.name] = enabled ? 'always_allow' : 'deny'; await dispatch(updateBuiltinPermissions(perms)); }; + const handleSectionEnabledChange = async (tls: BuiltinTool[], enabled: boolean) => { const perms: Record = {}; for (const t of tls) perms[t.name] = enabled ? 'always_allow' : 'deny'; await dispatch(UPDATE_BUILTIN_PERMISSIONS(perms)); }; const handleViewsSectionEnabledChange = async (enabled: boolean) => { for (const out of outputs) await dispatch(UPDATE_APP({ appId: out.id, permission: enabled ? 'ask' : 'deny' })); }; - const handleBuiltinPermissionChange = async (toolName: string, policy: string) => { await dispatch(updateBuiltinPermissions({ [toolName]: policy })); }; - const handleBuiltinCategoryPermissionChange = async (toolNames: string[], policy: string) => { const perms: Record = {}; for (const name of toolNames) perms[name] = policy; await dispatch(updateBuiltinPermissions(perms)); }; + const handleBuiltinPermissionChange = async (toolName: string, policy: string) => { await dispatch(UPDATE_BUILTIN_PERMISSIONS({ [toolName]: policy })); }; + const handleBuiltinCategoryPermissionChange = async (toolNames: string[], policy: string) => { const perms: Record = {}; for (const name of toolNames) perms[name] = policy; await dispatch(UPDATE_BUILTIN_PERMISSIONS(perms)); }; const handleViewPermissionChange = async (viewId: string, permission: string) => { await dispatch(UPDATE_APP({ appId: viewId, permission })); }; const toggleCategory = (cat: string) => setCollapsedCategories((p) => ({ ...p, [cat]: !p[cat] })); const toggleBuiltinExpand = (name: string) => setExpandedBuiltin((p) => (p === name ? null : name)); diff --git a/frontend/src/shared/state/toolsSlice.ts b/frontend/src/shared/state/toolsSlice.ts index 1aa9af3e..43cadfe2 100644 --- a/frontend/src/shared/state/toolsSlice.ts +++ b/frontend/src/shared/state/toolsSlice.ts @@ -1,7 +1,16 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { API_BASE } from '@/shared/config'; - -const TOOLS_API = `${API_BASE}/tools`; +import { createSlice } from '@reduxjs/toolkit'; +import { + LIST_TOOLS, + LIST_BUILTIN_TOOLS, + CREATE_TOOL, + UPDATE_TOOL, + DELETE_TOOL, + GET_TOOL, + DISCOVER_TOOL, + GET_BUILTIN_PERMISSIONS, + UPDATE_BUILTIN_PERMISSIONS, + OAUTH_DISCONNECT, +} from '@/shared/backend-bridge/apps/tools'; export interface ToolDefinition { id: string; @@ -36,122 +45,14 @@ interface ToolsState { builtinLoaded: boolean; } -const initialState: ToolsState = { items: {}, builtinTools: [], builtinPermissions: {}, loading: false, loaded: false, builtinLoaded: false }; - -export const fetchTools = createAsyncThunk( - 'tools/fetch', - async () => { - const res = await fetch(`${TOOLS_API}/list`); - const data = await res.json(); - return data.tools as ToolDefinition[]; - }, - { condition: (_, { getState }) => !(getState() as { tools: ToolsState }).tools.loading }, -); - -export const fetchBuiltinTools = createAsyncThunk( - 'tools/fetchBuiltin', - async () => { - const res = await fetch(`${TOOLS_API}/builtin`); - const data = await res.json(); - return data.tools as BuiltinTool[]; - }, - { condition: (_, { getState }) => !(getState() as { tools: ToolsState }).tools.builtinLoaded }, -); - -export const createTool = createAsyncThunk( - 'tools/create', - async (body: Partial> & { name: string }) => { - const res = await fetch(`${TOOLS_API}/create`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - const data = await res.json(); - return data.tool as ToolDefinition; - } -); - -export const updateTool = createAsyncThunk( - 'tools/update', - async ({ id, ...updates }: Partial & { id: string }) => { - const res = await fetch(`${TOOLS_API}/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updates), - }); - const data = await res.json(); - return data.tool as ToolDefinition; - } -); - -export const deleteTool = createAsyncThunk('tools/delete', async (id: string) => { - await fetch(`${TOOLS_API}/${id}`, { method: 'DELETE' }); - return id; -}); - -export const startOAuth = createAsyncThunk( - 'tools/startOAuth', - async (toolId: string) => { - const res = await fetch(`${TOOLS_API}/${toolId}/oauth/start`, { method: 'POST' }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error(err.detail || 'Failed to start OAuth'); - } - const data = await res.json(); - return data as { auth_url: string }; - } -); - -const disconnectOAuth = createAsyncThunk( - 'tools/disconnectOAuth', - async (toolId: string) => { - const res = await fetch(`${TOOLS_API}/${toolId}/oauth/disconnect`, { method: 'POST' }); - if (!res.ok) throw new Error('Failed to disconnect OAuth'); - const data = await res.json(); - return data.tool as ToolDefinition; - } -); - -export const fetchToolStatus = createAsyncThunk( - 'tools/fetchStatus', - async (toolId: string) => { - const res = await fetch(`${TOOLS_API}/${toolId}`); - const data = await res.json(); - return data as ToolDefinition; - } -); - -export const discoverTools = createAsyncThunk( - 'tools/discover', - async (toolId: string) => { - const res = await fetch(`${TOOLS_API}/${toolId}/discover`, { method: 'POST' }); - if (!res.ok) { - const err = await res.json().catch(() => ({ detail: 'Discovery failed' })); - throw new Error(err.detail || 'Discovery failed'); - } - const data = await res.json(); - return data.tool as ToolDefinition; - } -); - -export const fetchBuiltinPermissions = createAsyncThunk('tools/fetchBuiltinPermissions', async () => { - const res = await fetch(`${TOOLS_API}/builtin/permissions`); - const data = await res.json(); - return data.permissions as Record; -}); - -export const updateBuiltinPermissions = createAsyncThunk( - 'tools/updateBuiltinPermissions', - async (permissions: Record) => { - const res = await fetch(`${TOOLS_API}/builtin/permissions`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ permissions }), - }); - const data = await res.json(); - return data.permissions as Record; - } -); +const initialState: ToolsState = { + items: {}, + builtinTools: [], + builtinPermissions: {}, + loading: false, + loaded: false, + builtinLoaded: false, +}; const toolsSlice = createSlice({ name: 'tools', @@ -159,23 +60,53 @@ const toolsSlice = createSlice({ reducers: {}, extraReducers: (builder) => { builder - .addCase(fetchTools.pending, (state) => { state.loading = true; }) - .addCase(fetchTools.fulfilled, (state, action) => { + .addCase(LIST_TOOLS.pending, (state) => { + state.loading = true; + }) + .addCase(LIST_TOOLS.fulfilled, (state, action) => { state.loading = false; state.loaded = true; state.items = {}; - for (const t of action.payload) state.items[t.id] = t; + for (const t of action.payload.tools as ToolDefinition[]) state.items[t.id] = t; }) - .addCase(fetchTools.rejected, (state) => { state.loading = false; state.loaded = true; }) - .addCase(fetchBuiltinTools.fulfilled, (state, action) => { state.builtinTools = action.payload; state.builtinLoaded = true; }) - .addCase(createTool.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(updateTool.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(deleteTool.fulfilled, (state, action) => { delete state.items[action.payload]; }) - .addCase(disconnectOAuth.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(fetchToolStatus.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(discoverTools.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(fetchBuiltinPermissions.fulfilled, (state, action) => { state.builtinPermissions = action.payload; }) - .addCase(updateBuiltinPermissions.fulfilled, (state, action) => { state.builtinPermissions = action.payload; }); + .addCase(LIST_TOOLS.rejected, (state) => { + state.loading = false; + state.loaded = true; + }) + .addCase(LIST_BUILTIN_TOOLS.fulfilled, (state, action) => { + state.builtinTools = action.payload.tools as BuiltinTool[]; + state.builtinLoaded = true; + }) + .addCase(CREATE_TOOL.fulfilled, (state, action) => { + const tool = action.payload.tool as ToolDefinition; + state.items[tool.id] = tool; + }) + .addCase(UPDATE_TOOL.fulfilled, (state, action) => { + const tool = action.payload.tool as ToolDefinition; + state.items[tool.id] = tool; + }) + .addCase(DELETE_TOOL.fulfilled, (state, action) => { + const toolId = action.meta.arg; + delete state.items[toolId]; + }) + .addCase(GET_TOOL.fulfilled, (state, action) => { + const tool = action.payload as unknown as ToolDefinition; + if (tool?.id) state.items[tool.id] = tool; + }) + .addCase(DISCOVER_TOOL.fulfilled, (state, action) => { + const tool = action.payload.tool as ToolDefinition; + state.items[tool.id] = tool; + }) + .addCase(GET_BUILTIN_PERMISSIONS.fulfilled, (state, action) => { + state.builtinPermissions = action.payload.permissions; + }) + .addCase(UPDATE_BUILTIN_PERMISSIONS.fulfilled, (state, action) => { + state.builtinPermissions = action.payload.permissions; + }) + .addCase(OAUTH_DISCONNECT.fulfilled, (state, action) => { + const tool = action.payload.tool as ToolDefinition; + if (tool?.id) state.items[tool.id] = tool; + }); }, });