diff --git a/apps/desktop/electron/electron-env.d.ts b/apps/desktop/electron/electron-env.d.ts index 1fdef4b7..583a19b9 100644 --- a/apps/desktop/electron/electron-env.d.ts +++ b/apps/desktop/electron/electron-env.d.ts @@ -21,7 +21,86 @@ declare namespace NodeJS { } } +// ============================================================================ +// ElectronAPI type definitions +// ============================================================================ + +interface HubStatus { + hubId: string + status: string + agentCount: number + gatewayConnected: boolean + gatewayUrl?: string + defaultAgent?: { + agentId: string + status: string + } | null +} + +interface AgentInfo { + agentId: string + status: string +} + +interface ToolInfo { + name: string + group: string + enabled: boolean +} + +interface SkillInfo { + id: string + name: string + description: string + version: string + enabled: boolean + source: 'bundled' | 'global' | 'profile' + triggers: string[] +} + +interface SkillAddResult { + ok: boolean + message: string + path?: string + skills?: string[] +} + +interface ElectronAPI { + hub: { + init: () => Promise + getStatus: () => Promise + getAgentInfo: () => Promise + info: () => Promise + reconnect: (url: string) => Promise + listAgents: () => Promise + createAgent: (id?: string) => Promise + getAgent: (id: string) => Promise + closeAgent: (id: string) => Promise + sendMessage: (agentId: string, content: string) => Promise + } + tools: { + list: () => Promise + toggle: (name: string) => Promise + setStatus: (name: string, enabled: boolean) => Promise + active: () => Promise + reload: () => Promise + } + skills: { + list: () => Promise + get: (id: string) => Promise + toggle: (id: string) => Promise + setStatus: (id: string, enabled: boolean) => Promise + reload: () => Promise + add: (source: string, options?: { name?: string; force?: boolean }) => Promise + remove: (name: string) => Promise + } + agent: { + status: () => Promise + } +} + // Used in Renderer process, expose in `preload.ts` interface Window { ipcRenderer: import('electron').IpcRenderer + electronAPI: ElectronAPI } diff --git a/apps/desktop/electron/ipc/agent.ts b/apps/desktop/electron/ipc/agent.ts new file mode 100644 index 00000000..73ca65b5 --- /dev/null +++ b/apps/desktop/electron/ipc/agent.ts @@ -0,0 +1,220 @@ +/** + * Agent IPC handlers for Electron main process. + * + * These handlers get tool information from the real Agent instance + * managed by the Hub. + */ +import { ipcMain } from 'electron' +import { getCurrentHub } from './hub.js' + +// Tool groups (for UI display grouping) +const TOOL_GROUPS: Record = { + 'group:fs': ['read', 'write', 'edit', 'glob'], + 'group:runtime': ['exec', 'process'], + 'group:web': ['web_search', 'web_fetch'], + 'group:memory': ['memory_get', 'memory_set', 'memory_delete', 'memory_list'], +} + +// All known tool names (for display when agent not available) +const ALL_KNOWN_TOOLS = [ + ...TOOL_GROUPS['group:fs'], + ...TOOL_GROUPS['group:runtime'], + ...TOOL_GROUPS['group:web'], + ...TOOL_GROUPS['group:memory'], +] + +/** + * Get the group for a tool name. + */ +function getToolGroup(name: string): string { + for (const [groupKey, tools] of Object.entries(TOOL_GROUPS)) { + const groupId = groupKey.replace('group:', '') + if (tools.includes(name)) { + return groupId + } + } + return 'other' +} + +/** + * Get the default agent from Hub. + */ +function getDefaultAgent() { + const hub = getCurrentHub() + if (!hub) return null + + const agentIds = hub.listAgents() + if (agentIds.length === 0) return null + + return hub.getAgent(agentIds[0]) ?? null +} + +/** + * Register all Agent-related IPC handlers. + */ +export function registerAgentIpcHandlers(): void { + // ============================================================================ + // Agent lifecycle + // ============================================================================ + + /** + * Get agent status + */ + ipcMain.handle('agent:status', async () => { + const agent = getDefaultAgent() + if (!agent) { + return { + running: false, + error: 'No agent available', + } + } + + return { + running: !agent.closed, + agentId: agent.sessionId, + } + }) + + // ============================================================================ + // Tools management + // ============================================================================ + + /** + * Get list of all tools with their enabled status. + * Returns active tools from the real Agent instance. + */ + ipcMain.handle('tools:list', async () => { + const agent = getDefaultAgent() + + if (!agent) { + // Fallback: return all known tools as disabled when no agent + return ALL_KNOWN_TOOLS.map((name) => ({ + name, + enabled: false, + group: getToolGroup(name), + })) + } + + // Get active tools from agent + const activeTools = agent.getActiveTools() + const activeSet = new Set(activeTools) + + // Build list with all known tools, marking which are active + const toolList = ALL_KNOWN_TOOLS.map((name) => ({ + name, + enabled: activeSet.has(name), + group: getToolGroup(name), + })) + + // Add any active tools not in our known list + for (const name of activeTools) { + if (!ALL_KNOWN_TOOLS.includes(name)) { + toolList.push({ + name, + enabled: true, + group: getToolGroup(name), + }) + } + } + + return toolList + }) + + /** + * Toggle a tool's enabled status. + * Persists the change to profile config and reloads tools. + */ + ipcMain.handle('tools:toggle', async (_event, toolName: string) => { + console.log(`[IPC] tools:toggle called for: ${toolName}`) + + const agent = getDefaultAgent() + if (!agent) { + return { error: 'No agent available' } + } + + // Check current status + const activeTools = agent.getActiveTools() + const isCurrentlyEnabled = activeTools.includes(toolName) + + // Toggle the tool status (enable if disabled, disable if enabled) + const result = agent.setToolStatus(toolName, !isCurrentlyEnabled) + if (!result) { + return { error: 'No profile loaded - cannot persist tool status' } + } + + // Get updated status + const newActiveTools = agent.getActiveTools() + const isNowEnabled = newActiveTools.includes(toolName) + console.log(`[IPC] Tool ${toolName} toggled: ${isCurrentlyEnabled} -> ${isNowEnabled}`) + + return { + name: toolName, + enabled: isNowEnabled, + } + }) + + /** + * Set a tool's enabled status explicitly. + * Persists the change to profile config and reloads tools. + */ + ipcMain.handle('tools:setStatus', async (_event, toolName: string, enabled: boolean) => { + console.log(`[IPC] tools:setStatus called for: ${toolName}, enabled: ${enabled}`) + + const agent = getDefaultAgent() + if (!agent) { + return { error: 'No agent available' } + } + + // Set the tool status and persist to profile config + const result = agent.setToolStatus(toolName, enabled) + if (!result) { + return { error: 'No profile loaded - cannot persist tool status' } + } + + console.log(`[IPC] Tool ${toolName} status set to ${enabled}. Config: allow=${result.allow?.join(',') ?? 'none'}, deny=${result.deny?.join(',') ?? 'none'}`) + + // Get updated status + const activeTools = agent.getActiveTools() + const isEnabled = activeTools.includes(toolName) + + return { + name: toolName, + enabled: isEnabled, + config: result, + } + }) + + /** + * Get currently active tools in the agent. + */ + ipcMain.handle('tools:active', async () => { + const agent = getDefaultAgent() + if (!agent) { + return [] + } + return agent.getActiveTools() + }) + + /** + * Force reload tools in the agent. + * This picks up any changes made to credentials.json5. + */ + ipcMain.handle('tools:reload', async () => { + const agent = getDefaultAgent() + if (!agent) { + return { error: 'No agent available' } + } + + const reloadedTools = agent.reloadTools() + console.log(`[IPC] Reloaded ${reloadedTools.length} tools: ${reloadedTools.join(', ')}`) + + return reloadedTools + }) +} + +/** + * Cleanup agent resources. + */ +export function cleanupAgent(): void { + // Agent cleanup is handled by Hub +} diff --git a/apps/desktop/electron/ipc/hub.ts b/apps/desktop/electron/ipc/hub.ts new file mode 100644 index 00000000..6992d7ca --- /dev/null +++ b/apps/desktop/electron/ipc/hub.ts @@ -0,0 +1,242 @@ +/** + * Hub IPC handlers for Electron main process. + * + * Creates and manages a Hub instance that connects to the Gateway. + * This follows the same pattern as the Console app. + */ +import { ipcMain } from 'electron' +import { Hub } from '../../../../src/hub/hub.js' +import type { ConnectionState } from '@multica/sdk' +import type { AsyncAgent } from '../../../../src/agent/async-agent.js' + +// Singleton Hub instance +let hub: Hub | null = null +let defaultAgentId: string | null = null + +/** + * Initialize Hub on app startup. + * Creates Hub and a default Agent automatically. + */ +export async function initializeHub(): Promise { + if (hub) { + console.log('[Desktop] Hub already initialized') + return + } + + const gatewayUrl = process.env['GATEWAY_URL'] ?? 'http://localhost:3000' + console.log(`[Desktop] Initializing Hub, connecting to Gateway: ${gatewayUrl}`) + + hub = new Hub(gatewayUrl) + + // Create default agent if none exists + const agents = hub.listAgents() + if (agents.length === 0) { + console.log('[Desktop] Creating default agent...') + const agent = hub.createAgent() + defaultAgentId = agent.sessionId + console.log(`[Desktop] Default agent created: ${defaultAgentId}`) + } else { + defaultAgentId = agents[0] + console.log(`[Desktop] Using existing agent: ${defaultAgentId}`) + } +} + +/** + * Get or create the Hub instance. + */ +function getHub(): Hub { + if (!hub) { + const gatewayUrl = process.env['GATEWAY_URL'] ?? 'http://localhost:3000' + console.log(`[Desktop] Creating Hub, connecting to Gateway: ${gatewayUrl}`) + hub = new Hub(gatewayUrl) + } + return hub +} + +/** + * Get the default agent. + */ +function getDefaultAgent(): AsyncAgent | null { + if (!hub || !defaultAgentId) return null + return hub.getAgent(defaultAgentId) ?? null +} + +/** + * Hub info returned to renderer. + */ +export interface HubInfo { + hubId: string + url: string + connectionState: ConnectionState + agentCount: number +} + +/** + * Agent info returned to renderer. + */ +export interface AgentInfo { + id: string + closed: boolean +} + +/** + * Register all Hub-related IPC handlers. + */ +export function registerHubIpcHandlers(): void { + /** + * Initialize the Hub (creates singleton if not exists). + */ + ipcMain.handle('hub:init', async () => { + await initializeHub() + const h = getHub() + return { + hubId: h.hubId, + url: h.url, + connectionState: h.connectionState, + defaultAgentId, + } + }) + + /** + * Get Hub status info. + */ + ipcMain.handle('hub:info', async (): Promise => { + const h = getHub() + return { + hubId: h.hubId, + url: h.url, + connectionState: h.connectionState, + agentCount: h.listAgents().length, + } + }) + + /** + * Get Hub status with default agent info (for home page). + */ + ipcMain.handle('hub:getStatus', async () => { + const h = getHub() + const agent = getDefaultAgent() + + return { + hubId: h.hubId, + status: h.connectionState === 'connected' ? 'ready' : h.connectionState, + agentCount: h.listAgents().length, + gatewayConnected: h.connectionState === 'connected', + gatewayUrl: h.url, + defaultAgent: agent + ? { + agentId: agent.sessionId, + status: agent.closed ? 'closed' : 'idle', + } + : null, + } + }) + + /** + * Get default agent info. + */ + ipcMain.handle('hub:getAgentInfo', async () => { + const agent = getDefaultAgent() + if (!agent) { + return null + } + return { + agentId: agent.sessionId, + status: agent.closed ? 'closed' : 'idle', + } + }) + + /** + * Reconnect Hub to a different Gateway URL. + */ + ipcMain.handle('hub:reconnect', async (_event, url: string) => { + const h = getHub() + h.reconnect(url) + return { url: h.url } + }) + + /** + * List all agents. + */ + ipcMain.handle('hub:listAgents', async (): Promise => { + const h = getHub() + const agentIds = h.listAgents() + return agentIds.map((id) => { + const agent = h.getAgent(id) + return { + id, + closed: agent?.closed ?? true, + } + }) + }) + + /** + * Create a new agent. + */ + ipcMain.handle('hub:createAgent', async (_event, id?: string) => { + const h = getHub() + const agent = h.createAgent(id) + return { + id: agent.sessionId, + closed: agent.closed, + } + }) + + /** + * Get a specific agent. + */ + ipcMain.handle('hub:getAgent', async (_event, id: string) => { + const h = getHub() + const agent = h.getAgent(id) + if (!agent) { + return { error: `Agent not found: ${id}` } + } + return { + id: agent.sessionId, + closed: agent.closed, + } + }) + + /** + * Close/delete an agent. + */ + ipcMain.handle('hub:closeAgent', async (_event, id: string) => { + const h = getHub() + const result = h.closeAgent(id) + return { ok: result } + }) + + /** + * Send a message to an agent. + */ + ipcMain.handle('hub:sendMessage', async (_event, agentId: string, content: string) => { + const h = getHub() + const agent = h.getAgent(agentId) + if (!agent) { + return { error: `Agent not found: ${agentId}` } + } + if (agent.closed) { + return { error: `Agent is closed: ${agentId}` } + } + agent.write(content) + return { ok: true } + }) +} + +/** + * Cleanup Hub resources. + */ +export function cleanupHub(): void { + if (hub) { + console.log('[Desktop] Shutting down Hub') + hub.shutdown() + hub = null + } +} + +/** + * Get the current Hub instance (for use by other IPC modules). + */ +export function getCurrentHub(): Hub | null { + return hub +} diff --git a/apps/desktop/electron/ipc/index.ts b/apps/desktop/electron/ipc/index.ts new file mode 100644 index 00000000..71bbec88 --- /dev/null +++ b/apps/desktop/electron/ipc/index.ts @@ -0,0 +1,39 @@ +/** + * IPC handlers index - register all handlers from main process. + */ +export { registerAgentIpcHandlers, cleanupAgent } from './agent.js' +export { registerSkillsIpcHandlers } from './skills.js' +export { registerHubIpcHandlers, cleanupHub, initializeHub } from './hub.js' + +import { registerAgentIpcHandlers, cleanupAgent } from './agent.js' +import { registerSkillsIpcHandlers } from './skills.js' +import { registerHubIpcHandlers, cleanupHub, initializeHub } from './hub.js' + +/** + * Register all IPC handlers. + * Call this in main.ts after app is ready. + */ +export function registerAllIpcHandlers(): void { + registerHubIpcHandlers() + registerAgentIpcHandlers() + registerSkillsIpcHandlers() +} + +/** + * Initialize Hub and create default agent. + * Call this after IPC handlers are registered. + */ +export async function initializeApp(): Promise { + console.log('[Desktop] Initializing app...') + await initializeHub() + console.log('[Desktop] App initialized') +} + +/** + * Cleanup all resources. + * Call this before app quits. + */ +export function cleanupAll(): void { + cleanupHub() + cleanupAgent() +} diff --git a/apps/desktop/electron/ipc/skills.ts b/apps/desktop/electron/ipc/skills.ts new file mode 100644 index 00000000..71fea31a --- /dev/null +++ b/apps/desktop/electron/ipc/skills.ts @@ -0,0 +1,278 @@ +/** + * Skills IPC handlers for Electron main process. + * + * These handlers get skill information from the real Agent instance + * managed by the Hub. + */ +import { ipcMain } from 'electron' +import { getCurrentHub } from './hub.js' + +/** + * Skill info returned to renderer. + */ +export interface SkillInfo { + id: string + name: string + description: string + version: string + enabled: boolean + source: 'bundled' | 'global' | 'profile' + triggers: string[] +} + +/** + * Get the default agent from Hub. + */ +function getDefaultAgent() { + const hub = getCurrentHub() + if (!hub) return null + + const agentIds = hub.listAgents() + if (agentIds.length === 0) return null + + return hub.getAgent(agentIds[0]) ?? null +} + +/** + * Get default bundled skills (fallback when no agent). + */ +function getDefaultSkills(): SkillInfo[] { + return [ + { + id: 'commit', + name: 'Git Commit Helper', + description: 'Create well-formatted git commits following conventional commit standards', + version: '1.0.0', + enabled: true, + source: 'bundled', + triggers: ['/commit'], + }, + { + id: 'code-review', + name: 'Code Review', + description: 'Review code for bugs, security issues, and best practices', + version: '1.0.0', + enabled: true, + source: 'bundled', + triggers: ['/review'], + }, + { + id: 'skill-creator', + name: 'Skill Creator', + description: 'Create, edit, and manage custom skills', + version: '1.0.0', + enabled: true, + source: 'bundled', + triggers: ['/skill'], + }, + { + id: 'profile-setup', + name: 'Profile Setup', + description: 'Interactive setup wizard to personalize your agent profile', + version: '1.0.0', + enabled: true, + source: 'bundled', + triggers: ['/profile'], + }, + ] +} + +/** + * Register all Skills-related IPC handlers. + */ +export function registerSkillsIpcHandlers(): void { + /** + * Get list of all skills with their status. + * Returns skills from the real Agent instance. + */ + ipcMain.handle('skills:list', async () => { + const agent = getDefaultAgent() + + if (!agent) { + // Fallback: return default skills when no agent + console.log('[IPC] skills:list - No agent available, returning defaults') + return getDefaultSkills() + } + + try { + const skillsWithStatus = agent.getSkillsWithStatus() + + // Transform to SkillInfo format + const skills: SkillInfo[] = skillsWithStatus.map((skill) => ({ + id: skill.id, + name: skill.name, + description: skill.description, + version: '1.0.0', // Skills don't have version in current implementation + enabled: skill.eligible, + source: skill.source as 'bundled' | 'global' | 'profile', + triggers: [`/${skill.id}`], // Default trigger is / + })) + + console.log(`[IPC] skills:list - Returning ${skills.length} skills from agent`) + return skills + } catch (err) { + console.error('[IPC] skills:list - Error getting skills from agent:', err) + return getDefaultSkills() + } + }) + + /** + * Toggle a skill's enabled status. + * NOTE: Skills eligibility is determined by requirements (env vars, binaries, etc.) + * This handler reports the current eligibility status. + */ + ipcMain.handle('skills:toggle', async (_event, skillId: string) => { + console.log(`[IPC] skills:toggle called for: ${skillId}`) + + const agent = getDefaultAgent() + if (!agent) { + return { error: 'No agent available' } + } + + const skillsWithStatus = agent.getSkillsWithStatus() + const skill = skillsWithStatus.find((s) => s.id === skillId) + + if (!skill) { + return { error: `Skill not found: ${skillId}` } + } + + // Skills can't be manually toggled - eligibility is based on requirements + // Return current status + return { + id: skillId, + enabled: skill.eligible, + reasons: skill.reasons, + } + }) + + /** + * Set a skill's enabled status explicitly. + * NOTE: Skills eligibility is automatic based on requirements. + * This handler is a no-op but returns current status. + */ + ipcMain.handle('skills:setStatus', async (_event, skillId: string, enabled: boolean) => { + console.log(`[IPC] skills:setStatus called for: ${skillId}, enabled: ${enabled}`) + + const agent = getDefaultAgent() + if (!agent) { + return { error: 'No agent available' } + } + + const skillsWithStatus = agent.getSkillsWithStatus() + const skill = skillsWithStatus.find((s) => s.id === skillId) + + if (!skill) { + return { error: `Skill not found: ${skillId}` } + } + + // TODO: Implement skill disable via config + // For now, just return current eligibility status + return { + id: skillId, + enabled: skill.eligible, + reasons: skill.reasons, + } + }) + + /** + * Get skill details by ID. + */ + ipcMain.handle('skills:get', async (_event, skillId: string) => { + const agent = getDefaultAgent() + + if (!agent) { + // Fallback: check default skills + const defaults = getDefaultSkills() + const skill = defaults.find((s) => s.id === skillId) + if (skill) return skill + return { error: `Skill not found: ${skillId}` } + } + + const skillsWithStatus = agent.getSkillsWithStatus() + const skill = skillsWithStatus.find((s) => s.id === skillId) + + if (!skill) { + return { error: `Skill not found: ${skillId}` } + } + + return { + id: skill.id, + name: skill.name, + description: skill.description, + version: '1.0.0', + enabled: skill.eligible, + source: skill.source as 'bundled' | 'global' | 'profile', + triggers: [`/${skill.id}`], + reasons: skill.reasons, + } + }) + + /** + * Reload skills from disk. + */ + ipcMain.handle('skills:reload', async () => { + const agent = getDefaultAgent() + if (!agent) { + return { error: 'No agent available' } + } + + agent.reloadSkills() + console.log('[IPC] skills:reload - Skills reloaded') + + return { ok: true } + }) + + /** + * Add a skill from GitHub repository. + * Source formats: owner/repo, owner/repo/skill-name, or full GitHub URL + */ + ipcMain.handle( + 'skills:add', + async ( + _event, + source: string, + options?: { name?: string; force?: boolean }, + ) => { + console.log(`[IPC] skills:add called: source=${source}, options=${JSON.stringify(options)}`) + + const { addSkill } = await import('../../../../src/agent/skills/add.js') + + const result = await addSkill({ + source, + name: options?.name, + force: options?.force, + }) + + console.log(`[IPC] skills:add result: ${result.message}`) + + // Reload skills in agent if available + const agent = getDefaultAgent() + if (agent && result.ok) { + agent.reloadSkills() + } + + return result + }, + ) + + /** + * Remove an installed skill by name. + */ + ipcMain.handle('skills:remove', async (_event, name: string) => { + console.log(`[IPC] skills:remove called: name=${name}`) + + const { removeSkill } = await import('../../../../src/agent/skills/add.js') + + const result = await removeSkill(name) + + console.log(`[IPC] skills:remove result: ${result.message}`) + + // Reload skills in agent if available + const agent = getDefaultAgent() + if (agent && result.ok) { + agent.reloadSkills() + } + + return result + }) +} diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 94948866..a360a499 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' +import { registerAllIpcHandlers, initializeApp, cleanupAll } from './ipc/index.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -16,15 +17,19 @@ let win: BrowserWindow | null function createWindow() { win = new BrowserWindow({ + width: 1200, + height: 800, webPreferences: { preload: path.join(__dirname, 'preload.mjs'), + // Enable node integration for IPC + contextIsolation: true, + nodeIntegration: false, }, }) if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL) } else { - // win.loadFile('dist/index.html') win.loadFile(path.join(RENDERER_DIST, 'index.html')) } } @@ -42,4 +47,16 @@ app.on('activate', () => { } }) -app.whenReady().then(createWindow) +app.on('before-quit', () => { + cleanupAll() +}) + +app.whenReady().then(async () => { + // Register all IPC handlers before creating window + registerAllIpcHandlers() + + // Initialize Hub and create default agent + await initializeApp() + + createWindow() +}) diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts index 05e341de..8c898819 100644 --- a/apps/desktop/electron/preload.ts +++ b/apps/desktop/electron/preload.ts @@ -1,6 +1,95 @@ import { ipcRenderer, contextBridge } from 'electron' -// --------- Expose some API to the Renderer process --------- +// ============================================================================ +// Type definitions for IPC API +// ============================================================================ + +export interface HubStatus { + hubId: string + status: string + agentCount: number + gatewayConnected: boolean + gatewayUrl?: string + defaultAgent?: { + agentId: string + status: string + } | null +} + +export interface AgentInfo { + agentId: string + status: string +} + +export interface ToolInfo { + name: string + group: string + enabled: boolean +} + +export interface SkillInfo { + id: string + name: string + description: string + version: string + enabled: boolean + source: 'bundled' | 'global' | 'profile' + triggers: string[] +} + +// ============================================================================ +// Expose typed API to Renderer process +// ============================================================================ + +const electronAPI = { + // Hub management + hub: { + init: () => ipcRenderer.invoke('hub:init'), + getStatus: (): Promise => ipcRenderer.invoke('hub:getStatus'), + getAgentInfo: (): Promise => ipcRenderer.invoke('hub:getAgentInfo'), + info: () => ipcRenderer.invoke('hub:info'), + reconnect: (url: string) => ipcRenderer.invoke('hub:reconnect', url), + listAgents: () => ipcRenderer.invoke('hub:listAgents'), + createAgent: (id?: string) => ipcRenderer.invoke('hub:createAgent', id), + getAgent: (id: string) => ipcRenderer.invoke('hub:getAgent', id), + closeAgent: (id: string) => ipcRenderer.invoke('hub:closeAgent', id), + sendMessage: (agentId: string, content: string) => + ipcRenderer.invoke('hub:sendMessage', agentId, content), + }, + + // Tools management + tools: { + list: (): Promise => ipcRenderer.invoke('tools:list'), + toggle: (name: string) => ipcRenderer.invoke('tools:toggle', name), + setStatus: (name: string, enabled: boolean) => + ipcRenderer.invoke('tools:setStatus', name, enabled), + active: () => ipcRenderer.invoke('tools:active'), + reload: () => ipcRenderer.invoke('tools:reload'), + }, + + // Skills management + skills: { + list: (): Promise => ipcRenderer.invoke('skills:list'), + get: (id: string) => ipcRenderer.invoke('skills:get', id), + toggle: (id: string) => ipcRenderer.invoke('skills:toggle', id), + setStatus: (id: string, enabled: boolean) => + ipcRenderer.invoke('skills:setStatus', id, enabled), + reload: () => ipcRenderer.invoke('skills:reload'), + add: (source: string, options?: { name?: string; force?: boolean }) => + ipcRenderer.invoke('skills:add', source, options), + remove: (name: string) => ipcRenderer.invoke('skills:remove', name), + }, + + // Agent management + agent: { + status: () => ipcRenderer.invoke('agent:status'), + }, +} + +// Expose to renderer +contextBridge.exposeInMainWorld('electronAPI', electronAPI) + +// Also expose ipcRenderer for backward compatibility contextBridge.exposeInMainWorld('ipcRenderer', { on(...args: Parameters) { const [channel, listener] = args @@ -18,7 +107,7 @@ contextBridge.exposeInMainWorld('ipcRenderer', { const [channel, ...omit] = args return ipcRenderer.invoke(channel, ...omit) }, - - // You can expose other APTs you need here. - // ... }) + +// Type declaration for window object +export type ElectronAPI = typeof electronAPI