Ctrl K
ComunidadGuías…

preload.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const { contextBridge, ipcRenderer, webUtils } = require("electron");

// BYOK API-key bridges, built once instead of hand-listed per key. Sandboxed
// preloads can't require local modules, so the {base, get, save} tuples are
// inlined here; keep them in sync with the BYOK_API_KEYS manifest in
// src/config/secretKeys.js (the main process derives its plumbing from that).
const BYOK_KEY_BRIDGES = [
  { base: "openai", get: "getOpenAIKey", save: "saveOpenAIKey" },
  { base: "anthropic", get: "getAnthropicKey", save: "saveAnthropicKey" },
  { base: "gemini", get: "getGeminiKey", save: "saveGeminiKey" },
  { base: "groq", get: "getGroqKey", save: "saveGroqKey" },
  { base: "xai", get: "getXaiKey", save: "saveXaiKey" },
  { base: "mistral", get: "getMistralKey", save: "saveMistralKey" },
  { base: "openrouter", get: "getOpenrouterKey", save: "saveOpenrouterKey" },
  { base: "tinfoil", get: "getTinfoilKey", save: "saveTinfoilKey" },
  { base: "corti", get: "getCortiKey", save: "saveCortiKey" },
  { base: "deepgram", get: "getDeepgramKey", save: "saveDeepgramKey" },
  { base: "assemblyai", get: "getAssemblyAIKey", save: "saveAssemblyAIKey" },
  {
    base: "note-formatting-custom",
    get: "getNoteFormattingCustomKey",
    save: "saveNoteFormattingCustomKey",
  },
  { base: "translation-custom", get: "getTranslationCustomKey", save: "saveTranslationCustomKey" },
  {
    base: "dictation-agent-custom",
    get: "getDictationAgentCustomKey",
    save: "saveDictationAgentCustomKey",
  },
  {
    base: "dictation-agent-vision-custom",
    get: "getDictationAgentVisionCustomKey",
    save: "saveDictationAgentVisionCustomKey",
  },
  { base: "chat-agent-custom", get: "getChatAgentCustomKey", save: "saveChatAgentCustomKey" },
];
const secretKeyApi = {};
for (const k of BYOK_KEY_BRIDGES) {
  secretKeyApi[k.get] = () => ipcRenderer.invoke(`get-${k.base}-key`);
  secretKeyApi[k.save] = (key) => ipcRenderer.invoke(`save-${k.base}-key`, key);
}

/**
 * Helper to register an IPC listener and return a cleanup function.
 * Ensures renderer code can easily remove listeners to avoid leaks.
 */
const registerListener = (channel, handlerFactory) => {
  return (callback) => {
    if (typeof callback !== "function") {
      return () => {};
    }

    const listener =
      typeof handlerFactory === "function"
        ? handlerFactory(callback)
        : (event, ...args) => callback(event, ...args);

    ipcRenderer.on(channel, listener);
    return () => {
      ipcRenderer.removeListener(channel, listener);
    };
  };
};

