[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

This commit is contained in:
haikdc
2026-04-18 06:52:50 -07:00
parent 0d3c515616
commit ad8add82fd
17 changed files with 202 additions and 254 deletions
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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');
+5 -16
View File
@@ -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<ChatInputHandle, Props>(({
const c = useClaudeTokens();
const editorRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const generalFileInputRef = useRef<HTMLInputElement>(null);
const elementSelection = useElementSelection();
const fallbackOwnerId = useId();
@@ -60,7 +58,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({
const [images, setImages] = useState<AttachedImage[]>([]);
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null);
const [isDragOver, setIsDragOver] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [contextPaths, setContextPaths] = useState<ContextPath[]>([]);
const [forcedTools, setForcedTools] = useState<ForcedToolGroup[]>([]);
const [copiedPathIdx, setCopiedPathIdx] = useState<number | null>(null);
@@ -82,13 +79,13 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({
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<ChatInputHandle, Props>(({
</Box>
)}
{isUploading && (
<Box sx={{ position: 'absolute', inset: 0, bgcolor: 'rgba(174,86,48,0.04)', zIndex: 10,
display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '16px', pointerEvents: 'none' }}>
<CircularProgress size={14} sx={{ color: c.accent.primary, mr: 1 }} />
<Typography sx={{ color: c.accent.primary, fontSize: '0.85rem', fontWeight: 500 }}>Attaching files</Typography>
</Box>
)}
<CommandPicker trigger={picker.trigger} filter={picker.filter}
onSelect={handlePickerSelect}
onClose={() => setPicker((prev) => ({ ...prev, visible: false }))}
@@ -170,8 +159,8 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({
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} />
</Box>
);
});
@@ -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<HTMLInputElement | null>;
browseAndAttachFiles: () => void;
queueLength?: number;
}
@@ -57,7 +55,7 @@ const ModelModeSelector: React.FC<Props> = ({
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<Props> = ({
</Tooltip>
)}
<input ref={generalFileInputRef as React.RefObject<HTMLInputElement>} 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 = '';
}} />
<Tooltip title="Attach file">
<IconButton size="small" onClick={() => generalFileInputRef.current?.click()}
<IconButton size="small" onClick={browseAndAttachFiles}
sx={{ color: c.text.tertiary, p: 0.5, '&:hover': { color: c.text.secondary, bgcolor: 'rgba(0,0,0,0.04)' } }}>
<AttachFileIcon sx={{ fontSize: 18 }} />
</IconButton>
@@ -92,7 +92,7 @@ const OpenSwarmComposer: FC<OpenSwarmComposerProps> = ({
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<OpenSwarmComposerProps> = ({
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}
/>
</div>
</div>
</ComposerPrimitive.Root>
</ComposerPrimitive.Unstable_MentionRoot>
<input ref={att.generalFileInputRef} type="file" multiple className="hidden" onChange={att.handleFileInputChange} />
</div>
);
};
@@ -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<AttachedImage[]>([]);
const [contextPaths, setContextPaths] = useState<ContextPath[]>([]);
const [forcedTools, setForcedTools] = useState<ForcedToolGroup[]>([]);
const [attachedSkills, setAttachedSkills] = useState<Record<string, AttachedSkill>>({});
const [isUploading, setIsUploading] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const generalFileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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,
};
}
@@ -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<HTMLDivElement | null>; attachedSkillsRef: React.MutableRefObject<Record<string, AttachedSkill>>;
generalFileInputRef: React.RefObject<HTMLInputElement | null>;
disabled?: boolean; autoRunMode?: boolean; images: AttachedImage[]; contextPaths: ContextPath[];
forcedTools: ForcedToolGroup[]; picker: TriggerState;
skills: Record<string, { id: string; name: string; content: string }>; ownerId: string;
@@ -24,17 +22,17 @@ interface ChatSubmitParams {
setImages: React.Dispatch<React.SetStateAction<AttachedImage[]>>; setContextPaths: React.Dispatch<React.SetStateAction<ContextPath[]>>;
setForcedTools: React.Dispatch<React.SetStateAction<ForcedToolGroup[]>>; setPicker: React.Dispatch<React.SetStateAction<TriggerState>>;
setHasContent: React.Dispatch<React.SetStateAction<boolean>>; setAttachedSkills: React.Dispatch<React.SetStateAction<Record<string, AttachedSkill>>>;
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>; setIsDragOver: React.Dispatch<React.SetStateAction<boolean>>;
setIsDragOver: React.Dispatch<React.SetStateAction<boolean>>;
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 };
}
@@ -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(() => {
@@ -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());
@@ -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();
@@ -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
// ---------------------------------------------------------------------------
+4 -4
View File
@@ -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<string, any>) {
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<string, any>) {
}
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;
+105 -83
View File
@@ -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;
-49
View File
@@ -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<string, ModelOption[]>;
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<string, ModelOption[]>;
});
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;
+4 -4
View File
@@ -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,
},
});
+2 -2
View File
@@ -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`);
export const agentsWs = new WebSocketManager(AGENTS_WS_API);
+5
View File
@@ -35,6 +35,11 @@ declare global {
getBackendPort: () => number;
getWebviewPreloadPath: () => string;
getAppVersion: () => Promise<string>;
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 }>;