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:
parent
d6f79d2df6
commit
2a4fcded03
7 changed files with 970 additions and 6 deletions
79
apps/desktop/electron/electron-env.d.ts
vendored
79
apps/desktop/electron/electron-env.d.ts
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
220
apps/desktop/electron/ipc/agent.ts
Normal file
220
apps/desktop/electron/ipc/agent.ts
Normal 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
|
||||
}
|
||||
242
apps/desktop/electron/ipc/hub.ts
Normal file
242
apps/desktop/electron/ipc/hub.ts
Normal 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
|
||||
}
|
||||
39
apps/desktop/electron/ipc/index.ts
Normal file
39
apps/desktop/electron/ipc/index.ts
Normal 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()
|
||||
}
|
||||
278
apps/desktop/electron/ipc/skills.ts
Normal file
278
apps/desktop/electron/ipc/skills.ts
Normal 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
|
||||
})
|
||||
}
|
||||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue