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,529 @@
1
+ /**
2
+ * MenuManager - Native macOS Menu Bar Integration
3
+ *
4
+ * Creates a polished, native-feeling menu bar experience for markupr:
5
+ * - Standard macOS menus (File, Edit, View, Window, Help)
6
+ * - Proper keyboard shortcuts matching Apple HIG
7
+ * - Recent Sessions submenu with quick access
8
+ * - Dynamic menu updates based on app state
9
+ * - Theme switching via View menu
10
+ * - Auto-updater integration via Help menu
11
+ *
12
+ * This module follows the singleton pattern used throughout markupr.
13
+ */
14
+
15
+ import { Menu, app, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron';
16
+ import { sessionController } from './SessionController';
17
+ import { settingsManager } from './settings/SettingsManager';
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Recent session entry for the File > Recent Sessions submenu
25
+ */
26
+ export interface RecentSession {
27
+ id: string;
28
+ name: string;
29
+ path: string;
30
+ date: Date;
31
+ }
32
+
33
+ /**
34
+ * Menu action that can be triggered from menus
35
+ */
36
+ export type MenuAction =
37
+ | 'toggle-recording'
38
+ | 'show-settings'
39
+ | 'show-history'
40
+ | 'show-export'
41
+ | 'show-shortcuts'
42
+ | 'check-updates'
43
+ | 'open-session'
44
+ | 'open-session-path';
45
+
46
+ /**
47
+ * Callback for menu actions
48
+ */
49
+ type MenuActionCallback = (action: MenuAction, data?: unknown) => void;
50
+
51
+ // =============================================================================
52
+ // MenuManager Class
53
+ // =============================================================================
54
+
55
+ export class MenuManager {
56
+ private mainWindow: BrowserWindow | null = null;
57
+ private recentSessions: RecentSession[] = [];
58
+ private actionCallback: MenuActionCallback | null = null;
59
+ private stateChangeCleanup: (() => void) | null = null;
60
+
61
+ /**
62
+ * Initialize the menu manager with the main window
63
+ * Sets up menus and subscribes to state changes for dynamic updates
64
+ */
65
+ initialize(window: BrowserWindow): void {
66
+ this.mainWindow = window;
67
+ this.buildMenu();
68
+
69
+ // Subscribe to session state changes to update menu dynamically
70
+ // SessionController uses event callbacks, not an observable pattern
71
+ // We'll rebuild the menu when the renderer notifies us of state changes
72
+ console.log('[MenuManager] Initialized with main window');
73
+ }
74
+
75
+ /**
76
+ * Set callback for menu actions
77
+ * The main process can use this to handle menu interactions
78
+ */
79
+ onAction(callback: MenuActionCallback): () => void {
80
+ this.actionCallback = callback;
81
+ return () => {
82
+ this.actionCallback = null;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Update the recent sessions list and rebuild menu
88
+ * @param sessions - Array of recent sessions (max 10 will be kept)
89
+ */
90
+ setRecentSessions(sessions: RecentSession[]): void {
91
+ this.recentSessions = sessions.slice(0, 10);
92
+ this.buildMenu();
93
+ }
94
+
95
+ /**
96
+ * Add a single session to recent sessions
97
+ * @param session - Session to add to the front of the list
98
+ */
99
+ addRecentSession(session: RecentSession): void {
100
+ // Remove if already exists (by id or path)
101
+ this.recentSessions = this.recentSessions.filter(
102
+ (s) => s.id !== session.id && s.path !== session.path
103
+ );
104
+ // Add to front
105
+ this.recentSessions.unshift(session);
106
+ // Limit to 10
107
+ if (this.recentSessions.length > 10) {
108
+ this.recentSessions.splice(10);
109
+ }
110
+ this.buildMenu();
111
+ }
112
+
113
+ /**
114
+ * Trigger a menu rebuild (call when app state changes)
115
+ */
116
+ refresh(): void {
117
+ this.buildMenu();
118
+ }
119
+
120
+ /**
121
+ * Build the complete application menu
122
+ */
123
+ private buildMenu(): void {
124
+ const isRecording = sessionController.getStatus().state === 'recording';
125
+ const isMac = process.platform === 'darwin';
126
+
127
+ const template: MenuItemConstructorOptions[] = [
128
+ // =========================================================================
129
+ // App Menu (macOS only)
130
+ // =========================================================================
131
+ ...(isMac
132
+ ? [
133
+ {
134
+ label: app.name,
135
+ submenu: [
136
+ { role: 'about' as const },
137
+ { type: 'separator' as const },
138
+ {
139
+ label: 'Preferences...',
140
+ accelerator: 'Cmd+,',
141
+ click: () => this.handleAction('show-settings'),
142
+ },
143
+ { type: 'separator' as const },
144
+ { role: 'services' as const },
145
+ { type: 'separator' as const },
146
+ { role: 'hide' as const },
147
+ { role: 'hideOthers' as const },
148
+ { role: 'unhide' as const },
149
+ { type: 'separator' as const },
150
+ { role: 'quit' as const },
151
+ ],
152
+ } as MenuItemConstructorOptions,
153
+ ]
154
+ : []),
155
+
156
+ // =========================================================================
157
+ // File Menu
158
+ // =========================================================================
159
+ {
160
+ label: 'File',
161
+ submenu: [
162
+ {
163
+ label: isRecording ? 'Stop Recording' : 'New Recording',
164
+ accelerator: 'CmdOrCtrl+Shift+F',
165
+ click: () => this.handleAction('toggle-recording'),
166
+ },
167
+ { type: 'separator' },
168
+ {
169
+ label: 'Open Session...',
170
+ accelerator: 'CmdOrCtrl+O',
171
+ click: () => this.handleAction('open-session'),
172
+ },
173
+ {
174
+ label: 'Recent Sessions',
175
+ submenu: this.buildRecentSessionsMenu(),
176
+ },
177
+ { type: 'separator' },
178
+ {
179
+ label: 'Session History',
180
+ accelerator: 'CmdOrCtrl+H',
181
+ click: () => this.handleAction('show-history'),
182
+ },
183
+ { type: 'separator' },
184
+ {
185
+ label: 'Export...',
186
+ accelerator: 'CmdOrCtrl+E',
187
+ enabled: !isRecording,
188
+ click: () => this.handleAction('show-export'),
189
+ },
190
+ { type: 'separator' },
191
+ ...(isMac ? [] : [{ role: 'quit' as const }]),
192
+ ],
193
+ },
194
+
195
+ // =========================================================================
196
+ // Edit Menu
197
+ // =========================================================================
198
+ {
199
+ label: 'Edit',
200
+ submenu: [
201
+ { role: 'undo' },
202
+ { role: 'redo' },
203
+ { type: 'separator' },
204
+ { role: 'cut' },
205
+ { role: 'copy' },
206
+ { role: 'paste' },
207
+ ...(isMac
208
+ ? [
209
+ { role: 'pasteAndMatchStyle' as const },
210
+ { role: 'delete' as const },
211
+ { role: 'selectAll' as const },
212
+ { type: 'separator' as const },
213
+ {
214
+ label: 'Speech',
215
+ submenu: [
216
+ { role: 'startSpeaking' as const },
217
+ { role: 'stopSpeaking' as const },
218
+ ],
219
+ },
220
+ ]
221
+ : [
222
+ { role: 'delete' as const },
223
+ { type: 'separator' as const },
224
+ { role: 'selectAll' as const },
225
+ ]),
226
+ ],
227
+ },
228
+
229
+ // =========================================================================
230
+ // View Menu
231
+ // =========================================================================
232
+ {
233
+ label: 'View',
234
+ submenu: [
235
+ {
236
+ label: 'Toggle Transcription Preview',
237
+ accelerator: 'CmdOrCtrl+T',
238
+ type: 'checkbox',
239
+ checked: this.getSetting('showTranscriptionPreview'),
240
+ click: () => this.toggleSetting('showTranscriptionPreview'),
241
+ },
242
+ {
243
+ label: 'Toggle Audio Waveform',
244
+ type: 'checkbox',
245
+ checked: this.getSetting('showAudioWaveform'),
246
+ click: () => this.toggleSetting('showAudioWaveform'),
247
+ },
248
+ { type: 'separator' },
249
+ {
250
+ label: 'Theme',
251
+ submenu: [
252
+ {
253
+ label: 'Dark',
254
+ type: 'radio',
255
+ checked: this.getSetting('theme') === 'dark',
256
+ click: () => this.setSetting('theme', 'dark'),
257
+ },
258
+ {
259
+ label: 'Light',
260
+ type: 'radio',
261
+ checked: this.getSetting('theme') === 'light',
262
+ click: () => this.setSetting('theme', 'light'),
263
+ },
264
+ {
265
+ label: 'System',
266
+ type: 'radio',
267
+ checked: this.getSetting('theme') === 'system',
268
+ click: () => this.setSetting('theme', 'system'),
269
+ },
270
+ ],
271
+ },
272
+ { type: 'separator' },
273
+ { role: 'reload' },
274
+ { role: 'forceReload' },
275
+ { role: 'toggleDevTools' },
276
+ { type: 'separator' },
277
+ { role: 'resetZoom' },
278
+ { role: 'zoomIn' },
279
+ { role: 'zoomOut' },
280
+ { type: 'separator' },
281
+ { role: 'togglefullscreen' },
282
+ ],
283
+ },
284
+
285
+ // =========================================================================
286
+ // Window Menu
287
+ // =========================================================================
288
+ {
289
+ label: 'Window',
290
+ submenu: [
291
+ { role: 'minimize' },
292
+ { role: 'zoom' },
293
+ ...(isMac
294
+ ? [
295
+ { type: 'separator' as const },
296
+ { role: 'front' as const },
297
+ { type: 'separator' as const },
298
+ { role: 'window' as const },
299
+ ]
300
+ : [{ role: 'close' as const }]),
301
+ ],
302
+ },
303
+
304
+ // =========================================================================
305
+ // Help Menu
306
+ // =========================================================================
307
+ {
308
+ label: 'Help',
309
+ role: 'help',
310
+ submenu: [
311
+ {
312
+ label: 'Keyboard Shortcuts',
313
+ accelerator: 'CmdOrCtrl+/',
314
+ click: () => this.handleAction('show-shortcuts'),
315
+ },
316
+ { type: 'separator' },
317
+ {
318
+ label: 'markupr Documentation',
319
+ click: () =>
320
+ shell.openExternal('https://github.com/eddiesanjuan/markupr#readme'),
321
+ },
322
+ {
323
+ label: 'Release Notes',
324
+ click: () =>
325
+ shell.openExternal(
326
+ 'https://github.com/eddiesanjuan/markupr/releases'
327
+ ),
328
+ },
329
+ { type: 'separator' },
330
+ {
331
+ label: 'Report Issue...',
332
+ click: () =>
333
+ shell.openExternal('https://github.com/eddiesanjuan/markupr/issues'),
334
+ },
335
+ {
336
+ label: 'Feature Request...',
337
+ click: () =>
338
+ shell.openExternal(
339
+ 'https://github.com/eddiesanjuan/markupr/discussions/new?category=ideas'
340
+ ),
341
+ },
342
+ { type: 'separator' },
343
+ {
344
+ label: 'Check for Updates...',
345
+ click: () => this.handleAction('check-updates'),
346
+ },
347
+ { type: 'separator' },
348
+ {
349
+ label: `Version ${app.getVersion()}`,
350
+ enabled: false,
351
+ },
352
+ ],
353
+ },
354
+ ];
355
+
356
+ const menu = Menu.buildFromTemplate(template);
357
+ Menu.setApplicationMenu(menu);
358
+ }
359
+
360
+ /**
361
+ * Build the Recent Sessions submenu items
362
+ */
363
+ private buildRecentSessionsMenu(): MenuItemConstructorOptions[] {
364
+ if (this.recentSessions.length === 0) {
365
+ return [{ label: 'No Recent Sessions', enabled: false }];
366
+ }
367
+
368
+ const sessionItems: MenuItemConstructorOptions[] = this.recentSessions.map(
369
+ (session) => ({
370
+ label: `${session.name} - ${this.formatDate(session.date)}`,
371
+ click: () => this.openSessionByPath(session.path),
372
+ })
373
+ );
374
+
375
+ return [
376
+ ...sessionItems,
377
+ { type: 'separator' as const },
378
+ {
379
+ label: 'Clear Recent',
380
+ click: () => this.clearRecentSessions(),
381
+ },
382
+ ];
383
+ }
384
+
385
+ /**
386
+ * Format a date for display in the menu
387
+ */
388
+ private formatDate(date: Date): string {
389
+ const now = new Date();
390
+ const diff = now.getTime() - date.getTime();
391
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
392
+
393
+ if (days === 0) {
394
+ return date.toLocaleTimeString(undefined, {
395
+ hour: 'numeric',
396
+ minute: '2-digit',
397
+ });
398
+ } else if (days === 1) {
399
+ return 'Yesterday';
400
+ } else if (days < 7) {
401
+ return date.toLocaleDateString(undefined, { weekday: 'long' });
402
+ } else {
403
+ return date.toLocaleDateString(undefined, {
404
+ month: 'short',
405
+ day: 'numeric',
406
+ });
407
+ }
408
+ }
409
+
410
+ // ===========================================================================
411
+ // Settings Helpers
412
+ // ===========================================================================
413
+
414
+ /**
415
+ * Get a setting value with fallback
416
+ */
417
+ private getSetting<K extends keyof import('./settings/SettingsManager').AppSettings>(
418
+ key: K
419
+ ): import('./settings/SettingsManager').AppSettings[K] {
420
+ try {
421
+ return settingsManager.get(key);
422
+ } catch {
423
+ // Return sensible defaults if settings not available
424
+ const defaults: Record<string, unknown> = {
425
+ showTranscriptionPreview: true,
426
+ showAudioWaveform: true,
427
+ theme: 'system',
428
+ };
429
+ return defaults[key] as import('./settings/SettingsManager').AppSettings[K];
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Set a setting and rebuild menu
435
+ */
436
+ private setSetting<K extends keyof import('./settings/SettingsManager').AppSettings>(
437
+ key: K,
438
+ value: import('./settings/SettingsManager').AppSettings[K]
439
+ ): void {
440
+ try {
441
+ settingsManager.set(key, value);
442
+ this.buildMenu();
443
+ // Notify renderer of setting change
444
+ this.sendToRenderer('menu:setting-changed', { key, value });
445
+ } catch (error) {
446
+ console.error(`[MenuManager] Failed to set setting ${key}:`, error);
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Toggle a boolean setting
452
+ */
453
+ private toggleSetting(
454
+ key: 'showTranscriptionPreview' | 'showAudioWaveform'
455
+ ): void {
456
+ const current = this.getSetting(key);
457
+ this.setSetting(key, !current);
458
+ }
459
+
460
+ // ===========================================================================
461
+ // Action Handlers
462
+ // ===========================================================================
463
+
464
+ /**
465
+ * Handle a menu action
466
+ */
467
+ private handleAction(action: MenuAction, data?: unknown): void {
468
+ // Emit to callback if registered
469
+ if (this.actionCallback) {
470
+ this.actionCallback(action, data);
471
+ }
472
+
473
+ // Also send to renderer for UI updates
474
+ this.sendToRenderer(`menu:${action}`, data);
475
+ }
476
+
477
+ /**
478
+ * Open a session by its file path
479
+ */
480
+ private openSessionByPath(path: string): void {
481
+ this.handleAction('open-session-path', { path });
482
+ }
483
+
484
+ /**
485
+ * Clear all recent sessions
486
+ */
487
+ private clearRecentSessions(): void {
488
+ this.recentSessions = [];
489
+ this.buildMenu();
490
+ }
491
+
492
+ // ===========================================================================
493
+ // IPC Communication
494
+ // ===========================================================================
495
+
496
+ /**
497
+ * Send an event to the renderer process
498
+ */
499
+ private sendToRenderer(channel: string, data?: unknown): void {
500
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
501
+ this.mainWindow.webContents.send(channel, data);
502
+ }
503
+ }
504
+
505
+ // ===========================================================================
506
+ // Cleanup
507
+ // ===========================================================================
508
+
509
+ /**
510
+ * Clean up resources
511
+ */
512
+ destroy(): void {
513
+ if (this.stateChangeCleanup) {
514
+ this.stateChangeCleanup();
515
+ this.stateChangeCleanup = null;
516
+ }
517
+ this.mainWindow = null;
518
+ this.actionCallback = null;
519
+ this.recentSessions = [];
520
+ console.log('[MenuManager] Destroyed');
521
+ }
522
+ }
523
+
524
+ // =============================================================================
525
+ // Singleton Export
526
+ // =============================================================================
527
+
528
+ export const menuManager = new MenuManager();
529
+ export default menuManager;