From 3ab08dd4cfc4f54f00fe3fa8e28818879a06dadd Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 23 Feb 2026 11:14:00 -0600 Subject: [PATCH 1/5] 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 3c699b9cc..5c5b12b7c 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1703,6 +1703,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 e2b61a55c78608bebaa0eb02681e29315d685844 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 3 Mar 2026 22:36:15 -0600 Subject: [PATCH 2/5] 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')} - setSelectedTemplateId(v === '__none__' ? '' : v) - } - > - - - - - - {t('agents.no-templates')} - - {globalTemplates.map((tpl) => ( - - {tpl.name} - - ))} - - -
- )} -
@@ -776,33 +751,19 @@ export function AddWorker({ className="min-h-[120px] resize-y" /> -
- - - -
+ {edit && ( +
+ +
+ )} )} - {hasGlobalTemplatesApi && ( - + {hasGlobalTemplatesApi && !edit && ( +
+ +

+ {t('agents.global-agents-import-hint')} +

+
)} - {/* Model Configuration Section */} -
- - - {showModelConfig && ( -
- - - {useCustomModel && ( - <> -
- - -
- -
- - - setCustomModelType(e.target.value) - } - /> -
- + {!useWorkspaceCloudModel && ( +
+
- )} -
+ {t('workforce.advanced-model-config')} + + + {showModelConfig && ( +
+

+ {t('workforce.advanced-model-config-hint')} +

+ + + {useCustomModel && ( + <> +
+ + +
+ +
+ + + setCustomModelType(e.target.value) + } + /> +
+ + )} +
+ )} +
+ )} [w.type, w.name].filter(Boolean) as string[]) + ); + let candidate = `${baseName} (copy)`; + let i = 2; + while (used.has(candidate)) { + candidate = `${baseName} (copy ${i})`; + i += 1; + } + return candidate; +} + interface NodeProps { id: string; data: { @@ -476,7 +493,12 @@ export function Node({ id, data }: NodeProps) { onClick={(e) => { e.stopPropagation(); const base = data.agent as Agent; - const copyName = `${base.workerInfo?.name ?? base.name} (copy)`; + const baseName = + base.workerInfo?.name ?? base.name ?? 'Worker'; + const copyName = nextUniqueCustomWorkerName( + baseName, + workerList + ); const newWorker: Agent = { tasks: [], agent_id: copyName, diff --git a/src/i18n/locales/en-us/agents.json b/src/i18n/locales/en-us/agents.json index bd054a69b..1812b54ea 100644 --- a/src/i18n/locales/en-us/agents.json +++ b/src/i18n/locales/en-us/agents.json @@ -61,5 +61,11 @@ "global-agent-import-file": "Import from file", "global-agent-export-file": "Export to file", "global-agent-tools-count": "{{count}} tool(s)", - "no-templates": "No templates" + "no-templates": "No templates", + "global-agents-import-hint": "Import JSON and manage reusable templates under Agents → Global Agents. In Workforce, use the ▼ next to Add to start from a saved template.", + "global-agent-template-saved": "Saved as global agent template.", + "global-agent-import-success": "Agent template imported successfully.", + "global-agent-import-invalid": "This file is not a valid agent template. Use JSON exported from Global Agents or from “Export agent config” when editing a worker.", + "global-agent-duplicate-success": "Template duplicated.", + "global-agent-delete-success": "Template removed." } diff --git a/src/i18n/locales/en-us/workforce.json b/src/i18n/locales/en-us/workforce.json index f1ed9f0a2..4af95eba1 100644 --- a/src/i18n/locales/en-us/workforce.json +++ b/src/i18n/locales/en-us/workforce.json @@ -19,5 +19,7 @@ "worker-name-already-exists": "Worker name already exists", "duplicate-agent": "Duplicate agent", "export-agent": "Export agent config", - "import-agent": "Import agent config" + "export-agent-success": "Agent configuration exported", + "import-agent": "Import agent config", + "advanced-model-config-hint": "Only needed if this worker should use a different model than your workspace default (e.g. local or BYOK). Hidden while using Eigent cloud models." } diff --git a/src/pages/Agents/GlobalAgents.tsx b/src/pages/Agents/GlobalAgents.tsx index 3a2364536..e43776ef3 100644 --- a/src/pages/Agents/GlobalAgents.tsx +++ b/src/pages/Agents/GlobalAgents.tsx @@ -22,7 +22,9 @@ import { } from '@/components/ui/dropdown-menu'; import { useAuthStore } from '@/store/authStore'; import { + getGlobalAgentTemplatesStore, hasGlobalAgentTemplatesApi, + parseImportedAgentTemplateJson, useGlobalAgentTemplatesStore, type GlobalAgentTemplate, } from '@/store/globalAgentTemplatesStore'; @@ -99,26 +101,22 @@ export default function GlobalAgents() { if (!file) return; try { const text = await file.text(); - const data = JSON.parse(text); - const name = data.name ?? 'Imported'; - const template: GlobalAgentTemplate = { - id: `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, - name: data.name ?? 'Imported', - description: data.description ?? '', - tools: Array.isArray(data.tools) ? data.tools : [], - mcp_tools: data.mcp_tools ?? { mcpServers: {} }, - custom_model_config: data.custom_model_config, - updatedAt: Date.now(), - }; - const list = [...templates, template]; + const data = JSON.parse(text) as unknown; + const template = parseImportedAgentTemplateJson(data); + if (!template) { + toast.error(t('agents.global-agent-import-invalid')); + return; + } + const current = getGlobalAgentTemplatesStore().templates; + const list = [...current, template]; const ok = await saveTemplates(list); - if (ok) toast.success(t('agents.skill-added-success')); - else toast.error(t('agents.skill-add-error')); - } catch (err) { - toast.error(t('agents.skill-add-error')); + if (ok) toast.success(t('agents.global-agent-import-success')); + else toast.error(t('agents.global-agent-import-invalid')); + } catch { + toast.error(t('agents.global-agent-import-invalid')); } }, - [templates, saveTemplates, t] + [saveTemplates, t] ); if (!hasApi) { @@ -147,12 +145,7 @@ export default function GlobalAgents() {
- @@ -210,9 +203,13 @@ export default function GlobalAgents() { { - duplicateTemplate(template.id); - toast.success(t('agents.skill-added-success')); + onClick={async () => { + const copy = await duplicateTemplate(template.id); + if (copy) { + toast.success( + t('agents.global-agent-duplicate-success') + ); + } }} > @@ -243,7 +240,7 @@ export default function GlobalAgents() { if (deleteId) { await removeTemplate(deleteId); setDeleteId(null); - toast.success(t('agents.skill-deleted-success')); + toast.success(t('agents.global-agent-delete-success')); } }} title={t('agents.delete-skill')} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f3d7aaf25..fde812f1c 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -31,14 +31,33 @@ import { } from '@/components/MenuButton/MenuButton'; import { TriggerDialog } from '@/components/Trigger/TriggerDialog'; import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { useAuthStore } from '@/store/authStore'; +import { + hasGlobalAgentTemplatesApi, + useGlobalAgentTemplatesStore, +} from '@/store/globalAgentTemplatesStore'; import { usePageTabStore } from '@/store/pageTabStore'; import { useTriggerStore, WebSocketConnectionStatus, } from '@/store/triggerStore'; -import { Inbox, LayoutGrid, Plus, RefreshCw, Zap, ZapOff } from 'lucide-react'; +import { + ChevronDown, + Inbox, + LayoutGrid, + Plus, + RefreshCw, + Zap, + ZapOff, +} from 'lucide-react'; import Overview from './Project/Triggers'; import BottomBar from '@/components/BottomBar'; @@ -137,6 +156,13 @@ export default function Home() { const [activeWebviewId, setActiveWebviewId] = useState(null); const [isChatBoxVisible, setIsChatBoxVisible] = useState(true); const [addWorkerDialogOpen, setAddWorkerDialogOpen] = useState(false); + const [pendingWorkerTemplateId, setPendingWorkerTemplateId] = useState< + string | null + >(null); + const { + templates: globalAgentTemplates, + loadTemplates: loadGlobalAgentTemplates, + } = useGlobalAgentTemplatesStore(); const [triggerDialogOpen, setTriggerDialogOpen] = useState(false); const fileInputRef = useRef(null); @@ -186,6 +212,10 @@ export default function Home() { checkLocalServerStale(); }, []); + useEffect(() => { + if (hasGlobalAgentTemplatesApi()) loadGlobalAgentTemplates(); + }, [loadGlobalAgentTemplates]); + // Detect files and triggers when project loads useEffect(() => { const detectAgentFiles = async () => { @@ -603,24 +633,66 @@ export default function Home() {
- {activeWorkspaceTab !== 'inbox' && ( + {activeWorkspaceTab === 'workforce' && ( +
+ + {hasGlobalAgentTemplatesApi() && + globalAgentTemplates.length > 0 && ( + + + + + + + {t( + 'agents.global-agent-create-from-template' + )} + + {globalAgentTemplates.map((tpl) => ( + { + setPendingWorkerTemplateId(tpl.id); + setAddWorkerDialogOpen(true); + }} + > + {tpl.name} + + ))} + + + )} +
+ )} + {activeWorkspaceTab === 'triggers' && ( )}
@@ -637,7 +709,11 @@ export default function Home() { {/* AddWorker Dialog */} { + setAddWorkerDialogOpen(open); + if (!open) setPendingWorkerTemplateId(null); + }} + initialTemplateId={pendingWorkerTemplateId} /> {/* TriggerDialog */} @@ -763,24 +839,66 @@ export default function Home() {
- {activeWorkspaceTab !== 'inbox' && ( + {activeWorkspaceTab === 'workforce' && ( +
+ + {hasGlobalAgentTemplatesApi() && + globalAgentTemplates.length > 0 && ( + + + + + + + {t( + 'agents.global-agent-create-from-template' + )} + + {globalAgentTemplates.map((tpl) => ( + { + setPendingWorkerTemplateId(tpl.id); + setAddWorkerDialogOpen(true); + }} + > + {tpl.name} + + ))} + + + )} +
+ )} + {activeWorkspaceTab === 'triggers' && ( )}
@@ -796,7 +914,11 @@ export default function Home() { {/* AddWorker Dialog */} { + setAddWorkerDialogOpen(open); + if (!open) setPendingWorkerTemplateId(null); + }} + initialTemplateId={pendingWorkerTemplateId} /> {/* TriggerDialog */} diff --git a/src/store/globalAgentTemplatesStore.ts b/src/store/globalAgentTemplatesStore.ts index 5b95ac1d5..59245455c 100644 --- a/src/store/globalAgentTemplatesStore.ts +++ b/src/store/globalAgentTemplatesStore.ts @@ -36,6 +36,8 @@ export interface GlobalAgentTemplate { api_url?: string; extra_params?: Record; }; + /** Serialized ToolSelect rows so “Create from template” restores MCP/local tool picks */ + selected_tools_snapshot?: unknown[]; updatedAt: number; } @@ -60,6 +62,126 @@ function generateId(): string { return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; } +function normalizeMcpTools(mcp: unknown): GlobalAgentTemplate['mcp_tools'] { + if (mcp === undefined || mcp === null) return { mcpServers: {} }; + if (typeof mcp !== 'object' || Array.isArray(mcp)) return { mcpServers: {} }; + const obj = mcp as Record; + const servers = obj.mcpServers; + if ( + servers !== undefined && + servers !== null && + typeof servers === 'object' && + !Array.isArray(servers) + ) { + return { mcpServers: servers as Record }; + } + return { mcpServers: {} }; +} + +/** Validate JSON file content for “Import agent template” (export shape or backend-style). */ +export function parseImportedAgentTemplateJson( + data: unknown +): GlobalAgentTemplate | null { + if (data === null || typeof data !== 'object' || Array.isArray(data)) { + return null; + } + const o = data as Record; + const nameRaw = o.name; + if (typeof nameRaw !== 'string' || nameRaw.trim().length === 0) { + return null; + } + if (o.description !== undefined && typeof o.description !== 'string') { + return null; + } + const tools = o.tools; + if (tools !== undefined) { + if (!Array.isArray(tools) || !tools.every((x) => typeof x === 'string')) { + return null; + } + } + const mcp = o.mcp_tools; + if ( + mcp !== undefined && + (mcp === null || typeof mcp !== 'object' || Array.isArray(mcp)) + ) { + return null; + } + const cmc = o.custom_model_config; + if ( + cmc !== undefined && + (cmc === null || typeof cmc !== 'object' || Array.isArray(cmc)) + ) { + return null; + } + const snap = + o.selected_tools_snapshot !== undefined + ? o.selected_tools_snapshot + : o.selectedTools; + if (snap !== undefined && (snap === null || !Array.isArray(snap))) { + return null; + } + return { + id: generateId(), + name: nameRaw.trim(), + description: typeof o.description === 'string' ? o.description : '', + tools: Array.isArray(tools) ? [...tools] : [], + mcp_tools: normalizeMcpTools(mcp), + custom_model_config: + cmc && typeof cmc === 'object' && !Array.isArray(cmc) + ? (cmc as GlobalAgentTemplate['custom_model_config']) + : undefined, + selected_tools_snapshot: Array.isArray(snap) + ? JSON.parse(JSON.stringify(snap)) + : undefined, + updatedAt: Date.now(), + }; +} + +function isPersistedTemplateRow(t: unknown): t is GlobalAgentTemplate { + if (t === null || typeof t !== 'object' || Array.isArray(t)) return false; + const o = t as Record; + if (typeof o.id !== 'string' || !o.id.trim()) return false; + if (typeof o.name !== 'string' || !o.name.trim()) return false; + if (o.description !== undefined && typeof o.description !== 'string') { + return false; + } + if (!Array.isArray(o.tools) || !o.tools.every((x) => typeof x === 'string')) { + return false; + } + if ( + o.mcp_tools === null || + typeof o.mcp_tools !== 'object' || + Array.isArray(o.mcp_tools) + ) { + return false; + } + if (typeof o.updatedAt !== 'number' || Number.isNaN(o.updatedAt)) + return false; + if ( + o.selected_tools_snapshot !== undefined && + !Array.isArray(o.selected_tools_snapshot) + ) { + return false; + } + return true; +} + +function sanitizePersistedTemplates(rows: unknown[]): GlobalAgentTemplate[] { + return rows.filter(isPersistedTemplateRow).map((r) => ({ + ...r, + name: r.name.trim(), + description: typeof r.description === 'string' ? r.description : '', + tools: [...r.tools], + mcp_tools: + typeof r.mcp_tools === 'object' && r.mcp_tools !== null + ? JSON.parse(JSON.stringify(r.mcp_tools)) + : { mcpServers: {} }, + selected_tools_snapshot: Array.isArray(r.selected_tools_snapshot) + ? JSON.parse(JSON.stringify(r.selected_tools_snapshot)) + : undefined, + })); +} + function hasAgentTemplatesApi(): boolean { return ( typeof window !== 'undefined' && @@ -81,7 +203,12 @@ export const useGlobalAgentTemplatesStore = create()( try { const result = await window.electronAPI.agentTemplatesLoad(userId); if (result.success && result.templates) { - set({ templates: result.templates }); + const raw = result.templates as unknown[]; + const cleaned = sanitizePersistedTemplates(raw); + set({ templates: cleaned }); + if (cleaned.length !== raw.length) { + await window.electronAPI.agentTemplatesSave(userId, cleaned); + } } } catch (error) { console.error('[GlobalAgentTemplates] Load failed:', error); From 298f03bfe4ada5a06d16b3763f7411aff94dbd10 Mon Sep 17 00:00:00 2001 From: Clayton Date: Fri, 17 Apr 2026 14:18:02 -0500 Subject: [PATCH 4/5] fix: update --- src/components/AddWorker/index.tsx | 20 ++-- src/components/WorkFlow/node.tsx | 13 ++- src/i18n/locales/ar/agents.json | 4 +- src/i18n/locales/de/agents.json | 4 +- src/i18n/locales/en-us/agents.json | 2 + src/i18n/locales/es/agents.json | 4 +- src/i18n/locales/fr/agents.json | 4 +- src/i18n/locales/it/agents.json | 4 +- src/i18n/locales/ja/agents.json | 4 +- src/i18n/locales/ko/agents.json | 4 +- src/i18n/locales/ru/agents.json | 4 +- src/i18n/locales/zh-Hans/agents.json | 4 +- src/i18n/locales/zh-Hant/agents.json | 4 +- src/pages/Agents/GlobalAgents.tsx | 8 +- src/store/globalAgentTemplatesStore.ts | 121 ++++++++++++++++++------- 15 files changed, 147 insertions(+), 57 deletions(-) diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 4e43cafbd..d07f94f40 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -37,6 +37,7 @@ import { INIT_PROVODERS } from '@/lib/llm'; import { useAuthStore, useWorkerList } from '@/store/authStore'; import { hasGlobalAgentTemplatesApi, + sanitizeCustomModelConfigForPersistence, useGlobalAgentTemplatesStore, } from '@/store/globalAgentTemplatesStore'; import { @@ -105,6 +106,12 @@ function parseSelectedToolsSnapshot(raw: unknown): McpItem[] { return out; } +/** Safe download basename for agent JSON (matches GlobalAgents export pattern). */ +function sanitizeAgentExportFilenameStem(raw: string): string { + const stem = raw?.trim() || 'config'; + return stem.replace(/[^a-z0-9-_]/gi, '-'); +} + export function AddWorker({ edit = false, workerInfo = null, @@ -218,13 +225,14 @@ export function AddWorker({ (mcpLocal.mcpServers as Record)[tool.key] = {}; } }); - const custom_model_config = + const custom_model_config = sanitizeCustomModelConfigForPersistence( !useWorkspaceCloudModel && useCustomModel && customModelPlatform ? { model_platform: customModelPlatform, model_type: customModelType || undefined, } - : undefined; + : undefined + ); const blob = new Blob( [ JSON.stringify( @@ -245,7 +253,7 @@ export function AddWorker({ const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `agent-${workerName || 'config'}.json`; + a.download = `agent-${sanitizeAgentExportFilenameStem(workerName)}.json`; a.click(); URL.revokeObjectURL(url); toast.success(t('workforce.export-agent-success')); @@ -257,7 +265,6 @@ export function AddWorker({ customModelPlatform, customModelType, useWorkspaceCloudModel, - selectedTools, t, ]); @@ -541,13 +548,14 @@ export function AddWorker({ } if (saveAsGlobalTemplate && hasGlobalTemplatesApi) { - const customModelConfig = + const customModelConfig = sanitizeCustomModelConfigForPersistence( !useWorkspaceCloudModel && useCustomModel && customModelPlatform ? { model_platform: customModelPlatform, model_type: customModelType || undefined, } - : undefined; + : undefined + ); await addGlobalTemplate({ name: workerName, description: workerDescription, diff --git a/src/components/WorkFlow/node.tsx b/src/components/WorkFlow/node.tsx index f9848b4e3..081f113bf 100644 --- a/src/components/WorkFlow/node.tsx +++ b/src/components/WorkFlow/node.tsx @@ -16,7 +16,7 @@ import { AddWorker } from '@/components/AddWorker'; import { Button } from '@/components/ui/button'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { getToolkitIcon } from '@/lib/toolkitIcons'; -import { useAuthStore, useWorkerList } from '@/store/authStore'; +import { getWorkerList, useAuthStore, useWorkerList } from '@/store/authStore'; import { AgentStatusValue, ChatTaskStatus, @@ -58,7 +58,11 @@ function nextUniqueCustomWorkerName( workers: Agent[] ): string { const used = new Set( - workers.flatMap((w) => [w.type, w.name].filter(Boolean) as string[]) + workers.flatMap((w) => + [w.type, w.name, w.agent_id, w.workerInfo?.name].filter( + (x): x is string => typeof x === 'string' && x.length > 0 + ) + ) ); let candidate = `${baseName} (copy)`; let i = 2; @@ -495,9 +499,10 @@ export function Node({ id, data }: NodeProps) { const base = data.agent as Agent; const baseName = base.workerInfo?.name ?? base.name ?? 'Worker'; + const current = getWorkerList(); const copyName = nextUniqueCustomWorkerName( baseName, - workerList + current ); const newWorker: Agent = { tasks: [], @@ -519,7 +524,7 @@ export function Node({ id, data }: NodeProps) { base.workerInfo?.selectedTools ?? [], }, }; - setWorkerList([...workerList, newWorker]); + setWorkerList([...current, newWorker]); }} > x.id === deleteId)?.name ?? '') : '', diff --git a/src/store/globalAgentTemplatesStore.ts b/src/store/globalAgentTemplatesStore.ts index 59245455c..2087c0c8f 100644 --- a/src/store/globalAgentTemplatesStore.ts +++ b/src/store/globalAgentTemplatesStore.ts @@ -23,19 +23,22 @@ function emailToUserId(email: string | null): string | null { .replace(/^\.+|\.+$/g, ''); } +/** + * Only platform + model id are persisted or exported. + * Do not store api_key, api_url, or extra_params in templates — use workspace / env at runtime. + */ +export type GlobalAgentTemplateCustomModelConfig = { + model_platform?: string; + model_type?: string; +}; + 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; - }; + custom_model_config?: GlobalAgentTemplateCustomModelConfig; /** Serialized ToolSelect rows so “Create from template” restores MCP/local tool picks */ selected_tools_snapshot?: unknown[]; updatedAt: number; @@ -62,6 +65,40 @@ function generateId(): string { return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; } +/** Strip secrets and non-persisted fields from imported or legacy JSON. */ +export function sanitizeCustomModelConfigForPersistence( + cfg: unknown +): GlobalAgentTemplateCustomModelConfig | undefined { + if ( + cfg === null || + cfg === undefined || + typeof cfg !== 'object' || + Array.isArray(cfg) + ) { + return undefined; + } + const c = cfg as Record; + const out: GlobalAgentTemplateCustomModelConfig = {}; + if (typeof c.model_platform === 'string' && c.model_platform.trim()) { + out.model_platform = c.model_platform.trim(); + } + if (typeof c.model_type === 'string' && c.model_type.trim()) { + out.model_type = c.model_type.trim(); + } + return Object.keys(out).length ? out : undefined; +} + +export function sanitizeTemplateForPersistence( + t: GlobalAgentTemplate +): GlobalAgentTemplate { + return { + ...t, + custom_model_config: sanitizeCustomModelConfigForPersistence( + t.custom_model_config + ), + }; +} + function normalizeMcpTools(mcp: unknown): GlobalAgentTemplate['mcp_tools'] { if (mcp === undefined || mcp === null) return { mcpServers: {} }; if (typeof mcp !== 'object' || Array.isArray(mcp)) return { mcpServers: {} }; @@ -120,7 +157,7 @@ export function parseImportedAgentTemplateJson( if (snap !== undefined && (snap === null || !Array.isArray(snap))) { return null; } - return { + return sanitizeTemplateForPersistence({ id: generateId(), name: nameRaw.trim(), description: typeof o.description === 'string' ? o.description : '', @@ -134,7 +171,7 @@ export function parseImportedAgentTemplateJson( ? JSON.parse(JSON.stringify(snap)) : undefined, updatedAt: Date.now(), - }; + }); } function isPersistedTemplateRow(t: unknown): t is GlobalAgentTemplate { @@ -167,26 +204,36 @@ function isPersistedTemplateRow(t: unknown): t is GlobalAgentTemplate { } function sanitizePersistedTemplates(rows: unknown[]): GlobalAgentTemplate[] { - return rows.filter(isPersistedTemplateRow).map((r) => ({ - ...r, - name: r.name.trim(), - description: typeof r.description === 'string' ? r.description : '', - tools: [...r.tools], - mcp_tools: - typeof r.mcp_tools === 'object' && r.mcp_tools !== null - ? JSON.parse(JSON.stringify(r.mcp_tools)) - : { mcpServers: {} }, - selected_tools_snapshot: Array.isArray(r.selected_tools_snapshot) - ? JSON.parse(JSON.stringify(r.selected_tools_snapshot)) - : undefined, - })); + return rows.filter(isPersistedTemplateRow).map((r) => + sanitizeTemplateForPersistence({ + ...r, + name: r.name.trim(), + description: typeof r.description === 'string' ? r.description : '', + tools: [...r.tools], + mcp_tools: + typeof r.mcp_tools === 'object' && r.mcp_tools !== null + ? JSON.parse(JSON.stringify(r.mcp_tools)) + : { mcpServers: {} }, + selected_tools_snapshot: Array.isArray(r.selected_tools_snapshot) + ? JSON.parse(JSON.stringify(r.selected_tools_snapshot)) + : undefined, + }) + ); } function hasAgentTemplatesApi(): boolean { + if (typeof window === 'undefined') return false; + const api = ( + window as unknown as { + electronAPI?: { + agentTemplatesLoad?: unknown; + agentTemplatesSave?: unknown; + }; + } + ).electronAPI; return ( - typeof window !== 'undefined' && - !!(window as unknown as { electronAPI?: { agentTemplatesLoad?: unknown } }) - .electronAPI?.agentTemplatesLoad + typeof api?.agentTemplatesLoad === 'function' && + typeof api?.agentTemplatesSave === 'function' ); } @@ -221,12 +268,13 @@ export const useGlobalAgentTemplatesStore = create()( if (!hasAgentTemplatesApi()) return false; const userId = emailToUserId(useAuthStore.getState().email); if (!userId) return false; + const safe = templates.map(sanitizeTemplateForPersistence); try { const result = await window.electronAPI.agentTemplatesSave( userId, - templates + safe ); - if (result.success) set({ templates }); + if (result.success) set({ templates: safe }); return result.success ?? false; } catch (error) { console.error('[GlobalAgentTemplates] Save failed:', error); @@ -235,21 +283,26 @@ export const useGlobalAgentTemplatesStore = create()( }, addTemplate: async (template) => { - const tpl: GlobalAgentTemplate = { + const tpl = sanitizeTemplateForPersistence({ ...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 - ); + const templates = get().templates.map((t) => { + if (t.id !== id) return t; + return sanitizeTemplateForPersistence({ + ...t, + ...patch, + updatedAt: Date.now(), + }); + }); return get().saveTemplates(templates); }, @@ -261,12 +314,12 @@ export const useGlobalAgentTemplatesStore = create()( duplicateTemplate: async (id: string) => { const t = get().templates.find((x) => x.id === id); if (!t) return null; - const copy: GlobalAgentTemplate = { + const copy = sanitizeTemplateForPersistence({ ...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; From 3d07ca0fdc7b3aaae370582ab21b8e1307f9413e Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 20 Apr 2026 01:43:21 -0500 Subject: [PATCH 5/5] fix: update --- electron/main/index.ts | 38 ++- electron/preload/index.ts | 4 +- src/components/AddWorker/index.tsx | 418 ++++++------------------- src/components/WorkFlow/node.tsx | 68 +--- src/i18n/locales/ar/agents.json | 4 +- src/i18n/locales/de/agents.json | 4 +- src/i18n/locales/en-us/agents.json | 25 +- src/i18n/locales/en-us/workforce.json | 7 +- src/i18n/locales/es/agents.json | 4 +- src/i18n/locales/fr/agents.json | 4 +- src/i18n/locales/it/agents.json | 4 +- src/i18n/locales/ja/agents.json | 4 +- src/i18n/locales/ko/agents.json | 4 +- src/i18n/locales/ru/agents.json | 4 +- src/i18n/locales/zh-Hans/agents.json | 4 +- src/i18n/locales/zh-Hant/agents.json | 4 +- src/pages/Agents/GlobalAgents.tsx | 259 --------------- src/pages/Agents/index.tsx | 12 +- src/pages/Home.tsx | 176 ++--------- src/store/globalAgentTemplatesStore.ts | 75 +++-- src/store/skillsStore.ts | 11 +- src/store/userId.ts | 21 ++ src/types/electron.d.ts | 23 +- 23 files changed, 250 insertions(+), 927 deletions(-) delete mode 100644 src/pages/Agents/GlobalAgents.tsx create mode 100644 src/store/userId.ts diff --git a/electron/main/index.ts b/electron/main/index.ts index 5c5b12b7c..927c52530 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1705,6 +1705,16 @@ function registerIpcHandlers() { // ======================== agent-templates (global Worker Agent templates) ======================== const AGENT_TEMPLATES_FILE = 'agent-templates.json'; + type AgentTemplateIpcPayload = { + id: string; + name: string; + description: string; + tools: string[]; + mcp_tools: Record; + custom_model_config?: Record; + selected_tools_snapshot?: unknown[]; + updatedAt: number; + }; function getAgentTemplatesPath(userId: string): string { return path.join(os.homedir(), '.eigent', userId, AGENT_TEMPLATES_FILE); @@ -1712,15 +1722,7 @@ function registerIpcHandlers() { 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; - }>; + templates: AgentTemplateIpcPayload[]; }> { const configPath = getAgentTemplatesPath(userId); const defaultData = { version: 1, templates: [] }; @@ -1733,7 +1735,7 @@ function registerIpcHandlers() { 'utf-8' ); return defaultData; - } catch (error: any) { + } catch (error) { log.error('Failed to create default agent-templates', error); return defaultData; } @@ -1743,7 +1745,7 @@ function registerIpcHandlers() { const data = JSON.parse(content); if (!Array.isArray(data.templates)) data.templates = []; return { version: data.version ?? 1, templates: data.templates }; - } catch (error: any) { + } catch (error) { log.error('Failed to load agent-templates', error); return defaultData; } @@ -1751,7 +1753,7 @@ function registerIpcHandlers() { async function saveAgentTemplates( userId: string, - data: { version: number; templates: any[] } + data: { version: number; templates: AgentTemplateIpcPayload[] } ): Promise { const configPath = getAgentTemplatesPath(userId); await fsp.mkdir(path.dirname(configPath), { recursive: true }); @@ -1762,15 +1764,16 @@ function registerIpcHandlers() { try { const data = await loadAgentTemplates(userId); return { success: true, templates: data.templates }; - } catch (error: any) { + } catch (error) { + const message = error instanceof Error ? error.message : String(error); log.error('agent-templates-load failed', error); - return { success: false, error: error?.message, templates: [] }; + return { success: false, error: message, templates: [] }; } }); ipcMain.handle( 'agent-templates-save', - async (_event, userId: string, templates: any[]) => { + async (_event, userId: string, templates: AgentTemplateIpcPayload[]) => { try { const current = await loadAgentTemplates(userId); await saveAgentTemplates(userId, { @@ -1778,9 +1781,10 @@ function registerIpcHandlers() { templates, }); return { success: true }; - } catch (error: any) { + } catch (error) { + const message = error instanceof Error ? error.message : String(error); log.error('agent-templates-save failed', error); - return { success: false, error: error?.message }; + return { success: false, error: message }; } } ); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 2668fc1c2..e0cf3b180 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -14,6 +14,8 @@ import { contextBridge, ipcRenderer, webUtils } from 'electron'; +type AgentTemplateIpcPayload = Record; + contextBridge.exposeInMainWorld('ipcRenderer', { on(...args: Parameters) { const [channel, listener] = args; @@ -204,7 +206,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // Global Agent Templates (~/.eigent//agent-templates.json) agentTemplatesLoad: (userId: string) => ipcRenderer.invoke('agent-templates-load', userId), - agentTemplatesSave: (userId: string, templates: any[]) => + agentTemplatesSave: (userId: string, templates: AgentTemplateIpcPayload[]) => ipcRenderer.invoke('agent-templates-save', userId, templates), }); diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index d07f94f40..64fc05f49 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -35,23 +35,9 @@ import { Textarea } from '@/components/ui/textarea'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { INIT_PROVODERS } from '@/lib/llm'; import { useAuthStore, useWorkerList } from '@/store/authStore'; -import { - hasGlobalAgentTemplatesApi, - sanitizeCustomModelConfigForPersistence, - useGlobalAgentTemplatesStore, -} from '@/store/globalAgentTemplatesStore'; -import { - Bot, - ChevronDown, - ChevronUp, - Download, - Edit, - Eye, - EyeOff, -} from 'lucide-react'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { Bot, ChevronDown, ChevronUp, Edit, Eye, EyeOff } from 'lucide-react'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; import ToolSelect from './ToolSelect'; interface EnvValue { @@ -76,57 +62,18 @@ interface McpItem { mcp_name?: string; } -/** Restore ToolSelect rows from template export / disk (drops invalid entries). */ -function parseSelectedToolsSnapshot(raw: unknown): McpItem[] { - if (!Array.isArray(raw)) return []; - const out: McpItem[] = []; - for (const item of raw) { - if (!item || typeof item !== 'object' || Array.isArray(item)) continue; - const o = item as Record; - if ( - typeof o.id !== 'number' || - typeof o.name !== 'string' || - typeof o.key !== 'string' - ) { - continue; - } - out.push({ - id: o.id, - name: o.name, - key: o.key, - description: typeof o.description === 'string' ? o.description : '', - category: o.category as McpItem['category'], - home_page: typeof o.home_page === 'string' ? o.home_page : undefined, - install_command: o.install_command as McpItem['install_command'], - toolkit: typeof o.toolkit === 'string' ? o.toolkit : undefined, - isLocal: o.isLocal === true, - mcp_name: typeof o.mcp_name === 'string' ? o.mcp_name : undefined, - }); - } - return out; -} - -/** Safe download basename for agent JSON (matches GlobalAgents export pattern). */ -function sanitizeAgentExportFilenameStem(raw: string): string { - const stem = raw?.trim() || 'config'; - return stem.replace(/[^a-z0-9-_]/gi, '-'); -} - export function AddWorker({ edit = false, workerInfo = null, variant: _variant = 'default', isOpen, onOpenChange, - initialTemplateId = null, }: { edit?: boolean; workerInfo?: Agent | null; variant?: 'default' | 'icon'; isOpen?: boolean; onOpenChange?: (open: boolean) => void; - /** When opening from workforce "Add ▼ template", prefills name/description/model from disk */ - initialTemplateId?: string | null; }) { const { t } = useTranslation(); const [internalOpen, setInternalOpen] = useState(false); @@ -147,9 +94,8 @@ export function AddWorker({ const toolSelectRef = useRef<{ installMcp: (id: number, env?: any, activeMcp?: any) => Promise; } | null>(null); - const { email, setWorkerList, modelType } = useAuthStore(); + const { email, setWorkerList } = useAuthStore(); const workerList = useWorkerList(); - const useWorkspaceCloudModel = modelType === 'cloud'; // save AddWorker form data const [workerName, setWorkerName] = useState(''); const [workerDescription, setWorkerDescription] = useState(''); @@ -164,113 +110,13 @@ export function AddWorker({ const [customModelPlatform, setCustomModelPlatform] = useState(''); const [customModelType, setCustomModelType] = useState(''); - const [saveAsGlobalTemplate, setSaveAsGlobalTemplate] = useState(false); - const { addTemplate: addGlobalTemplate, getTemplate: getGlobalTemplate } = - useGlobalAgentTemplatesStore(); - const hasGlobalTemplatesApi = hasGlobalAgentTemplatesApi(); - - const resetForm = useCallback(() => { - setWorkerName(''); - setWorkerDescription(''); - setSelectedTools([]); - setShowEnvConfig(false); - setActiveMcp(null); - setEnvValues({}); - setSecretVisible({}); - setNameError(''); - setShowModelConfig(false); - setUseCustomModel(false); - setCustomModelPlatform(''); - setCustomModelType(''); - setSaveAsGlobalTemplate(false); - }, []); - - useEffect(() => { - if (!dialogOpen || edit) return; - resetForm(); - if (!initialTemplateId) return; - const tpl = getGlobalTemplate(initialTemplateId); - if (!tpl) return; - setWorkerName(tpl.name); - setWorkerDescription(tpl.description); - setSelectedTools(parseSelectedToolsSnapshot(tpl.selected_tools_snapshot)); - if (tpl.custom_model_config && !useWorkspaceCloudModel) { - setUseCustomModel(true); - setShowModelConfig(true); - setCustomModelPlatform(tpl.custom_model_config.model_platform ?? ''); - setCustomModelType(tpl.custom_model_config.model_type ?? ''); - } - }, [ - dialogOpen, - edit, - initialTemplateId, - getGlobalTemplate, - resetForm, - useWorkspaceCloudModel, - ]); - - 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 = sanitizeCustomModelConfigForPersistence( - !useWorkspaceCloudModel && 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, - selected_tools_snapshot: JSON.parse(JSON.stringify(selectedTools)), - }, - null, - 2 - ), - ], - { type: 'application/json' } - ); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `agent-${sanitizeAgentExportFilenameStem(workerName)}.json`; - a.click(); - URL.revokeObjectURL(url); - toast.success(t('workforce.export-agent-success')); - }, [ - selectedTools, - workerName, - workerDescription, - useCustomModel, - customModelPlatform, - customModelType, - useWorkspaceCloudModel, - t, - ]); + if (!chatStore) { + return null; + } 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) => { @@ -410,6 +256,21 @@ export function AddWorker({ setSelectedTools(tools); }; + const resetForm = () => { + setWorkerName(''); + setWorkerDescription(''); + setSelectedTools([]); + setShowEnvConfig(false); + setActiveMcp(null); + setEnvValues({}); + setSecretVisible({}); + setNameError(''); + setShowModelConfig(false); + setUseCustomModel(false); + setCustomModelPlatform(''); + setCustomModelType(''); + }; + // tool function const getCategoryIcon = (categoryName?: string) => { if (!categoryName) return ; @@ -513,8 +374,9 @@ export function AddWorker({ }; setWorkerList([...workerList, worker]); } else { + // Build custom model config if custom model is enabled const customModelConfig = - !useWorkspaceCloudModel && useCustomModel && customModelPlatform + useCustomModel && customModelPlatform ? { model_platform: customModelPlatform, model_type: customModelType || undefined, @@ -547,26 +409,6 @@ export function AddWorker({ setWorkerList([...workerList, worker]); } - if (saveAsGlobalTemplate && hasGlobalTemplatesApi) { - const customModelConfig = sanitizeCustomModelConfigForPersistence( - !useWorkspaceCloudModel && 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, - selected_tools_snapshot: JSON.parse(JSON.stringify(selectedTools)), - }); - toast.success(t('agents.global-agent-template-saved')); - } - setDialogOpen(false); // reset form @@ -756,23 +598,8 @@ 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" /> - {edit && ( -
- -
- )} - - {selectedTools.length > 0 && ( -
-
- {t('workforce.agent-tool')} ({selectedTools.length}) -
-
- {selectedTools.map((tool, idx) => ( - - {tool.name || - tool.mcp_name || - tool.key || - `Tool ${idx + 1}`} - - ))} -
-
- )} - - {hasGlobalTemplatesApi && !edit && ( -
- -

- {t('agents.global-agents-import-hint')} -

-
- )} - - {!useWorkspaceCloudModel && ( -
- - - {showModelConfig && ( -
-

- {t('workforce.advanced-model-config-hint')} -

- - - {useCustomModel && ( - <> -
- - -
- -
- - - setCustomModelType(e.target.value) - } - /> -
- - )} -
+ {/* Model Configuration Section */} +
+
- )} + {t('workforce.advanced-model-config')} + + + {showModelConfig && ( +
+ + + {useCustomModel && ( + <> +
+ + +
+ +
+ + + setCustomModelType(e.target.value) + } + /> +
+ + )} +
+ )} +
- [w.type, w.name, w.agent_id, w.workerInfo?.name].filter( - (x): x is string => typeof x === 'string' && x.length > 0 - ) - ) - ); - let candidate = `${baseName} (copy)`; - let i = 2; - while (used.has(candidate)) { - candidate = `${baseName} (copy ${i})`; - i += 1; - } - return candidate; -} - interface NodeProps { id: string; data: { @@ -489,51 +468,6 @@ export function Node({ id, data }: NodeProps) { workerInfo={data.agent as Agent} /> - - - - -
- - {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)} - -
-
- - - - - - { - const copy = await duplicateTemplate(template.id); - if (copy) { - toast.success( - t('agents.global-agent-duplicate-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.global-agent-delete-success')); - } - }} - title={t('agents.delete-template')} - message={t('agents.delete-template-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 9d4130f9d..ba89977fb 100644 --- a/src/pages/Agents/index.tsx +++ b/src/pages/Agents/index.tsx @@ -17,7 +17,6 @@ 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'; @@ -27,10 +26,6 @@ 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'), @@ -51,8 +46,8 @@ export default function Capabilities() { return (
-
-
+
+
({ @@ -64,7 +59,7 @@ export default function Capabilities() { } value={activeTab} onValueChange={handleTabChange} - className="min-h-0 gap-0 h-full w-full flex-1" + className="h-full min-h-0 w-full flex-1 gap-0" listClassName="w-full h-full overflow-y-auto" contentClassName="hidden" /> @@ -72,7 +67,6 @@ export default function Capabilities() {
- {activeTab === 'global-agents' && } {activeTab === 'models' && } {activeTab === 'skills' && } {activeTab === 'memory' && } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index fde812f1c..f3d7aaf25 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -31,33 +31,14 @@ import { } from '@/components/MenuButton/MenuButton'; import { TriggerDialog } from '@/components/Trigger/TriggerDialog'; import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { useAuthStore } from '@/store/authStore'; -import { - hasGlobalAgentTemplatesApi, - useGlobalAgentTemplatesStore, -} from '@/store/globalAgentTemplatesStore'; import { usePageTabStore } from '@/store/pageTabStore'; import { useTriggerStore, WebSocketConnectionStatus, } from '@/store/triggerStore'; -import { - ChevronDown, - Inbox, - LayoutGrid, - Plus, - RefreshCw, - Zap, - ZapOff, -} from 'lucide-react'; +import { Inbox, LayoutGrid, Plus, RefreshCw, Zap, ZapOff } from 'lucide-react'; import Overview from './Project/Triggers'; import BottomBar from '@/components/BottomBar'; @@ -156,13 +137,6 @@ export default function Home() { const [activeWebviewId, setActiveWebviewId] = useState(null); const [isChatBoxVisible, setIsChatBoxVisible] = useState(true); const [addWorkerDialogOpen, setAddWorkerDialogOpen] = useState(false); - const [pendingWorkerTemplateId, setPendingWorkerTemplateId] = useState< - string | null - >(null); - const { - templates: globalAgentTemplates, - loadTemplates: loadGlobalAgentTemplates, - } = useGlobalAgentTemplatesStore(); const [triggerDialogOpen, setTriggerDialogOpen] = useState(false); const fileInputRef = useRef(null); @@ -212,10 +186,6 @@ export default function Home() { checkLocalServerStale(); }, []); - useEffect(() => { - if (hasGlobalAgentTemplatesApi()) loadGlobalAgentTemplates(); - }, [loadGlobalAgentTemplates]); - // Detect files and triggers when project loads useEffect(() => { const detectAgentFiles = async () => { @@ -633,66 +603,24 @@ export default function Home() {
- {activeWorkspaceTab === 'workforce' && ( -
- - {hasGlobalAgentTemplatesApi() && - globalAgentTemplates.length > 0 && ( - - - - - - - {t( - 'agents.global-agent-create-from-template' - )} - - {globalAgentTemplates.map((tpl) => ( - { - setPendingWorkerTemplateId(tpl.id); - setAddWorkerDialogOpen(true); - }} - > - {tpl.name} - - ))} - - - )} -
- )} - {activeWorkspaceTab === 'triggers' && ( + {activeWorkspaceTab !== 'inbox' && ( )}
@@ -709,11 +637,7 @@ export default function Home() { {/* AddWorker Dialog */} { - setAddWorkerDialogOpen(open); - if (!open) setPendingWorkerTemplateId(null); - }} - initialTemplateId={pendingWorkerTemplateId} + onOpenChange={setAddWorkerDialogOpen} /> {/* TriggerDialog */} @@ -839,66 +763,24 @@ export default function Home() {
- {activeWorkspaceTab === 'workforce' && ( -
- - {hasGlobalAgentTemplatesApi() && - globalAgentTemplates.length > 0 && ( - - - - - - - {t( - 'agents.global-agent-create-from-template' - )} - - {globalAgentTemplates.map((tpl) => ( - { - setPendingWorkerTemplateId(tpl.id); - setAddWorkerDialogOpen(true); - }} - > - {tpl.name} - - ))} - - - )} -
- )} - {activeWorkspaceTab === 'triggers' && ( + {activeWorkspaceTab !== 'inbox' && ( )}
@@ -914,11 +796,7 @@ export default function Home() { {/* AddWorker Dialog */} { - setAddWorkerDialogOpen(open); - if (!open) setPendingWorkerTemplateId(null); - }} - initialTemplateId={pendingWorkerTemplateId} + onOpenChange={setAddWorkerDialogOpen} /> {/* TriggerDialog */} diff --git a/src/store/globalAgentTemplatesStore.ts b/src/store/globalAgentTemplatesStore.ts index 2087c0c8f..1970edd28 100644 --- a/src/store/globalAgentTemplatesStore.ts +++ b/src/store/globalAgentTemplatesStore.ts @@ -14,14 +14,7 @@ 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, ''); -} +import { emailToUserId } from './userId'; /** * Only platform + model id are persisted or exported. @@ -47,6 +40,7 @@ export interface GlobalAgentTemplate { interface GlobalAgentTemplatesState { templates: GlobalAgentTemplate[]; isLoading: boolean; + lastError: string | null; loadTemplates: () => Promise; saveTemplates: (templates: GlobalAgentTemplate[]) => Promise; addTemplate: ( @@ -65,6 +59,24 @@ function generateId(): string { return `tpl_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; } +function cloneValue(value: T): T { + if (typeof structuredClone === 'function') { + return structuredClone(value); + } + return value; +} + +let saveQueue: Promise = Promise.resolve(); + +function enqueueSave(operation: () => Promise): Promise { + const run = saveQueue.then(operation, operation); + saveQueue = run.then( + () => undefined, + () => undefined + ); + return run; +} + /** Strip secrets and non-persisted fields from imported or legacy JSON. */ export function sanitizeCustomModelConfigForPersistence( cfg: unknown @@ -167,9 +179,7 @@ export function parseImportedAgentTemplateJson( cmc && typeof cmc === 'object' && !Array.isArray(cmc) ? (cmc as GlobalAgentTemplate['custom_model_config']) : undefined, - selected_tools_snapshot: Array.isArray(snap) - ? JSON.parse(JSON.stringify(snap)) - : undefined, + selected_tools_snapshot: Array.isArray(snap) ? cloneValue(snap) : undefined, updatedAt: Date.now(), }); } @@ -212,10 +222,10 @@ function sanitizePersistedTemplates(rows: unknown[]): GlobalAgentTemplate[] { tools: [...r.tools], mcp_tools: typeof r.mcp_tools === 'object' && r.mcp_tools !== null - ? JSON.parse(JSON.stringify(r.mcp_tools)) + ? cloneValue(r.mcp_tools) : { mcpServers: {} }, selected_tools_snapshot: Array.isArray(r.selected_tools_snapshot) - ? JSON.parse(JSON.stringify(r.selected_tools_snapshot)) + ? cloneValue(r.selected_tools_snapshot) : undefined, }) ); @@ -241,12 +251,13 @@ export const useGlobalAgentTemplatesStore = create()( (set, get) => ({ templates: [], isLoading: false, + lastError: null, loadTemplates: async () => { if (!hasAgentTemplatesApi()) return; const userId = emailToUserId(useAuthStore.getState().email); if (!userId) return; - set({ isLoading: true }); + set({ isLoading: true, lastError: null }); try { const result = await window.electronAPI.agentTemplatesLoad(userId); if (result.success && result.templates) { @@ -256,9 +267,12 @@ export const useGlobalAgentTemplatesStore = create()( if (cleaned.length !== raw.length) { await window.electronAPI.agentTemplatesSave(userId, cleaned); } + } else { + set({ lastError: result.error ?? 'Failed to load agent templates' }); } } catch (error) { console.error('[GlobalAgentTemplates] Load failed:', error); + set({ lastError: 'Failed to load agent templates' }); } finally { set({ isLoading: false }); } @@ -269,17 +283,26 @@ export const useGlobalAgentTemplatesStore = create()( const userId = emailToUserId(useAuthStore.getState().email); if (!userId) return false; const safe = templates.map(sanitizeTemplateForPersistence); - try { - const result = await window.electronAPI.agentTemplatesSave( - userId, - safe - ); - if (result.success) set({ templates: safe }); - return result.success ?? false; - } catch (error) { - console.error('[GlobalAgentTemplates] Save failed:', error); - return false; - } + return enqueueSave(async () => { + try { + const result = await window.electronAPI.agentTemplatesSave( + userId, + safe + ); + if (result.success) { + set({ templates: safe, lastError: null }); + } else { + set({ + lastError: result.error ?? 'Failed to save agent templates', + }); + } + return result.success ?? false; + } catch (error) { + console.error('[GlobalAgentTemplates] Save failed:', error); + set({ lastError: 'Failed to save agent templates' }); + return false; + } + }); }, addTemplate: async (template) => { @@ -315,7 +338,7 @@ export const useGlobalAgentTemplatesStore = create()( const t = get().templates.find((x) => x.id === id); if (!t) return null; const copy = sanitizeTemplateForPersistence({ - ...JSON.parse(JSON.stringify(t)), + ...cloneValue(t), id: generateId(), name: `${t.name} (copy)`, updatedAt: Date.now(), diff --git a/src/store/skillsStore.ts b/src/store/skillsStore.ts index 51a5916e4..6dde69723 100644 --- a/src/store/skillsStore.ts +++ b/src/store/skillsStore.ts @@ -21,16 +21,7 @@ import { import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { useAuthStore } from './authStore'; - -// Helper function to normalize email to user_id format -// Matches the logic in backend's file_save_path -function emailToUserId(email: string | null): string | null { - if (!email) return null; - return email - .split('@')[0] - .replace(/[\\/*?:"<>|\s]/g, '_') - .replace(/^\.+|\.+$/g, ''); -} +import { emailToUserId } from './userId'; // Skill scope interface export interface SkillScope { diff --git a/src/store/userId.ts b/src/store/userId.ts new file mode 100644 index 000000000..8e7e7c863 --- /dev/null +++ b/src/store/userId.ts @@ -0,0 +1,21 @@ +// ========= 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. ========= + +export function emailToUserId(email: string | null): string | null { + if (!email) return null; + return email + .split('@')[0] + .replace(/[\\/*?:"<>|\s]/g, '_') + .replace(/^\.+|\.+$/g, ''); +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 5b5a68b36..c9265c978 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -22,6 +22,17 @@ interface IpcRenderer { invoke: (channel: string, ...args: any[]) => Promise; } +type AgentTemplateIpcPayload = { + id: string; + name: string; + description: string; + tools: string[]; + mcp_tools: Record; + custom_model_config?: Record; + selected_tools_snapshot?: unknown[]; + updatedAt: number; +}; + interface ElectronAPI { closeWindow: () => void; minimizeWindow: () => void; @@ -210,20 +221,12 @@ interface ElectronAPI { // 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; - }>; + templates?: AgentTemplateIpcPayload[]; error?: string; }>; agentTemplatesSave: ( userId: string, - templates: any[] + templates: AgentTemplateIpcPayload[] ) => Promise<{ success: boolean; error?: string }>; setBrowserPort: (port: number, isExternal?: boolean) => Promise; getBrowserPort: () => Promise;