markupr 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. package/vitest.config.ts +46 -0
@@ -0,0 +1,1047 @@
1
+ /**
2
+ * markupr - Preload Script
3
+ *
4
+ * Exposes a safe API to the renderer process via contextBridge.
5
+ * This is the ONLY way the renderer can communicate with the main process.
6
+ *
7
+ * API Design:
8
+ * - Organized by domain (session, capture, audio, transcript, settings, permissions, output)
9
+ * - invoke() for request/response patterns
10
+ * - on/send for event streams (returns cleanup function)
11
+ * - Type-safe channel names from shared types
12
+ */
13
+
14
+ import { contextBridge, ipcRenderer } from 'electron';
15
+ import {
16
+ IPC_CHANNELS,
17
+ type AppSettings,
18
+ type CaptureSource,
19
+ type AudioDevice,
20
+ type PermissionType,
21
+ type PermissionStatus,
22
+ type SessionStatusPayload,
23
+ type SessionPayload,
24
+ type FeedbackItemPayload,
25
+ type TranscriptChunkPayload,
26
+ type ScreenshotCapturedPayload,
27
+ type OutputReadyPayload,
28
+ type SaveResult,
29
+ type HotkeyConfig,
30
+ type SessionState,
31
+ type UpdateStatusPayload,
32
+ type WhisperDownloadProgressPayload,
33
+ type WhisperModelInfoPayload,
34
+ type WhisperModelCheckResult,
35
+ type TranscriptionTier,
36
+ type TranscriptionTierStatus,
37
+ type ApiKeyValidationResult,
38
+ type ProcessingProgressPayload,
39
+ } from '../shared/types';
40
+
41
+ // =============================================================================
42
+ // Type Definitions for Event Handlers
43
+ // =============================================================================
44
+
45
+ type Unsubscribe = () => void;
46
+
47
+ // =============================================================================
48
+ // Helper: Create typed event subscriber
49
+ // =============================================================================
50
+
51
+ function createEventSubscriber<T>(channel: string) {
52
+ return (callback: (data: T) => void): Unsubscribe => {
53
+ const handler = (_: Electron.IpcRendererEvent, data: T) => callback(data);
54
+ ipcRenderer.on(channel, handler);
55
+ return () => ipcRenderer.removeListener(channel, handler);
56
+ };
57
+ }
58
+
59
+ // =============================================================================
60
+ // markupr API
61
+ // =============================================================================
62
+
63
+ const markuprApi = {
64
+ // ===========================================================================
65
+ // Session API
66
+ // ===========================================================================
67
+ session: {
68
+ /**
69
+ * Start a recording session
70
+ * @param sourceId - ID of the capture source (screen or window)
71
+ */
72
+ start: (
73
+ sourceId?: string,
74
+ sourceName?: string
75
+ ): Promise<{ success: boolean; sessionId?: string; error?: string }> => {
76
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_START, sourceId, sourceName);
77
+ },
78
+
79
+ /**
80
+ * Stop the current recording session
81
+ */
82
+ stop: (): Promise<{ success: boolean; session?: SessionPayload; error?: string }> => {
83
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_STOP);
84
+ },
85
+
86
+ /**
87
+ * Pause the current recording session
88
+ */
89
+ pause: (): Promise<{ success: boolean; error?: string }> => {
90
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_PAUSE);
91
+ },
92
+
93
+ /**
94
+ * Resume a paused recording session
95
+ */
96
+ resume: (): Promise<{ success: boolean; error?: string }> => {
97
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_RESUME);
98
+ },
99
+
100
+ /**
101
+ * Cancel the current session without saving
102
+ */
103
+ cancel: (): Promise<{ success: boolean }> => {
104
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_CANCEL);
105
+ },
106
+
107
+ /**
108
+ * Get current session status
109
+ */
110
+ getStatus: (): Promise<SessionStatusPayload> => {
111
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_GET_STATUS);
112
+ },
113
+
114
+ /**
115
+ * Get current session data
116
+ */
117
+ getCurrent: (): Promise<SessionPayload | null> => {
118
+ return ipcRenderer.invoke(IPC_CHANNELS.SESSION_GET_CURRENT);
119
+ },
120
+
121
+ /**
122
+ * Subscribe to session state changes
123
+ */
124
+ onStateChange: createEventSubscriber<{
125
+ state: SessionState;
126
+ session: SessionPayload | null;
127
+ }>(IPC_CHANNELS.SESSION_STATE_CHANGED),
128
+
129
+ /**
130
+ * Subscribe to session status updates (periodic updates during recording)
131
+ */
132
+ onStatusUpdate: createEventSubscriber<SessionStatusPayload>(IPC_CHANNELS.SESSION_STATUS),
133
+
134
+ /**
135
+ * Subscribe to session completion
136
+ */
137
+ onComplete: createEventSubscriber<SessionPayload>(IPC_CHANNELS.SESSION_COMPLETE),
138
+
139
+ /**
140
+ * Subscribe to new feedback items
141
+ */
142
+ onFeedbackItem: createEventSubscriber<FeedbackItemPayload>(IPC_CHANNELS.SESSION_FEEDBACK_ITEM),
143
+
144
+ /**
145
+ * Subscribe to voice activity changes
146
+ */
147
+ onVoiceActivity: createEventSubscriber<{ active: boolean }>(IPC_CHANNELS.SESSION_VOICE_ACTIVITY),
148
+
149
+ /**
150
+ * Subscribe to session errors
151
+ */
152
+ onError: createEventSubscriber<{ message: string }>(IPC_CHANNELS.SESSION_ERROR),
153
+ },
154
+
155
+ // ===========================================================================
156
+ // Capture API
157
+ // ===========================================================================
158
+ capture: {
159
+ /**
160
+ * Get available capture sources (screens and windows)
161
+ */
162
+ getSources: (): Promise<CaptureSource[]> => {
163
+ return ipcRenderer.invoke(IPC_CHANNELS.CAPTURE_GET_SOURCES);
164
+ },
165
+
166
+ /**
167
+ * Trigger a manual screenshot during recording
168
+ */
169
+ manualScreenshot: (): Promise<{ success: boolean; error?: string }> => {
170
+ return ipcRenderer.invoke(IPC_CHANNELS.CAPTURE_MANUAL_SCREENSHOT);
171
+ },
172
+
173
+ /**
174
+ * Subscribe to screenshot captured events
175
+ */
176
+ onScreenshot: createEventSubscriber<ScreenshotCapturedPayload>(IPC_CHANNELS.SCREENSHOT_CAPTURED),
177
+
178
+ /**
179
+ * Subscribe to manual screenshot trigger events (from hotkey)
180
+ */
181
+ onManualTrigger: createEventSubscriber<{ timestamp: number }>(IPC_CHANNELS.MANUAL_SCREENSHOT),
182
+ },
183
+
184
+ // ===========================================================================
185
+ // Audio API
186
+ // ===========================================================================
187
+ audio: {
188
+ /**
189
+ * Get available audio input devices
190
+ * Note: Device enumeration happens in renderer via Web Audio API
191
+ */
192
+ getDevices: (): Promise<AudioDevice[]> => {
193
+ return ipcRenderer.invoke(IPC_CHANNELS.AUDIO_GET_DEVICES);
194
+ },
195
+
196
+ /**
197
+ * Set the preferred audio input device
198
+ */
199
+ setDevice: (deviceId: string): Promise<{ success: boolean }> => {
200
+ return ipcRenderer.invoke(IPC_CHANNELS.AUDIO_SET_DEVICE, deviceId);
201
+ },
202
+
203
+ /**
204
+ * Subscribe to audio level updates (for visualization)
205
+ */
206
+ onLevel: createEventSubscriber<number>(IPC_CHANNELS.AUDIO_LEVEL),
207
+
208
+ /**
209
+ * Subscribe to voice activity detection updates
210
+ */
211
+ onVoiceActivity: createEventSubscriber<boolean>(IPC_CHANNELS.AUDIO_VOICE_ACTIVITY),
212
+
213
+ // -------------------------------------------------------------------------
214
+ // Audio Capture Bridge (Renderer -> Main communication)
215
+ // These are used by the AudioCaptureRenderer to communicate with AudioCapture in main
216
+ // -------------------------------------------------------------------------
217
+
218
+ /**
219
+ * Respond to device enumeration request from main
220
+ */
221
+ onRequestDevices: (callback: () => void): Unsubscribe => {
222
+ const handler = () => callback();
223
+ ipcRenderer.on(IPC_CHANNELS.AUDIO_REQUEST_DEVICES, handler);
224
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.AUDIO_REQUEST_DEVICES, handler);
225
+ },
226
+
227
+ /**
228
+ * Send device list back to main
229
+ */
230
+ sendDevices: (devices: AudioDevice[]): void => {
231
+ ipcRenderer.send(IPC_CHANNELS.AUDIO_DEVICES_RESPONSE, devices);
232
+ },
233
+
234
+ /**
235
+ * Handle start capture command from main
236
+ */
237
+ onStartCapture: (
238
+ callback: (config: {
239
+ deviceId: string | null;
240
+ sampleRate: number;
241
+ channels: number;
242
+ chunkDurationMs: number;
243
+ }) => void
244
+ ): Unsubscribe => {
245
+ const handler = (
246
+ _: Electron.IpcRendererEvent,
247
+ config: {
248
+ deviceId: string | null;
249
+ sampleRate: number;
250
+ channels: number;
251
+ chunkDurationMs: number;
252
+ }
253
+ ) => callback(config);
254
+ ipcRenderer.on(IPC_CHANNELS.AUDIO_START_CAPTURE, handler);
255
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.AUDIO_START_CAPTURE, handler);
256
+ },
257
+
258
+ /**
259
+ * Handle stop capture command from main
260
+ */
261
+ onStopCapture: (callback: () => void): Unsubscribe => {
262
+ const handler = () => callback();
263
+ ipcRenderer.on(IPC_CHANNELS.AUDIO_STOP_CAPTURE, handler);
264
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.AUDIO_STOP_CAPTURE, handler);
265
+ },
266
+
267
+ /**
268
+ * Handle device change command from main
269
+ */
270
+ onSetDevice: (callback: (deviceId: string) => void): Unsubscribe => {
271
+ const handler = (_: Electron.IpcRendererEvent, deviceId: string) => callback(deviceId);
272
+ ipcRenderer.on(IPC_CHANNELS.AUDIO_SET_DEVICE, handler);
273
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.AUDIO_SET_DEVICE, handler);
274
+ },
275
+
276
+ /**
277
+ * Send audio chunk to main for transcription
278
+ */
279
+ sendAudioChunk: (data: {
280
+ timestamp: number;
281
+ duration: number;
282
+ samples?: number[];
283
+ encodedChunk?: Uint8Array;
284
+ mimeType?: string;
285
+ audioLevel?: number;
286
+ rms?: number;
287
+ }): void => {
288
+ ipcRenderer.send(IPC_CHANNELS.AUDIO_CHUNK, data);
289
+ },
290
+
291
+ /**
292
+ * Notify main that capture started
293
+ */
294
+ notifyCaptureStarted: (): void => {
295
+ ipcRenderer.send(IPC_CHANNELS.AUDIO_CAPTURE_STARTED);
296
+ },
297
+
298
+ /**
299
+ * Notify main that capture stopped
300
+ */
301
+ notifyCaptureStopped: (): void => {
302
+ ipcRenderer.send(IPC_CHANNELS.AUDIO_CAPTURE_STOPPED);
303
+ },
304
+
305
+ /**
306
+ * Send capture error to main
307
+ */
308
+ sendCaptureError: (error: string): void => {
309
+ ipcRenderer.send(IPC_CHANNELS.AUDIO_CAPTURE_ERROR, error);
310
+ },
311
+ },
312
+
313
+ // ===========================================================================
314
+ // Screen Recording API
315
+ // ===========================================================================
316
+ screenRecording: {
317
+ /**
318
+ * Start persisted screen recording for a session
319
+ */
320
+ start: (
321
+ sessionId: string,
322
+ mimeType: string,
323
+ startTime?: number
324
+ ): Promise<{ success: boolean; path?: string; error?: string }> => {
325
+ return ipcRenderer.invoke(IPC_CHANNELS.SCREEN_RECORDING_START, sessionId, mimeType, startTime);
326
+ },
327
+
328
+ /**
329
+ * Append a video chunk to the active recording file
330
+ */
331
+ appendChunk: (
332
+ sessionId: string,
333
+ chunk: Uint8Array
334
+ ): Promise<{ success: boolean; error?: string }> => {
335
+ return ipcRenderer.invoke(IPC_CHANNELS.SCREEN_RECORDING_CHUNK, sessionId, chunk);
336
+ },
337
+
338
+ /**
339
+ * Finalize persisted recording for a session
340
+ */
341
+ stop: (
342
+ sessionId: string
343
+ ): Promise<{ success: boolean; path?: string; bytes?: number; mimeType?: string; error?: string }> => {
344
+ return ipcRenderer.invoke(IPC_CHANNELS.SCREEN_RECORDING_STOP, sessionId);
345
+ },
346
+ },
347
+
348
+ // ===========================================================================
349
+ // Transcription API
350
+ // ===========================================================================
351
+ transcript: {
352
+ /**
353
+ * Subscribe to transcription chunks (interim and final)
354
+ */
355
+ onChunk: createEventSubscriber<TranscriptChunkPayload>(IPC_CHANNELS.TRANSCRIPTION_UPDATE),
356
+
357
+ /**
358
+ * Subscribe to final transcription results
359
+ */
360
+ onFinal: createEventSubscriber<{
361
+ text: string;
362
+ confidence: number;
363
+ timestamp: number;
364
+ }>(IPC_CHANNELS.TRANSCRIPTION_FINAL),
365
+ },
366
+
367
+ // ===========================================================================
368
+ // Processing Pipeline API (Main -> Renderer events)
369
+ // ===========================================================================
370
+ processing: {
371
+ /**
372
+ * Subscribe to post-process pipeline progress updates
373
+ */
374
+ onProgress: createEventSubscriber<ProcessingProgressPayload>(IPC_CHANNELS.PROCESSING_PROGRESS),
375
+
376
+ /**
377
+ * Subscribe to post-process pipeline completion
378
+ */
379
+ onComplete: createEventSubscriber<OutputReadyPayload>(IPC_CHANNELS.PROCESSING_COMPLETE),
380
+ },
381
+
382
+ // ===========================================================================
383
+ // Transcription Control API
384
+ // ===========================================================================
385
+ transcription: {
386
+ /**
387
+ * Get runtime availability for all transcription tiers.
388
+ */
389
+ getTierStatuses: (): Promise<TranscriptionTierStatus[]> => {
390
+ return ipcRenderer.invoke(IPC_CHANNELS.TRANSCRIPTION_GET_TIER_STATUSES);
391
+ },
392
+
393
+ /**
394
+ * Get current preferred/active transcription tier.
395
+ */
396
+ getCurrentTier: (): Promise<TranscriptionTier | null> => {
397
+ return ipcRenderer.invoke(IPC_CHANNELS.TRANSCRIPTION_GET_CURRENT_TIER);
398
+ },
399
+
400
+ /**
401
+ * Set preferred transcription tier.
402
+ */
403
+ setTier: (tier: TranscriptionTier): Promise<{ success: boolean; error?: string }> => {
404
+ return ipcRenderer.invoke(IPC_CHANNELS.TRANSCRIPTION_SET_TIER, tier);
405
+ },
406
+
407
+ /**
408
+ * Download a specific Whisper model via transcription controls.
409
+ */
410
+ downloadModel: (model: string): Promise<{ success: boolean; error?: string }> => {
411
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_DOWNLOAD_MODEL, model);
412
+ },
413
+
414
+ /**
415
+ * Cancel Whisper model download via transcription controls.
416
+ */
417
+ cancelDownload: (model: string): Promise<{ success: boolean }> => {
418
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_CANCEL_DOWNLOAD, model);
419
+ },
420
+
421
+ /**
422
+ * Subscribe to Whisper model progress updates.
423
+ */
424
+ onModelProgress: createEventSubscriber<WhisperDownloadProgressPayload>(
425
+ IPC_CHANNELS.WHISPER_DOWNLOAD_PROGRESS
426
+ ),
427
+ },
428
+
429
+ // ===========================================================================
430
+ // Settings API
431
+ // ===========================================================================
432
+ settings: {
433
+ /**
434
+ * Get a specific setting
435
+ */
436
+ get: <K extends keyof AppSettings>(key: K): Promise<AppSettings[K]> => {
437
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET, key);
438
+ },
439
+
440
+ /**
441
+ * Get all settings
442
+ */
443
+ getAll: (): Promise<AppSettings> => {
444
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET_ALL);
445
+ },
446
+
447
+ /**
448
+ * Set a specific setting
449
+ */
450
+ set: <K extends keyof AppSettings>(key: K, value: AppSettings[K]): Promise<AppSettings> => {
451
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SET, key, value);
452
+ },
453
+
454
+ /**
455
+ * Get an API key from secure storage
456
+ */
457
+ getApiKey: (service: string): Promise<string | null> => {
458
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_GET_API_KEY, service);
459
+ },
460
+
461
+ /**
462
+ * Set an API key in secure storage
463
+ */
464
+ setApiKey: (service: string, key: string): Promise<boolean> => {
465
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SET_API_KEY, service, key);
466
+ },
467
+
468
+ /**
469
+ * Delete an API key from secure storage
470
+ */
471
+ deleteApiKey: (service: string): Promise<boolean> => {
472
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_DELETE_API_KEY, service);
473
+ },
474
+
475
+ /**
476
+ * Check if an API key exists in secure storage
477
+ */
478
+ hasApiKey: (service: string): Promise<boolean> => {
479
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_HAS_API_KEY, service);
480
+ },
481
+
482
+ /**
483
+ * Validate an API key by performing a provider request from main process.
484
+ */
485
+ testApiKey: (
486
+ service: 'openai' | 'anthropic',
487
+ key: string
488
+ ): Promise<ApiKeyValidationResult> => {
489
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_TEST_API_KEY, service, key);
490
+ },
491
+
492
+ /**
493
+ * Open native directory picker for output path selection
494
+ */
495
+ selectDirectory: (): Promise<string | null> => {
496
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SELECT_DIRECTORY);
497
+ },
498
+
499
+ /**
500
+ * Clear app data and reset settings
501
+ */
502
+ clearAllData: (): Promise<void> => {
503
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_CLEAR_ALL_DATA);
504
+ },
505
+
506
+ /**
507
+ * Export settings to a JSON file
508
+ */
509
+ export: (): Promise<void> => {
510
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_EXPORT);
511
+ },
512
+
513
+ /**
514
+ * Import settings from a JSON file
515
+ */
516
+ import: (): Promise<AppSettings | null> => {
517
+ return ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_IMPORT);
518
+ },
519
+ },
520
+
521
+ // ===========================================================================
522
+ // Hotkey API
523
+ // ===========================================================================
524
+ hotkeys: {
525
+ /**
526
+ * Get current hotkey configuration
527
+ */
528
+ getConfig: (): Promise<HotkeyConfig> => {
529
+ return ipcRenderer.invoke(IPC_CHANNELS.HOTKEY_CONFIG);
530
+ },
531
+
532
+ /**
533
+ * Update hotkey configuration
534
+ */
535
+ updateConfig: (
536
+ config: Partial<HotkeyConfig>
537
+ ): Promise<{ config: HotkeyConfig; results: unknown[] }> => {
538
+ return ipcRenderer.invoke(IPC_CHANNELS.HOTKEY_UPDATE, config);
539
+ },
540
+
541
+ /**
542
+ * Subscribe to hotkey triggered events
543
+ */
544
+ onTriggered: createEventSubscriber<{ action: string; accelerator: string }>(
545
+ IPC_CHANNELS.HOTKEY_TRIGGERED
546
+ ),
547
+ },
548
+
549
+ // ===========================================================================
550
+ // Permissions API
551
+ // ===========================================================================
552
+ permissions: {
553
+ /**
554
+ * Check if a permission is granted
555
+ */
556
+ check: (type: PermissionType): Promise<boolean> => {
557
+ return ipcRenderer.invoke(IPC_CHANNELS.PERMISSIONS_CHECK, type);
558
+ },
559
+
560
+ /**
561
+ * Request a permission
562
+ */
563
+ request: (type: PermissionType): Promise<boolean> => {
564
+ return ipcRenderer.invoke(IPC_CHANNELS.PERMISSIONS_REQUEST, type);
565
+ },
566
+
567
+ /**
568
+ * Get all permission statuses
569
+ */
570
+ getAll: (): Promise<PermissionStatus> => {
571
+ return ipcRenderer.invoke(IPC_CHANNELS.PERMISSIONS_GET_ALL);
572
+ },
573
+ },
574
+
575
+ // ===========================================================================
576
+ // Output API
577
+ // ===========================================================================
578
+ output: {
579
+ /**
580
+ * Save the current session to disk
581
+ */
582
+ save: (session?: SessionPayload): Promise<SaveResult> => {
583
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_SAVE, session);
584
+ },
585
+
586
+ /**
587
+ * Copy session summary to clipboard
588
+ */
589
+ copyClipboard: (): Promise<boolean> => {
590
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_COPY_CLIPBOARD);
591
+ },
592
+
593
+ /**
594
+ * Open the session output folder in file explorer
595
+ */
596
+ openFolder: (sessionDir?: string): Promise<{ success: boolean; error?: string }> => {
597
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_OPEN_FOLDER, sessionDir);
598
+ },
599
+
600
+ /**
601
+ * Subscribe to output ready events
602
+ */
603
+ onReady: createEventSubscriber<OutputReadyPayload>(
604
+ IPC_CHANNELS.OUTPUT_READY
605
+ ),
606
+
607
+ /**
608
+ * Subscribe to output error events
609
+ */
610
+ onError: createEventSubscriber<{ message: string }>(IPC_CHANNELS.OUTPUT_ERROR),
611
+
612
+ // -------------------------------------------------------------------------
613
+ // Session History Browser API
614
+ // -------------------------------------------------------------------------
615
+
616
+ /**
617
+ * List all saved sessions
618
+ */
619
+ listSessions: (): Promise<Array<{
620
+ id: string;
621
+ startTime: number;
622
+ endTime: number;
623
+ itemCount: number;
624
+ screenshotCount: number;
625
+ sourceName: string;
626
+ firstThumbnail?: string;
627
+ folder: string;
628
+ transcriptionPreview?: string;
629
+ }>> => {
630
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_LIST_SESSIONS);
631
+ },
632
+
633
+ /**
634
+ * Get metadata for a specific session
635
+ */
636
+ getSessionMetadata: (sessionId: string): Promise<{
637
+ id: string;
638
+ startTime: number;
639
+ endTime: number;
640
+ itemCount: number;
641
+ screenshotCount: number;
642
+ sourceName: string;
643
+ firstThumbnail?: string;
644
+ folder: string;
645
+ transcriptionPreview?: string;
646
+ } | null> => {
647
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_GET_SESSION_METADATA, sessionId);
648
+ },
649
+
650
+ /**
651
+ * Delete a single session
652
+ */
653
+ deleteSession: (sessionId: string): Promise<{ success: boolean; error?: string }> => {
654
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_DELETE_SESSION, sessionId);
655
+ },
656
+
657
+ /**
658
+ * Delete multiple sessions
659
+ */
660
+ deleteSessions: (sessionIds: string[]): Promise<{ success: boolean; deleted: string[]; failed: string[] }> => {
661
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_DELETE_SESSIONS, sessionIds);
662
+ },
663
+
664
+ /**
665
+ * Export a single session
666
+ */
667
+ exportSession: (
668
+ sessionId: string,
669
+ format: 'markdown' | 'json' | 'pdf' = 'markdown'
670
+ ): Promise<{ success: boolean; path?: string; error?: string }> => {
671
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_EXPORT_SESSION, sessionId, format);
672
+ },
673
+
674
+ /**
675
+ * Export multiple sessions
676
+ */
677
+ exportSessions: (
678
+ sessionIds: string[],
679
+ format: 'markdown' | 'json' | 'pdf' = 'markdown'
680
+ ): Promise<{ success: boolean; path?: string; error?: string }> => {
681
+ return ipcRenderer.invoke(IPC_CHANNELS.OUTPUT_EXPORT_SESSIONS, sessionIds, format);
682
+ },
683
+ },
684
+
685
+ // ===========================================================================
686
+ // Crash Recovery API
687
+ // ===========================================================================
688
+ crashRecovery: {
689
+ /**
690
+ * Check for incomplete sessions from crashes
691
+ */
692
+ check: (): Promise<{
693
+ hasIncomplete: boolean;
694
+ session: {
695
+ id: string;
696
+ startTime: number;
697
+ lastSaveTime: number;
698
+ feedbackItems: Array<{
699
+ id: string;
700
+ timestamp: number;
701
+ text: string;
702
+ confidence: number;
703
+ hasScreenshot: boolean;
704
+ screenshotId?: string;
705
+ }>;
706
+ sourceName: string;
707
+ screenshotCount: number;
708
+ metadata?: {
709
+ appVersion: string;
710
+ platform: string;
711
+ sessionDurationMs: number;
712
+ };
713
+ } | null;
714
+ }> => {
715
+ return ipcRenderer.invoke(IPC_CHANNELS.CRASH_RECOVERY_CHECK);
716
+ },
717
+
718
+ /**
719
+ * Recover an incomplete session
720
+ */
721
+ recover: (
722
+ sessionId: string
723
+ ): Promise<{
724
+ success: boolean;
725
+ session?: {
726
+ id: string;
727
+ feedbackItems: Array<{
728
+ id: string;
729
+ timestamp: number;
730
+ text: string;
731
+ confidence: number;
732
+ hasScreenshot: boolean;
733
+ }>;
734
+ };
735
+ error?: string;
736
+ }> => {
737
+ return ipcRenderer.invoke(IPC_CHANNELS.CRASH_RECOVERY_RECOVER, sessionId);
738
+ },
739
+
740
+ /**
741
+ * Discard an incomplete session
742
+ */
743
+ discard: (): Promise<{ success: boolean }> => {
744
+ return ipcRenderer.invoke(IPC_CHANNELS.CRASH_RECOVERY_DISCARD);
745
+ },
746
+
747
+ /**
748
+ * Get recent crash logs for debugging
749
+ */
750
+ getLogs: (
751
+ limit?: number
752
+ ): Promise<
753
+ Array<{
754
+ timestamp: string;
755
+ error: { name: string; message: string; stack?: string };
756
+ appVersion: string;
757
+ platform: string;
758
+ sessionId?: string;
759
+ }>
760
+ > => {
761
+ return ipcRenderer.invoke(IPC_CHANNELS.CRASH_RECOVERY_GET_LOGS, limit);
762
+ },
763
+
764
+ /**
765
+ * Clear crash logs
766
+ */
767
+ clearLogs: (): Promise<{ success: boolean }> => {
768
+ return ipcRenderer.invoke(IPC_CHANNELS.CRASH_RECOVERY_CLEAR_LOGS);
769
+ },
770
+
771
+ /**
772
+ * Update crash recovery settings
773
+ */
774
+ updateSettings: (settings: {
775
+ enableAutoSave?: boolean;
776
+ autoSaveIntervalMs?: number;
777
+ enableCrashReporting?: boolean;
778
+ maxCrashLogs?: number;
779
+ }): Promise<{ success: boolean }> => {
780
+ return ipcRenderer.invoke(IPC_CHANNELS.CRASH_RECOVERY_UPDATE_SETTINGS, settings);
781
+ },
782
+
783
+ /**
784
+ * Subscribe to incomplete session found events (on startup)
785
+ */
786
+ onIncompleteFound: createEventSubscriber<{
787
+ session: {
788
+ id: string;
789
+ startTime: number;
790
+ lastSaveTime: number;
791
+ feedbackItems: Array<{
792
+ id: string;
793
+ timestamp: number;
794
+ text: string;
795
+ confidence: number;
796
+ hasScreenshot: boolean;
797
+ }>;
798
+ sourceName: string;
799
+ screenshotCount: number;
800
+ };
801
+ }>(IPC_CHANNELS.CRASH_RECOVERY_FOUND),
802
+ },
803
+
804
+ // ===========================================================================
805
+ // Updates API
806
+ // ===========================================================================
807
+ updates: {
808
+ /**
809
+ * Check for available updates
810
+ */
811
+ check: (): Promise<unknown> => {
812
+ return ipcRenderer.invoke(IPC_CHANNELS.UPDATE_CHECK);
813
+ },
814
+
815
+ /**
816
+ * Download the available update
817
+ */
818
+ download: (): Promise<void> => {
819
+ return ipcRenderer.invoke(IPC_CHANNELS.UPDATE_DOWNLOAD);
820
+ },
821
+
822
+ /**
823
+ * Install the downloaded update (quits and restarts app)
824
+ */
825
+ install: (): Promise<void> => {
826
+ return ipcRenderer.invoke(IPC_CHANNELS.UPDATE_INSTALL);
827
+ },
828
+
829
+ /**
830
+ * Subscribe to update status changes
831
+ */
832
+ onStatus: (callback: (status: UpdateStatusPayload) => void): Unsubscribe => {
833
+ const handler = (_: Electron.IpcRendererEvent, status: UpdateStatusPayload) => callback(status);
834
+ ipcRenderer.on(IPC_CHANNELS.UPDATE_STATUS, handler);
835
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.UPDATE_STATUS, handler);
836
+ },
837
+ },
838
+
839
+ // ===========================================================================
840
+ // Whisper Model API
841
+ // ===========================================================================
842
+ whisper: {
843
+ /**
844
+ * Check if any Whisper model is downloaded and get recommended model
845
+ */
846
+ checkModel: (): Promise<WhisperModelCheckResult> => {
847
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_CHECK_MODEL);
848
+ },
849
+
850
+ /**
851
+ * Check if we have any tier that can actually transcribe
852
+ * (OpenAI key or Whisper with model)
853
+ */
854
+ hasTranscriptionCapability: (): Promise<boolean> => {
855
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_HAS_TRANSCRIPTION_CAPABILITY);
856
+ },
857
+
858
+ /**
859
+ * Get available models with their info
860
+ */
861
+ getAvailableModels: (): Promise<WhisperModelInfoPayload[]> => {
862
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_GET_AVAILABLE_MODELS);
863
+ },
864
+
865
+ /**
866
+ * Download a specific Whisper model
867
+ * @param model - Model name: 'tiny', 'base', 'small', 'medium', or 'large'
868
+ */
869
+ downloadModel: (model: string): Promise<{ success: boolean; error?: string }> => {
870
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_DOWNLOAD_MODEL, model);
871
+ },
872
+
873
+ /**
874
+ * Cancel an active download
875
+ * @param model - Model name to cancel
876
+ */
877
+ cancelDownload: (model: string): Promise<{ success: boolean }> => {
878
+ return ipcRenderer.invoke(IPC_CHANNELS.WHISPER_CANCEL_DOWNLOAD, model);
879
+ },
880
+
881
+ /**
882
+ * Subscribe to download progress events
883
+ */
884
+ onDownloadProgress: createEventSubscriber<WhisperDownloadProgressPayload>(
885
+ IPC_CHANNELS.WHISPER_DOWNLOAD_PROGRESS
886
+ ),
887
+
888
+ /**
889
+ * Subscribe to download complete events
890
+ */
891
+ onDownloadComplete: createEventSubscriber<{ model: string; path: string }>(
892
+ IPC_CHANNELS.WHISPER_DOWNLOAD_COMPLETE
893
+ ),
894
+
895
+ /**
896
+ * Subscribe to download error events
897
+ */
898
+ onDownloadError: createEventSubscriber<{ model: string; error: string }>(
899
+ IPC_CHANNELS.WHISPER_DOWNLOAD_ERROR
900
+ ),
901
+ },
902
+
903
+ // ===========================================================================
904
+ // App Version
905
+ // ===========================================================================
906
+ version: (): Promise<string> => {
907
+ return ipcRenderer.invoke(IPC_CHANNELS.APP_VERSION);
908
+ },
909
+
910
+ // ===========================================================================
911
+ // Legacy API (for backwards compatibility)
912
+ // ===========================================================================
913
+ startSession: (): Promise<{ success: boolean; sessionId?: string }> => {
914
+ return ipcRenderer.invoke(IPC_CHANNELS.START_SESSION);
915
+ },
916
+
917
+ stopSession: (): Promise<{ success: boolean }> => {
918
+ return ipcRenderer.invoke(IPC_CHANNELS.STOP_SESSION);
919
+ },
920
+
921
+ getSettings: (): Promise<AppSettings> => {
922
+ return ipcRenderer.invoke(IPC_CHANNELS.GET_SETTINGS);
923
+ },
924
+
925
+ setSettings: (settings: Partial<AppSettings>): Promise<AppSettings> => {
926
+ return ipcRenderer.invoke(IPC_CHANNELS.SET_SETTINGS, settings);
927
+ },
928
+
929
+ copyToClipboard: (text: string): Promise<{ success: boolean }> => {
930
+ return ipcRenderer.invoke(IPC_CHANNELS.COPY_TO_CLIPBOARD, text);
931
+ },
932
+
933
+ // Window controls
934
+ window: {
935
+ minimize: (): Promise<{ success: boolean }> => {
936
+ return ipcRenderer.invoke(IPC_CHANNELS.WINDOW_MINIMIZE);
937
+ },
938
+ hide: (): Promise<{ success: boolean }> => {
939
+ return ipcRenderer.invoke(IPC_CHANNELS.WINDOW_HIDE);
940
+ },
941
+ close: (): Promise<{ success: boolean }> => {
942
+ return ipcRenderer.invoke(IPC_CHANNELS.WINDOW_CLOSE);
943
+ },
944
+ },
945
+
946
+ // Popover controls
947
+ popover: {
948
+ resize: (width: number, height: number): Promise<{ success: boolean }> => {
949
+ return ipcRenderer.invoke(IPC_CHANNELS.POPOVER_RESIZE, width, height);
950
+ },
951
+ resizeToState: (state: string): Promise<{ success: boolean }> => {
952
+ return ipcRenderer.invoke(IPC_CHANNELS.POPOVER_RESIZE_TO_STATE, state);
953
+ },
954
+ },
955
+
956
+ // ===========================================================================
957
+ // Navigation API (Main -> Renderer navigation events)
958
+ // ===========================================================================
959
+ navigation: {
960
+ onShowSettings: (callback: () => void): Unsubscribe => {
961
+ const handler = () => callback();
962
+ ipcRenderer.on(IPC_CHANNELS.SHOW_SETTINGS, handler);
963
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SHOW_SETTINGS, handler);
964
+ },
965
+ onShowHistory: (callback: () => void): Unsubscribe => {
966
+ const handler = () => callback();
967
+ ipcRenderer.on(IPC_CHANNELS.SHOW_HISTORY, handler);
968
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SHOW_HISTORY, handler);
969
+ },
970
+ onShowShortcuts: (callback: () => void): Unsubscribe => {
971
+ const handler = () => callback();
972
+ ipcRenderer.on(IPC_CHANNELS.SHOW_SHORTCUTS, handler);
973
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SHOW_SHORTCUTS, handler);
974
+ },
975
+ onShowOnboarding: (callback: () => void): Unsubscribe => {
976
+ const handler = () => callback();
977
+ ipcRenderer.on(IPC_CHANNELS.SHOW_ONBOARDING, handler);
978
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SHOW_ONBOARDING, handler);
979
+ },
980
+ onShowExport: (callback: () => void): Unsubscribe => {
981
+ const handler = () => callback();
982
+ ipcRenderer.on(IPC_CHANNELS.SHOW_EXPORT, handler);
983
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SHOW_EXPORT, handler);
984
+ },
985
+ onShowWindowSelector: (callback: () => void): Unsubscribe => {
986
+ const handler = () => callback();
987
+ ipcRenderer.on(IPC_CHANNELS.SHOW_WINDOW_SELECTOR, handler);
988
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SHOW_WINDOW_SELECTOR, handler);
989
+ },
990
+ },
991
+
992
+ onSessionStatus: (
993
+ callback: (status: { action: string; status?: SessionStatusPayload }) => void
994
+ ): Unsubscribe => {
995
+ const handler = (
996
+ _: Electron.IpcRendererEvent,
997
+ data: { action: string; status?: SessionStatusPayload }
998
+ ) => callback(data);
999
+ ipcRenderer.on(IPC_CHANNELS.SESSION_STATUS, handler);
1000
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SESSION_STATUS, handler);
1001
+ },
1002
+
1003
+ onTranscriptionUpdate: (callback: (data: { text: string; isFinal: boolean }) => void): Unsubscribe => {
1004
+ const handler = (_: Electron.IpcRendererEvent, data: { text: string; isFinal: boolean }) =>
1005
+ callback(data);
1006
+ ipcRenderer.on(IPC_CHANNELS.TRANSCRIPTION_UPDATE, handler);
1007
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.TRANSCRIPTION_UPDATE, handler);
1008
+ },
1009
+
1010
+ onScreenshotCaptured: (callback: (data: { id: string; timestamp: number }) => void): Unsubscribe => {
1011
+ const handler = (_: Electron.IpcRendererEvent, data: { id: string; timestamp: number }) =>
1012
+ callback(data);
1013
+ ipcRenderer.on(IPC_CHANNELS.SCREENSHOT_CAPTURED, handler);
1014
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.SCREENSHOT_CAPTURED, handler);
1015
+ },
1016
+
1017
+ onOutputReady: (callback: (data: OutputReadyPayload) => void): Unsubscribe => {
1018
+ const handler = (_: Electron.IpcRendererEvent, data: OutputReadyPayload) =>
1019
+ callback(data);
1020
+ ipcRenderer.on(IPC_CHANNELS.OUTPUT_READY, handler);
1021
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.OUTPUT_READY, handler);
1022
+ },
1023
+
1024
+ onOutputError: (callback: (error: { message: string }) => void): Unsubscribe => {
1025
+ const handler = (_: Electron.IpcRendererEvent, error: { message: string }) => callback(error);
1026
+ ipcRenderer.on(IPC_CHANNELS.OUTPUT_ERROR, handler);
1027
+ return () => ipcRenderer.removeListener(IPC_CHANNELS.OUTPUT_ERROR, handler);
1028
+ },
1029
+ };
1030
+
1031
+ // =============================================================================
1032
+ // Expose API to Renderer
1033
+ // =============================================================================
1034
+
1035
+ contextBridge.exposeInMainWorld('markupr', markuprApi);
1036
+
1037
+ // =============================================================================
1038
+ // Type Exports
1039
+ // =============================================================================
1040
+
1041
+ export type MarkuprAPI = typeof markuprApi;
1042
+
1043
+ declare global {
1044
+ interface Window {
1045
+ markupr: MarkuprAPI;
1046
+ }
1047
+ }