feat(desktop): add IPC handlers for Hub, Tools, and Skills management

- Create hub.ts IPC handlers for Hub initialization and agent management
- Create agent.ts IPC handlers for tools list, toggle, setStatus, reload
- Create skills.ts IPC handlers for skills list, get, toggle, add, remove
- Expose typed electronAPI via preload.ts with contextBridge
- Add TypeScript definitions in electron-env.d.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-02-03 18:25:20 +08:00
parent d6f79d2df6
commit 2a4fcded03
7 changed files with 970 additions and 6 deletions

View file

@ -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<unknown>
getStatus: () => Promise<HubStatus>
getAgentInfo: () => Promise<AgentInfo | null>
info: () => Promise<unknown>
reconnect: (url: string) => Promise<unknown>
listAgents: () => Promise<unknown>
createAgent: (id?: string) => Promise<unknown>
getAgent: (id: string) => Promise<unknown>
closeAgent: (id: string) => Promise<unknown>
sendMessage: (agentId: string, content: string) => Promise<unknown>
}
tools: {
list: () => Promise<ToolInfo[]>
toggle: (name: string) => Promise<unknown>
setStatus: (name: string, enabled: boolean) => Promise<unknown>
active: () => Promise<unknown>
reload: () => Promise<unknown>
}
skills: {
list: () => Promise<SkillInfo[]>
get: (id: string) => Promise<unknown>
toggle: (id: string) => Promise<unknown>
setStatus: (id: string, enabled: boolean) => Promise<unknown>
reload: () => Promise<unknown>
add: (source: string, options?: { name?: string; force?: boolean }) => Promise<SkillAddResult>
remove: (name: string) => Promise<SkillAddResult>
}
agent: {
status: () => Promise<unknown>
}
}
// Used in Renderer process, expose in `preload.ts`
interface Window {
ipcRenderer: import('electron').IpcRenderer
electronAPI: ElectronAPI
}

View file

@ -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<string, string[]> = {
'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
}

View file

@ -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<void> {
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<HubInfo> => {
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<AgentInfo[]> => {
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
}

View file

@ -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<void> {
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()
}

View file

@ -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 /<skill-id>
}))
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
})
}

View file

@ -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()
})

View file

@ -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<HubStatus> => ipcRenderer.invoke('hub:getStatus'),
getAgentInfo: (): Promise<AgentInfo | null> => 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<ToolInfo[]> => 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<SkillInfo[]> => 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<typeof ipcRenderer.on>) {
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