const { contextBridge, ipcRenderer } = require('electron') function onIpc(channel, callback) { const handler = (_event, data) => callback(data) ipcRenderer.on(channel, handler) return () => ipcRenderer.removeListener(channel, handler) } contextBridge.exposeInMainWorld('electronAPI', { // Auth token persistence (Electron userData/auth.json, all load protocols) getAuthTokens: () => ipcRenderer.invoke('auth-get-tokens'), setAuthTokens: (tokens) => ipcRenderer.invoke('auth-set-tokens', tokens), clearAuthTokens: () => ipcRenderer.invoke('auth-clear-tokens'), // Multi-server baseUrl configuration serverConfig: { get: () => ipcRenderer.invoke('server-config-get'), set: (payload) => ipcRenderer.invoke('server-config-set', payload), probe: (serverBaseUrl, options) => ipcRenderer.invoke('server-config-probe', { serverBaseUrl, allowHttp: options?.allowHttp === true, }), openPicker: () => ipcRenderer.invoke('server-config-open-picker'), closePicker: () => ipcRenderer.invoke('server-picker-close'), removeHistory: (serverBaseUrl) => ipcRenderer.invoke('server-config-remove-history', { serverBaseUrl }), }, // Original APIs getAppVersion: () => ipcRenderer.invoke('get-app-version'), getPlatform: () => ipcRenderer.invoke('get-platform'), getEnvironmentInfo: (workspace) => ipcRenderer.invoke('get-environment-info', { workspace }), gitStatus: (workspace, repoPath) => ipcRenderer.invoke('git-status', { workspace, repoPath }), refreshGitStatus: (workspace, repoPath) => ipcRenderer.invoke('git-status-refresh', { workspace, repoPath }), gitFileDiff: (workspace, repoPath, filePath) => ipcRenderer.invoke('git-file-diff', { workspace, repoPath, path: filePath }), gitRepos: (workspace) => ipcRenderer.invoke('git-repos', { workspace }), gitCommitInput: (workspace, repoPath) => ipcRenderer.invoke('git-commit-input', { workspace, repoPath }), gitCommit: (workspace, repoPath, message) => ipcRenderer.invoke('git-commit', { workspace, repoPath, message }), gitPull: (workspace, repoPath) => ipcRenderer.invoke('git-pull', { workspace, repoPath }), gitPush: (workspace, repoPath) => ipcRenderer.invoke('git-push', { workspace, repoPath }), gitLog: (workspace, repoPath, options) => ipcRenderer.invoke('git-log', { workspace, repoPath, options }), gitCommitFiles: (workspace, repoPath, hash) => ipcRenderer.invoke('git-commit-files', { workspace, repoPath, hash }), gitCommitFileDiff: (workspace, repoPath, hash, path, oldPath) => ipcRenderer.invoke('git-commit-file-diff', { workspace, repoPath, hash, path, oldPath }), gitBranchCompare: (workspace, repoPath, base, target) => ipcRenderer.invoke('git-branch-compare', { workspace, repoPath, base, target }), gitBranchCompareFileDiff: (workspace, repoPath, base, target, path, oldPath) => ipcRenderer.invoke('git-branch-compare-file-diff', { workspace, repoPath, base, target, path, oldPath }), gitBranches: (workspace, repoPath) => ipcRenderer.invoke('git-branches', { workspace, repoPath }), gitStashes: (workspace, repoPath) => ipcRenderer.invoke('git-stashes', { workspace, repoPath }), gitRemotes: (workspace, repoPath) => ipcRenderer.invoke('git-remotes', { workspace, repoPath }), gitFetch: (workspace, repoPath) => ipcRenderer.invoke('git-fetch', { workspace, repoPath }), gitSwitchBranch: (workspace, repoPath, branch, create) => ipcRenderer.invoke('git-switch-branch', { workspace, repoPath, branch, create }), gitStashOp: (workspace, repoPath, action, ref) => ipcRenderer.invoke('git-stash-op', { workspace, repoPath, action, ref }), checkForUpdate: () => ipcRenderer.invoke('check-for-update'), installUpdate: () => ipcRenderer.invoke('install-update'), onUpdateChecking: (callback) => onIpc('update-checking', callback), onDownloadProgress: (callback) => onIpc('download-progress', callback), onUpdateAvailable: (callback) => onIpc('update-available', callback), onUpdateNotAvailable: (callback) => onIpc('update-not-available', callback), onUpdateDownloaded: (callback) => onIpc('update-downloaded', callback), onUpdateError: (callback) => onIpc('update-error', callback), minimizeWindow: () => ipcRenderer.invoke('window-minimize'), maximizeWindow: () => ipcRenderer.invoke('window-maximize'), closeWindow: () => ipcRenderer.invoke('window-close'), openExternal: (url) => ipcRenderer.invoke('open-external', url), openFeishuAuthWindow: (authUrl, callbackPath) => ipcRenderer.invoke('open-feishu-auth-window', { authUrl, callbackPath }), selectDirectory: () => ipcRenderer.invoke('select-directory'), openFolder: (folderPath) => ipcRenderer.invoke('open-folder', folderPath), openTerminal: (folderPath) => ipcRenderer.invoke('open-terminal', folderPath), showItemInFolder: (fullPath) => ipcRenderer.invoke('show-item-in-folder', fullPath), // Tool execution via Streaming WS (called by renderer when server sends tool_execute) // approvalHint(V135)为第 8 参:服务端生成的「总是允许」模式提示,透传给审批卡片;旧调用缺省 null toolExecute: (toolName, args, requestId, workspace, sessionId, needApproval, dangerReason, approvalHint) => ipcRenderer.invoke('tool-execute', { toolName, args, requestId, workspace, sessionId, needApproval, dangerReason, approvalHint: approvalHint ?? null }), // Local tool execution - direct IPC (for renderer-initiated calls) localReadFile: (args) => ipcRenderer.invoke('local-read-file', args), localReadBytes: (args) => ipcRenderer.invoke('local-read-bytes', args), localWriteFile: (args) => ipcRenderer.invoke('local-write-file', args), localEditFile: (args) => ipcRenderer.invoke('local-edit-file', args), // Workspace file listing for file references listWorkspaceFiles: (workspace, filter, limit) => ipcRenderer.invoke('list-workspace-files', { workspace, filter, limit }), // Directory listing for file browser (non-recursive, single level) listDirectory: (dirPath, workspace) => ipcRenderer.invoke('list-directory', { dirPath, workspace }), // Tool approval — renderer UI, main process waits for response onToolApprovalRequest: (callback) => { ipcRenderer.on('tool-approval-request', (event, data) => callback(data)) }, removeToolApprovalRequestListener: () => { ipcRenderer.removeAllListeners('tool-approval-request') }, onToolApprovalDismiss: (callback) => { ipcRenderer.on('tool-approval-dismiss', (event, data) => callback(data)) }, removeToolApprovalDismissListener: () => { ipcRenderer.removeAllListeners('tool-approval-dismiss') }, // 多端审批(技术方案 2026-11-06):本地卡点击 / 远端回声 / 清理都走这一个解锁入口。 // alwaysAllow 仅在需要时透传,主进程不消费(规则由服务端按 hint 生成)。 respondToolApprovalRemote: (requestId, approved, alwaysAllow) => ipcRenderer.invoke('tool-approval-remote-response', { requestId, approved, alwaysAllow: alwaysAllow === true }), // Skill sync — renderer triggers, main process downloads & extracts zip skillSync: (sessionId, syncUrl, token, workspace, apiBase) => ipcRenderer.invoke('skill-sync', { sessionId, syncUrl, token, workspace, apiBase }), onSkillSyncComplete: (callback) => { ipcRenderer.on('skill-sync-complete', (event, data) => callback(data)) }, removeSkillSyncCompleteListener: () => { ipcRenderer.removeAllListeners('skill-sync-complete') }, // MCP — renderer triggers, main process connects & lists tools mcpSync: (sessionId, servers) => ipcRenderer.invoke('mcp-sync', { sessionId, servers }), mcpClose: (sessionId) => ipcRenderer.invoke('mcp-close', { sessionId }), // Local skills in ~/.agents/skills listLocalSkills: () => ipcRenderer.invoke('list-local-skills'), getLocalSkillDetail: (folderName) => ipcRenderer.invoke('get-local-skill-detail', { folderName }), readLocalSkillFiles: (folderName) => ipcRenderer.invoke('read-local-skill-files', { folderName }), // Read AGENTS.md from workspace root (for LOCAL mode system prompt injection) readAgentsMd: (workspace) => ipcRenderer.invoke('read-agents-md', { workspace }), // Terminal (node-pty) — embedded terminal panel terminal: { create: (options) => ipcRenderer.invoke('terminal:create', options), write: (id, data) => ipcRenderer.send('terminal:data', { id, data }), resize: (id, cols, rows) => ipcRenderer.send('terminal:resize', { id, cols, rows }), kill: (id) => ipcRenderer.invoke('terminal:kill', { id }), list: () => ipcRenderer.invoke('terminal:list'), onData: (callback) => { const handler = (_event, payload) => callback(payload) ipcRenderer.on('terminal:data', handler) return () => ipcRenderer.removeListener('terminal:data', handler) }, onExit: (callback) => { const handler = (_event, payload) => callback(payload) ipcRenderer.on('terminal:exit', handler) return () => ipcRenderer.removeListener('terminal:exit', handler) }, }, // LOCAL attachments (P1) — copy a machine-local file into ~/.mao/attachments/// localCopyAttachment: (payload) => ipcRenderer.invoke('local-copy-attachment', payload), // LOCAL text dump — Agent 的 dump_user_messages 经服务端委托走 tool-execute;本桥为备用入口。 localWriteTextFile: (payload) => ipcRenderer.invoke('local-write-text-file', payload), // Electron 32.0 removes File.path; webUtils.getPathForFile (same version) replaces it. // Implemented in the preload (a File object cannot cross structured-clone IPC); 28-31 keep // using file.path and never call this. Renderer compat: file.path ?? electronAPI.getPathForFile(file). getPathForFile: (file) => { try { const { webUtils } = require('electron') if (webUtils && typeof webUtils.getPathForFile === 'function') { return webUtils.getPathForFile(file) || '' } } catch { // Electron < 32: webUtils missing — caller falls back to File.path } return '' }, // Desktop shell (P2/P3) — main-process notification / badge / prefs / shortcut / tray desktopNotify: (payload) => ipcRenderer.invoke('desktop-notify', payload), onDesktopNotificationClick: (callback) => onIpc('desktop-notification-click', callback), // 桥接就绪后主动拉取主进程积存的点击深链(点击时监听未注册会丢);收到后 ack 清除 consumePendingDesktopNotificationClick: () => ipcRenderer.invoke('desktop-notification-click-consume'), ackDesktopNotificationClick: () => ipcRenderer.invoke('desktop-notification-click-ack'), onDesktopNotifyResult: (callback) => onIpc('desktop-notify-result', callback), desktopBadge: (count) => ipcRenderer.invoke('desktop-badge', { count }), desktopPrefsGet: () => ipcRenderer.invoke('desktop-prefs-get'), desktopPrefsSet: (patch) => ipcRenderer.invoke('desktop-prefs-set', patch), getDeviceInfo: () => ipcRenderer.invoke('device-info-get'), desktopShortcutSet: (accel) => ipcRenderer.invoke('desktop-shortcut-set', accel), onDesktopShortcutFailed: (callback) => onIpc('desktop-shortcut-failed', callback), onTrayNewTask: (callback) => onIpc('tray-new-task', callback), onTrayFirstCloseHint: (callback) => onIpc('tray-first-close-hint', callback), })