Skip to content

Commit f87f471

Browse files
feat: add global worker agent templates and improve agent config ui
1 parent 3166ca9 commit f87f471

File tree

10 files changed

+825
-4
lines changed

10 files changed

+825
-4
lines changed

electron/main/index.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,88 @@ function registerIpcHandlers() {
16111611
}
16121612
);
16131613

1614+
// ======================== agent-templates (global Worker Agent templates) ========================
1615+
const AGENT_TEMPLATES_FILE = 'agent-templates.json';
1616+
1617+
function getAgentTemplatesPath(userId: string): string {
1618+
return path.join(os.homedir(), '.eigent', userId, AGENT_TEMPLATES_FILE);
1619+
}
1620+
1621+
async function loadAgentTemplates(userId: string): Promise<{
1622+
version: number;
1623+
templates: Array<{
1624+
id: string;
1625+
name: string;
1626+
description: string;
1627+
tools: string[];
1628+
mcp_tools: any;
1629+
custom_model_config?: any;
1630+
updatedAt: number;
1631+
}>;
1632+
}> {
1633+
const configPath = getAgentTemplatesPath(userId);
1634+
const defaultData = { version: 1, templates: [] };
1635+
if (!existsSync(configPath)) {
1636+
try {
1637+
await fsp.mkdir(path.dirname(configPath), { recursive: true });
1638+
await fsp.writeFile(
1639+
configPath,
1640+
JSON.stringify(defaultData, null, 2),
1641+
'utf-8'
1642+
);
1643+
return defaultData;
1644+
} catch (error: any) {
1645+
log.error('Failed to create default agent-templates', error);
1646+
return defaultData;
1647+
}
1648+
}
1649+
try {
1650+
const content = await fsp.readFile(configPath, 'utf-8');
1651+
const data = JSON.parse(content);
1652+
if (!Array.isArray(data.templates)) data.templates = [];
1653+
return { version: data.version ?? 1, templates: data.templates };
1654+
} catch (error: any) {
1655+
log.error('Failed to load agent-templates', error);
1656+
return defaultData;
1657+
}
1658+
}
1659+
1660+
async function saveAgentTemplates(
1661+
userId: string,
1662+
data: { version: number; templates: any[] }
1663+
): Promise<void> {
1664+
const configPath = getAgentTemplatesPath(userId);
1665+
await fsp.mkdir(path.dirname(configPath), { recursive: true });
1666+
await fsp.writeFile(configPath, JSON.stringify(data, null, 2), 'utf-8');
1667+
}
1668+
1669+
ipcMain.handle('agent-templates-load', async (_event, userId: string) => {
1670+
try {
1671+
const data = await loadAgentTemplates(userId);
1672+
return { success: true, templates: data.templates };
1673+
} catch (error: any) {
1674+
log.error('agent-templates-load failed', error);
1675+
return { success: false, error: error?.message, templates: [] };
1676+
}
1677+
});
1678+
1679+
ipcMain.handle(
1680+
'agent-templates-save',
1681+
async (_event, userId: string, templates: any[]) => {
1682+
try {
1683+
const current = await loadAgentTemplates(userId);
1684+
await saveAgentTemplates(userId, {
1685+
version: current.version,
1686+
templates,
1687+
});
1688+
return { success: true };
1689+
} catch (error: any) {
1690+
log.error('agent-templates-save failed', error);
1691+
return { success: false, error: error?.message };
1692+
}
1693+
}
1694+
);
1695+
16141696
// Initialize skills config for a user (ensures config file exists)
16151697
ipcMain.handle('skill-config-init', async (_event, userId: string) => {
16161698
try {

electron/preload/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
201201
ipcRenderer.invoke('skill-config-update', userId, skillName, skillConfig),
202202
skillConfigDelete: (userId: string, skillName: string) =>
203203
ipcRenderer.invoke('skill-config-delete', userId, skillName),
204+
// Global Agent Templates (~/.eigent/<userId>/agent-templates.json)
205+
agentTemplatesLoad: (userId: string) =>
206+
ipcRenderer.invoke('agent-templates-load', userId),
207+
agentTemplatesSave: (userId: string, templates: any[]) =>
208+
ipcRenderer.invoke('agent-templates-save', userId, templates),
204209
});
205210

206211
// --------- Preload scripts loading ---------

src/components/AddWorker/index.tsx

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,24 @@ import { Textarea } from '@/components/ui/textarea';
3535
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
3636
import { INIT_PROVODERS } from '@/lib/llm';
3737
import { useAuthStore, useWorkerList } from '@/store/authStore';
38+
import {
39+
hasGlobalAgentTemplatesApi,
40+
useGlobalAgentTemplatesStore,
41+
} from '@/store/globalAgentTemplatesStore';
3842
import {
3943
Bot,
4044
ChevronDown,
4145
ChevronUp,
46+
Download,
4247
Edit,
4348
Eye,
4449
EyeOff,
50+
FileUp,
4551
Plus,
4652
} from 'lucide-react';
47-
import { useRef, useState } from 'react';
53+
import { useCallback, useEffect, useRef, useState } from 'react';
4854
import { useTranslation } from 'react-i18next';
55+
import { toast } from 'sonner';
4956
import ToolSelect from './ToolSelect';
5057

5158
interface EnvValue {
@@ -110,6 +117,121 @@ export function AddWorker({
110117
const [customModelPlatform, setCustomModelPlatform] = useState('');
111118
const [customModelType, setCustomModelType] = useState('');
112119

120+
// Global template and export/import
121+
const [saveAsGlobalTemplate, setSaveAsGlobalTemplate] = useState(false);
122+
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('');
123+
const importFileRef = useRef<HTMLInputElement>(null);
124+
const {
125+
templates: globalTemplates,
126+
loadTemplates: loadGlobalTemplates,
127+
addTemplate: addGlobalTemplate,
128+
getTemplate: getGlobalTemplate,
129+
} = useGlobalAgentTemplatesStore();
130+
const hasGlobalTemplatesApi = hasGlobalAgentTemplatesApi();
131+
132+
useEffect(() => {
133+
if (hasGlobalTemplatesApi && dialogOpen) loadGlobalTemplates();
134+
}, [hasGlobalTemplatesApi, dialogOpen, loadGlobalTemplates]);
135+
136+
useEffect(() => {
137+
if (selectedTemplateId && dialogOpen) {
138+
const tpl = getGlobalTemplate(selectedTemplateId);
139+
if (tpl) {
140+
setWorkerName(tpl.name);
141+
setWorkerDescription(tpl.description);
142+
if (tpl.custom_model_config) {
143+
setUseCustomModel(true);
144+
setShowModelConfig(true);
145+
setCustomModelPlatform(tpl.custom_model_config.model_platform ?? '');
146+
setCustomModelType(tpl.custom_model_config.model_type ?? '');
147+
}
148+
}
149+
}
150+
}, [selectedTemplateId, dialogOpen, getGlobalTemplate]);
151+
152+
const handleExportConfig = useCallback(() => {
153+
const localTool: string[] = [];
154+
const mcpList: string[] = [];
155+
selectedTools.forEach((tool: McpItem) => {
156+
if (tool.isLocal) {
157+
localTool.push(tool.toolkit as string);
158+
} else {
159+
mcpList.push(tool?.key || tool?.mcp_name);
160+
}
161+
});
162+
let mcpLocal: Record<string, unknown> = { mcpServers: {} };
163+
selectedTools.forEach((tool: McpItem) => {
164+
if (!tool.isLocal && tool.key) {
165+
(mcpLocal.mcpServers as Record<string, unknown>)[tool.key] = {};
166+
}
167+
});
168+
const custom_model_config =
169+
useCustomModel && customModelPlatform
170+
? {
171+
model_platform: customModelPlatform,
172+
model_type: customModelType || undefined,
173+
}
174+
: undefined;
175+
const blob = new Blob(
176+
[
177+
JSON.stringify(
178+
{
179+
name: workerName,
180+
description: workerDescription,
181+
tools: localTool,
182+
mcp_tools: mcpLocal,
183+
custom_model_config,
184+
},
185+
null,
186+
2
187+
),
188+
],
189+
{ type: 'application/json' }
190+
);
191+
const url = URL.createObjectURL(blob);
192+
const a = document.createElement('a');
193+
a.href = url;
194+
a.download = `agent-${workerName || 'config'}.json`;
195+
a.click();
196+
URL.revokeObjectURL(url);
197+
toast.success(t('workforce.save-changes'));
198+
}, [
199+
selectedTools,
200+
workerName,
201+
workerDescription,
202+
useCustomModel,
203+
customModelPlatform,
204+
customModelType,
205+
t,
206+
]);
207+
208+
const handleImportConfig = useCallback(
209+
(e: React.ChangeEvent<HTMLInputElement>) => {
210+
const file = e.target.files?.[0];
211+
e.target.value = '';
212+
if (!file) return;
213+
file.text().then((text) => {
214+
try {
215+
const data = JSON.parse(text);
216+
setWorkerName(data.name ?? '');
217+
setWorkerDescription(data.description ?? '');
218+
if (data.custom_model_config) {
219+
setUseCustomModel(true);
220+
setShowModelConfig(true);
221+
setCustomModelPlatform(
222+
data.custom_model_config.model_platform ?? ''
223+
);
224+
setCustomModelType(data.custom_model_config.model_type ?? '');
225+
}
226+
toast.success(t('workforce.save-changes'));
227+
} catch {
228+
toast.error(t('agents.skill-add-error'));
229+
}
230+
});
231+
},
232+
[t]
233+
);
234+
113235
// environment variable management
114236
const initializeEnvValues = (mcp: McpItem) => {
115237
console.log(mcp);
@@ -261,6 +383,8 @@ export function AddWorker({
261383
setUseCustomModel(false);
262384
setCustomModelPlatform('');
263385
setCustomModelType('');
386+
setSaveAsGlobalTemplate(false);
387+
setSelectedTemplateId('');
264388
};
265389

266390
// tool function
@@ -401,6 +525,24 @@ export function AddWorker({
401525
setWorkerList([...workerList, worker]);
402526
}
403527

528+
if (saveAsGlobalTemplate && hasGlobalTemplatesApi) {
529+
const customModelConfig =
530+
useCustomModel && customModelPlatform
531+
? {
532+
model_platform: customModelPlatform,
533+
model_type: customModelType || undefined,
534+
}
535+
: undefined;
536+
await addGlobalTemplate({
537+
name: workerName,
538+
description: workerDescription,
539+
tools: localTool,
540+
mcp_tools: mcpLocal,
541+
custom_model_config: customModelConfig,
542+
});
543+
toast.success(t('agents.skill-added-success'));
544+
}
545+
404546
setDialogOpen(false);
405547

406548
// reset form
@@ -568,6 +710,34 @@ export function AddWorker({
568710
// default add interface
569711
<>
570712
<DialogContentSection className="flex flex-col gap-3 bg-white-100% p-md">
713+
{hasGlobalTemplatesApi &&
714+
globalTemplates.length > 0 &&
715+
!edit && (
716+
<div className="flex flex-col gap-1">
717+
<label className="text-xs text-text-body">
718+
{t('agents.global-agent-create-from-template')}
719+
</label>
720+
<Select
721+
value={selectedTemplateId}
722+
onValueChange={setSelectedTemplateId}
723+
>
724+
<SelectTrigger className="w-full">
725+
<SelectValue placeholder={t('agents.no-templates')} />
726+
</SelectTrigger>
727+
<SelectContent>
728+
<SelectItem value="">
729+
{t('agents.no-templates')}
730+
</SelectItem>
731+
{globalTemplates.map((tpl) => (
732+
<SelectItem key={tpl.id} value={tpl.id}>
733+
{tpl.name}
734+
</SelectItem>
735+
))}
736+
</SelectContent>
737+
</Select>
738+
</div>
739+
)}
740+
571741
<div className="flex flex-col gap-4">
572742
<div className="flex items-center gap-sm">
573743
<div className="flex h-16 w-16 items-center justify-center">
@@ -597,15 +767,80 @@ export function AddWorker({
597767
placeholder={t('layout.im-an-agent-specially-designed-for')}
598768
value={workerDescription}
599769
onChange={(e) => setWorkerDescription(e.target.value)}
770+
className="min-h-[120px] resize-y"
600771
/>
601772

773+
<div className="flex flex-row flex-wrap items-center gap-2">
774+
<Button
775+
type="button"
776+
variant="ghost"
777+
size="sm"
778+
onClick={handleExportConfig}
779+
>
780+
<Download className="mr-1 h-4 w-4" />
781+
{t('workforce.export-agent')}
782+
</Button>
783+
<Button
784+
type="button"
785+
variant="ghost"
786+
size="sm"
787+
onClick={() => importFileRef.current?.click()}
788+
>
789+
<FileUp className="mr-1 h-4 w-4" />
790+
{t('workforce.import-agent')}
791+
</Button>
792+
<input
793+
ref={importFileRef}
794+
type="file"
795+
accept=".json,application/json"
796+
className="hidden"
797+
onChange={handleImportConfig}
798+
/>
799+
</div>
800+
602801
<ToolSelect
603802
onShowEnvConfig={handleShowEnvConfig}
604803
onSelectedToolsChange={handleSelectedToolsChange}
605804
initialSelectedTools={selectedTools}
606805
ref={toolSelectRef}
607806
/>
608807

808+
{selectedTools.length > 0 && (
809+
<div className="rounded-lg border border-border-subtle-strong bg-surface-tertiary-subtle px-3 py-2">
810+
<div className="text-body-xs font-medium text-text-body">
811+
{t('workforce.agent-tool')} ({selectedTools.length})
812+
</div>
813+
<div className="mt-1 flex flex-wrap gap-1">
814+
{selectedTools.map((tool, idx) => (
815+
<span
816+
key={idx}
817+
className="rounded bg-surface-primary px-1.5 py-0.5 text-body-xs text-text-body"
818+
title={tool.description || tool.name || tool.key}
819+
>
820+
{tool.name ||
821+
tool.mcp_name ||
822+
tool.key ||
823+
`Tool ${idx + 1}`}
824+
</span>
825+
))}
826+
</div>
827+
</div>
828+
)}
829+
830+
{hasGlobalTemplatesApi && (
831+
<label className="flex cursor-pointer items-center gap-2 text-body-sm text-text-body">
832+
<input
833+
type="checkbox"
834+
checked={saveAsGlobalTemplate}
835+
onChange={(e) =>
836+
setSaveAsGlobalTemplate(e.target.checked)
837+
}
838+
className="rounded border-border-subtle-strong"
839+
/>
840+
{t('agents.global-agent-save-as-template')}
841+
</label>
842+
)}
843+
609844
{/* Model Configuration Section */}
610845
<div className="mt-2 flex flex-col gap-2">
611846
<button

0 commit comments

Comments
 (0)