import { AppError, mapBackendErrorCode, normalizeAppError, type AppErrorCode, } from './error-model'; export { AppError } from './error-model'; type UnifiedRequest = { id: string; module: string; action: string; payload?: unknown; }; type UnifiedResponse = { id?: string; ok: boolean; data?: unknown; error?: { code?: string; message?: string; details?: unknown; }; }; const UNIFIED_CHANNELS = new Set([ 'app:version', 'app:name', 'app:platform', 'opencode:status', 'settings:getAll', 'settings:get', 'settings:set', 'settings:setMany', 'settings:reset', 'provider:list', 'provider:get', 'provider:getDefault', 'provider:hasApiKey', 'provider:getApiKey', 'provider:validateKey', 'provider:save', 'provider:delete', 'provider:setApiKey', 'provider:updateWithKey', 'provider:deleteApiKey', 'provider:setDefault', 'update:status', 'update:version', 'update:check', 'update:download', 'update:install', 'update:setChannel', 'update:setAutoDownload', 'update:cancelAutoInstall', ]); function toUnifiedRequest(channel: string, args: unknown[]): UnifiedRequest { const splitIndex = channel.indexOf(':'); return { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, module: channel.slice(0, splitIndex), action: channel.slice(splitIndex + 1), payload: args.length <= 1 ? args[0] : args, }; } function mapUnifiedError(response: UnifiedResponse): AppError { const message = response.error?.message || 'Unified IPC request failed'; return new AppError(mapBackendErrorCode(response.error?.code), message, response.error?.details); } async function invokeViaIpc(channel: string, args: unknown[]): Promise { if (channel !== 'app:request' && UNIFIED_CHANNELS.has(channel)) { const request = toUnifiedRequest(channel, args); try { const response = await window.electron.ipcRenderer.invoke('app:request', request) as UnifiedResponse; if (!response?.ok) { const message = response?.error?.message || 'Unified IPC request failed'; if (message.includes('APP_REQUEST_UNSUPPORTED:')) { throw new Error(message); } throw mapUnifiedError(response); } return response.data as T; } catch (error) { const message = error instanceof Error ? error.message : String(error); if ( !message.includes('APP_REQUEST_UNSUPPORTED:') && !message.includes('Invalid IPC channel: app:request') ) { throw normalizeAppError(error, { transport: 'ipc', channel, source: 'app:request' }); } } } try { return await window.electron.ipcRenderer.invoke(channel, ...args) as T; } catch (error) { throw normalizeAppError(error, { transport: 'ipc', channel, source: 'legacy-ipc' }); } } export async function invokeApi(channel: string, ...args: unknown[]): Promise { return invokeViaIpc(channel, args); } export async function invokeIpc(channel: string, ...args: unknown[]): Promise { return invokeApi(channel, ...args); } export async function invokeIpcWithRetry( channel: string, args: unknown[] = [], retries = 1, retryable: AppErrorCode[] = ['TIMEOUT', 'NETWORK'], ): Promise { let lastError: unknown; for (let attempt = 0; attempt <= retries; attempt += 1) { try { return await invokeApi(channel, ...args); } catch (error) { lastError = error; if (!(error instanceof AppError) || !retryable.includes(error.code) || attempt === retries) { throw error; } } } throw normalizeAppError(lastError); } export function initializeDefaultTransports(): void { // IPC is the only renderer-to-main transport in the opencode-based app. } export function toUserMessage(error: unknown): string { const appError = error instanceof AppError ? error : normalizeAppError(error); switch (appError.code) { case 'AUTH_INVALID': return 'Authentication failed. Check API key or login session and retry.'; case 'TIMEOUT': return 'Request timed out. Please retry.'; case 'RATE_LIMIT': return 'Too many requests. Please wait and try again.'; case 'PERMISSION': return 'Permission denied. Check your configuration and retry.'; case 'CHANNEL_UNAVAILABLE': return 'Service channel unavailable. Retry after restarting Makelore.'; case 'NETWORK': return 'Network error. Please verify connectivity and retry.'; case 'CONFIG': return 'Configuration is invalid. Please review settings.'; case 'RUNTIME': return 'Runtime is unavailable. Start or restart it and retry.'; default: return appError.message || 'Unexpected error occurred.'; } } export type FilePreviewError = | 'outsideSandbox' | 'readOnlyRoot' | 'tooLarge' | 'binary' | 'notFound' | 'notDirectory' | 'invalidContent' | string; export interface ReadTextFileResult { ok: boolean; content?: string; mimeType?: string; size?: number; readOnly?: boolean; error?: FilePreviewError; } export interface ReadBinaryFileResult { ok: boolean; data?: Uint8Array; mimeType?: string; size?: number; readOnly?: boolean; error?: FilePreviewError; } export interface ReadBinaryFileOptions { maxBytes?: number; } export interface WriteTextFileResult { ok: boolean; error?: FilePreviewError; } export interface StatFileResult { ok: boolean; size?: number; mtime?: number; isFile?: boolean; isDir?: boolean; readOnly?: boolean; error?: FilePreviewError; } export interface ListDirEntry { name: string; path: string; isDir: boolean; size: number; } export interface ListDirResult { ok: boolean; entries?: ListDirEntry[]; error?: FilePreviewError; } export interface TreeNode { name: string; relPath: string; absPath: string; isDir: boolean; size?: number; mtime?: number; children?: TreeNode[]; } export interface ListTreeOptions { maxDepth?: number; maxNodes?: number; includeHidden?: boolean; } export interface ListTreeResult { ok: boolean; root?: TreeNode; truncated?: boolean; error?: FilePreviewError; } export const readTextFile = (path: string): Promise => invokeIpc('file:readText', path); export const readBinaryFile = ( path: string, opts?: ReadBinaryFileOptions, ): Promise => invokeIpc('file:readBinary', path, opts); export const writeTextFile = (path: string, content: string): Promise => invokeIpc('file:writeText', path, content); interface SaveTranscriptResult { status: 'saved' | 'cancelled'; } export async function saveMarkdownTranscript( defaultPath: string, markdown: string, ): Promise<'saved' | 'cancelled'> { const result = await invokeIpc('transcript:save', { defaultPath, markdown, }); if (result.status !== 'saved' && result.status !== 'cancelled') { throw new Error('Invalid transcript save result'); } return result.status; } export const statFile = (path: string): Promise => invokeIpc('file:stat', path); export const listDir = (path: string): Promise => invokeIpc('file:listDir', path); export const listTree = (path: string, opts?: ListTreeOptions): Promise => invokeIpc('file:listTree', path, opts);