contextBridge.exposeInMainWorld("electronAPI", {
  initialModelRegistrySnapshot: ipcRenderer.sendSync?.("model-registry-initial"),
  getModelRegistry: () => ipcRenderer.invoke("get-model-registry"),
  onModelRegistryUpdated: registerListener(
    "model-registry-updated",
    (callback) => (_event, snapshot) => callback(snapshot)
  ),
  setOnboardingWindowMode: (mode) => ipcRenderer.invoke("onboarding-set-window-mode", mode),
  setOnboardingActive: (active) => ipcRenderer.invoke("onboarding-set-active", active),
  openPermissionGuide: (state) => ipcRenderer.invoke("permission-guide-open", state),
  closePermissionGuide: () => ipcRenderer.invoke("permission-guide-close"),
  getPermissionGuideState: () => ipcRenderer.invoke("permission-guide-state"),
  permissionGuideAction: (action) => ipcRenderer.send("permission-guide-action", action),
  startPermissionGuideDrag: (target) => ipcRenderer.send("permission-guide-drag", target),
  onPermissionGuideState: registerListener(
    "permission-guide-state-changed",
    (callback) => (_event, state) => callback(state)
  ),
  onPermissionGuideAction: registerListener(
    "permission-guide-action",
    (callback) => (_event, action) => callback(action)
  ),
  // The agent window (`?agent=true`) and the dictation window's ways into it.
  agentWindowReady: () => ipcRenderer.send("agent-window-ready"),
  agentWindowSetLayout: (request) => ipcRenderer.invoke("agent-window-set-layout", request),
  agentWindowHide: () => ipcRenderer.invoke("agent-window-hide"),
  agentWindowSetDragging: (dragging) => ipcRenderer.send("agent-window-drag", dragging),
  agentWindowSetVoiceState: (state, attempt) =>
    ipcRenderer.invoke("agent-window-voice-state", state, attempt),
  openAgentWindow: () => ipcRenderer.invoke("open-agent-window"),
  submitAgentCommand: (command) => ipcRenderer.invoke("agent-window-submit-command", command),
  onAgentWindowHotkey: registerListener(
    "agent-window-hotkey",
    (callback) => (_event, payload) => callback(payload)
  ),
  onAgentWindowOpen: registerListener(
    "agent-window-open",
    (callback) => (_event, payload) => callback(payload)
  ),
  onAgentWindowCommand: registerListener(
    "agent-window-command",
    (callback) => (_event, command) => callback(command)
  ),
  onAgentWindowDismiss: registerListener("agent-window-dismiss", (callback) => () => callback()),
  onAgentRecordingChanged: registerListener(
    "agent-recording-changed",
    (callback) => (_event, recording) => callback(recording)
  ),
  markMacAccessibilityFeaturesReady: (expectedAccountScope) =>
    expectedAccountScope
      ? ipcRenderer.send("mac-accessibility-features-ready", expectedAccountScope)
      : ipcRenderer.send("mac-accessibility-features-ready"),
  beginOnboardingDemo: (session) => ipcRenderer.invoke("onboarding-demo-begin", session),
  endOnboardingDemo: (id) => ipcRenderer.invoke("onboarding-demo-end", id),
  stopOnboardingDemo: (id) => ipcRenderer.invoke("onboarding-demo-stop", id),
  publishOnboardingDemoEvent: (event) => ipcRenderer.invoke("onboarding-demo-publish", event),
  onOnboardingDemoEvent: registerListener(
    "onboarding-demo-event",
    (callback) => (_event, payload) => callback(payload)
  ),
  testProviderConnection: (config) => ipcRenderer.invoke("test-provider-connection", config),
  pasteText: (text, options) => ipcRenderer.invoke("paste-text", text, options),
  captureSelectedText: () => ipcRenderer.invoke("capture-selected-text"),
  cancelSelectionEdit: (sessionId) => ipcRenderer.invoke("cancel-selection-edit", sessionId),
  replaceSelectedText: (sessionId, text, options) =>
    ipcRenderer.invoke("replace-selected-text", sessionId, text, options),
  hideWindow: () => ipcRenderer.invoke("hide-window"),
  showDictationPanel: () => ipcRenderer.invoke("show-dictation-panel"),
  openSettingsSection: (section) => ipcRenderer.invoke("open-settings-section", section),
  captureDictationTarget: () => ipcRenderer.invoke("capture-dictation-target"),
  captureDictationAppCategory: () => ipcRenderer.invoke("capture-dictation-app-category"),
  onToggleDictation: registerListener("toggle-dictation", (callback) => () => callback()),
  onToggleVoiceAgent: registerListener("toggle-voice-agent", (callback) => () => callback()),
  onToggleTranslation: registerListener("toggle-translation", (callback) => () => callback()),
  onMeetingStartRefused: registerListener(
    "meeting-start-refused",
    (callback) => (_event, code) => callback(code)
  ),
  onStartDictation: registerListener("start-dictation", (callback) => () => callback()),
  onStopDictation: registerListener("stop-dictation", (callback) => () => callback()),
  onPrepareDictation: registerListener("prepare-dictation", (callback) => () => callback()),
  onCancelDictationPreparation: registerListener(
    "cancel-dictation-preparation",
    (callback) => () => callback()
  ),
  onDictationForceStopped: registerListener(
    "dictation-force-stopped",
    (callback) => (_event, payload) => callback(payload)
  ),
  micWarmHoldChanged: (active) => ipcRenderer.send("mic-warm-hold-changed", active),
  dictationLifecycleStateChanged: (state, attempt) =>
    ipcRenderer.invoke("dictation-lifecycle-state-changed", state, attempt),

  // Database functions
  saveTranscription: (text, rawText, options) =>
    ipcRenderer.invoke("db-save-transcription", text, rawText, options),
  getTranscriptions: (limit, options) =>
    ipcRenderer.invoke("db-get-transcriptions", limit, options),
  recordAnalyticsEvent: (input) => ipcRenderer.invoke("analytics-record-event", input),
  closeActivationStream: () => ipcRenderer.invoke("activation-close"),
  beginActivationCapture: () => ipcRenderer.invoke("activation-begin"),
  stopActivationCapture: (id) => ipcRenderer.invoke("activation-stop", id),
  finishActivationCapture: (id, words) => ipcRenderer.invoke("activation-finish", id, words),
  cancelActivationCapture: (id) => ipcRenderer.invoke("activation-cancel", id),
  getAnalyticsSummary: () => ipcRenderer.invoke("analytics-get-summary"),
  getPendingAnalyticsEvents: (limit, context) =>
    ipcRenderer.invoke("analytics-get-pending", limit, context),
  markAnalyticsEventsSynced: (eventIds, context) =>
    ipcRenderer.invoke("analytics-mark-synced", eventIds, context),
  getPendingAnalyticsDeletes: (limit, context) =>
    ipcRenderer.invoke("analytics-get-pending-deletes", limit, context),
  hardDeleteAnalyticsEvents: (eventIds, context) =>
    ipcRenderer.invoke("analytics-hard-delete", eventIds, context),
  getPendingAnalyticsClear: (context) => ipcRenderer.invoke("analytics-get-pending-clear", context),
  completeAnalyticsClear: (clearedThrough, context) =>
    ipcRenderer.invoke("analytics-complete-clear", clearedThrough, context),
  countUnclaimedAnalyticsEvents: (context) =>
    ipcRenderer.invoke("analytics-count-unclaimed", context),
  countAnalyticsEventsAwaitingUpload: (context) =>
    ipcRenderer.invoke("analytics-count-awaiting-upload", context),
  claimAnonymousAnalyticsEvents: (accountId, expectedAuthGeneration) =>
    ipcRenderer.invoke("analytics-claim-anonymous", accountId, expectedAuthGeneration),
  clearTranscriptions: () => ipcRenderer.invoke("db-clear-transcriptions"),
  deleteTranscription: (id) => ipcRenderer.invoke("db-delete-transcription", id),

  // Audio storage functions
  saveTranscriptionAudio: (id, audioBuffer, metadata) =>
    ipcRenderer.invoke("save-transcription-audio", id, audioBuffer, metadata),
  mergeAudioSegments: (segments) => ipcRenderer.invoke("merge-audio-segments", segments),
  getAudioPath: (id) => ipcRenderer.invoke("get-audio-path", id),
  showAudioInFolder: (id) => ipcRenderer.invoke("show-audio-in-folder", id),
  getAudioBuffer: (id) => ipcRenderer.invoke("get-audio-buffer", id),
  deleteTranscriptionAudio: (id) => ipcRenderer.invoke("delete-transcription-audio", id),
  getAudioStorageUsage: () => ipcRenderer.invoke("get-audio-storage-usage"),
  deleteAllAudio: () => ipcRenderer.invoke("delete-all-audio"),
  syncRetentionSettings: (settings) => ipcRenderer.send("retention-settings-changed", settings),
  retryTranscription: (id, settings) => ipcRenderer.invoke("retry-transcription", id, settings),
  updateTranscriptionText: (id, text, rawText) =>
    ipcRenderer.invoke("update-transcription-text", id, text, rawText),
  getTranscriptionById: (id) => ipcRenderer.invoke("get-transcription-by-id", id),

  // Dictionary functions
  getDictionary: () => ipcRenderer.invoke("db-get-dictionary"),
  setDictionary: (words, listsAccountId) =>
    ipcRenderer.invoke("db-set-dictionary", words, listsAccountId),
  seedDictionary: (words) => ipcRenderer.invoke("db-seed-dictionary", words),
  applyDictionaryChanges: (changes, listsAccountId) =>
    ipcRenderer.invoke("db-apply-dictionary-changes", changes, listsAccountId),
  onDictionaryUpdated: (callback) => {
    const listener = (_event, words) => callback?.(words);
    ipcRenderer.on("dictionary-updated", listener);
    return () => ipcRenderer.removeListener("dictionary-updated", listener);
  },
  getSnippets: () => ipcRenderer.invoke("db-get-snippets"),
  setSnippets: (snippets, listsAccountId) =>
    ipcRenderer.invoke("db-set-snippets", snippets, listsAccountId),
  seedSnippets: (snippets) => ipcRenderer.invoke("db-seed-snippets", snippets),
  onSnippetsUpdated: (callback) => {
    const listener = (_event, snippets) => callback?.(snippets);
    ipcRenderer.on("snippets-updated", listener);
    return () => ipcRenderer.removeListener("snippets-updated", listener);
  },
  skillsList: () => ipcRenderer.invoke("skills-list"),
  skillsGet: (name) => ipcRenderer.invoke("skills-get", name),
  skillsReadFile: (name, path) => ipcRenderer.invoke("skills-read-file", name, path),
  skillsSetEnabled: (name, enabled) => ipcRenderer.invoke("skills-set-enabled", name, enabled),
  skillsUninstall: (name) => ipcRenderer.invoke("skills-uninstall", name),
  skillsReveal: (name) => ipcRenderer.invoke("skills-reveal", name),
  skillsPreviewUpload: (items) => ipcRenderer.invoke("skills-preview-upload", items),
  skillsCommitUpload: (request) => ipcRenderer.invoke("skills-commit-upload", request),
  skillsDiscardUploads: (tokens) => ipcRenderer.invoke("skills-discard-uploads", tokens),
  skillsCreate: (input) => ipcRenderer.invoke("skills-create", input),
  skillsAddRecommended: (id, copy) => ipcRenderer.invoke("skills-add-recommended", id, copy),
  skillsReadForModel: (ref) => ipcRenderer.invoke("skills-read-for-model", ref),
  skillsCheckProposal: (request) => ipcRenderer.invoke("skills-check-proposal", request),
  skillsSaveProposal: (proposal) => ipcRenderer.invoke("skills-save-proposal", proposal),
  skillsApplyUpdate: (update) => ipcRenderer.invoke("skills-apply-update", update),
  skillsMarkUsed: (name) => ipcRenderer.invoke("skills-mark-used", name),
  skillsUndo: (name, versionId) => ipcRenderer.invoke("skills-undo", name, versionId),
  onSkillsChanged: (callback) => {
    const listener = () => callback?.();
    ipcRenderer.on("skills-changed", listener);
    return () => ipcRenderer.removeListener("skills-changed", listener);
  },
  setAutoLearnEnabled: (enabled) => ipcRenderer.send("auto-learn-changed", enabled),
  onCorrectionsLearned: (callback) => {
    const listener = (_event, words) => callback?.(words);
    ipcRenderer.on("corrections-learned", listener);
    return () => ipcRenderer.removeListener("corrections-learned", listener);
  },
  undoLearnedCorrections: (words) => ipcRenderer.invoke("undo-learned-corrections", words),

  // Note functions
  saveNote: (title, content, noteType, sourceFile, audioDuration, folderId, spaceId, transcript) =>
    ipcRenderer.invoke(
      "db-save-note",
      title,
      content,
      noteType,
      sourceFile,
      audioDuration,
      folderId,
      spaceId,
      transcript
    ),
  getNote: (id) => ipcRenderer.invoke("db-get-note", id),
  getNotes: (noteType, limit, folderId, spaceId, spaceIds) =>
    ipcRenderer.invoke("db-get-notes", noteType, limit, folderId, spaceId, spaceIds),
  getSpaceNotes: (spaceId, limit) => ipcRenderer.invoke("db-get-space-notes", spaceId, limit),
  updateNote: (id, updates, options) => ipcRenderer.invoke("db-update-note", id, updates, options),
  getNoteUndos: () => ipcRenderer.invoke("db-get-note-undos"),
  claimNoteUndo: (token) => ipcRenderer.invoke("db-claim-note-undo", token),
  undoNoteUpdate: (token) => ipcRenderer.invoke("db-undo-note-update", token),
  discardNoteUndo: (id, token) => ipcRenderer.invoke("db-discard-note-undo", id, token),
  deleteNote: (id) => ipcRenderer.invoke("db-delete-note", id),
  exportNote: (noteId, format) => ipcRenderer.invoke("export-note", noteId, format),
  exportTranscript: (noteId, format) => ipcRenderer.invoke("export-transcript", noteId, format),
  exportDictionary: (words) => ipcRenderer.invoke("export-dictionary", words),
  searchNotes: (query, limit, spaceId, folderId, spaceIds) =>
    ipcRenderer.invoke("db-search-notes", query, limit, spaceId, folderId, spaceIds),
  semanticSearchNotes: (query, limit, spaceId, folderId) =>
    ipcRenderer.invoke("db-semantic-search-notes", query, limit, spaceId, folderId),
  updateNoteCloudId: (id, cloudId) => ipcRenderer.invoke("db-update-note-cloud-id", id, cloudId),
  updateNoteShareState: (id, state) => ipcRenderer.invoke("db-update-note-share-state", id, state),

  // Folder functions
  getFolders: (spaceId) => ipcRenderer.invoke("db-get-folders", spaceId),
  createFolder: (name, spaceId) => ipcRenderer.invoke("db-create-folder", name, spaceId),
  deleteFolder: (id) => ipcRenderer.invoke("db-delete-folder", id),
  renameFolder: (id, name) => ipcRenderer.invoke("db-rename-folder", id, name),
  moveFolderToSpace: (id, spaceId) => ipcRenderer.invoke("db-move-folder-to-space", id, spaceId),
  getFolderNoteCounts: () => ipcRenderer.invoke("db-get-folder-note-counts"),

  // Space functions
  getSpaces: () => ipcRenderer.invoke("db-get-spaces"),
  hasOtherAccountRows: () => ipcRenderer.invoke("db-has-other-account-rows"),
  accountChangedSince: (occurredAt) => ipcRenderer.invoke("db-account-changed-since", occurredAt),
  hasUnclaimedLegacyRows: () => ipcRenderer.invoke("db-has-unclaimed-legacy-rows"),
  setActiveAccountScope: (accountId, expectedAuthGeneration) =>
    ipcRenderer.invoke("set-active-account-scope", accountId, expectedAuthGeneration),
  getActiveAccountScope: () => ipcRenderer.invoke("get-active-account-scope"),
  getDatabaseAccountScope: () => ipcRenderer.invoke("get-database-account-scope"),
  onDatabaseAccountScopeChanged: registerListener(
    "database-account-scope-changed",
    (callback) => (_event, scope) => callback(scope)
  ),
  onActiveAccountScopeChanged: registerListener(
    "active-account-scope-changed",
    (callback) => (_event, scope) => callback(scope)
  ),
  deleteAccountData: (accountId, expectedAuthGeneration, options) =>
    ipcRenderer.invoke("delete-account-data", accountId, expectedAuthGeneration, options),
  updateSpace: (id, updates) => ipcRenderer.invoke("db-update-space", id, updates),
  purgeSpace: (id, options) => ipcRenderer.invoke("db-purge-space", id, options),
  upsertSpaceFromCloud: (space) => ipcRenderer.invoke("db-upsert-space-from-cloud", space),
  setSpaceSyncStatus: (id, status) => ipcRenderer.invoke("db-set-space-sync-status", id, status),
  onSpacePurged: (callback) => {
    const listener = (_event, payload) => callback?.(payload);
    ipcRenderer.on("space-purged", listener);
    return () => ipcRenderer.removeListener("space-purged", listener);
  },
  onSpaceSynced: (callback) => {
    const listener = (_event, space) => callback?.(space);
    ipcRenderer.on("space-synced", listener);
    return () => ipcRenderer.removeListener("space-synced", listener);
  },

  // Note files (markdown mirror) functions
  noteFilesSetEnabled: (enabled, customPath, options) =>
    ipcRenderer.invoke("note-files-set-enabled", enabled, customPath, options),
  noteFilesSetPath: (path) => ipcRenderer.invoke("note-files-set-path", path),
  noteFilesRebuild: () => ipcRenderer.invoke("note-files-rebuild"),
  noteFilesGetDefaultPath: () => ipcRenderer.invoke("note-files-get-default-path"),
  noteFilesPickFolder: () => ipcRenderer.invoke("note-files-pick-folder"),
  granolaImportPickAndPreview: () => ipcRenderer.invoke("granola-import-pick-and-preview"),
  granolaImportRun: () => ipcRenderer.invoke("granola-import-run"),
  dragonImportPickFile: () => ipcRenderer.invoke("dragon-import-pick-file"),
  showNoteFile: (noteId) => ipcRenderer.invoke("show-note-file", noteId),
  showFolderInExplorer: (folderName) => ipcRenderer.invoke("show-folder-in-explorer", folderName),

  // Action functions
  getActions: () => ipcRenderer.invoke("db-get-actions"),
  getAction: (id) => ipcRenderer.invoke("db-get-action", id),
  createAction: (name, description, prompt, icon, fields) =>
    ipcRenderer.invoke("db-create-action", name, description, prompt, icon, fields),
  updateAction: (id, updates) => ipcRenderer.invoke("db-update-action", id, updates),
  deleteAction: (id) => ipcRenderer.invoke("db-delete-action", id),

  // Audio file operations
  selectAudioFile: (options) => ipcRenderer.invoke("select-audio-file", options),
  getFileSize: (filePath) => ipcRenderer.invoke("get-file-size", filePath),
  transcribeAudioFile: (filePath, options) =>
    ipcRenderer.invoke("transcribe-audio-file", filePath, options),
  getPathForFile: (file) => {
    const filePath = webUtils.getPathForFile(file);
    // Register real dropped-file paths so the main-process audio allowlist accepts them.
    if (filePath) ipcRenderer.send("approve-audio-path", filePath);
    return filePath;
  },

  // URL audio download
  downloadUrlAudio: (url, downloadId) => ipcRenderer.invoke("download-url-audio", url, downloadId),
  cancelUrlDownload: (downloadId) => ipcRenderer.invoke("cancel-url-download", downloadId),
  deleteTempFile: (filePath) => ipcRenderer.invoke("delete-temp-file", filePath),
  onUrlDownloadProgress: registerListener(
    "url-download-progress",
    (callback) => (_event, data) => callback(data)
  ),

  onNoteAdded: (callback) => {
    const listener = (_event, note) => callback?.(note);
    ipcRenderer.on("note-added", listener);
    return () => ipcRenderer.removeListener("note-added", listener);
  },
  onNoteUpdated: (callback) => {
    const listener = (_event, note) => callback?.(note);
    ipcRenderer.on("note-updated", listener);
    return () => ipcRenderer.removeListener("note-updated", listener);
  },
  onNoteDeleted: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("note-deleted", listener);
    return () => ipcRenderer.removeListener("note-deleted", listener);
  },
  onNoteSynced: (callback) => {
    const listener = (_event, note) => callback?.(note);
    ipcRenderer.on("note-synced", listener);
    return () => ipcRenderer.removeListener("note-synced", listener);
  },
  onFolderSynced: (callback) => {
    const listener = (_event, folder) => callback?.(folder);
    ipcRenderer.on("folder-synced", listener);
    return () => ipcRenderer.removeListener("folder-synced", listener);
  },
  onFolderDeleted: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("folder-deleted", listener);
    return () => ipcRenderer.removeListener("folder-deleted", listener);
  },
  emitSyncEvent: (name, payload) => ipcRenderer.invoke("broadcast-sync-event", name, payload),
  onSyncEvent: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("sync-event", listener);
    return () => ipcRenderer.removeListener("sync-event", listener);
  },

  onActionCreated: (callback) => {
    const listener = (_event, action) => callback?.(action);
    ipcRenderer.on("action-created", listener);
    return () => ipcRenderer.removeListener("action-created", listener);
  },
  onActionUpdated: (callback) => {
    const listener = (_event, action) => callback?.(action);
    ipcRenderer.on("action-updated", listener);
    return () => ipcRenderer.removeListener("action-updated", listener);
  },
  onActionDeleted: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("action-deleted", listener);
    return () => ipcRenderer.removeListener("action-deleted", listener);
  },

  onTranscriptionAdded: (callback) => {
    const listener = (_event, transcription) => callback?.(transcription);
    ipcRenderer.on("transcription-added", listener);
    return () => ipcRenderer.removeListener("transcription-added", listener);
  },
  onTranscriptionDeleted: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("transcription-deleted", listener);
    return () => ipcRenderer.removeListener("transcription-deleted", listener);
  },
  onTranscriptionsCleared: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("transcriptions-cleared", listener);
    return () => ipcRenderer.removeListener("transcriptions-cleared", listener);
  },
  onTranscriptionUpdated: (callback) => {
    const listener = (_event, transcription) => callback?.(transcription);
    ipcRenderer.on("transcription-updated", listener);
    return () => ipcRenderer.removeListener("transcription-updated", listener);
  },
  onAnalyticsChanged: (callback) => {
    const listener = () => callback?.();
    ipcRenderer.on("analytics-changed", listener);
    return () => ipcRenderer.removeListener("analytics-changed", listener);
  },

  // BYOK API keys (get/save for every provider in the secretKeys manifest)
  ...secretKeyApi,

  // Clipboard functions
  checkAccessibilityPermission: (silent) =>
    ipcRenderer.invoke("check-accessibility-permission", silent),
  promptAccessibilityPermission: () => ipcRenderer.invoke("prompt-accessibility-permission"),
  readClipboard: () => ipcRenderer.invoke("read-clipboard"),
  writeClipboard: (text) => ipcRenderer.invoke("write-clipboard", text),
  copyLeaderboardImage: (dataUrl) => ipcRenderer.invoke("leaderboard-copy-image", dataUrl),
  saveLeaderboardImage: (dataUrl, suggestedName) =>
    ipcRenderer.invoke("leaderboard-save-image", dataUrl, suggestedName),
  checkPasteTools: () => ipcRenderer.invoke("check-paste-tools"),

  // Voice drafts (chat input recordings)
  saveTempAudio: (buffer) => ipcRenderer.invoke("save-temp-audio", buffer),
  deleteTempAudio: (tempPath) => ipcRenderer.invoke("delete-temp-audio", tempPath),

  // Local Whisper functions (whisper.cpp)
  transcribeLocalWhisper: (audioBlob, options) =>
    ipcRenderer.invoke("transcribe-local-whisper", audioBlob, options),
  checkWhisperInstallation: () => ipcRenderer.invoke("check-whisper-installation"),
  downloadWhisperModel: (modelName) => ipcRenderer.invoke("download-whisper-model", modelName),
  onWhisperDownloadProgress: registerListener("whisper-download-progress"),
  checkModelStatus: (modelName) => ipcRenderer.invoke("check-model-status", modelName),
  listWhisperModels: () => ipcRenderer.invoke("list-whisper-models"),
  deleteWhisperModel: (modelName) => ipcRenderer.invoke("delete-whisper-model", modelName),
  deleteAllWhisperModels: () => ipcRenderer.invoke("delete-all-whisper-models"),
  cancelWhisperDownload: () => ipcRenderer.invoke("cancel-whisper-download"),
  checkFFmpegAvailability: () => ipcRenderer.invoke("check-ffmpeg-availability"),
  getAudioDiagnostics: () => ipcRenderer.invoke("get-audio-diagnostics"),

  // Whisper server functions (faster repeated transcriptions)
  whisperServerStart: (modelName) => ipcRenderer.invoke("whisper-server-start", modelName),
  whisperServerStop: () => ipcRenderer.invoke("whisper-server-stop"),
  whisperServerStatus: () => ipcRenderer.invoke("whisper-server-status"),
  whisperGpuRetry: () => ipcRenderer.invoke("whisper-gpu-retry"),

  // CUDA GPU acceleration
  listGpus: () => ipcRenderer.invoke("list-gpus"),
  setGpuDeviceIndex: (purpose, uuid) => ipcRenderer.invoke("set-gpu-device-index", purpose, uuid),
  getGpuDeviceIndex: (purpose) => ipcRenderer.invoke("get-gpu-device-index", purpose),
  detectGpu: () => ipcRenderer.invoke("detect-gpu"),
  getCudaWhisperStatus: () => ipcRenderer.invoke("get-cuda-whisper-status"),
  downloadCudaWhisperBinary: () => ipcRenderer.invoke("download-cuda-whisper-binary"),
  cancelCudaWhisperDownload: () => ipcRenderer.invoke("cancel-cuda-whisper-download"),
  deleteCudaWhisperBinary: () => ipcRenderer.invoke("delete-cuda-whisper-binary"),
  onCudaDownloadProgress: registerListener(
    "cuda-download-progress",
    (callback) => (_event, data) => callback(data)
  ),
  onCudaFallbackNotification: registerListener(
    "cuda-fallback-notification",
    (callback) => () => callback()
  ),

  // Vulkan GPU acceleration (whisper on AMD/Intel GPUs)
  getVulkanWhisperStatus: () => ipcRenderer.invoke("get-vulkan-whisper-status"),
  downloadVulkanWhisperBinary: () => ipcRenderer.invoke("download-vulkan-whisper-binary"),
  cancelVulkanWhisperDownload: () => ipcRenderer.invoke("cancel-vulkan-whisper-download"),
  deleteVulkanWhisperBinary: () => ipcRenderer.invoke("delete-vulkan-whisper-binary"),
  onVulkanWhisperDownloadProgress: registerListener(
    "vulkan-whisper-download-progress",
    (callback) => (_event, data) => callback(data)
  ),
  onGpuFallbackNotification: registerListener(
    "gpu-fallback-notification",
    (callback) => () => callback()
  ),
  // Main changed the installed packs or the remembered GPU failure (#1736)
  onWhisperGpuStatusChanged: registerListener(
    "whisper-gpu-status-changed",
    (callback) => () => callback()
  ),

  // One-time "GPU pack needs re-downloading" notice
  takeGpuPackMigrationNotice: () => ipcRenderer.invoke("take-gpu-pack-migration-notice"),
  onGpuPackMigrationNoticeReady: registerListener(
    "gpu-pack-migration-notice-ready",
    (callback) => () => callback()
  ),

  // Local Parakeet (NVIDIA) functions
  transcribeLocalParakeet: (audioBlob, options) =>
    ipcRenderer.invoke("transcribe-local-parakeet", audioBlob, options),
  checkParakeetInstallation: () => ipcRenderer.invoke("check-parakeet-installation"),
  downloadParakeetModel: (modelName) => ipcRenderer.invoke("download-parakeet-model", modelName),
  onParakeetDownloadProgress: registerListener("parakeet-download-progress"),
  checkParakeetModelStatus: (modelName) =>
    ipcRenderer.invoke("check-parakeet-model-status", modelName),
  listParakeetModels: () => ipcRenderer.invoke("list-parakeet-models"),
  deleteParakeetModel: (modelName) => ipcRenderer.invoke("delete-parakeet-model", modelName),
  deleteAllParakeetModels: () => ipcRenderer.invoke("delete-all-parakeet-models"),
  cancelParakeetDownload: () => ipcRenderer.invoke("cancel-parakeet-download"),
  getParakeetDiagnostics: () => ipcRenderer.invoke("get-parakeet-diagnostics"),

  // Parakeet server functions (faster repeated transcriptions)
  parakeetServerStart: (modelName) => ipcRenderer.invoke("parakeet-server-start", modelName),
  parakeetServerStop: () => ipcRenderer.invoke("parakeet-server-stop"),
  parakeetServerStatus: () => ipcRenderer.invoke("parakeet-server-status"),

  // Diarization (speaker identification) functions
  downloadDiarizationModels: () => ipcRenderer.invoke("download-diarization-models"),
  getDiarizationModelStatus: () => ipcRenderer.invoke("get-diarization-model-status"),
  deleteDiarizationModels: () => ipcRenderer.invoke("delete-diarization-models"),
  cancelDiarizationDownload: () => ipcRenderer.invoke("cancel-diarization-download"),
  diarizeAudioFile: (filePath, options) =>
    ipcRenderer.invoke("diarize-audio-file", filePath, options),
  mergeSpeakerText: (segments, text, duration) =>
    ipcRenderer.invoke("merge-speaker-text", { segments, text, duration }),
  onDiarizationDownloadProgress: registerListener(
    "diarization-download-progress",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingDiarizationComplete: registerListener(
    "meeting-diarization-complete",
    (callback) => (_event, data) => callback(data)
  ),

  // Speaker name mapping
  getSpeakerMappings: (noteId) => ipcRenderer.invoke("get-speaker-mappings", noteId),
  setSpeakerMapping: (noteId, speakerId, displayName, email, profileId) =>
    ipcRenderer.invoke("set-speaker-mapping", noteId, speakerId, displayName, email, profileId),
  removeSpeakerMapping: (noteId, speakerId) =>
    ipcRenderer.invoke("remove-speaker-mapping", noteId, speakerId),
  getSpeakerProfiles: () => ipcRenderer.invoke("get-speaker-profiles"),
  attachSpeakerEmail: (profileId, email) =>
    ipcRenderer.invoke("attach-speaker-email", profileId, email),
  saveNoteSpeakerEmbeddings: (noteId, embeddings) =>
    ipcRenderer.invoke("save-note-speaker-embeddings", noteId, embeddings),

  // Window control functions
  windowMinimize: () => ipcRenderer.invoke("window-minimize"),
  windowMaximize: () => ipcRenderer.invoke("window-maximize"),
  windowClose: () => ipcRenderer.invoke("window-close"),
  windowIsMaximized: () => ipcRenderer.invoke("window-is-maximized"),
  snapToMeetingMode: () => ipcRenderer.invoke("snap-to-meeting-mode"),
  restoreFromMeetingMode: () => ipcRenderer.invoke("restore-from-meeting-mode"),
  meetingNoteOpenedWithoutRecording: (id) =>
    ipcRenderer.invoke("meeting-note-opened-without-recording", id),
  meetingNoteClosed: (id) => ipcRenderer.invoke("meeting-note-closed", id),
  getPlatform: () => process.platform,

  // Cleanup function
  cleanupApp: () => ipcRenderer.invoke("cleanup-app"),
  relaunchApp: () => ipcRenderer.invoke("relaunch-app"),
  updateHotkey: (hotkey) => ipcRenderer.invoke("update-hotkey", hotkey),
  setHotkeyListeningMode: (enabled) => ipcRenderer.invoke("set-hotkey-listening-mode", enabled),
  getHotkeyModeInfo: (hotkey) => ipcRenderer.invoke("get-hotkey-mode-info", hotkey),
  getHyprlandConfigStatus: () => ipcRenderer.invoke("get-hyprland-config-status"),
  startWindowDrag: () => ipcRenderer.invoke("start-window-drag"),
  stopWindowDrag: () => ipcRenderer.invoke("stop-window-drag"),
  startControlPanelDrag: () => ipcRenderer.invoke("start-control-panel-drag"),
  stopControlPanelDrag: () => ipcRenderer.invoke("stop-control-panel-drag"),
  getMainWindowHorizontalDirection: () =>
    ipcRenderer.invoke("get-main-window-horizontal-direction"),
  onMainWindowHorizontalDirectionChanged: registerListener(
    "main-window-horizontal-direction-changed",
    (callback) => (_event, direction) => callback(direction)
  ),
  onMainWindowWillResize: registerListener(
    "main-window-will-resize",
    (callback) => (_event, resize) => callback(resize)
  ),
  ackMainWindowResizeMask: (token) => ipcRenderer.send("main-window-resize-mask-ready", token),
  setMainWindowInteractivity: (interactive) =>
    ipcRenderer.invoke("set-main-window-interactivity", interactive),
  setMainWindowInputRegion: (region) => ipcRenderer.invoke("set-main-window-input-region", region),
  onMainWindowVisibilityChanged: registerListener(
    "main-window-visibility-changed",
    (callback) => (_event, visible) => callback(visible)
  ),
  setNotificationInteractivity: (interactive) =>
    ipcRenderer.invoke("set-notification-interactivity", interactive),
  resizeMainWindow: (sizeKey) => ipcRenderer.invoke("resize-main-window", sizeKey),
  resizeAssistantWindowToContent: (surfaceHeight) =>
    ipcRenderer.invoke("resize-assistant-window-to-content", surfaceHeight),
  resizeDictationErrorWindowToContent: (surfaceHeight) =>
    ipcRenderer.invoke("resize-dictation-error-window-to-content", surfaceHeight),

  // Update functions
  checkForUpdates: () => ipcRenderer.invoke("check-for-updates"),
  downloadUpdate: () => ipcRenderer.invoke("download-update"),
  installUpdate: () => ipcRenderer.invoke("install-update"),
  getAppVersion: () => ipcRenderer.invoke("get-app-version"),
  getPostMigrationState: () => ipcRenderer.invoke("get-post-migration-state"),
  getOAuthProtocolRegistered: () => ipcRenderer.invoke("get-oauth-protocol-registered"),
  getOAuthProtocol: () => ipcRenderer.invoke("get-oauth-protocol"),
  markBundleMigrated: () => ipcRenderer.invoke("mark-bundle-migrated"),
  markBundleMigrationDismissed: () => ipcRenderer.invoke("mark-bundle-migration-dismissed"),
  getUpdateStatus: () => ipcRenderer.invoke("get-update-status"),
  getUpdateInfo: () => ipcRenderer.invoke("get-update-info"),
  setAutoUpdatesEnabled: (enabled) => ipcRenderer.invoke("set-auto-updates-enabled", enabled),

  // Update event listeners
  onUpdateAvailable: registerListener("update-available"),
  onUpdateNotAvailable: registerListener("update-not-available"),
  onUpdateDownloaded: registerListener("update-downloaded"),
  onUpdateDownloadProgress: registerListener("update-download-progress"),
  onUpdateError: registerListener("update-error"),

  // Audio event listeners
  onCancelHotkeyPressed: registerListener("cancel-hotkey-pressed", (cb) => () => cb()),
  registerCancelHotkey: (key) => ipcRenderer.invoke("register-cancel-hotkey", key),
  unregisterCancelHotkey: () => ipcRenderer.invoke("unregister-cancel-hotkey"),

  // External link opener
  openExternal: (url) => ipcRenderer.invoke("open-external", url),

  // Model management functions
  modelGetAll: () => ipcRenderer.invoke("model-get-all"),
  modelGetActiveDownloads: () => ipcRenderer.invoke("model-get-active-downloads"),
  modelCheck: (modelId) => ipcRenderer.invoke("model-check", modelId),
  modelDownload: (modelId) => ipcRenderer.invoke("model-download", modelId),
  listRetiredLocalModels: (inUse) => ipcRenderer.invoke("list-retired-local-models", inUse),
  deleteRetiredLocalModels: (inUse) => ipcRenderer.invoke("delete-retired-local-models", inUse),
  modelDelete: (modelId) => ipcRenderer.invoke("model-delete", modelId),
  modelDeleteAll: () => ipcRenderer.invoke("model-delete-all"),
  modelCheckRuntime: () => ipcRenderer.invoke("model-check-runtime"),
  modelCancelDownload: (modelId) => ipcRenderer.invoke("model-cancel-download", modelId),
  onModelDownloadProgress: registerListener("model-download-progress"),

  getUiLanguage: () => ipcRenderer.invoke("get-ui-language"),
  saveUiLanguage: (language) => ipcRenderer.invoke("save-ui-language", language),
  setUiLanguage: (language) => ipcRenderer.invoke("set-ui-language", language),

  // xAI / Mistral transcription proxies (keys handled by the manifest bridge)
  proxyXaiTranscription: (data) => ipcRenderer.invoke("proxy-xai-transcription", data),
  proxyMistralTranscription: (data) => ipcRenderer.invoke("proxy-mistral-transcription", data),
  proxyGeminiTranscription: (data) => ipcRenderer.invoke("proxy-gemini-transcription", data),

  // Corti API
  getCortiClientId: () => ipcRenderer.invoke("get-corti-client-id"),
  saveCortiClientId: (key) => ipcRenderer.invoke("save-corti-client-id", key),
  getCortiClientSecret: () => ipcRenderer.invoke("get-corti-client-secret"),
  saveCortiClientSecret: (key) => ipcRenderer.invoke("save-corti-client-secret", key),
  proxyCortiTranscription: (data) => ipcRenderer.invoke("proxy-corti-transcription", data),
  getTinfoilChatModels: () => ipcRenderer.invoke("get-tinfoil-chat-models"),
  proxyTinfoilTranscription: (data) => ipcRenderer.invoke("proxy-tinfoil-transcription", data),

  // Custom endpoint API keys
  getCustomTranscriptionKey: () => ipcRenderer.invoke("get-custom-transcription-key"),
  saveCustomTranscriptionKey: (key) => ipcRenderer.invoke("save-custom-transcription-key", key),
  getCleanupCustomKey: () => ipcRenderer.invoke("get-cleanup-custom-key"),
  saveCleanupCustomKey: (key) => ipcRenderer.invoke("save-cleanup-custom-key", key),

  // Enterprise provider key management
  getBedrockRegion: () => ipcRenderer.invoke("get-bedrock-region"),
  saveBedrockRegion: (value) => ipcRenderer.invoke("save-bedrock-region", value),
  getBedrockProfile: () => ipcRenderer.invoke("get-bedrock-profile"),
  saveBedrockProfile: (value) => ipcRenderer.invoke("save-bedrock-profile", value),
  getBedrockAccessKeyId: () => ipcRenderer.invoke("get-bedrock-access-key-id"),
  saveBedrockAccessKeyId: (key) => ipcRenderer.invoke("save-bedrock-access-key-id", key),
  getBedrockSecretAccessKey: () => ipcRenderer.invoke("get-bedrock-secret-access-key"),
  saveBedrockSecretAccessKey: (key) => ipcRenderer.invoke("save-bedrock-secret-access-key", key),
  getBedrockSessionToken: () => ipcRenderer.invoke("get-bedrock-session-token"),
  saveBedrockSessionToken: (key) => ipcRenderer.invoke("save-bedrock-session-token", key),
  getAzureEndpoint: () => ipcRenderer.invoke("get-azure-endpoint"),
  saveAzureEndpoint: (value) => ipcRenderer.invoke("save-azure-endpoint", value),
  getAzureApiKey: () => ipcRenderer.invoke("get-azure-api-key"),
  saveAzureApiKey: (key) => ipcRenderer.invoke("save-azure-api-key", key),
  getAzureDeployment: () => ipcRenderer.invoke("get-azure-deployment"),
  saveAzureDeployment: (value) => ipcRenderer.invoke("save-azure-deployment", value),
  getAzureApiVersion: () => ipcRenderer.invoke("get-azure-api-version"),
  saveAzureApiVersion: (value) => ipcRenderer.invoke("save-azure-api-version", value),
  getVertexProject: () => ipcRenderer.invoke("get-vertex-project"),
  saveVertexProject: (value) => ipcRenderer.invoke("save-vertex-project", value),
  getVertexLocation: () => ipcRenderer.invoke("get-vertex-location"),
  saveVertexLocation: (value) => ipcRenderer.invoke("save-vertex-location", value),
  getVertexApiKey: () => ipcRenderer.invoke("get-vertex-api-key"),
  saveVertexApiKey: (key) => ipcRenderer.invoke("save-vertex-api-key", key),
  testEnterpriseConnection: (provider, config) =>
    ipcRenderer.invoke("test-enterprise-connection", provider, config),

  // Dictation key persistence (file-based for reliable startup)
  getDictationKey: () => ipcRenderer.invoke("get-dictation-key"),
  getActiveDictationKey: () => ipcRenderer.invoke("get-active-dictation-key"),
  getEffectiveDefaultHotkey: () => ipcRenderer.invoke("get-effective-default-hotkey"),
  saveDictationKey: (key) => ipcRenderer.invoke("save-dictation-key", key),

  // Activation mode persistence (file-based for reliable startup)
  getActivationMode: () => ipcRenderer.invoke("get-activation-mode"),
  saveActivationMode: (mode) => ipcRenderer.invoke("save-activation-mode", mode),

  saveAllKeysToEnv: () => ipcRenderer.invoke("save-all-keys-to-env"),
  syncStartupPreferences: (prefs) => ipcRenderer.invoke("sync-startup-preferences", prefs),

  // Local reasoning
  processLocalReasoning: (text, modelId, agentName, config) =>
    ipcRenderer.invoke("process-local-reasoning", text, modelId, agentName, config),
  checkLocalReasoningAvailable: () => ipcRenderer.invoke("check-local-reasoning-available"),
  getLocalContextBudget: (modelId) => ipcRenderer.invoke("get-local-context-budget", modelId),
  cancelLocalReasoning: (requestId) => ipcRenderer.invoke("cancel-local-reasoning", requestId),

  // Anthropic reasoning
  processAnthropicReasoning: (text, modelId, agentName, config) =>
    ipcRenderer.invoke("process-anthropic-reasoning", text, modelId, agentName, config),

  // Enterprise reasoning (Bedrock, Azure, Vertex) — runs in main process so
  // Node-only SDKs (AWS/Azure/Google credential providers) can resolve.
  processEnterpriseReasoning: (text, modelId, agentName, config) =>
    ipcRenderer.invoke("process-enterprise-reasoning", text, modelId, agentName, config),
  cancelEnterpriseReasoning: () => ipcRenderer.send("enterprise-reasoning-cancel"),
  enterpriseStreamStart: (payload) => ipcRenderer.invoke("enterprise-stream-start", payload),
  enterpriseStreamCancel: (streamId) => ipcRenderer.invoke("enterprise-stream-cancel", streamId),
  onEnterpriseStreamPart: registerListener(
    "enterprise-stream-part",
    (callback) => (_event, payload) => callback(payload)
  ),
  listBedrockModels: (config) => ipcRenderer.invoke("bedrock-list-models", config),
  getManagedEnterpriseConfig: (accountId, workspaceId, expectedAuthGeneration, forceRefresh) =>
    ipcRenderer.invoke(
      "get-managed-enterprise-config",
      accountId,
      workspaceId,
      expectedAuthGeneration,
      forceRefresh
    ),
  onManagedEnterpriseConfigChanged: registerListener(
    "managed-enterprise-config-changed",
    (callback) => (_event, snapshot) => callback(snapshot)
  ),
  clearManagedEnterpriseIdentity: (options) =>
    ipcRenderer.invoke("clear-managed-enterprise-identity", options),
  managedTranscribe: (data) => ipcRenderer.invoke("managed-transcribe", data),
  getPendingEnterpriseAudioTest: (consume = false) =>
    ipcRenderer.invoke("get-pending-enterprise-audio-test", consume),
  claimEnterpriseAudioTest: () => ipcRenderer.invoke("claim-enterprise-audio-test"),
  onEnterpriseAudioTestRequested: registerListener(
    "enterprise-audio-test-requested",
    (callback) => () => callback()
  ),
  enterpriseAudioImportBootstrap: (data) =>
    ipcRenderer.invoke("enterprise-audio-import-bootstrap", data),
  enterpriseAudioStatus: (data) => ipcRenderer.invoke("enterprise-audio-status", data),
  enterpriseAudioSaveCredential: (data) =>
    ipcRenderer.invoke("enterprise-audio-save-credential", data),
  enterpriseAudioTest: (data) => ipcRenderer.invoke("enterprise-audio-test", data),

  // llama.cpp
  llamaCppCheck: () => ipcRenderer.invoke("llama-cpp-check"),
  llamaCppInstall: () => ipcRenderer.invoke("llama-cpp-install"),
  llamaCppUninstall: () => ipcRenderer.invoke("llama-cpp-uninstall"),

  // llama-server
  llamaServerStart: (modelId, requestId) =>
    ipcRenderer.invoke("llama-server-start", modelId, requestId),
  llamaServerCancelStart: (requestId) => ipcRenderer.invoke("llama-server-cancel-start", requestId),
  llamaServerStatus: () => ipcRenderer.invoke("llama-server-status"),
  llamaGpuReset: () => ipcRenderer.invoke("llama-gpu-reset"),

  // Vulkan GPU acceleration
  detectVulkanGpu: () => ipcRenderer.invoke("detect-vulkan-gpu"),
  getComputerProfile: () => ipcRenderer.invoke("get-computer-profile"),
  getLocalSetupSpace: () => ipcRenderer.invoke("get-local-setup-space"),
  getLlamaVulkanStatus: () => ipcRenderer.invoke("get-llama-vulkan-status"),
  downloadLlamaVulkanBinary: () => ipcRenderer.invoke("download-llama-vulkan-binary"),
  cancelLlamaVulkanDownload: () => ipcRenderer.invoke("cancel-llama-vulkan-download"),
  deleteLlamaVulkanBinary: () => ipcRenderer.invoke("delete-llama-vulkan-binary"),
  onLlamaVulkanDownloadProgress: registerListener(
    "llama-vulkan-download-progress",
    (callback) => (_event, data) => callback(data)
  ),

  getLogLevel: () => ipcRenderer.invoke("get-log-level"),
  onLogLevelChanged: registerListener("log-level-changed", (callback) => () => callback()),
  log: (entry) => ipcRenderer.invoke("app-log", entry),

  // ydotool status check
  getYdotoolStatus: () => ipcRenderer.invoke("get-ydotool-status"),

  // Debug logging management
  getDebugState: () => ipcRenderer.invoke("get-debug-state"),
  setDebugLogging: (enabled) => ipcRenderer.invoke("set-debug-logging", enabled),
  getModelRegistryUpdates: () => ipcRenderer.invoke("get-model-registry-updates"),
  setModelRegistryUpdates: (enabled) => ipcRenderer.invoke("set-model-registry-updates", enabled),
  openLogsFolder: () => ipcRenderer.invoke("open-logs-folder"),

  // System settings helpers for microphone/audio permissions
  requestMicrophoneAccess: () => ipcRenderer.invoke("request-microphone-access"),
  checkMicrophoneAccess: () => ipcRenderer.invoke("check-microphone-access"),
  getSystemDefaultMicrophone: (options) =>
    ipcRenderer.invoke("get-system-default-microphone", options),
  checkSystemAudioAccess: () => ipcRenderer.invoke("check-system-audio-access"),
  verifySystemAudioAccess: () => ipcRenderer.invoke("permission-guide-verify-system-audio"),
  requestSystemAudioAccess: () => ipcRenderer.invoke("request-system-audio-access"),
  openMicrophoneSettings: () => ipcRenderer.invoke("open-microphone-settings"),
  openSoundInputSettings: () => ipcRenderer.invoke("open-sound-input-settings"),
  openAccessibilitySettings: () => ipcRenderer.invoke("open-accessibility-settings"),
  openSystemAudioSettings: () => ipcRenderer.invoke("open-system-audio-settings"),
  openScreenRecordingSettings: () => ipcRenderer.invoke("open-screen-recording-settings"),
  openLoginItemsSettings: () => ipcRenderer.invoke("open-login-items-settings"),
  checkScreenRecordingAccess: () => ipcRenderer.invoke("check-screen-recording-access"),
  requestScreenRecordingAccess: () => ipcRenderer.invoke("request-screen-recording-access"),
  captureScreenContext: () => ipcRenderer.invoke("capture-screen-context"),
  setScreenContextEnabled: (enabled) => ipcRenderer.invoke("screen-context-set-enabled", enabled),
  toggleMediaPlayback: () => ipcRenderer.invoke("toggle-media-playback"),
  pauseMediaPlayback: () => ipcRenderer.invoke("pause-media-playback"),
  resumeMediaPlayback: () => ipcRenderer.invoke("resume-media-playback"),
  getModelCacheRoot: () => ipcRenderer.invoke("get-model-cache-root"),
  openWhisperModelsFolder: () => ipcRenderer.invoke("open-whisper-models-folder"),
  authClearSession: () => ipcRenderer.invoke("auth-clear-session"),
  authGetToken: () => ipcRenderer.invoke("auth-get-token"),
  authGetTokenState: () => ipcRenderer.invoke("auth-get-token-state"),
  authSetToken: (token, expectedGeneration) =>
    ipcRenderer.invoke("auth-set-token", token, expectedGeneration),
  onAuthTokenStateChanged: registerListener(
    "auth-token-state-changed",
    (callback) => (_event, state) => callback(state)
  ),

  // OpenWhispr Cloud API
  cloudHealthCheck: () => ipcRenderer.invoke("cloud-health-check"),
  cloudTranscribe: (audioBuffer, opts) => ipcRenderer.invoke("cloud-transcribe", audioBuffer, opts),
  cancelCloudTranscription: () => ipcRenderer.send("cloud-transcribe-cancel"),
  cloudReason: (text, opts) => ipcRenderer.invoke("cloud-reason", text, opts),
  cancelCloudReason: () => ipcRenderer.send("cloud-reason-cancel"),
  cloudStreamingUsage: (text, audioDurationSeconds, opts) =>
    ipcRenderer.invoke("cloud-streaming-usage", text, audioDurationSeconds, opts),
  cloudUsage: () => ipcRenderer.invoke("cloud-usage"),
  cloudCheckout: (opts) => ipcRenderer.invoke("cloud-checkout", opts),
  cloudBillingPortal: () => ipcRenderer.invoke("cloud-billing-portal"),
  cloudSwitchPlan: (opts) => ipcRenderer.invoke("cloud-switch-plan", opts),
  cloudPreviewSwitch: (opts) => ipcRenderer.invoke("cloud-preview-switch", opts),
  cloudApiRequest: (opts) => ipcRenderer.invoke("cloud-api-request", opts),
  getSttConfig: () => ipcRenderer.invoke("get-stt-config"),
  getWorkspacePolicy: (accountId, expectedAuthGeneration) =>
    ipcRenderer.invoke("get-workspace-policy", accountId, expectedAuthGeneration),
  onWorkspacePolicyChanged: (callback) => {
    const listener = (_event, snapshot) => callback(snapshot);
    ipcRenderer.on("workspace-policy-changed", listener);
    return () => ipcRenderer.removeListener("workspace-policy-changed", listener);
  },
  getNoteRecordingConfig: () => ipcRenderer.invoke("get-note-recording-config"),

  // Cloud audio file transcription
  transcribeAudioFileCloud: (filePath, options) =>
    ipcRenderer.invoke("transcribe-audio-file-cloud", filePath, options),
  cancelUploadTranscription: (requestId) =>
    ipcRenderer.invoke("cancel-upload-transcription", requestId),
  transcribeAudioFileByok: (options) => ipcRenderer.invoke("transcribe-audio-file-byok", options),
  onUploadTranscriptionProgress: registerListener(
    "upload-transcription-progress",
    (callback) => (_event, data) => callback(data)
  ),

  // Referral stats
  getReferralStats: () => ipcRenderer.invoke("get-referral-stats"),
  sendReferralInvite: (email) => ipcRenderer.invoke("send-referral-invite", email),
  getReferralInvites: () => ipcRenderer.invoke("get-referral-invites"),

  // Assembly AI Streaming
  assemblyAiStreamingWarmup: (options) =>
    ipcRenderer.invoke("assemblyai-streaming-warmup", options),
  assemblyAiStreamingStart: (options) => ipcRenderer.invoke("assemblyai-streaming-start", options),
  assemblyAiStreamingSend: (audioBuffer) =>
    ipcRenderer.send("assemblyai-streaming-send", audioBuffer),
  assemblyAiStreamingForceEndpoint: () => ipcRenderer.send("assemblyai-streaming-force-endpoint"),
  assemblyAiStreamingStop: () => ipcRenderer.invoke("assemblyai-streaming-stop"),
  assemblyAiStreamingStatus: () => ipcRenderer.invoke("assemblyai-streaming-status"),
  onAssemblyAiPartialTranscript: registerListener(
    "assemblyai-partial-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onAssemblyAiFinalTranscript: registerListener(
    "assemblyai-final-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onAssemblyAiError: registerListener(
    "assemblyai-error",
    (callback) => (_event, error) => callback(error)
  ),
  onAssemblyAiSessionEnd: registerListener(
    "assemblyai-session-end",
    (callback) => (_event, data) => callback(data)
  ),

  // Deepgram Streaming
  deepgramStreamingWarmup: (options) => ipcRenderer.invoke("deepgram-streaming-warmup", options),
  deepgramStreamingStart: (options) => ipcRenderer.invoke("deepgram-streaming-start", options),
  deepgramStreamingSend: (audioBuffer) => ipcRenderer.send("deepgram-streaming-send", audioBuffer),
  deepgramStreamingFinalize: () => ipcRenderer.send("deepgram-streaming-finalize"),
  deepgramStreamingStop: () => ipcRenderer.invoke("deepgram-streaming-stop"),
  deepgramStreamingStatus: () => ipcRenderer.invoke("deepgram-streaming-status"),
  onDeepgramPartialTranscript: registerListener(
    "deepgram-partial-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onDeepgramFinalTranscript: registerListener(
    "deepgram-final-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onDeepgramError: registerListener(
    "deepgram-error",
    (callback) => (_event, error) => callback(error)
  ),
  onDeepgramSessionEnd: registerListener(
    "deepgram-session-end",
    (callback) => (_event, data) => callback(data)
  ),

  // Gemini Live Streaming
  geminiStreamingWarmup: (options) => ipcRenderer.invoke("gemini-streaming-warmup", options),
  geminiStreamingStart: (options) => ipcRenderer.invoke("gemini-streaming-start", options),
  geminiStreamingSend: (audioBuffer) => ipcRenderer.send("gemini-streaming-send", audioBuffer),
  geminiStreamingFinalize: () => ipcRenderer.send("gemini-streaming-finalize"),
  geminiStreamingStop: () => ipcRenderer.invoke("gemini-streaming-stop"),
  geminiStreamingStatus: () => ipcRenderer.invoke("gemini-streaming-status"),
  onGeminiPartialTranscript: registerListener(
    "gemini-partial-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onGeminiFinalTranscript: registerListener(
    "gemini-final-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onGeminiError: registerListener("gemini-error", (callback) => (_event, error) => callback(error)),
  onGeminiSessionEnd: registerListener(
    "gemini-session-end",
    (callback) => (_event, data) => callback(data)
  ),

  // Corti streaming (BYOK)
  cortiStreamingWarmup: (options) => ipcRenderer.invoke("corti-streaming-warmup", options),
  cortiStreamingStart: (options) => ipcRenderer.invoke("corti-streaming-start", options),
  cortiStreamingSend: (audioBuffer) => ipcRenderer.send("corti-streaming-send", audioBuffer),
  cortiStreamingFinalize: () => ipcRenderer.send("corti-streaming-finalize"),
  cortiStreamingStop: () => ipcRenderer.invoke("corti-streaming-stop"),
  cortiStreamingStatus: () => ipcRenderer.invoke("corti-streaming-status"),
  onCortiPartialTranscript: registerListener(
    "corti-partial-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onCortiFinalTranscript: registerListener(
    "corti-final-transcript",
    (callback) => (_event, text) => callback(text)
  ),
  onCortiError: registerListener("corti-error", (callback) => (_event, error) => callback(error)),
  onCortiSessionEnd: registerListener(
    "corti-session-end",
    (callback) => (_event, data) => callback(data)
  ),

  // Meeting transcription (streaming, dual-channel)
  meetingTranscriptionPrepare: (options) =>
    ipcRenderer.invoke("meeting-transcription-prepare", options),
  meetingTranscriptionStart: (options) =>
    ipcRenderer.invoke("meeting-transcription-start", options),
  meetingTranscriptionSend: (buffer, source, capturedAt) =>
    ipcRenderer.send("meeting-transcription-send", buffer, source, capturedAt),
  meetingTranscriptionSetSystemAudioAvailable: (sessionId, available) =>
    ipcRenderer.invoke("meeting-transcription-set-system-audio-available", sessionId, available),
  meetingTranscriptionStop: (expectedSessionId) =>
    ipcRenderer.invoke("meeting-transcription-stop", expectedSessionId),
  meetingTranscriptionCancel: () => ipcRenderer.invoke("meeting-transcription-cancel"),
  onMeetingTranscriptionSegment: registerListener(
    "meeting-transcription-segment",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingSpeakerIdentified: registerListener(
    "meeting-speaker-identified",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingSpeakersMerged: registerListener(
    "meeting-speakers-merged",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingSessionSpeakerConfigUpdated: registerListener(
    "meeting-session-speaker-config-updated",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingTranscriptionError: registerListener(
    "meeting-transcription-error",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingTranscriptionFatalError: registerListener(
    "meeting-transcription-fatal-error",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingSystemAudioSilent: registerListener(
    "meeting-system-audio-silent",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingSystemAudioDegraded: registerListener(
    "meeting-system-audio-degraded",
    (callback) => () => callback()
  ),
  onMeetingSystemAudioInterrupted: registerListener(
    "meeting-system-audio-interrupted",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingSystemAudioResumed: registerListener(
    "meeting-system-audio-resumed",
    (callback) => () => callback()
  ),

  // Dictation realtime streaming
  dictationRealtimeWarmup: (options) => ipcRenderer.invoke("dictation-realtime-warmup", options),
  dictationRealtimeStart: (options) => ipcRenderer.invoke("dictation-realtime-start", options),
  dictationRealtimeSend: (buffer) => ipcRenderer.send("dictation-realtime-send", buffer),
  dictationRealtimeFinalize: () => ipcRenderer.invoke("dictation-realtime-finalize"),
  dictationRealtimeStop: () => ipcRenderer.invoke("dictation-realtime-stop"),
  onDictationRealtimePartial: registerListener(
    "dictation-realtime-partial",
    (callback) => (_event, data) => callback(data)
  ),
  onDictationRealtimeFinal: registerListener(
    "dictation-realtime-final",
    (callback) => (_event, data) => callback(data)
  ),
  onDictationRealtimeLanguage: registerListener(
    "dictation-realtime-language",
    (callback) => (_event, data) => callback(data)
  ),
  onDictationRealtimeError: registerListener(
    "dictation-realtime-error",
    (callback) => (_event, data) => callback(data)
  ),
  onDictationRealtimeSessionEnd: registerListener(
    "dictation-realtime-session-end",
    (callback) => (_event, data) => callback(data)
  ),

  // Usage limit events (for showing UpgradePrompt in ControlPanel)
  notifyLimitReached: (data) => ipcRenderer.send("limit-reached", data),
  onLimitReached: registerListener("limit-reached", (callback) => (_event, data) => callback(data)),

  // Workspace invitation deep link
  onWorkspaceInvitationToken: registerListener(
    "workspace-invitation-token",
    (callback) => (_event, token) => callback(token)
  ),
  getPendingInvitationToken: () => ipcRenderer.invoke("get-pending-invitation-token"),

  // Globe key listener for hotkey capture (macOS only)
  onGlobeKeyPressed: (callback) => {
    const listener = () => callback?.();
    ipcRenderer.on("globe-key-pressed", listener);
    return () => ipcRenderer.removeListener("globe-key-pressed", listener);
  },
  onGlobeKeyReleased: (callback) => {
    const listener = () => callback?.();
    ipcRenderer.on("globe-key-released", listener);
    return () => ipcRenderer.removeListener("globe-key-released", listener);
  },

  // Hotkey registration events (for notifying user when hotkey fails)
  onHotkeyFallbackUsed: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("hotkey-fallback-used", listener);
    return () => ipcRenderer.removeListener("hotkey-fallback-used", listener);
  },
  onHotkeyRegistrationFailed: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("hotkey-registration-failed", listener);
    return () => ipcRenderer.removeListener("hotkey-registration-failed", listener);
  },
  onApiKeyUpdated: registerListener(
    "api-key-updated",
    (callback) => (_event, storeKey) => callback(storeKey)
  ),
  onSettingUpdated: (callback) => {
    const listener = (_event, data) => callback?.(data);
    ipcRenderer.on("setting-updated", listener);
    return () => ipcRenderer.removeListener("setting-updated", listener);
  },
  onDictationKeyActive: (callback) => {
    const listener = (_event, key) => callback?.(key);
    ipcRenderer.on("dictation-key-active", listener);
    return () => ipcRenderer.removeListener("dictation-key-active", listener);
  },
  onWindowsPushToTalkUnavailable: registerListener("windows-ptt-unavailable"),
  onLinuxPttPermissionDenied: registerListener(
    "linux-ptt-permission-denied",
    (callback) => () => callback()
  ),

  // Settings shortcut (Cmd+, / Ctrl+,)
  onShowSettings: registerListener("show-settings", (callback) => () => callback()),
  getPendingSettingsSection: () => ipcRenderer.invoke("get-pending-settings-section"),

  // Accessibility permission events (macOS)
  onAccessibilityMissing: (callback) => {
    const listener = () => callback?.();
    ipcRenderer.on("accessibility-missing", listener);
    return () => ipcRenderer.removeListener("accessibility-missing", listener);
  },
  checkAccessibilityTrusted: () => ipcRenderer.invoke("check-accessibility-trusted"),

  // Notify main process of activation mode changes (for Windows Push-to-Talk)
  notifyActivationModeChanged: (mode) => ipcRenderer.send("activation-mode-changed", mode),
  notifyHotkeyChanged: (hotkey) => ipcRenderer.send("hotkey-changed", hotkey),
  registerMeetingHotkey: (hotkey) => ipcRenderer.invoke("register-meeting-hotkey", hotkey),

  // Floating icon auto-hide
  notifyFloatingIconAutoHideChanged: (enabled) =>
    ipcRenderer.send("floating-icon-auto-hide-changed", enabled),
  onFloatingIconAutoHideChanged: registerListener(
    "floating-icon-auto-hide-changed",
    (callback) => (_event, enabled) => callback(enabled)
  ),

  // Panel start position
  notifyPanelStartPositionChanged: (position) =>
    ipcRenderer.send("panel-start-position-changed", position),

  // Start minimized
  notifyStartMinimizedChanged: (enabled) => ipcRenderer.send("start-minimized-changed", enabled),

  // Auto-start management
  getAutoStartEnabled: () => ipcRenderer.invoke("get-auto-start-enabled"),
  setAutoStartEnabled: (enabled) => ipcRenderer.invoke("set-auto-start-enabled", enabled),

  // Agent mode
  updateVoiceAgentHotkey: (hotkey) => ipcRenderer.invoke("update-voice-agent-hotkey", hotkey),
  getVoiceAgentKey: () => ipcRenderer.invoke("get-voice-agent-key"),
  updateTranslationHotkey: (hotkey) => ipcRenderer.invoke("update-translation-hotkey", hotkey),
  getTranslationKey: () => ipcRenderer.invoke("get-translation-key"),
  onPreviewText: registerListener("preview-text", (callback) => (_event, text) => callback(text)),
  onPreviewAppend: registerListener(
    "preview-append",
    (callback) => (_event, text) => callback(text)
  ),
  onPreviewHold: registerListener(
    "preview-hold",
    (callback) => (_event, payload) => callback(payload)
  ),
  onPreviewResult: registerListener(
    "preview-result",
    (callback) => (_event, payload) => callback(payload)
  ),
  onPreviewHide: registerListener("preview-hide", (callback) => () => callback()),
  startDictationPreview: (opts) => ipcRenderer.invoke("start-dictation-preview", opts),
  stopDictationPreview: (opts) => ipcRenderer.invoke("stop-dictation-preview", opts),
  dismissDictationPreview: () => ipcRenderer.invoke("dismiss-dictation-preview"),
  updateDictationPreview: (text) => ipcRenderer.invoke("update-dictation-preview", text),
  completeDictationPreview: (payload) => ipcRenderer.invoke("complete-dictation-preview", payload),
  hideDictationPreview: () => ipcRenderer.invoke("hide-dictation-preview"),
  sendDictationPreviewAudio: (data) => ipcRenderer.send("dictation-preview-audio", data),
  acquireRecordingLock: (pipeline) => ipcRenderer.invoke("acquire-recording-lock", pipeline),
  releaseRecordingLock: (pipeline) => ipcRenderer.invoke("release-recording-lock", pipeline),

  // Agent cloud streaming (event-based for real-time chunks)
  startAgentStream: (requestId, messages, opts) =>
    ipcRenderer.send("cloud-agent-stream-start", requestId, messages, opts),
  cancelAgentStream: (requestId) => ipcRenderer.send("cloud-agent-stream-cancel", requestId),
  onAgentStreamChunk: registerListener(
    "cloud-agent-stream-chunk",
    (callback) => (_event, payload) => callback(payload)
  ),
  onAgentStreamError: registerListener(
    "cloud-agent-stream-error",
    (callback) => (_event, payload) => callback(payload)
  ),
  onAgentStreamEnd: registerListener(
    "cloud-agent-stream-end",
    (callback) => (_event, payload) => callback(payload)
  ),

  // Agent cloud tools
  productHelp: (id, input) => ipcRenderer.invoke("product-help", id, input),
  cancelProductHelp: (id) => ipcRenderer.send("product-help-cancel", id),
  productHelpBasics: () => ipcRenderer.invoke("product-help-basics"),
  agentWebSearch: (query, numResults) => ipcRenderer.invoke("agent-web-search", query, numResults),
  agentOpenNote: (noteId) => ipcRenderer.invoke("agent-open-note", noteId),

  // Agent conversation persistence
  claimAgentConversation: (id, ownerId) =>
    ipcRenderer.invoke("claim-agent-conversation", id, ownerId),
  releaseAgentConversation: (id, ownerId) =>
    ipcRenderer.invoke("release-agent-conversation", id, ownerId),
  onAgentConversationReleased: registerListener(
    "agent-conversation-released",
    (callback) => (_event, payload) => callback(payload)
  ),
  createAgentConversation: (title, noteId, spaceId, folderId) =>
    ipcRenderer.invoke("db-create-agent-conversation", title, noteId, spaceId, folderId),
  getAgentConversations: (limit) => ipcRenderer.invoke("db-get-agent-conversations", limit),
  getAgentConversation: (id) => ipcRenderer.invoke("db-get-agent-conversation", id),
  deleteAgentConversation: (id) => ipcRenderer.invoke("db-delete-agent-conversation", id),
  updateAgentConversationTitle: (id, title) =>
    ipcRenderer.invoke("db-update-agent-conversation-title", id, title),
  addAgentMessage: (conversationId, role, content, metadata) =>
    ipcRenderer.invoke("db-add-agent-message", conversationId, role, content, metadata),
  updateAgentMessage: (id, content, metadata) =>
    ipcRenderer.invoke("db-update-agent-message", id, content, metadata),
  forkAgentConversation: (conversationId, throughMessageId, forkContext) =>
    ipcRenderer.invoke("db-fork-agent-conversation", conversationId, throughMessageId, forkContext),
  getAgentMessages: (conversationId) => ipcRenderer.invoke("db-get-agent-messages", conversationId),
  getAgentConversationsWithPreview: (limit, offset, includeArchived) =>
    ipcRenderer.invoke("db-get-agent-conversations-with-preview", limit, offset, includeArchived),
  searchAgentConversations: (query, limit) =>
    ipcRenderer.invoke("db-search-agent-conversations", query, limit),
  getConversationsForNote: (noteId, limit) =>
    ipcRenderer.invoke("db-get-conversations-for-note", noteId, limit),
  getConversationsForContainer: (spaceId, folderId, limit) =>
    ipcRenderer.invoke("db-get-conversations-for-container", spaceId, folderId, limit),
  archiveAgentConversation: (id) => ipcRenderer.invoke("db-archive-agent-conversation", id),
  unarchiveAgentConversation: (id) => ipcRenderer.invoke("db-unarchive-agent-conversation", id),
  updateAgentConversationCloudId: (id, cloudId) =>
    ipcRenderer.invoke("db-update-agent-conversation-cloud-id", id, cloudId),
  semanticSearchConversations: (query, limit) =>
    ipcRenderer.invoke("db-semantic-search-conversations", query, limit),

  // Sync operations
  getPendingNotes: (spaceKind) => ipcRenderer.invoke("db-get-pending-notes", spaceKind),
  getPendingNoteDeletes: () => ipcRenderer.invoke("db-get-pending-note-deletes"),
  getNoteByClientId: (clientNoteId) => ipcRenderer.invoke("db-get-note-by-client-id", clientNoteId),
  getNoteForCloudRow: (clientNoteId, cloudId) =>
    ipcRenderer.invoke("db-get-note-for-cloud-row", clientNoteId, cloudId),
  upsertNoteFromCloud: (cloudNote, localFolderId, localSpaceId) =>
    ipcRenderer.invoke("db-upsert-note-from-cloud", cloudNote, localFolderId, localSpaceId),
  markNoteCreatesAttempted: (clientNoteIds) =>
    ipcRenderer.invoke("db-mark-note-creates-attempted", clientNoteIds),
  acknowledgeNoteCreate: (id, snapshot, cloudId, cloudUpdatedAt, ownerUserId, options) =>
    ipcRenderer.invoke(
      "db-acknowledge-note-create",
      id,
      snapshot,
      cloudId,
      cloudUpdatedAt,
      ownerUserId,
      options
    ),
  markNoteSyncedIfUnchanged: (
    id,
    snapshot,
    expectedCloudId,
    cloudUpdatedAt,
    ownerUserId,
    cloudRevision,
    cloudNote
  ) =>
    ipcRenderer.invoke(
      "db-mark-note-synced-if-unchanged",
      id,
      snapshot,
      expectedCloudId,
      cloudUpdatedAt,
      ownerUserId,
      cloudRevision,
      cloudNote
    ),
  setNoteCloudBase: (id, cloudUpdatedAt, cloudRevision, options) =>
    ipcRenderer.invoke("db-set-note-cloud-base", id, cloudUpdatedAt, cloudRevision, options),
  setNoteOwnerFromCloud: (id, ownerUserId) =>
    ipcRenderer.invoke("db-set-note-owner-from-cloud", id, ownerUserId),
  countTeamNotesMissingOwner: () => ipcRenderer.invoke("db-count-team-notes-missing-owner"),
  countNotesMissingRevision: (spaceKind) =>
    ipcRenderer.invoke("db-count-notes-missing-revision", spaceKind),
  markNoteSyncError: (id) => ipcRenderer.invoke("db-mark-note-sync-error", id),
  restoreNoteAfterDeniedDelete: (id) =>
    ipcRenderer.invoke("db-restore-note-after-denied-delete", id),
  hardDeleteNote: (id) => ipcRenderer.invoke("db-hard-delete-note", id),

  getPendingFolders: (spaceKind) => ipcRenderer.invoke("db-get-pending-folders", spaceKind),
  getFolderByClientId: (clientFolderId) =>
    ipcRenderer.invoke("db-get-folder-by-client-id", clientFolderId),
  upsertFolderFromCloud: (cloudFolder, localSpaceId) =>
    ipcRenderer.invoke("db-upsert-folder-from-cloud", cloudFolder, localSpaceId),
  acknowledgeFolderCreate: (
    id,
    snapshot,
    expectedCloudId,
    responseClientFolderId,
    cloudId,
    cloudUpdatedAt
  ) =>
    ipcRenderer.invoke(
      "db-acknowledge-folder-create",
      id,
      snapshot,
      expectedCloudId,
      responseClientFolderId,
      cloudId,
      cloudUpdatedAt
    ),
  markFolderSyncedIfUnchanged: (id, snapshot, expectedCloudId) =>
    ipcRenderer.invoke("db-mark-folder-synced-if-unchanged", id, snapshot, expectedCloudId),
  getFolderIdMap: () => ipcRenderer.invoke("db-get-folder-id-map"),
  getPendingFolderDeletes: () => ipcRenderer.invoke("db-get-pending-folder-deletes"),
  restoreFolderAfterDeniedDelete: (id) =>
    ipcRenderer.invoke("db-restore-folder-after-denied-delete", id),
  hardDeleteFolder: (id) => ipcRenderer.invoke("db-hard-delete-folder", id),
  relocateRevokedFolder: (id, privateSpaceId, preserveFolder) =>
    ipcRenderer.invoke("db-relocate-revoked-folder", id, privateSpaceId, preserveFolder),

  getPendingConversations: () => ipcRenderer.invoke("db-get-pending-conversations"),
  getPendingConversationDeletes: () => ipcRenderer.invoke("db-get-pending-conversation-deletes"),
  getConversationByClientId: (clientId) =>
    ipcRenderer.invoke("db-get-conversation-by-client-id", clientId),
  claimConversationFromCloud: (cloudConv, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-claim-conversation-from-cloud", cloudConv, expectedAuthGeneration),
  upsertConversationFromCloud: (cloudConv, messages, snapshot, expectedAuthGeneration) =>
    ipcRenderer.invoke(
      "db-upsert-conversation-from-cloud",
      cloudConv,
      messages,
      snapshot,
      expectedAuthGeneration
    ),
  markConversationCreateAttempted: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-mark-conversation-create-attempted", id, expectedAuthGeneration),
  clearConversationCreateAttempted: (id) =>
    ipcRenderer.invoke("db-clear-conversation-create-attempted", id),
  acknowledgeConversationCreate: (id, snapshot, cloudId, accountId, authGeneration) =>
    ipcRenderer.invoke(
      "db-acknowledge-conversation-create",
      id,
      snapshot,
      cloudId,
      accountId,
      authGeneration
    ),
  markConversationSynced: (id, cloudId, snapshot, cloudUpdatedAt, accountId, authGeneration) =>
    ipcRenderer.invoke(
      "db-mark-conversation-synced",
      id,
      cloudId,
      snapshot,
      cloudUpdatedAt,
      accountId,
      authGeneration
    ),
  replaceConversationClientId: (id, expectedClientId, accountId, authGeneration) =>
    ipcRenderer.invoke(
      "db-replace-conversation-client-id",
      id,
      expectedClientId,
      accountId,
      authGeneration
    ),
  hardDeleteConversation: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-hard-delete-conversation", id, expectedAuthGeneration),

  getPendingTranscriptions: () => ipcRenderer.invoke("db-get-pending-transcriptions"),
  getTranscriptionByClientId: (clientId) =>
    ipcRenderer.invoke("db-get-transcription-by-client-id", clientId),
  upsertTranscriptionFromCloud: (cloudTranscription, expectedAuthGeneration) =>
    ipcRenderer.invoke(
      "db-upsert-transcription-from-cloud",
      cloudTranscription,
      expectedAuthGeneration
    ),
  markTranscriptionSynced: (id, cloudId, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-mark-transcription-synced", id, cloudId, expectedAuthGeneration),
  getPendingTranscriptionDeletes: () => ipcRenderer.invoke("db-get-pending-transcription-deletes"),
  hardDeleteTranscription: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-hard-delete-transcription", id, expectedAuthGeneration),

  getPendingDictionary: () => ipcRenderer.invoke("db-get-pending-dictionary"),
  getPendingDictionaryDeletes: () => ipcRenderer.invoke("db-get-pending-dictionary-deletes"),
  getDictionaryByClientId: (clientDictId) =>
    ipcRenderer.invoke("db-get-dictionary-by-client-id", clientDictId),
  upsertDictionaryFromCloud: (cloudEntry, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-upsert-dictionary-from-cloud", cloudEntry, expectedAuthGeneration),
  markDictionarySynced: (
    id,
    cloudId,
    serverUpdatedAt,
    expectedWord,
    expectedSource,
    serverSource,
    pushed,
    expectedAuthGeneration
  ) =>
    ipcRenderer.invoke(
      "db-mark-dictionary-synced",
      id,
      cloudId,
      serverUpdatedAt,
      expectedWord,
      expectedSource,
      serverSource,
      pushed,
      expectedAuthGeneration
    ),
  hardDeleteDictionary: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-hard-delete-dictionary", id, expectedAuthGeneration),
  clearDictionaryCloudId: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-clear-dictionary-cloud-id", id, expectedAuthGeneration),
  keepNewerDictionaryEdit: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-keep-newer-dictionary-edit", id, expectedAuthGeneration),
  broadcastDictionaryUpdated: () => ipcRenderer.invoke("db-broadcast-dictionary-updated"),
  getDictionaryEntriesByWords: (words) =>
    ipcRenderer.invoke("db-get-dictionary-entries-by-words", words),
  markDictionaryWordsShared: (words, sharedAt, options) =>
    ipcRenderer.invoke("db-mark-dictionary-words-shared", words, sharedAt, options),
  getMovedDictionaryWords: () => ipcRenderer.invoke("db-get-moved-dictionary-words"),
  retireMovedDictionaryWords: (words, options) =>
    ipcRenderer.invoke("db-retire-moved-dictionary-words", words, options),
  reviveRetiredDictionaryWords: (words, options) =>
    ipcRenderer.invoke("db-revive-retired-dictionary-words", words, options),

  getSharedDictionary: (workspaceId) => ipcRenderer.invoke("db-shared-dictionary-get", workspaceId),
  applySharedDictionaryPull: (workspaceId, change) =>
    ipcRenderer.invoke("db-shared-dictionary-apply-pull", workspaceId, change),
  replaceSharedDictionary: (workspaceId, full) =>
    ipcRenderer.invoke("db-shared-dictionary-replace", workspaceId, full),
  applySharedDictionaryHides: (workspaceId, change) =>
    ipcRenderer.invoke("db-shared-dictionary-apply-hides", workspaceId, change),
  replaceSharedDictionaryHides: (workspaceId, hidden) =>
    ipcRenderer.invoke("db-shared-dictionary-replace-hides", workspaceId, hidden),
  clearSharedDictionary: (workspaceId) =>
    ipcRenderer.invoke("db-shared-dictionary-clear", workspaceId),
  pruneSharedDictionary: (keepWorkspaceIds) =>
    ipcRenderer.invoke("db-shared-dictionary-prune", keepWorkspaceIds),
  setSharedDictionaryHints: (promptHints, hotwordHints) =>
    ipcRenderer.invoke("dictionary-set-shared-hints", promptHints, hotwordHints),

  getPendingSnippets: () => ipcRenderer.invoke("db-get-pending-snippets"),
  getPendingSnippetDeletes: () => ipcRenderer.invoke("db-get-pending-snippet-deletes"),
  getSnippetForCloudMerge: (cloudEntry, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-get-snippet-for-cloud-merge", cloudEntry, expectedAuthGeneration),
  upsertSnippetFromCloud: (cloudEntry, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-upsert-snippet-from-cloud", cloudEntry, expectedAuthGeneration),
  markSnippetSynced: (
    id,
    cloudId,
    serverUpdatedAt,
    expectedTrigger,
    expectedReplacement,
    expectedAuthGeneration
  ) =>
    ipcRenderer.invoke(
      "db-mark-snippet-synced",
      id,
      cloudId,
      serverUpdatedAt,
      expectedTrigger,
      expectedReplacement,
      expectedAuthGeneration
    ),
  hardDeleteSnippet: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-hard-delete-snippet", id, expectedAuthGeneration),
  clearSnippetCloudId: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-clear-snippet-cloud-id", id, expectedAuthGeneration),
  keepNewerSnippetEdit: (id, expectedAuthGeneration) =>
    ipcRenderer.invoke("db-keep-newer-snippet-edit", id, expectedAuthGeneration),
  broadcastSnippetsUpdated: () => ipcRenderer.invoke("db-broadcast-snippets-updated"),

  // Google Calendar
  gcalStartOAuth: (requestId) => ipcRenderer.invoke("gcal-start-oauth", requestId),
  calendarCancelConnect: (requestId) => ipcRenderer.invoke("calendar-cancel-connect", requestId),
  gcalDisconnect: (email) => ipcRenderer.invoke("gcal-disconnect", email),
  gcalGetConnectionStatus: () => ipcRenderer.invoke("gcal-get-connection-status"),
  gcalGetCalendars: () => ipcRenderer.invoke("gcal-get-calendars"),
  gcalSetCalendarSelection: (calendarId, isSelected) =>
    ipcRenderer.invoke("gcal-set-calendar-selection", calendarId, isSelected),
  gcalSetPrimaryOnly: (value) => ipcRenderer.invoke("gcal-set-primary-only", value),
  gcalSyncEvents: () => ipcRenderer.invoke("gcal-sync-events"),
  gcalGetUpcomingEvents: (windowMinutes, options) =>
    ipcRenderer.invoke("gcal-get-upcoming-events", windowMinutes, options),
  connectorStatus: () => ipcRenderer.invoke("connector-status"),
  connectorPrepare: (connectorId, action, args) =>
    ipcRenderer.invoke("connector-prepare", connectorId, action, args),
  connectorQuery: (connectorId, action, args) =>
    ipcRenderer.invoke("connector-query", connectorId, action, args),
  connectorCommit: (actionId, edits) => ipcRenderer.invoke("connector-commit", actionId, edits),
  connectorCancel: (actionId, reason) => ipcRenderer.invoke("connector-cancel", actionId, reason),
  connectorRunDirect: (connectorId, action, args, runId) =>
    ipcRenderer.invoke("connector-run-direct", connectorId, action, args, runId),
  connectorRecentActions: (connectorId, limit) =>
    ipcRenderer.invoke("connector-recent-actions", connectorId, limit),
  connectorFindContacts: (query) => ipcRenderer.invoke("connector-find-contacts", query),
  connectorNoteAttendees: (request) => ipcRenderer.invoke("connector-note-attendees", request),
  connectorConnect: (connectorId) => ipcRenderer.invoke("connector-connect", connectorId),
  connectorCancelConnect: (connectorId) =>
    ipcRenderer.invoke("connector-cancel-connect", connectorId),
  connectorDisconnect: (connectorId) => ipcRenderer.invoke("connector-disconnect", connectorId),
  onConnectorStatusChanged: (callback) => {
    const listener = (_event, statuses) => callback(statuses);
    ipcRenderer.on("connector-status-changed", listener);
    return () => ipcRenderer.removeListener("connector-status-changed", listener);
  },
  // A connect's progress, such as the code GitHub's device flow shows.
  onConnectorConnectProgress: (callback) => {
    const listener = (_event, progress) => callback(progress);
    ipcRenderer.on("connector-connect-progress", listener);
    return () => ipcRenderer.removeListener("connector-connect-progress", listener);
  },
  calendarGetAvailability: (request) => ipcRenderer.invoke("calendar-get-availability", request),
  gcalGetEvent: (eventId) => ipcRenderer.invoke("gcal-get-event", eventId),
  calendarGetLinkableEvents: () => ipcRenderer.invoke("calendar-get-linkable-events"),

  // Microsoft Calendar
  mcalStartOAuth: (requestId) => ipcRenderer.invoke("mcal-start-oauth", requestId),
  mcalDisconnect: (email) => ipcRenderer.invoke("mcal-disconnect", email),
  mcalGetConnectionStatus: () => ipcRenderer.invoke("mcal-get-connection-status"),
  mcalSetPrimaryOnly: (value) => ipcRenderer.invoke("mcal-set-primary-only", value),

  // Apple Calendar (macOS EventKit)
  acalConnect: (requestId) => ipcRenderer.invoke("acal-connect", requestId),
  acalDisconnect: () => ipcRenderer.invoke("acal-disconnect"),
  acalGetConnectionStatus: () => ipcRenderer.invoke("acal-get-connection-status"),
  openCalendarPrivacySettings: () => ipcRenderer.invoke("open-calendar-privacy-settings"),

  // Contacts
  searchContacts: (query) => ipcRenderer.invoke("search-contacts", query),
  searchInviteContacts: (query, exclude) =>
    ipcRenderer.invoke("search-invite-contacts", query, exclude),
  upsertContact: (contact) => ipcRenderer.invoke("upsert-contact", contact),
  getMD5Hash: (text) => ipcRenderer.invoke("get-md5-hash", text),

  // Google Calendar event listeners
  onGcalConnectionChanged: registerListener(
    "gcal-connection-changed",
    (callback) => (_event, data) => callback(data)
  ),
  onGcalEventsSynced: registerListener(
    "gcal-events-synced",
    (callback) => (_event, data) => callback(data)
  ),

  // Microsoft Calendar event listeners
  onMcalConnectionChanged: registerListener(
    "mcal-connection-changed",
    (callback) => (_event, data) => callback(data)
  ),
  onMcalEventsSynced: registerListener(
    "mcal-events-synced",
    (callback) => (_event, data) => callback(data)
  ),

  // Apple Calendar event listeners
  onAcalConnectionChanged: registerListener(
    "acal-connection-changed",
    (callback) => (_event, data) => callback(data)
  ),
  onAcalEventsSynced: registerListener(
    "acal-events-synced",
    (callback) => (_event, data) => callback(data)
  ),

  // Meeting detection
  syncNotificationPreferences: (prefs) =>
    ipcRenderer.invoke("sync-notification-preferences", prefs),
  setSpeakerDiarizationEnabled: (enabled) =>
    ipcRenderer.invoke("meeting-set-speaker-diarization-enabled", { enabled }),
  setMeetingSessionSpeakerConfig: (config) =>
    ipcRenderer.invoke("meeting-set-session-speaker-config", config),
  getWhisperVadConfig: () => ipcRenderer.invoke("whisper-vad-get-config"),
  setWhisperVadConfig: (config) => ipcRenderer.invoke("whisper-vad-set-config", config),
  onMeetingNotificationData: registerListener(
    "meeting-notification-data",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingAutoEndRequested: registerListener(
    "meeting-auto-end-requested",
    (callback) => (_event, data) => callback(data)
  ),
  getMeetingNotificationDestination: () =>
    ipcRenderer.invoke("get-meeting-notification-destination"),
  selectMeetingNotificationFolder: (folder) =>
    ipcRenderer.invoke("select-meeting-notification-folder", folder),
  createMeetingNotificationFolder: (request) =>
    ipcRenderer.invoke("create-meeting-notification-folder", request),
  onMeetingNotificationFolderCreated: registerListener(
    "meeting-notification-folder-created",
    (callback) => (_event, data) => callback(data)
  ),
  setMeetingNotificationSurface: (state) =>
    ipcRenderer.invoke("set-meeting-notification-surface", state),
  onMeetingNotificationSurfaceClosed: registerListener(
    "meeting-notification-surface-closed",
    (callback) => (_event, data) => callback(data)
  ),
  onMeetingNotificationSurfaceResized: registerListener(
    "meeting-notification-surface-resized",
    (callback) => (_event, data) => callback(data)
  ),
  getMeetingNotificationData: () => ipcRenderer.invoke("get-meeting-notification-data"),
  meetingNotificationReady: () => ipcRenderer.invoke("meeting-notification-ready"),
  meetingNotificationRespond: (detectionId, action, options) =>
    ipcRenderer.invoke("meeting-notification-respond", detectionId, action, options),
  confirmMeetingNoteNavigation: (navigationId, status) =>
    ipcRenderer.invoke("confirm-meeting-note-navigation", navigationId, status),
  joinCalendarMeeting: (eventId) => ipcRenderer.invoke("join-calendar-meeting", eventId),
  startManualMeeting: () => ipcRenderer.invoke("start-manual-meeting"),
  getPendingMeetingNoteNavigation: () => ipcRenderer.invoke("get-pending-meeting-note-navigation"),
  onMeetingNoteNavigationPending: registerListener(
    "meeting-note-navigation-pending",
    (callback) => () => callback()
  ),
  getPendingNoteNavigation: () => ipcRenderer.invoke("get-pending-note-navigation"),
  onNoteNavigationPending: registerListener(
    "note-navigation-pending",
    (callback) => () => callback()
  ),
});