Files
bDS/src/main/ipc/chatHandlers.ts
2026-02-25 20:29:01 +01:00

395 lines
12 KiB
TypeScript

/**
* Chat IPC handlers - AI chat functionality using OpenCode Zen API
*/
import { ipcMain, BrowserWindow } from 'electron';
import { ChatEngine } from '../engine/ChatEngine';
import { OpenCodeManager } from '../engine/OpenCodeManager';
import { getPostEngine } from '../engine/PostEngine';
import { getMediaEngine } from '../engine/MediaEngine';
import { getDatabase } from '../database';
import { getProtocolTelemetryService } from '../agentic/observability/protocolTelemetry';
let chatEngine: ChatEngine | null = null;
let openCodeManager: OpenCodeManager | null = null;
let openCodeManagerInitPromise: Promise<void> | null = null;
let mainWindowGetter: (() => BrowserWindow | null) | null = null;
/**
* Initialize chat handlers with the main window reference
*/
export function initializeChatHandlers(getMainWindow: () => BrowserWindow | null): void {
mainWindowGetter = getMainWindow;
}
/**
* Get or create the ChatEngine instance
*/
function getChatEngine(): ChatEngine {
if (!chatEngine) {
chatEngine = new ChatEngine(getDatabase());
}
return chatEngine;
}
/**
* Get or create the OpenCodeManager instance.
* Returns a promise that resolves when the manager is fully initialized
* (including loading the API key from settings).
*/
async function getOpenCodeManager(): Promise<OpenCodeManager> {
if (!openCodeManager) {
openCodeManager = new OpenCodeManager(
getChatEngine(),
getPostEngine(),
getMediaEngine(),
() => mainWindowGetter?.() || null
);
// Load API key from settings and await it
const engine = getChatEngine();
openCodeManagerInitPromise = (async () => {
try {
const key = await engine.getSetting('opencode_api_key');
if (key) {
openCodeManager!.setApiKey(key);
}
} catch {
// Silently ignore errors loading the key
}
})();
}
// Always wait for initialization to complete before returning
if (openCodeManagerInitPromise) {
await openCodeManagerInitPromise;
}
return openCodeManager;
}
/**
* Register all chat-related IPC handlers
*/
export function registerChatHandlers(): void {
// ============ API Key & Status ============
// Check if service is ready
ipcMain.handle('chat:checkReady', async () => {
try {
const manager = await getOpenCodeManager();
const result = await manager.checkReady();
return {
ready: result.ready,
error: result.error,
backend: 'opencode',
};
} catch (error) {
console.error('[Chat IPC] Error checking ready:', error);
return { ready: false, error: (error as Error).message };
}
});
// Validate API key
ipcMain.handle('chat:validateApiKey', async (_, apiKey: string) => {
try {
const manager = await getOpenCodeManager();
const result = await manager.validateApiKey(apiKey);
return result;
} catch (error) {
console.error('[Chat IPC] Error validating API key:', error);
return { isValid: false, models: [] };
}
});
// Set API key
ipcMain.handle('chat:setApiKey', async (_, apiKey: string) => {
try {
const manager = await getOpenCodeManager();
manager.setApiKey(apiKey);
// Persist to settings
const engine = getChatEngine();
await engine.setSetting('opencode_api_key', apiKey);
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error setting API key:', error);
return { success: false, error: (error as Error).message };
}
});
// Get API key (masked)
ipcMain.handle('chat:getApiKey', async () => {
try {
const manager = await getOpenCodeManager();
const key = manager.getApiKey();
if (!key) return { hasKey: false, maskedKey: '' };
// Mask all but last 4 characters
const masked = '•'.repeat(Math.max(0, key.length - 4)) + key.slice(-4);
return { hasKey: true, maskedKey: masked };
} catch (error) {
console.error('[Chat IPC] Error getting API key:', error);
return { hasKey: false, maskedKey: '' };
}
});
// ============ Chat Settings ============
ipcMain.handle('chat:getProtocolHealth', async () => {
return getProtocolTelemetryService().getSnapshot();
});
// Get available models
ipcMain.handle('chat:getAvailableModels', async () => {
try {
const manager = await getOpenCodeManager();
const models = await manager.getAvailableModels();
const engine = getChatEngine();
const selectedModel = await engine.getSelectedModel();
return { success: true, models, selectedModel };
} catch (error) {
console.error('[Chat IPC] Error getting models:', error);
return { success: false, error: (error as Error).message };
}
});
// Set default model
ipcMain.handle('chat:setDefaultModel', async (_, modelId: string) => {
try {
const engine = getChatEngine();
await engine.setSelectedModel(modelId);
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error setting model:', error);
return { success: false, error: (error as Error).message };
}
});
// Get system prompt
ipcMain.handle('chat:getSystemPrompt', async () => {
try {
const engine = getChatEngine();
const prompt = await engine.getDefaultSystemPrompt();
return { success: true, prompt };
} catch (error) {
console.error('[Chat IPC] Error getting system prompt:', error);
return { success: false, error: (error as Error).message };
}
});
// Set system prompt
ipcMain.handle('chat:setSystemPrompt', async (_, prompt: string) => {
try {
const engine = getChatEngine();
await engine.setDefaultSystemPrompt(prompt);
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error setting system prompt:', error);
return { success: false, error: (error as Error).message };
}
});
// ============ Conversation CRUD ============
// Get all conversations
ipcMain.handle('chat:getConversations', async () => {
try {
const engine = getChatEngine();
return await engine.getRecentConversations();
} catch (error) {
console.error('[Chat IPC] Error getting conversations:', error);
return [];
}
});
// Create new conversation
ipcMain.handle('chat:createConversation', async (_, title?: string, model?: string) => {
try {
const engine = getChatEngine();
const systemPrompt = await engine.getDefaultSystemPrompt();
const selectedModel = model || (await engine.getSelectedModel());
const conversation = await engine.createConversation({
title: title || 'New Chat',
model: selectedModel,
systemPrompt,
});
return conversation;
} catch (error) {
console.error('[Chat IPC] Error creating conversation:', error);
return { error: (error as Error).message };
}
});
// Get conversation by ID
ipcMain.handle('chat:getConversation', async (_, id: string) => {
try {
const engine = getChatEngine();
return await engine.getConversation(id);
} catch (error) {
console.error('[Chat IPC] Error getting conversation:', error);
return null;
}
});
// Update conversation
ipcMain.handle('chat:updateConversation', async (_, id: string, updates: { title?: string; model?: string }) => {
try {
const engine = getChatEngine();
await engine.updateConversation(id, updates);
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error updating conversation:', error);
return { success: false, error: (error as Error).message };
}
});
// Delete conversation
ipcMain.handle('chat:deleteConversation', async (_, id: string) => {
try {
const engine = getChatEngine();
await engine.deleteConversation(id);
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error deleting conversation:', error);
return { success: false, error: (error as Error).message };
}
});
// ============ Chat Messaging ============
// Send a message
ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string, metadata?: { surface?: 'tab' | 'sidebar' }) => {
try {
const manager = await getOpenCodeManager();
const mainWindow = mainWindowGetter?.();
const result = await manager.sendMessage(conversationId, message, {
metadata,
onDelta: (delta) => {
if (mainWindow) {
mainWindow.webContents.send('chat-stream-delta', { conversationId, delta });
}
},
onToolCall: (toolCall) => {
if (mainWindow) {
mainWindow.webContents.send('chat-tool-call', { conversationId, toolCall });
}
},
onToolResult: (result) => {
if (mainWindow) {
mainWindow.webContents.send('chat-tool-result', { conversationId, result });
}
},
});
return result;
} catch (error) {
console.error('[Chat IPC] Error sending message:', error);
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('chat:addSystemEvent', async (_, conversationId: string, content: string) => {
try {
const engine = getChatEngine();
await engine.addMessage({
conversationId,
role: 'system',
content,
createdAt: new Date(),
});
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error adding system event:', error);
return { success: false, error: (error as Error).message };
}
});
// Abort a running message
ipcMain.handle('chat:abortMessage', async (_, conversationId: string) => {
try {
const manager = await getOpenCodeManager();
return await manager.abortMessage(conversationId);
} catch (error) {
console.error('[Chat IPC] Error aborting message:', error);
return { success: false, error: (error as Error).message };
}
});
// Get message history for a conversation
ipcMain.handle('chat:getHistory', async (_, conversationId: string) => {
try {
const engine = getChatEngine();
return await engine.getMessages(conversationId);
} catch (error) {
console.error('[Chat IPC] Error getting history:', error);
return [];
}
});
// Clear messages from a conversation
ipcMain.handle('chat:clearMessages', async (_, conversationId: string) => {
try {
const engine = getChatEngine();
await engine.clearMessages(conversationId);
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error clearing messages:', error);
return { success: false, error: (error as Error).message };
}
});
// Set conversation model
ipcMain.handle('chat:setConversationModel', async (_, conversationId: string, modelId: string) => {
try {
const engine = getChatEngine();
await engine.updateConversation(conversationId, { model: modelId });
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error setting conversation model:', error);
return { success: false, error: (error as Error).message };
}
});
// ============ Taxonomy Analysis ============
// Analyze taxonomy items (tags/categories) and suggest mappings
ipcMain.handle('chat:analyzeTaxonomy', async (_, categories: Array<{ name: string; slug: string; existsInProject: boolean }>, tags: Array<{ name: string; slug: string; existsInProject: boolean }>, modelId: string) => {
try {
const manager = await getOpenCodeManager();
return await manager.analyzeTaxonomy(categories, tags, modelId);
} catch (error) {
console.error('[Chat IPC] Error analyzing taxonomy:', error);
return { success: false, error: (error as Error).message };
}
});
// ============ Media Analysis ============
// Analyze a media image and generate title, alt text, and caption
ipcMain.handle('chat:analyzeMediaImage', async (_, mediaId: string, language?: string) => {
try {
const manager = await getOpenCodeManager();
return await manager.analyzeMediaImage(mediaId, language || 'en');
} catch (error) {
console.error('[Chat IPC] Error analyzing media image:', error);
return { success: false, error: (error as Error).message };
}
});
}
/**
* Cleanup chat resources
*/
export async function cleanupChatHandlers(): Promise<void> {
if (openCodeManager) {
await openCodeManager.stop();
openCodeManager = null;
}
openCodeManagerInitPromise = null;
chatEngine = null;
}