From abf44260caaad02f5fb265fb227836214535f19e Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 23 Feb 2026 11:14:00 -0600 Subject: [PATCH 1/2] feat: add global worker agent templates and improve agent config ui --- electron/main/index.ts | 82 ++++++++ electron/preload/index.ts | 5 + src/components/AddWorker/index.tsx | 253 +++++++++++++++++++++++- src/components/WorkFlow/node.tsx | 39 ++++ src/i18n/locales/en-us/agents.json | 17 +- src/i18n/locales/en-us/workforce.json | 5 +- src/pages/Agents/GlobalAgents.tsx | 260 +++++++++++++++++++++++++ src/pages/Agents/index.tsx | 8 +- src/store/globalAgentTemplatesStore.ts | 158 +++++++++++++++ src/types/electron.d.ts | 18 ++ 10 files changed, 835 insertions(+), 10 deletions(-) create mode 100644 src/pages/Agents/GlobalAgents.tsx create mode 100644 src/store/globalAgentTemplatesStore.ts diff --git a/electron/main/index.ts b/electron/main/index.ts index adad48041..943e4fb4a 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1621,6 +1621,88 @@ function registerIpcHandlers() { } ); + // ======================== agent-templates (global Worker Agent templates) ======================== + const AGENT_TEMPLATES_FILE = 'agent-templates.json'; + + function getAgentTemplatesPath(userId: string): string { + return path.join(os.homedir(), '.eigent', userId, AGENT_TEMPLATES_FILE); + } + + async function loadAgentTemplates(userId: string): Promise<{ + version: number; + templates: Array<{ + id: string; + name: string; + description: string; + tools: string[]; + mcp_tools: any; + custom_model_config?: any; + updatedAt: number; + }>; + }> { + const configPath = getAgentTemplatesPath(userId); + const defaultData = { version: 1, templates: [] }; + if (!existsSync(configPath)) { + try { + await fsp.mkdir(path.dirname(configPath), { recursive: true }); + await fsp.writeFile( + configPath, + JSON.stringify(defaultData, null, 2), + 'utf-8' + ); + return defaultData; + } catch (error: any) { + log.error('Failed to create default agent-templates', error); + return defaultData; + } + } + try { + const content = await fsp.readFile(configPath, 'utf-8'); + const data = JSON.parse(content); + if (!Array.isArray(data.templates)) data.templates = []; + return { version: data.version ?? 1, templates: data.templates }; + } catch (error: any) { + log.error('Failed to load agent-templates', error); + return defaultData; + } + } + + async function saveAgentTemplates( + userId: string, + data: { version: number; templates: any[] } + ): Promise { + const configPath = getAgentTemplatesPath(userId); + await fsp.mkdir(path.dirname(configPath), { recursive: true }); + await fsp.writeFile(configPath, JSON.stringify(data, null, 2), 'utf-8'); + } + + ipcMain.handle('agent-templates-load', async (_event, userId: string) => { + try { + const data = await loadAgentTemplates(userId); + return { success: true, templates: data.templates }; + } catch (error: any) { + log.error('agent-templates-load failed', error); + return { success: false, error: error?.message, templates: [] }; + } + }); + + ipcMain.handle( + 'agent-templates-save', + async (_event, userId: string, templates: any[]) => { + try { + const current = await loadAgentTemplates(userId); + await saveAgentTemplates(userId, { + version: current.version, + templates, + }); + return { success: true }; + } catch (error: any) { + log.error('agent-templates-save failed', error); + return { success: false, error: error?.message }; + } + } + ); + // Initialize skills config for a user (ensures config file exists) ipcMain.handle('skill-config-init', async (_event, userId: string) => { try { diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 910a670d7..2668fc1c2 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -201,6 +201,11 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('skill-config-update', userId, skillName, skillConfig), skillConfigDelete: (userId: string, skillName: string) => ipcRenderer.invoke('skill-config-delete', userId, skillName), + // Global Agent Templates (~/.eigent//agent-templates.json) + agentTemplatesLoad: (userId: string) => + ipcRenderer.invoke('agent-templates-load', userId), + agentTemplatesSave: (userId: string, templates: any[]) => + ipcRenderer.invoke('agent-templates-save', userId, templates), }); // --------- Preload scripts loading --------- diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 64fc05f49..30a4e6aa3 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -35,9 +35,24 @@ import { Textarea } from '@/components/ui/textarea'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { INIT_PROVODERS } from '@/lib/llm'; import { useAuthStore, useWorkerList } from '@/store/authStore'; -import { Bot, ChevronDown, ChevronUp, Edit, Eye, EyeOff } from 'lucide-react'; -import { useRef, useState } from 'react'; +import { + hasGlobalAgentTemplatesApi, + useGlobalAgentTemplatesStore, +} from '@/store/globalAgentTemplatesStore'; +import { + Bot, + ChevronDown, + ChevronUp, + Download, + Edit, + Eye, + EyeOff, + FileUp, + Plus, +} from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import ToolSelect from './ToolSelect'; interface EnvValue { @@ -110,13 +125,124 @@ export function AddWorker({ const [customModelPlatform, setCustomModelPlatform] = useState(''); const [customModelType, setCustomModelType] = useState(''); - if (!chatStore) { - return null; - } + // Global template and export/import + const [saveAsGlobalTemplate, setSaveAsGlobalTemplate] = useState(false); + const [selectedTemplateId, setSelectedTemplateId] = useState(''); + const importFileRef = useRef(null); + const { + templates: globalTemplates, + loadTemplates: loadGlobalTemplates, + addTemplate: addGlobalTemplate, + getTemplate: getGlobalTemplate, + } = useGlobalAgentTemplatesStore(); + const hasGlobalTemplatesApi = hasGlobalAgentTemplatesApi(); + + useEffect(() => { + if (hasGlobalTemplatesApi && dialogOpen) loadGlobalTemplates(); + }, [hasGlobalTemplatesApi, dialogOpen, loadGlobalTemplates]); + + useEffect(() => { + if (selectedTemplateId && dialogOpen) { + const tpl = getGlobalTemplate(selectedTemplateId); + if (tpl) { + setWorkerName(tpl.name); + setWorkerDescription(tpl.description); + if (tpl.custom_model_config) { + setUseCustomModel(true); + setShowModelConfig(true); + setCustomModelPlatform(tpl.custom_model_config.model_platform ?? ''); + setCustomModelType(tpl.custom_model_config.model_type ?? ''); + } + } + } + }, [selectedTemplateId, dialogOpen, getGlobalTemplate]); + + const handleExportConfig = useCallback(() => { + const localTool: string[] = []; + const mcpList: string[] = []; + selectedTools.forEach((tool: McpItem) => { + if (tool.isLocal) { + localTool.push(tool.toolkit as string); + } else { + mcpList.push(tool?.key || tool?.mcp_name || ''); + } + }); + let mcpLocal: Record = { mcpServers: {} }; + selectedTools.forEach((tool: McpItem) => { + if (!tool.isLocal && tool.key) { + (mcpLocal.mcpServers as Record)[tool.key] = {}; + } + }); + const custom_model_config = + useCustomModel && customModelPlatform + ? { + model_platform: customModelPlatform, + model_type: customModelType || undefined, + } + : undefined; + const blob = new Blob( + [ + JSON.stringify( + { + name: workerName, + description: workerDescription, + tools: localTool, + mcp_tools: mcpLocal, + custom_model_config, + }, + null, + 2 + ), + ], + { type: 'application/json' } + ); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `agent-${workerName || 'config'}.json`; + a.click(); + URL.revokeObjectURL(url); + toast.success(t('workforce.save-changes')); + }, [ + selectedTools, + workerName, + workerDescription, + useCustomModel, + customModelPlatform, + customModelType, + t, + ]); + + const handleImportConfig = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + e.target.value = ''; + if (!file) return; + file.text().then((text) => { + try { + const data = JSON.parse(text); + setWorkerName(data.name ?? ''); + setWorkerDescription(data.description ?? ''); + if (data.custom_model_config) { + setUseCustomModel(true); + setShowModelConfig(true); + setCustomModelPlatform( + data.custom_model_config.model_platform ?? '' + ); + setCustomModelType(data.custom_model_config.model_type ?? ''); + } + toast.success(t('workforce.save-changes')); + } catch { + toast.error(t('agents.skill-add-error')); + } + }); + }, + [t] + ); const activeProjectId = projectStore.activeProjectId; - const activeTaskId = chatStore.activeTaskId; - const tasks = chatStore.tasks; + const activeTaskId = chatStore?.activeTaskId; + const tasks = chatStore?.tasks; // environment variable management const initializeEnvValues = (mcp: McpItem) => { @@ -269,6 +395,8 @@ export function AddWorker({ setUseCustomModel(false); setCustomModelPlatform(''); setCustomModelType(''); + setSaveAsGlobalTemplate(false); + setSelectedTemplateId(''); }; // tool function @@ -409,6 +537,24 @@ export function AddWorker({ setWorkerList([...workerList, worker]); } + if (saveAsGlobalTemplate && hasGlobalTemplatesApi) { + const customModelConfig = + useCustomModel && customModelPlatform + ? { + model_platform: customModelPlatform, + model_type: customModelType || undefined, + } + : undefined; + await addGlobalTemplate({ + name: workerName, + description: workerDescription, + tools: localTool, + mcp_tools: mcpLocal, + custom_model_config: customModelConfig, + }); + toast.success(t('agents.skill-added-success')); + } + setDialogOpen(false); // reset form @@ -569,6 +715,34 @@ export function AddWorker({ // default add interface <> + {hasGlobalTemplatesApi && + globalTemplates.length > 0 && + !edit && ( +
+ + +
+ )} +
@@ -598,8 +772,37 @@ export function AddWorker({ placeholder={t('layout.im-an-agent-specially-designed-for')} value={workerDescription} onChange={(e) => setWorkerDescription(e.target.value)} + className="min-h-[120px] resize-y" /> +
+ + + +
+ + {selectedTools.length > 0 && ( +
+
+ {t('workforce.agent-tool')} ({selectedTools.length}) +
+
+ {selectedTools.map((tool, idx) => ( + + {tool.name || + tool.mcp_name || + tool.key || + `Tool ${idx + 1}`} + + ))} +
+
+ )} + + {hasGlobalTemplatesApi && ( + + )} + {/* Model Configuration Section */}
+ + + + +
+ + {isLoading ? ( +
+ + Loading… +
+ ) : templates.length === 0 ? ( +
+

+ {t('agents.global-agents-empty')} +

+
+ ) : ( +
+ {templates.map((template) => ( +
+
+
+ {template.name} +
+
+ {template.description || '—'} +
+
+ + {t('agents.global-agent-tools-count', { + count: template.tools?.length ?? 0, + })} + + + {t('agents.global-agent-last-edited')}:{' '} + {formatDate(template.updatedAt)} + +
+
+ + + + + + { + duplicateTemplate(template.id); + toast.success(t('agents.skill-added-success')); + }} + > + + {t('agents.global-agent-duplicate')} + + exportTemplate(template)}> + + {t('agents.global-agent-export-file')} + + setDeleteId(template.id)} + > + + {t('agents.global-agent-delete')} + + + +
+ ))} +
+ )} + + setDeleteId(null)} + onConfirm={async () => { + if (deleteId) { + await removeTemplate(deleteId); + setDeleteId(null); + toast.success(t('agents.skill-deleted-success')); + } + }} + title={t('agents.delete-skill')} + message={t('agents.delete-skill-confirmation', { + name: deleteId + ? (templates.find((x) => x.id === deleteId)?.name ?? '') + : '', + })} + confirmText={t('layout.delete')} + cancelText={t('workforce.cancel')} + /> +
+ ); +} diff --git a/src/pages/Agents/index.tsx b/src/pages/Agents/index.tsx index 97d34b194..9d4130f9d 100644 --- a/src/pages/Agents/index.tsx +++ b/src/pages/Agents/index.tsx @@ -17,6 +17,7 @@ import VerticalNavigation, { } from '@/components/Navigation'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import GlobalAgents from './GlobalAgents'; import Memory from './Memory'; import Models from './Models'; import Skills from './Skills'; @@ -26,6 +27,10 @@ export default function Capabilities() { const [activeTab, setActiveTab] = useState('models'); const menuItems = [ + { + id: 'global-agents', + name: t('agents.global-agents'), + }, { id: 'models', name: t('setting.models'), @@ -66,7 +71,8 @@ export default function Capabilities() {
-
+
+ {activeTab === 'global-agents' && } {activeTab === 'models' && } {activeTab === 'skills' && } {activeTab === 'memory' && } diff --git a/src/store/globalAgentTemplatesStore.ts b/src/store/globalAgentTemplatesStore.ts new file mode 100644 index 000000000..5b95ac1d5 --- /dev/null +++ b/src/store/globalAgentTemplatesStore.ts @@ -0,0 +1,158 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { create } from 'zustand'; +import { useAuthStore } from './authStore'; + +function emailToUserId(email: string | null): string | null { + if (!email) return null; + return email + .split('@')[0] + .replace(/[\\/*?:"<>|\s]/g, '_') + .replace(/^\.+|\.+$/g, ''); +} + +export interface GlobalAgentTemplate { + id: string; + name: string; + description: string; + tools: string[]; + mcp_tools: Record | { mcpServers?: Record }; + custom_model_config?: { + model_platform?: string; + model_type?: string; + api_key?: string; + api_url?: string; + extra_params?: Record; + }; + updatedAt: number; +} + +interface GlobalAgentTemplatesState { + templates: GlobalAgentTemplate[]; + isLoading: boolean; + loadTemplates: () => Promise; + saveTemplates: (templates: GlobalAgentTemplate[]) => Promise; + addTemplate: ( + template: Omit + ) => Promise; + updateTemplate: ( + id: string, + patch: Partial + ) => Promise; + removeTemplate: (id: string) => Promise; + duplicateTemplate: (id: string) => Promise; + getTemplate: (id: string) => GlobalAgentTemplate | undefined; +} + +function generateId(): string { + return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; +} + +function hasAgentTemplatesApi(): boolean { + return ( + typeof window !== 'undefined' && + !!(window as unknown as { electronAPI?: { agentTemplatesLoad?: unknown } }) + .electronAPI?.agentTemplatesLoad + ); +} + +export const useGlobalAgentTemplatesStore = create()( + (set, get) => ({ + templates: [], + isLoading: false, + + loadTemplates: async () => { + if (!hasAgentTemplatesApi()) return; + const userId = emailToUserId(useAuthStore.getState().email); + if (!userId) return; + set({ isLoading: true }); + try { + const result = await window.electronAPI.agentTemplatesLoad(userId); + if (result.success && result.templates) { + set({ templates: result.templates }); + } + } catch (error) { + console.error('[GlobalAgentTemplates] Load failed:', error); + } finally { + set({ isLoading: false }); + } + }, + + saveTemplates: async (templates: GlobalAgentTemplate[]) => { + if (!hasAgentTemplatesApi()) return false; + const userId = emailToUserId(useAuthStore.getState().email); + if (!userId) return false; + try { + const result = await window.electronAPI.agentTemplatesSave( + userId, + templates + ); + if (result.success) set({ templates }); + return result.success ?? false; + } catch (error) { + console.error('[GlobalAgentTemplates] Save failed:', error); + return false; + } + }, + + addTemplate: async (template) => { + const tpl: GlobalAgentTemplate = { + ...template, + id: generateId(), + updatedAt: Date.now(), + mcp_tools: template.mcp_tools ?? { mcpServers: {} }, + }; + const templates = [...get().templates, tpl]; + const ok = await get().saveTemplates(templates); + return ok ? tpl : null; + }, + + updateTemplate: async (id: string, patch: Partial) => { + const templates = get().templates.map((t) => + t.id === id ? { ...t, ...patch, updatedAt: Date.now() } : t + ); + return get().saveTemplates(templates); + }, + + removeTemplate: async (id: string) => { + const templates = get().templates.filter((t) => t.id !== id); + return get().saveTemplates(templates); + }, + + duplicateTemplate: async (id: string) => { + const t = get().templates.find((x) => x.id === id); + if (!t) return null; + const copy: GlobalAgentTemplate = { + ...JSON.parse(JSON.stringify(t)), + id: generateId(), + name: `${t.name} (copy)`, + updatedAt: Date.now(), + }; + const templates = [...get().templates, copy]; + const ok = await get().saveTemplates(templates); + return ok ? copy : null; + }, + + getTemplate: (id: string) => get().templates.find((t) => t.id === id), + }) +); + +export function getGlobalAgentTemplatesStore() { + return useGlobalAgentTemplatesStore.getState(); +} + +export function hasGlobalAgentTemplatesApi(): boolean { + return hasAgentTemplatesApi(); +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 778f4a51c..5b5a68b36 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -207,6 +207,24 @@ interface ElectronAPI { userId: string, skillName: string ) => Promise<{ success: boolean; error?: string }>; + // Global Agent Templates + agentTemplatesLoad: (userId: string) => Promise<{ + success: boolean; + templates?: Array<{ + id: string; + name: string; + description: string; + tools: string[]; + mcp_tools: any; + custom_model_config?: any; + updatedAt: number; + }>; + error?: string; + }>; + agentTemplatesSave: ( + userId: string, + templates: any[] + ) => Promise<{ success: boolean; error?: string }>; setBrowserPort: (port: number, isExternal?: boolean) => Promise; getBrowserPort: () => Promise; getCdpBrowsers: () => Promise; From 668b314db2b9b38a4aa90bb3f563c45483af8ee1 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 3 Mar 2026 22:36:15 -0600 Subject: [PATCH 2/2] fix: select issue --- src/components/AddWorker/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 30a4e6aa3..8fbb59c97 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -48,7 +48,6 @@ import { Eye, EyeOff, FileUp, - Plus, } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -723,14 +722,16 @@ export function AddWorker({ {t('agents.global-agent-create-from-template')}