diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 2cbcde9..c90f92f 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { IPC, ChatRequest, + SimpleCompletionRequest, AppSettings, McpServerConfig, Message, @@ -675,6 +676,56 @@ export function registerIpcHandlers(): void { autoUpdater.checkForUpdates(); }); + // ─── Chat Complete (headless, no streaming events) ──────────────────────── + ipcMain.handle(IPC.CHAT_COMPLETE, async (_e, request: SimpleCompletionRequest) => { + const settings = getSettings(); + const provider = settings.providers.find((p) => p.id === request.providerId); + if (!provider) throw new Error(`Provider "${request.providerId}" not found.`); + + const messages: Message[] = request.messages.map((m) => ({ + id: uuidv4(), + role: m.role, + content: m.content, + timestamp: Date.now(), + })); + + // Providers require the conversation to end with a user message. + // Append a closing instruction if the last message is from the assistant. + if (messages.length > 0 && messages[messages.length - 1].role !== 'user') { + messages.push({ + id: uuidv4(), + role: 'user', + content: 'Please provide your analysis per your system prompt.', + timestamp: Date.now(), + }); + } + + const emptyParams = { temperature: 0.7, maxTokens: 2048, topP: 1 }; + + const getStream = () => { + switch (provider.type) { + case 'anthropic': + return streamAnthropic(provider, messages, request.model, emptyParams, request.systemPrompt, []); + case 'openai': + return streamOpenAI(provider, messages, request.model, emptyParams, request.systemPrompt, []); + case 'lmstudio': + return streamLmStudio(provider, messages, request.model, emptyParams, request.systemPrompt, []); + case 'ollama': + return streamOllama(provider, messages, request.model, emptyParams, request.systemPrompt, []); + case 'gemini': + return streamGemini(provider, messages, request.model, emptyParams, request.systemPrompt, []); + default: + throw new Error(`Provider type "${provider.type}" is not supported for background calls.`); + } + }; + + let fullText = ''; + for await (const event of getStream()) { + if (event.type === 'delta') fullText += event.text; + } + return { text: fullText }; + }); + // ─── Abort ─────────────────────────────────────────────────────────────── ipcMain.on(IPC.CHAT_ABORT, (_e, conversationId: string) => { abortControllers.get(conversationId)?.abort(); diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index e92b4e2..5d129c9 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -25,6 +25,9 @@ contextBridge.exposeInMainWorld('api', { send: (request: ChatRequest): Promise<{ messageId: string }> => ipcRenderer.invoke(IPC.CHAT_SEND, request), + complete: (request: import('./shared/types').SimpleCompletionRequest): Promise<{ text: string }> => + ipcRenderer.invoke(IPC.CHAT_COMPLETE, request), + abort: (conversationId: string): void => ipcRenderer.send(IPC.CHAT_ABORT, conversationId), diff --git a/packages/desktop/src/renderer/env.d.ts b/packages/desktop/src/renderer/env.d.ts index 25f7ec6..7e20917 100644 --- a/packages/desktop/src/renderer/env.d.ts +++ b/packages/desktop/src/renderer/env.d.ts @@ -1,6 +1,7 @@ import type { AppSettings, ChatRequest, + SimpleCompletionRequest, FolderEntry, McpServerConfig, McpTool, @@ -22,6 +23,7 @@ declare global { api: { chat: { send: (request: ChatRequest) => Promise<{ messageId: string }>; + complete: (request: SimpleCompletionRequest) => Promise<{ text: string }>; abort: (conversationId: string) => void; onChunk: (cb: (data: StreamChunk) => void) => UnsubFn; onEnd: (cb: (data: StreamEnd) => void) => UnsubFn;