import { writeFile } from 'node:fs/promises'; import { BrowserWindow, dialog, ipcMain } from 'electron'; import { resolveMainWindow, type MainWindowProvider } from '../window-provider'; interface TranscriptSaveRequest { defaultPath?: unknown; markdown?: unknown; } const MAX_TRANSCRIPT_MARKDOWN_BYTES = 10 * 1024 * 1024; function safeDefaultPath(value: unknown): string { if (typeof value !== 'string') return 'chat-transcript.md'; const fileName = value.split(/[\\/]/).filter(Boolean).at(-1)?.trim(); if (!fileName) return 'chat-transcript.md'; return fileName.toLowerCase().endsWith('.md') ? fileName : `${fileName}.md`; } export function registerTranscriptExportHandler(mainWindow: MainWindowProvider): void { ipcMain.handle('transcript:save', async (event, request: TranscriptSaveRequest) => { if (typeof request?.markdown !== 'string') { throw new Error('Invalid transcript markdown'); } if (Buffer.byteLength(request.markdown, 'utf8') > MAX_TRANSCRIPT_MARKDOWN_BYTES) { throw new Error('Transcript is too large'); } let selected: Electron.SaveDialogReturnValue; try { const owner = (event?.sender && BrowserWindow.fromWebContents(event.sender)) || resolveMainWindow(mainWindow); const options: Electron.SaveDialogOptions = { defaultPath: safeDefaultPath(request.defaultPath), filters: [{ name: 'Markdown', extensions: ['md'] }], }; selected = owner ? await dialog.showSaveDialog(owner, options) : await dialog.showSaveDialog(options); } catch { throw new Error('Failed to save transcript'); } if (selected.canceled || !selected.filePath) return { status: 'cancelled' as const }; try { await writeFile(selected.filePath, request.markdown, 'utf8'); } catch { throw new Error('Failed to save transcript'); } return { status: 'saved' as const }; }); }