import { ipcMain, dialog, BrowserWindow, Menu } from 'electron' import { listStories, readStory, writeStory, createStory, renameStory, deleteStory, getStoryMeta, setStoryMeta, getCollectionConfig, setCollectionContext, saveOrderList, readSession, writeSession, listMarkets, upsertMarket, deleteMarket, listSubmissions, addSubmission, updateSubmission, saveRevision, listRevisions, loadRevision, appendTelemetrySession, readTelemetry } from './fileSystem' import type { Market, Submission, StoryMeta, TelemetrySession } from './fileSystem' import { streamMessage, streamPrompt, resetClient } from './aiService' import type { AIPayload } from './aiService' import { readGlobalConfig, writeGlobalConfig } from './globalConfig' import type { GlobalConfig } from './globalConfig' export function registerIpcHandlers(): void { // ── Stories ────────────────────────────────────────────────────────────────── ipcMain.handle('stories:list', async () => listStories()) ipcMain.handle('stories:read', async (_e, path: string) => readStory(path)) ipcMain.handle('stories:write', async (_e, path: string, content: string) => writeStory(path, content)) ipcMain.handle('stories:create', async (_e, name: string) => createStory(name)) ipcMain.handle('stories:rename', async (_e, oldPath: string, newName: string) => renameStory(oldPath, newName)) ipcMain.handle('stories:delete', async (_e, path: string) => deleteStory(path)) ipcMain.handle('stories:getMeta', async (_e, storyId: string) => getStoryMeta(storyId)) ipcMain.handle('stories:setMeta', async (_e, storyId: string, meta: StoryMeta) => setStoryMeta(storyId, meta)) ipcMain.handle('stories:saveOrder', async (_e, order: string[]) => saveOrderList(order)) // ── Collection config ───────────────────────────────────────────────────────── ipcMain.handle('collection:getConfig', async () => getCollectionConfig()) ipcMain.handle('collection:setContext', async (_e, context: string) => setCollectionContext(context)) // ── Session ────────────────────────────────────────────────────────────────── ipcMain.handle('session:read', async () => readSession()) ipcMain.handle('session:write', async (_e, data: Record) => writeSession(data)) // ── Markets ────────────────────────────────────────────────────────────────── ipcMain.handle('markets:list', async () => listMarkets()) ipcMain.handle('markets:upsert', async (_e, market: Market) => upsertMarket(market)) ipcMain.handle('markets:delete', async (_e, id: string) => deleteMarket(id)) // ── Submissions ────────────────────────────────────────────────────────────── ipcMain.handle('submissions:list', async () => listSubmissions()) ipcMain.handle('submissions:add', async (_e, sub: Submission) => addSubmission(sub)) ipcMain.handle('submissions:update', async (_e, id: string, updates: Partial) => updateSubmission(id, updates)) // ── Revisions ───────────────────────────────────────────────────────────────── ipcMain.handle('revisions:save', async (_e, path: string, content: string) => saveRevision(path, content)) ipcMain.handle('revisions:list', async (_e, path: string) => listRevisions(path)) ipcMain.handle('revisions:load', async (_e, path: string, id: string) => loadRevision(path, id)) // ── Telemetry ───────────────────────────────────────────────────────────────── ipcMain.handle('telemetry:append', async (_e, session: TelemetrySession) => appendTelemetrySession(session)) ipcMain.handle('telemetry:read', async () => readTelemetry()) // ── Config ──────────────────────────────────────────────────────────────────── ipcMain.handle('config:read', async () => readGlobalConfig()) ipcMain.handle('config:write', async (_e, updates: Partial) => { writeGlobalConfig(updates) resetClient() // always reset — baseURL or model may have changed }) ipcMain.handle('config:pickFolder', async (event): Promise => { const win = BrowserWindow.fromWebContents(event.sender) const result = await dialog.showOpenDialog(win!, { properties: ['openDirectory'] }) return result.canceled ? null : result.filePaths[0] }) // ── Native context menus ───────────────────────────────────────────────────── ipcMain.handle('menu:editorContext', async (event) => { const win = BrowserWindow.fromWebContents(event.sender) if (!win) return const menu = Menu.buildFromTemplate([ { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { type: 'separator' }, { role: 'selectAll' }, ]) menu.popup({ window: win }) }) ipcMain.handle('menu:storyContext', async (event, _storyId: string) => { const win = BrowserWindow.fromWebContents(event.sender) if (!win) return return new Promise((resolve) => { const menu = Menu.buildFromTemplate([ { label: 'Rename', click: () => resolve('rename') }, { type: 'separator' }, { label: 'Delete', click: () => resolve('delete') }, ]) menu.on('menu-will-close', () => setTimeout(() => resolve(null), 100)) menu.popup({ window: win }) }) }) // ── AI streaming ────────────────────────────────────────────────────────────── ipcMain.handle('ai:generatePrompt', async (event) => { try { let pending = '' let flushTimer: ReturnType | null = null const flush = (): void => { if (flushTimer) { clearTimeout(flushTimer); flushTimer = null } if (pending && !event.sender.isDestroyed()) { event.sender.send('ai:promptChunk', pending) pending = '' } } await streamPrompt((chunk: string) => { pending += chunk if (!flushTimer) flushTimer = setTimeout(flush, 30) }) flush() if (!event.sender.isDestroyed()) event.sender.send('ai:promptDone') } catch (err) { const message = err instanceof Error ? err.message : String(err) if (!event.sender.isDestroyed()) event.sender.send('ai:promptError', message) } }) ipcMain.handle('ai:streamMessage', async (event, payload: AIPayload) => { try { let pending = '' let flushTimer: ReturnType | null = null const flush = (): void => { if (flushTimer) { clearTimeout(flushTimer); flushTimer = null } if (pending && !event.sender.isDestroyed()) { event.sender.send('ai:chunk', pending) pending = '' } } await streamMessage(payload, (chunk: string) => { pending += chunk if (!flushTimer) flushTimer = setTimeout(flush, 30) }) flush() if (!event.sender.isDestroyed()) event.sender.send('ai:done') } catch (err) { console.error('[ai:streamMessage] error:', err) const message = err instanceof Error ? `${err.message}${(err as NodeJS.ErrnoException & { status?: number; url?: string }).status ? ` (HTTP ${(err as NodeJS.ErrnoException & { status?: number }).status})` : ''}` : String(err) if (!event.sender.isDestroyed()) event.sender.send('ai:error', message) } }) }