From ad8add82fd6904f9ef35cadb641bda0881dad7db Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 06:52:50 -0700 Subject: [PATCH] [hAIk]: replace HTTP file upload with native Electron showOpenDialog: remove uploadAndAttachFiles fetch-based upload in useChatSubmit and useComposerAttachments, add browseAndAttachFiles using window.openswarm.showOpenDialog with ContextPath-based attachment, declare showOpenDialog in electron.d.ts, remove hidden file input refs and upload spinner from ChatInput/ModelModeSelector/OpenSwarmComposer. Rename WebSocket from /ws/dashboard to /ws on backend and rename dashboardWs to agentsWs across WebSocketManager, useDashboardInit, and browserCommandHandler. Export AGENTS_WS_API from agents bridge. Disable modelsSlice (delete file, comment out store registration and fetchModels dispatches) and stub mcpRegistrySlice thunks with not-implemented errors, move clearDetail to local action creator in useToolsState --- backend/apps/agents/agents.py | 2 +- frontend/src/app/Main.tsx | 4 +- .../src/app/pages/AgentChat/ChatInput.tsx | 21 +- .../app/pages/AgentChat/ModelModeSelector.tsx | 17 +- .../AgentChat/composer/OpenSwarmComposer.tsx | 8 +- .../composer/useComposerAttachments.ts | 79 +++----- .../pages/AgentChat/hooks/useChatSubmit.ts | 42 ++-- .../pages/Dashboard/hooks/useDashboardInit.ts | 6 +- .../app/pages/Settings/hooks/useSettings.ts | 6 +- .../app/pages/Tools/hooks/useToolsState.ts | 7 +- .../src/shared/backend-bridge/apps/agents.ts | 2 + frontend/src/shared/browserCommandHandler.ts | 8 +- frontend/src/shared/state/mcpRegistrySlice.ts | 188 ++++++++++-------- frontend/src/shared/state/modelsSlice.ts | 49 ----- frontend/src/shared/state/store.ts | 8 +- frontend/src/shared/ws/WebSocketManager.ts | 4 +- frontend/src/types/electron.d.ts | 5 + 17 files changed, 202 insertions(+), 254 deletions(-) delete mode 100644 frontend/src/shared/state/modelsSlice.ts diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index d13f0e3e..79a4fa38 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -84,7 +84,7 @@ agents = SubApp("agents", agents_lifespan) # WebSocket # --------------------------------------------------------------------------- # TODO: type spec this more -@agents.router.websocket("/ws/dashboard") +@agents.router.websocket("/ws") async def websocket_dashboard(websocket: WebSocket): await COMMS_MANAGER.broadcaster.connect(websocket) try: diff --git a/frontend/src/app/Main.tsx b/frontend/src/app/Main.tsx index d8c3d80b..5ee58835 100644 --- a/frontend/src/app/Main.tsx +++ b/frontend/src/app/Main.tsx @@ -5,7 +5,7 @@ import { ThemeProvider as MuiThemeProvider, CssBaseline } from '@mui/material'; import { store } from '../shared/state/store'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { GET_SETTINGS } from '@/shared/backend-bridge/apps/settings'; -import { fetchModels } from '@/shared/state/modelsSlice'; +// import { fetchModels } from '@/shared/state/modelsSlice'; import { setAppVersion, setUpdateAvailable, @@ -40,7 +40,7 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) = const loaded = useAppSelector((s) => s.settings.loaded); useEffect(() => { dispatch(GET_SETTINGS()); - dispatch(fetchModels()); + // dispatch(fetchModels()); }, [dispatch]); useEffect(() => { if (loaded) setThemeMode(theme as 'light' | 'dark'); diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index f6552da2..63073420 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect, useId, forwardRef, useImperativeHandle } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import CircularProgress from '@mui/material/CircularProgress'; import AttachFileIcon from '@mui/icons-material/AttachFile'; import CommandPicker from '@/app/components/CommandPicker'; import { useElementSelection } from '@/app/components/ElementSelectionContext'; @@ -41,7 +40,6 @@ const ChatInput = forwardRef(({ const c = useClaudeTokens(); const editorRef = useRef(null); const containerRef = useRef(null); - const generalFileInputRef = useRef(null); const elementSelection = useElementSelection(); const fallbackOwnerId = useId(); @@ -60,7 +58,6 @@ const ChatInput = forwardRef(({ const [images, setImages] = useState([]); const [lightboxSrc, setLightboxSrc] = useState(null); const [isDragOver, setIsDragOver] = useState(false); - const [isUploading, setIsUploading] = useState(false); const [contextPaths, setContextPaths] = useState([]); const [forcedTools, setForcedTools] = useState([]); const [copiedPathIdx, setCopiedPathIdx] = useState(null); @@ -82,13 +79,13 @@ const ChatInput = forwardRef(({ const { handleSend, handlePickerSelect, handlePaste, handleKeyDown, handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop, - addImageFiles, uploadAndAttachFiles, removeImage, + addImageFiles, browseAndAttachFiles, removeImage, } = useChatSubmit({ - editorRef, attachedSkillsRef, generalFileInputRef, disabled, autoRunMode, + editorRef, attachedSkillsRef, disabled, autoRunMode, images, contextPaths, forcedTools, picker, skills, ownerId, elementSelection, onSend, onModeChange, setImages, setContextPaths, setForcedTools, setPicker, setHasContent, setAttachedSkills, - setIsUploading, setIsDragOver, c, + setIsDragOver, c, }); const handleCopyPath = (idx: number) => { @@ -120,14 +117,6 @@ const ChatInput = forwardRef(({ )} - {isUploading && ( - - - Attaching files… - - )} - setPicker((prev) => ({ ...prev, visible: false }))} @@ -170,8 +159,8 @@ const ChatInput = forwardRef(({ provider={provider} onProviderChange={onProviderChange} contextEstimate={contextEstimate} ownerId={ownerId} sessionId={sessionId} autoRunMode={autoRunMode} hasContent={hasContent} isRunning={isRunning} disabled={disabled} onSend={handleSend} onStop={onStop} - addImageFiles={addImageFiles} uploadAndAttachFiles={uploadAndAttachFiles} - generalFileInputRef={generalFileInputRef} queueLength={queueLength} /> + browseAndAttachFiles={browseAndAttachFiles} + queueLength={queueLength} /> ); }); diff --git a/frontend/src/app/pages/AgentChat/ModelModeSelector.tsx b/frontend/src/app/pages/AgentChat/ModelModeSelector.tsx index 21439cd9..6598ccc4 100644 --- a/frontend/src/app/pages/AgentChat/ModelModeSelector.tsx +++ b/frontend/src/app/pages/AgentChat/ModelModeSelector.tsx @@ -47,9 +47,7 @@ interface Props { autoRunMode?: boolean; hasContent: boolean; isRunning?: boolean; disabled?: boolean; onSend: () => void; onStop?: () => void; - addImageFiles: (files: FileList | File[]) => void; - uploadAndAttachFiles: (files: File[]) => void; - generalFileInputRef: React.RefObject; + browseAndAttachFiles: () => void; queueLength?: number; } @@ -57,7 +55,7 @@ const ModelModeSelector: React.FC = ({ mode, onModeChange, model, onModelChange, provider, onProviderChange, contextEstimate, ownerId, sessionId, autoRunMode, hasContent, isRunning, disabled, onSend, onStop, - addImageFiles, uploadAndAttachFiles, generalFileInputRef, queueLength = 0, + browseAndAttachFiles, queueLength = 0, }) => { const c = useClaudeTokens(); const dispatch = useAppDispatch(); @@ -189,17 +187,8 @@ const ModelModeSelector: React.FC = ({ )} - } type="file" multiple hidden onChange={(e) => { - if (!e.target.files) return; - const all = Array.from(e.target.files); - const imgs = all.filter((f) => f.type.startsWith('image/')); - const rest = all.filter((f) => !f.type.startsWith('image/')); - if (imgs.length > 0) addImageFiles(imgs); - if (rest.length > 0) uploadAndAttachFiles(rest); - e.target.value = ''; - }} /> - generalFileInputRef.current?.click()} + diff --git a/frontend/src/app/pages/AgentChat/composer/OpenSwarmComposer.tsx b/frontend/src/app/pages/AgentChat/composer/OpenSwarmComposer.tsx index 11d49fb2..07fd402d 100644 --- a/frontend/src/app/pages/AgentChat/composer/OpenSwarmComposer.tsx +++ b/frontend/src/app/pages/AgentChat/composer/OpenSwarmComposer.tsx @@ -92,7 +92,7 @@ const OpenSwarmComposer: FC = ({ onModeChange(item.id); return true; case 'file': - att.generalFileInputRef.current?.click(); + att.browseAndAttachFiles(); return true; case 'tool-group': case 'output': @@ -143,15 +143,13 @@ const OpenSwarmComposer: FC = ({ mode={mode} onModeChange={onModeChange} model={model} onModelChange={onModelChange} contextEstimate={contextEstimate} ownerId={sessionId || 'composer'} sessionId={sessionId} hasContent={hasContent} isRunning={isRunning} onSend={handleSendClick} onStop={onStop} - addImageFiles={att.addImageFiles} uploadAndAttachFiles={att.uploadAndAttachFiles} - generalFileInputRef={att.generalFileInputRef} queueLength={queueLength} + browseAndAttachFiles={att.browseAndAttachFiles} + queueLength={queueLength} /> - - ); }; diff --git a/frontend/src/app/pages/AgentChat/composer/useComposerAttachments.ts b/frontend/src/app/pages/AgentChat/composer/useComposerAttachments.ts index 6a74db4c..422af839 100644 --- a/frontend/src/app/pages/AgentChat/composer/useComposerAttachments.ts +++ b/frontend/src/app/pages/AgentChat/composer/useComposerAttachments.ts @@ -1,5 +1,4 @@ -import { useState, useCallback, useRef } from 'react'; -import { API_BASE } from '@/shared/config'; +import { useState, useCallback } from 'react'; import type { ContextPath } from '@/shared/state/agentsTypes'; interface AttachedImage { @@ -20,14 +19,15 @@ interface AttachedSkill { content: string; } +const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']); +const isImagePath = (p: string) => IMAGE_EXTS.has(p.slice(p.lastIndexOf('.')).toLowerCase()); + export function useComposerAttachments() { const [images, setImages] = useState([]); const [contextPaths, setContextPaths] = useState([]); const [forcedTools, setForcedTools] = useState([]); const [attachedSkills, setAttachedSkills] = useState>({}); - const [isUploading, setIsUploading] = useState(false); const [isDragOver, setIsDragOver] = useState(false); - const generalFileInputRef = useRef(null); const addImageFiles = useCallback((files: FileList | File[]) => { Array.from(files).forEach((file) => { @@ -44,30 +44,32 @@ export function useComposerAttachments() { }); }, []); - const uploadAndAttachFiles = useCallback(async (files: File[]) => { - if (files.length === 0) return; - setIsUploading(true); - try { - const formData = new FormData(); - files.forEach((f) => formData.append('files', f)); - const resp = await fetch(`${API_BASE}/settings/upload-files`, { - method: 'POST', - body: formData, - }); - if (!resp.ok) throw new Error('Upload failed'); - const data = await resp.json(); - const newPaths: ContextPath[] = (data.files || []).map((f: { path: string }) => ({ - path: f.path, - type: 'file' as const, - })); - setContextPaths((prev) => [...prev, ...newPaths]); - } catch (err) { - console.error('File upload failed:', err); - } finally { - setIsUploading(false); - } + const attachFilePaths = useCallback((paths: string[]) => { + if (paths.length === 0) return; + const newPaths: ContextPath[] = paths + .filter((p) => !isImagePath(p)) + .map((p) => ({ path: p, type: 'file' as const })); + if (newPaths.length > 0) setContextPaths((prev) => [...prev, ...newPaths]); }, []); + /** Attach non-image files using their native Electron File.path. */ + const attachFiles = useCallback((files: File[]) => { + if (files.length === 0) return; + const paths = files + .map((f) => (f as File & { path?: string }).path) + .filter((p): p is string => Boolean(p)); + attachFilePaths(paths); + }, [attachFilePaths]); + + /** Open native OS file picker and attach selected files as context paths. */ + const browseAndAttachFiles = useCallback(async () => { + const result = await window.openswarm.showOpenDialog({ + properties: ['openFile', 'multiSelections'], + }); + if (result.canceled || !result.filePaths?.length) return; + attachFilePaths(result.filePaths); + }, [attachFilePaths]); + const removeImage = useCallback( (idx: number) => setImages((prev) => prev.filter((_, i) => i !== idx)), [], @@ -121,31 +123,16 @@ export function useComposerAttachments() { const imageFiles = allFiles.filter((f) => f.type.startsWith('image/')); const otherFiles = allFiles.filter((f) => !f.type.startsWith('image/')); if (imageFiles.length > 0) addImageFiles(imageFiles); - if (otherFiles.length > 0) uploadAndAttachFiles(otherFiles); + if (otherFiles.length > 0) attachFiles(otherFiles); }, - [addImageFiles, uploadAndAttachFiles], - ); - - const handleFileInputChange = useCallback( - (e: React.ChangeEvent) => { - const files = e.target.files; - if (!files || files.length === 0) return; - const allFiles = Array.from(files); - const imgFiles = allFiles.filter((f) => f.type.startsWith('image/')); - const otherFiles = allFiles.filter((f) => !f.type.startsWith('image/')); - if (imgFiles.length > 0) addImageFiles(imgFiles); - if (otherFiles.length > 0) uploadAndAttachFiles(otherFiles); - e.target.value = ''; - }, - [addImageFiles, uploadAndAttachFiles], + [addImageFiles, attachFiles], ); return { - images, contextPaths, forcedTools, attachedSkills, isUploading, isDragOver, - generalFileInputRef, + images, contextPaths, forcedTools, attachedSkills, isDragOver, setImages, setContextPaths, setForcedTools, setAttachedSkills, - addImageFiles, uploadAndAttachFiles, removeImage, removeContextPath, + addImageFiles, attachFiles, browseAndAttachFiles, removeImage, removeContextPath, removeForcedTool, removeSkill, clearAll, - handleDragOver, handleDragLeave, handleDrop, handleFileInputChange, + handleDragOver, handleDragLeave, handleDrop, }; } diff --git a/frontend/src/app/pages/AgentChat/hooks/useChatSubmit.ts b/frontend/src/app/pages/AgentChat/hooks/useChatSubmit.ts index ca134667..78b2a82c 100644 --- a/frontend/src/app/pages/AgentChat/hooks/useChatSubmit.ts +++ b/frontend/src/app/pages/AgentChat/hooks/useChatSubmit.ts @@ -3,7 +3,6 @@ import type { CommandPickerItem } from '@/app/components/CommandPicker'; import { useElementSelection, type SelectedElement } from '@/app/components/ElementSelectionContext'; import { getClipboardCards, clearClipboard } from '@/shared/dashboardClipboard'; import { getWebview } from '@/shared/browserRegistry'; -import { API_BASE } from '@/shared/config'; import type { ContextPath } from '@/shared/state/agentsTypes'; import { SKILL_PILL_ATTR, type AttachedSkill, createSkillPillElement, @@ -14,7 +13,6 @@ import type { ForcedToolGroup } from '../AttachmentChips'; interface ChatSubmitParams { editorRef: React.RefObject; attachedSkillsRef: React.MutableRefObject>; - generalFileInputRef: React.RefObject; disabled?: boolean; autoRunMode?: boolean; images: AttachedImage[]; contextPaths: ContextPath[]; forcedTools: ForcedToolGroup[]; picker: TriggerState; skills: Record; ownerId: string; @@ -24,17 +22,17 @@ interface ChatSubmitParams { setImages: React.Dispatch>; setContextPaths: React.Dispatch>; setForcedTools: React.Dispatch>; setPicker: React.Dispatch>; setHasContent: React.Dispatch>; setAttachedSkills: React.Dispatch>>; - setIsUploading: React.Dispatch>; setIsDragOver: React.Dispatch>; + setIsDragOver: React.Dispatch>; c: { font: { mono: string }; status: { error: string } }; } export function useChatSubmit(p: ChatSubmitParams) { const { - editorRef, attachedSkillsRef, generalFileInputRef, disabled, autoRunMode, + editorRef, attachedSkillsRef, disabled, autoRunMode, images, contextPaths, forcedTools, picker, skills, ownerId, elementSelection, onSend, onModeChange, setImages, setContextPaths, setForcedTools, setPicker, setHasContent, setAttachedSkills, - setIsUploading, setIsDragOver, c, + setIsDragOver, c, } = p; const updateHasContent = useCallback(() => { const editor = editorRef.current; @@ -81,19 +79,21 @@ export function useChatSubmit(p: ChatSubmitParams) { reader.readAsDataURL(file); }); }, []); - const uploadAndAttachFiles = useCallback(async (files: File[]) => { + const attachFiles = useCallback((files: File[]) => { if (files.length === 0) return; - setIsUploading(true); - try { - const formData = new FormData(); - files.forEach((f) => formData.append('files', f)); - const resp = await fetch(`${API_BASE}/settings/upload-files`, { method: 'POST', body: formData }); - if (!resp.ok) throw new Error('Upload failed'); - const data = await resp.json(); - const newPaths: ContextPath[] = (data.files || []).map((f: { path: string }) => ({ path: f.path, type: 'file' as const })); - setContextPaths((prev) => [...prev, ...newPaths]); - } catch (err) { console.error('File upload failed:', err); } - finally { setIsUploading(false); } + const newPaths: ContextPath[] = files + .map((f) => (f as File & { path?: string }).path) + .filter((p): p is string => Boolean(p)) + .map((p) => ({ path: p, type: 'file' as const })); + if (newPaths.length > 0) setContextPaths((prev) => [...prev, ...newPaths]); + }, []); + const browseAndAttachFiles = useCallback(async () => { + const result = await window.openswarm.showOpenDialog({ + properties: ['openFile', 'multiSelections'], + }); + if (result.canceled || !result.filePaths?.length) return; + const newPaths: ContextPath[] = result.filePaths.map((p) => ({ path: p, type: 'file' as const })); + setContextPaths((prev) => [...prev, ...newPaths]); }, []); const handleSend = useCallback(async () => { const editor = editorRef.current; @@ -178,7 +178,7 @@ export function useChatSubmit(p: ChatSubmitParams) { } else if (item.type === 'mode') { onModeChange(item.id); } else if (item.type === 'context') { - if (item.command === 'file') generalFileInputRef.current?.click(); + if (item.command === 'file') { browseAndAttachFiles(); return; } else if (item.toolNames && item.toolNames.length > 0) setForcedTools((prev) => [...prev, { label: item.name, tools: item.toolNames!, icon: item.icon, iconKey: item.iconKey }]); else document.execCommand('insertText', false, `@${item.command} `); } @@ -233,9 +233,9 @@ export function useChatSubmit(p: ChatSubmitParams) { const imageFiles = allFiles.filter((f) => f.type.startsWith('image/')); const otherFiles = allFiles.filter((f) => !f.type.startsWith('image/')); if (imageFiles.length > 0) addImageFiles(imageFiles); - if (otherFiles.length > 0) uploadAndAttachFiles(otherFiles); - }, [addImageFiles, uploadAndAttachFiles]); + if (otherFiles.length > 0) attachFiles(otherFiles); + }, [addImageFiles, attachFiles]); const removeImage = useCallback((idx: number) => setImages((prev) => prev.filter((_, i) => i !== idx)), []); - return { handleSend, handlePickerSelect, handlePaste, handleKeyDown, handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop, addImageFiles, uploadAndAttachFiles, removeImage }; + return { handleSend, handlePickerSelect, handlePaste, handleKeyDown, handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop, addImageFiles, attachFiles, browseAndAttachFiles, removeImage }; } diff --git a/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts b/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts index 12b783ee..737f2bd6 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts +++ b/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts @@ -11,7 +11,7 @@ import { EXPANDED_CARD_MIN_H, } from '@/shared/state/dashboardLayoutSlice'; import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; -import { dashboardWs } from '@/shared/ws/WebSocketManager'; +import { agentsWs } from '@/shared/ws/WebSocketManager'; import { initBrowserCommandHandler } from '@/shared/browserCommandHandler'; import { clearPendingBrowserUrl, clearPendingFocusAgentId } from '@/shared/state/tempStateSlice'; import type { CanvasActions } from '../useCanvasControls'; @@ -53,9 +53,9 @@ export function useDashboardInit(deps: InitDeps) { dispatch(GET_HISTORY({})); dispatch(GET_DASHBOARD(dashboardId)); dispatch(LIST_APPS()); - dashboardWs.connect(); + agentsWs.connect(); const cleanupBrowserHandler = initBrowserCommandHandler(); - return () => { cleanupBrowserHandler(); dashboardWs.disconnect(); }; + return () => { cleanupBrowserHandler(); agentsWs.disconnect(); }; }, [dispatch, dashboardId]); useEffect(() => { diff --git a/frontend/src/app/pages/Settings/hooks/useSettings.ts b/frontend/src/app/pages/Settings/hooks/useSettings.ts index bd6c1215..6e56abb9 100644 --- a/frontend/src/app/pages/Settings/hooks/useSettings.ts +++ b/frontend/src/app/pages/Settings/hooks/useSettings.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { UPDATE_SETTINGS, AppSettings } from '@/shared/backend-bridge/apps/settings'; import { closeSettingsModal } from '@/shared/state/settingsSlice'; -import { fetchModels } from '@/shared/state/modelsSlice'; +// import { fetchModels } from '@/shared/state/modelsSlice'; import { setChecking, setUpdateError } from '@/shared/state/updateSlice'; import { LIST_MODES } from '@/shared/state/modesSlice'; import { useClaudeTokens, useThemeMode } from '@/shared/styles/ThemeContext'; @@ -35,7 +35,7 @@ export function useSettings() { const handleSave = async () => { await dispatch(UPDATE_SETTINGS(form)); if (form.theme !== settings.theme) setThemeMode(form.theme); - dispatch(fetchModels()); + // dispatch(fetchModels()); setSaved(true); }; const handleRequestClose = useCallback(() => { @@ -50,7 +50,7 @@ export function useSettings() { const handleSaveAndClose = useCallback(async () => { await dispatch(UPDATE_SETTINGS(form)); if (form.theme !== settings.theme) setThemeMode(form.theme); - dispatch(fetchModels()); + // dispatch(fetchModels()); setSaved(true); setConfirmDiscard(false); dispatch(closeSettingsModal()); diff --git a/frontend/src/app/pages/Tools/hooks/useToolsState.ts b/frontend/src/app/pages/Tools/hooks/useToolsState.ts index 17c700e0..87780d38 100644 --- a/frontend/src/app/pages/Tools/hooks/useToolsState.ts +++ b/frontend/src/app/pages/Tools/hooks/useToolsState.ts @@ -6,11 +6,16 @@ import { 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 { searchRegistry, fetchRegistryStats, fetchServerDetail, McpServer } from '@/shared/state/mcpRegistrySlice'; import { LIST_APPS, UPDATE_APP } from '@/shared/backend-bridge/apps/app_builder'; import { INTEGRATIONS, Integration, CATEGORY_ORDER } from '../integrations'; import { ToolForm, emptyForm, serverToToolForm, serverToMcpConfig, groupTools } from '../toolUtils'; +const clearDetail = () => { + return { + type: 'mcpRegistry/clearDetail', + }; +}; export function useToolsState() { const dispatch = useAppDispatch(); diff --git a/frontend/src/shared/backend-bridge/apps/agents.ts b/frontend/src/shared/backend-bridge/apps/agents.ts index 3370128b..a0b0e07f 100644 --- a/frontend/src/shared/backend-bridge/apps/agents.ts +++ b/frontend/src/shared/backend-bridge/apps/agents.ts @@ -6,6 +6,8 @@ import type { const AGENTS_API: string = `${API_BASE}/agents`; +export const AGENTS_WS_API: string = `${API_BASE}/agents/ws`; + // --------------------------------------------------------------------------- // Session CRUD // --------------------------------------------------------------------------- diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index 0e41a2cb..e01968d4 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -1,5 +1,5 @@ import { getWebview } from './browserRegistry'; -import { dashboardWs } from './ws/WebSocketManager'; +import { agentsWs } from './ws/WebSocketManager'; import { type BrowserAction, setActivity } from './browserCommandTypes'; import { handleScreenshot, @@ -24,7 +24,7 @@ async function handleBrowserCommand(data: Record) { const wv = getWebview(browser_id, tab_id || undefined); if (!wv) { - dashboardWs.send('browser:result', { + agentsWs.send('browser:result', { request_id, error: `Browser card '${browser_id}'${tab_id ? ` tab '${tab_id}'` : ''} not found or not an Electron webview`, }); @@ -79,13 +79,13 @@ async function handleBrowserCommand(data: Record) { } setActivity(browser_id, null); - dashboardWs.send('browser:result', { request_id, ...result }); + agentsWs.send('browser:result', { request_id, ...result }); } export function initBrowserCommandHandler(): () => void { if (initialized) return () => {}; initialized = true; - const unsub = dashboardWs.on('browser:command', handleBrowserCommand); + const unsub = agentsWs.on('browser:command', handleBrowserCommand); return () => { unsub(); initialized = false; diff --git a/frontend/src/shared/state/mcpRegistrySlice.ts b/frontend/src/shared/state/mcpRegistrySlice.ts index c7fdc11f..e06fbbeb 100644 --- a/frontend/src/shared/state/mcpRegistrySlice.ts +++ b/frontend/src/shared/state/mcpRegistrySlice.ts @@ -1,7 +1,14 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { API_BASE } from '@/shared/config'; -const MCP_REGISTRY_API = `${API_BASE}/mcp-registry`; + + +// TODO: Re-implement the mcp registry (needs to also be implemented in the backend) + + +// import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +// import { API_BASE } from '@/shared/config'; + +// const MCP_REGISTRY_API = `${API_BASE}/mcp-registry`; export interface McpServer { name: string; @@ -17,99 +24,114 @@ export interface McpServer { source: string; } -interface McpServerDetail extends McpServer { - environmentVariables: { name: string; description: string; default?: string; format?: string }[]; - keywords: string[]; - license: string; -} +// interface McpServerDetail extends McpServer { +// environmentVariables: { name: string; description: string; default?: string; format?: string }[]; +// keywords: string[]; +// license: string; +// } -interface McpRegistryState { - servers: McpServer[]; - total: number; - loading: boolean; - query: string; - offset: number; - stats: { total: number; google: number; community: number; lastUpdated: number } | null; - detail: McpServerDetail | null; - detailLoading: boolean; -} +// interface McpRegistryState { +// servers: McpServer[]; +// total: number; +// loading: boolean; +// query: string; +// offset: number; +// stats: { total: number; google: number; community: number; lastUpdated: number } | null; +// detail: McpServerDetail | null; +// detailLoading: boolean; +// } -const initialState: McpRegistryState = { - servers: [], - total: 0, - loading: false, - query: '', - offset: 0, - stats: null, - detail: null, - detailLoading: false, -}; +// const initialState: McpRegistryState = { +// servers: [], +// total: 0, +// loading: false, +// query: '', +// offset: 0, +// stats: null, +// detail: null, +// detailLoading: false, +// }; export const searchRegistry = createAsyncThunk( 'mcpRegistry/search', - async ({ q, limit = 20, offset = 0, sort = 'name', source = '' }: { q: string; limit?: number; offset?: number; sort?: string; source?: string }) => { - const params = new URLSearchParams({ q, limit: String(limit), offset: String(offset), sort, source }); - const res = await fetch(`${MCP_REGISTRY_API}/search?${params}`); - return (await res.json()) as { servers: McpServer[]; total: number; offset: number; limit: number }; + // async ({ q, limit = 20, offset = 0, sort = 'name', source = '' }: { q: string; limit?: number; offset?: number; sort?: string; source?: string }) => { + // const params = new URLSearchParams({ q, limit: String(limit), offset: String(offset), sort, source }); + // const res = await fetch(`${MCP_REGISTRY_API}/search?${params}`); + // return (await res.json()) as { servers: McpServer[]; total: number; offset: number; limit: number }; + // } + // Throw not implemented error + async () => { + throw new Error('Not implemented'); } ); -export const fetchRegistryStats = createAsyncThunk('mcpRegistry/stats', async () => { - const res = await fetch(`${MCP_REGISTRY_API}/stats`); - return (await res.json()) as { total: number; google: number; community: number; lastUpdated: number }; -}); +export const fetchRegistryStats = createAsyncThunk( + 'mcpRegistry/stats', + // async () => { + // const res = await fetch(`${MCP_REGISTRY_API}/stats`); + // return (await res.json()) as { total: number; google: number; community: number; lastUpdated: number }; + // } + // Throw not implemented error + async () => { + throw new Error('Not implemented'); + } +); export const fetchServerDetail = createAsyncThunk( 'mcpRegistry/detail', + // async (name: string) => { + // const res = await fetch(`${MCP_REGISTRY_API}/detail/${encodeURIComponent(name)}`); + // const data = await res.json(); + // return data.server as McpServerDetail; + // } + // Throw not implemented error async (name: string) => { - const res = await fetch(`${MCP_REGISTRY_API}/detail/${encodeURIComponent(name)}`); - const data = await res.json(); - return data.server as McpServerDetail; + throw new Error('Not implemented but arg is: ' + name); } ); -const mcpRegistrySlice = createSlice({ - name: 'mcpRegistry', - initialState, - reducers: { - clearDetail(state) { - state.detail = null; - }, - }, - extraReducers: (builder) => { - builder - .addCase(searchRegistry.pending, (state, action) => { - state.loading = true; - state.query = action.meta.arg.q; - state.offset = action.meta.arg.offset ?? 0; - }) - .addCase(searchRegistry.fulfilled, (state, action) => { - state.loading = false; - if (action.meta.arg.offset && action.meta.arg.offset > 0) { - state.servers = [...state.servers, ...action.payload.servers]; - } else { - state.servers = action.payload.servers; - } - state.total = action.payload.total; - }) - .addCase(searchRegistry.rejected, (state) => { - state.loading = false; - }) - .addCase(fetchRegistryStats.fulfilled, (state, action) => { - state.stats = action.payload; - }) - .addCase(fetchServerDetail.pending, (state) => { - state.detailLoading = true; - }) - .addCase(fetchServerDetail.fulfilled, (state, action) => { - state.detailLoading = false; - state.detail = action.payload; - }) - .addCase(fetchServerDetail.rejected, (state) => { - state.detailLoading = false; - }); - }, -}); +// const mcpRegistrySlice = createSlice({ +// name: 'mcpRegistry', +// initialState, +// reducers: { +// clearDetail(state) { +// state.detail = null; +// }, +// }, +// extraReducers: (builder) => { +// builder +// .addCase(searchRegistry.pending, (state, action) => { +// state.loading = true; +// state.query = action.meta.arg.q; +// state.offset = action.meta.arg.offset ?? 0; +// }) +// .addCase(searchRegistry.fulfilled, (state, action) => { +// state.loading = false; +// if (action.meta.arg.offset && action.meta.arg.offset > 0) { +// state.servers = [...state.servers, ...action.payload.servers]; +// } else { +// state.servers = action.payload.servers; +// } +// state.total = action.payload.total; +// }) +// .addCase(searchRegistry.rejected, (state) => { +// state.loading = false; +// }) +// .addCase(fetchRegistryStats.fulfilled, (state, action) => { +// state.stats = action.payload; +// }) +// .addCase(fetchServerDetail.pending, (state) => { +// state.detailLoading = true; +// }) +// .addCase(fetchServerDetail.fulfilled, (state, action) => { +// state.detailLoading = false; +// state.detail = action.payload; +// }) +// .addCase(fetchServerDetail.rejected, (state) => { +// state.detailLoading = false; +// }); +// }, +// }); -export const { clearDetail } = mcpRegistrySlice.actions; -export default mcpRegistrySlice.reducer; +// export const { clearDetail } = mcpRegistrySlice.actions; +// export default mcpRegistrySlice.reducer; diff --git a/frontend/src/shared/state/modelsSlice.ts b/frontend/src/shared/state/modelsSlice.ts deleted file mode 100644 index 5d1677bf..00000000 --- a/frontend/src/shared/state/modelsSlice.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { API_BASE } from '@/shared/config'; - -const AGENTS_API = `${API_BASE}/agents`; - -interface ModelOption { - value: string; - label: string; - version?: string; - context_window: number; -} - -interface ModelsState { - byProvider: Record; - loaded: boolean; -} - -const initialState: ModelsState = { - byProvider: {}, - loaded: false, -}; - -export const fetchModels = createAsyncThunk('models/fetchModels', async () => { - const res = await fetch(`${AGENTS_API}/models`); - if (!res.ok) throw new Error('Failed to fetch models'); - const data = await res.json(); - // API returns { models: { provider: [...] } } - const models = data.models || data; - return models as Record; -}); - -const modelsSlice = createSlice({ - name: 'models', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder - .addCase(fetchModels.fulfilled, (state, action) => { - state.byProvider = action.payload; - state.loaded = true; - }) - .addCase(fetchModels.rejected, (state) => { - // Mark as loaded even on failure so we fall back to hardcoded options - state.loaded = true; - }); - }, -}); - -export default modelsSlice.reducer; diff --git a/frontend/src/shared/state/store.ts b/frontend/src/shared/state/store.ts index 6e78f1cd..a27fd919 100644 --- a/frontend/src/shared/state/store.ts +++ b/frontend/src/shared/state/store.ts @@ -5,12 +5,12 @@ import skillsReducer from './skillsSlice'; import toolsReducer from './toolsSlice'; import modesReducer from './modesSlice'; import settingsReducer from './settingsSlice'; -import mcpRegistryReducer from './mcpRegistrySlice'; +// import mcpRegistryReducer from './mcpRegistrySlice'; import skillRegistryReducer from './skillRegistrySlice'; import dashboardLayoutReducer from './dashboardLayoutSlice'; import dashboardsReducer from './dashboardsSlice'; import updateReducer from './updateSlice'; -import modelsReducer from './modelsSlice'; +// import modelsReducer from './modelsSlice'; export const store = configureStore({ reducer: { @@ -20,12 +20,12 @@ export const store = configureStore({ tools: toolsReducer, modes: modesReducer, settings: settingsReducer, - mcpRegistry: mcpRegistryReducer, + // mcpRegistry: mcpRegistryReducer, skillRegistry: skillRegistryReducer, dashboardLayout: dashboardLayoutReducer, dashboards: dashboardsReducer, update: updateReducer, - models: modelsReducer, + // models: modelsReducer, }, }); diff --git a/frontend/src/shared/ws/WebSocketManager.ts b/frontend/src/shared/ws/WebSocketManager.ts index 1422bafc..632ea370 100644 --- a/frontend/src/shared/ws/WebSocketManager.ts +++ b/frontend/src/shared/ws/WebSocketManager.ts @@ -1,6 +1,7 @@ import { store } from '../state/store'; import { streamDelta } from '../state/agentsSlice'; import { type WSEvent, dispatchWsEvent } from './wsEventHandlers'; +import { AGENTS_WS_API } from '@/shared/backend-bridge/apps/agents'; class WebSocketManager { private ws: WebSocket | null = null; @@ -115,6 +116,5 @@ class WebSocketManager { } } -import { WS_BASE } from '@/shared/config'; -export const dashboardWs = new WebSocketManager(`${WS_BASE}/ws/dashboard`); \ No newline at end of file +export const agentsWs = new WebSocketManager(AGENTS_WS_API); \ No newline at end of file diff --git a/frontend/src/types/electron.d.ts b/frontend/src/types/electron.d.ts index ddc99fb7..a8c885ef 100644 --- a/frontend/src/types/electron.d.ts +++ b/frontend/src/types/electron.d.ts @@ -35,6 +35,11 @@ declare global { getBackendPort: () => number; getWebviewPreloadPath: () => string; getAppVersion: () => Promise; + showOpenDialog: (options: { + properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'>; + defaultPath?: string; + filters?: Array<{ name: string; extensions: string[] }>; + }) => Promise<{ canceled: boolean; filePaths: string[] }>; getUpdateStatus: () => Promise<{ status: string; info: any; error: string | null }>; checkForUpdates: () => Promise<{ success: boolean; version?: string; error?: string }>; downloadUpdate: () => Promise<{ success: boolean; error?: string }>;