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,272 @@
1
+ /**
2
+ * Capture IPC Handlers
3
+ *
4
+ * Registers IPC handlers for screen capture source enumeration,
5
+ * persisted screen recording (start/chunk/stop), and audio device management.
6
+ */
7
+
8
+ import { ipcMain, desktopCapturer, app } from 'electron';
9
+ import * as fs from 'fs/promises';
10
+ import { join } from 'path';
11
+ import { sessionController } from '../SessionController';
12
+ import {
13
+ IPC_CHANNELS,
14
+ DEFAULT_SETTINGS,
15
+ type CaptureSource,
16
+ type AudioDevice,
17
+ } from '../../shared/types';
18
+ import type { IpcContext } from './types';
19
+
20
+ // =============================================================================
21
+ // Screen Recording State
22
+ // =============================================================================
23
+
24
+ interface RecordingArtifact {
25
+ tempPath: string;
26
+ mimeType: string;
27
+ bytesWritten: number;
28
+ writeChain: Promise<void>;
29
+ lastChunkAt: number;
30
+ startTime?: number;
31
+ }
32
+
33
+ interface FinalizedRecordingArtifact {
34
+ tempPath: string;
35
+ mimeType: string;
36
+ bytesWritten: number;
37
+ startTime?: number;
38
+ }
39
+
40
+ const activeScreenRecordings = new Map<string, RecordingArtifact>();
41
+ const finalizedScreenRecordings = new Map<string, FinalizedRecordingArtifact>();
42
+
43
+ function sleep(ms: number): Promise<void> {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ export function extensionFromMimeType(mimeType?: string): string {
48
+ const normalized = (mimeType || '').toLowerCase();
49
+ if (normalized.includes('mp4')) {
50
+ return '.mp4';
51
+ }
52
+ if (normalized.includes('quicktime') || normalized.includes('mov')) {
53
+ return '.mov';
54
+ }
55
+ return '.webm';
56
+ }
57
+
58
+ export async function finalizeScreenRecording(sessionId: string): Promise<FinalizedRecordingArtifact | null> {
59
+ const active = activeScreenRecordings.get(sessionId);
60
+ if (active) {
61
+ const QUIET_PERIOD_MS = 750;
62
+ const MAX_WAIT_MS = 6000;
63
+ const waitStartedAt = Date.now();
64
+
65
+ while (Date.now() - waitStartedAt < MAX_WAIT_MS) {
66
+ try {
67
+ await active.writeChain;
68
+ } catch (error) {
69
+ console.warn('[Main] Screen recording write chain failed during finalize:', error);
70
+ }
71
+
72
+ const idleMs = Date.now() - active.lastChunkAt;
73
+ if (idleMs >= QUIET_PERIOD_MS) {
74
+ break;
75
+ }
76
+
77
+ await sleep(Math.min(180, QUIET_PERIOD_MS - idleMs));
78
+ }
79
+
80
+ try {
81
+ await active.writeChain;
82
+ } catch (error) {
83
+ console.warn('[Main] Screen recording write chain failed during finalize:', error);
84
+ }
85
+
86
+ activeScreenRecordings.delete(sessionId);
87
+ finalizedScreenRecordings.set(sessionId, {
88
+ tempPath: active.tempPath,
89
+ mimeType: active.mimeType,
90
+ bytesWritten: active.bytesWritten,
91
+ startTime: active.startTime,
92
+ });
93
+ }
94
+
95
+ return finalizedScreenRecordings.get(sessionId) || null;
96
+ }
97
+
98
+ export function getScreenRecordingSnapshot(sessionId: string): FinalizedRecordingArtifact | null {
99
+ const active = activeScreenRecordings.get(sessionId);
100
+ if (active) {
101
+ return {
102
+ tempPath: active.tempPath,
103
+ mimeType: active.mimeType,
104
+ bytesWritten: active.bytesWritten,
105
+ startTime: active.startTime,
106
+ };
107
+ }
108
+
109
+ return finalizedScreenRecordings.get(sessionId) || null;
110
+ }
111
+
112
+ export function deleteFinalizedRecording(sessionId: string): void {
113
+ finalizedScreenRecordings.delete(sessionId);
114
+ }
115
+
116
+ export function getActiveScreenRecordings(): Map<string, RecordingArtifact> {
117
+ return activeScreenRecordings;
118
+ }
119
+
120
+ export function getFinalizedScreenRecordings(): Map<string, FinalizedRecordingArtifact> {
121
+ return finalizedScreenRecordings;
122
+ }
123
+
124
+ // =============================================================================
125
+ // IPC Registration
126
+ // =============================================================================
127
+
128
+ export function registerCaptureHandlers(ctx: IpcContext): void {
129
+ ipcMain.handle(IPC_CHANNELS.CAPTURE_GET_SOURCES, async (): Promise<CaptureSource[]> => {
130
+ try {
131
+ const sources = await desktopCapturer.getSources({
132
+ types: ['screen', 'window'],
133
+ thumbnailSize: { width: 320, height: 180 },
134
+ fetchWindowIcons: true,
135
+ });
136
+
137
+ return sources.map((source) => ({
138
+ id: source.id,
139
+ name: source.name,
140
+ type: source.id.startsWith('screen') ? 'screen' : 'window',
141
+ thumbnail: source.thumbnail.toDataURL(),
142
+ appIcon: source.appIcon?.toDataURL(),
143
+ }));
144
+ } catch (error) {
145
+ console.error('[Main] Failed to get capture sources:', error);
146
+ return [];
147
+ }
148
+ });
149
+
150
+ ipcMain.handle(IPC_CHANNELS.CAPTURE_MANUAL_SCREENSHOT, async () => {
151
+ const cue = sessionController.registerCaptureCue('manual');
152
+ if (!cue) {
153
+ return { success: false, error: 'Manual capture is only available while recording and not paused.' };
154
+ }
155
+ return { success: true };
156
+ });
157
+
158
+ ipcMain.handle(
159
+ IPC_CHANNELS.SCREEN_RECORDING_START,
160
+ async (_, sessionId: string, mimeType: string, startTime?: number): Promise<{ success: boolean; path?: string; error?: string }> => {
161
+ try {
162
+ const currentSession = sessionController.getSession();
163
+ if (!currentSession || currentSession.id !== sessionId) {
164
+ return { success: false, error: 'No matching active session for screen recording.' };
165
+ }
166
+
167
+ const extension = extensionFromMimeType(mimeType);
168
+ const recordingsDir = join(app.getPath('temp'), 'markupr-recordings');
169
+ await fs.mkdir(recordingsDir, { recursive: true });
170
+
171
+ const tempPath = join(recordingsDir, `${sessionId}${extension}`);
172
+ await fs.writeFile(tempPath, Buffer.alloc(0));
173
+
174
+ activeScreenRecordings.set(sessionId, {
175
+ tempPath,
176
+ mimeType: mimeType || 'video/webm',
177
+ bytesWritten: 0,
178
+ writeChain: Promise.resolve(),
179
+ lastChunkAt: Date.now(),
180
+ startTime,
181
+ });
182
+
183
+ return { success: true, path: tempPath };
184
+ } catch (error) {
185
+ return {
186
+ success: false,
187
+ error: error instanceof Error ? error.message : 'Failed to initialize screen recording.',
188
+ };
189
+ }
190
+ }
191
+ );
192
+
193
+ ipcMain.handle(
194
+ IPC_CHANNELS.SCREEN_RECORDING_CHUNK,
195
+ async (
196
+ _,
197
+ sessionId: string,
198
+ chunk: Uint8Array | ArrayBuffer
199
+ ): Promise<{ success: boolean; error?: string }> => {
200
+ const recording = activeScreenRecordings.get(sessionId);
201
+ if (!recording) {
202
+ return { success: false, error: 'No active recording writer for this session.' };
203
+ }
204
+
205
+ let buffer: Buffer;
206
+ if (chunk instanceof ArrayBuffer) {
207
+ buffer = Buffer.from(chunk);
208
+ } else if (ArrayBuffer.isView(chunk)) {
209
+ buffer = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
210
+ } else {
211
+ return { success: false, error: 'Unsupported recording chunk format.' };
212
+ }
213
+
214
+ recording.writeChain = recording.writeChain
215
+ .then(() => fs.appendFile(recording.tempPath, buffer))
216
+ .then(() => {
217
+ recording.bytesWritten += buffer.byteLength;
218
+ recording.lastChunkAt = Date.now();
219
+ });
220
+
221
+ try {
222
+ await recording.writeChain;
223
+ return { success: true };
224
+ } catch (error) {
225
+ return {
226
+ success: false,
227
+ error: error instanceof Error ? error.message : 'Failed to append recording chunk.',
228
+ };
229
+ }
230
+ }
231
+ );
232
+
233
+ ipcMain.handle(
234
+ IPC_CHANNELS.SCREEN_RECORDING_STOP,
235
+ async (
236
+ _,
237
+ sessionId: string
238
+ ): Promise<{ success: boolean; path?: string; bytes?: number; mimeType?: string; error?: string }> => {
239
+ try {
240
+ const artifact = await finalizeScreenRecording(sessionId);
241
+ if (!artifact) {
242
+ return { success: true };
243
+ }
244
+
245
+ return {
246
+ success: true,
247
+ path: artifact.tempPath,
248
+ bytes: artifact.bytesWritten,
249
+ mimeType: artifact.mimeType,
250
+ };
251
+ } catch (error) {
252
+ return {
253
+ success: false,
254
+ error: error instanceof Error ? error.message : 'Failed to finalize screen recording.',
255
+ };
256
+ }
257
+ }
258
+ );
259
+
260
+ // Audio device handlers
261
+ ipcMain.handle(IPC_CHANNELS.AUDIO_GET_DEVICES, async (): Promise<AudioDevice[]> => {
262
+ return [];
263
+ });
264
+
265
+ ipcMain.handle(IPC_CHANNELS.AUDIO_SET_DEVICE, async (_, deviceId: string) => {
266
+ const settingsManager = ctx.getSettingsManager();
267
+ const settings = settingsManager?.getAll() || DEFAULT_SETTINGS;
268
+ settingsManager?.update({ ...settings, preferredAudioDevice: deviceId });
269
+ ctx.getMainWindow()?.webContents.send(IPC_CHANNELS.AUDIO_SET_DEVICE, deviceId);
270
+ return { success: true };
271
+ });
272
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * IPC Handler Barrel
3
+ *
4
+ * Aggregates all domain-specific IPC handler modules and
5
+ * provides a single registration entry point for the main process.
6
+ */
7
+
8
+ export { registerSessionHandlers } from './sessionHandlers';
9
+ export { registerCaptureHandlers } from './captureHandlers';
10
+ export { registerSettingsHandlers } from './settingsHandlers';
11
+ export { registerOutputHandlers } from './outputHandlers';
12
+ export { registerWindowHandlers } from './windowHandlers';
13
+ export type { IpcContext, SessionActions } from './types';
14
+
15
+ // Re-export capture utilities used by the main entry point
16
+ export {
17
+ extensionFromMimeType,
18
+ finalizeScreenRecording,
19
+ getScreenRecordingSnapshot,
20
+ deleteFinalizedRecording,
21
+ getActiveScreenRecordings,
22
+ getFinalizedScreenRecordings,
23
+ } from './captureHandlers';
24
+
25
+ // Re-export session history used by the main entry point
26
+ export { listSessionHistoryItems } from './outputHandlers';
27
+
28
+ import type { IpcContext, SessionActions } from './types';
29
+ import { registerSessionHandlers } from './sessionHandlers';
30
+ import { registerCaptureHandlers } from './captureHandlers';
31
+ import { registerSettingsHandlers } from './settingsHandlers';
32
+ import { registerOutputHandlers } from './outputHandlers';
33
+ import { registerWindowHandlers } from './windowHandlers';
34
+
35
+ /**
36
+ * Register all IPC handlers in a single call.
37
+ * Called from the main entry point after all services are initialized.
38
+ */
39
+ export function registerAllHandlers(ctx: IpcContext, actions: SessionActions): void {
40
+ registerSessionHandlers(ctx, actions);
41
+ registerCaptureHandlers(ctx);
42
+ registerSettingsHandlers(ctx, actions);
43
+ registerOutputHandlers(ctx);
44
+ registerWindowHandlers(ctx);
45
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Output IPC Handlers
3
+ *
4
+ * Registers IPC handlers for session output operations:
5
+ * save, clipboard, session history, export, and deletion.
6
+ */
7
+
8
+ import { ipcMain, shell } from 'electron';
9
+ import * as fs from 'fs/promises';
10
+ import { join, basename } from 'path';
11
+ import { sessionController } from '../SessionController';
12
+ import {
13
+ fileManager,
14
+ outputManager,
15
+ clipboardService,
16
+ generateDocumentForFileManager,
17
+ } from '../output';
18
+ import { processSession as aiProcessSession } from '../ai';
19
+ import { IPC_CHANNELS, type SaveResult } from '../../shared/types';
20
+ import type { IpcContext } from './types';
21
+
22
+ // =============================================================================
23
+ // Session History Types and Helpers
24
+ // =============================================================================
25
+
26
+ interface ListedSessionMetadata {
27
+ sessionId: string;
28
+ startTime: number;
29
+ endTime?: number;
30
+ itemCount: number;
31
+ screenshotCount: number;
32
+ source?: {
33
+ id: string;
34
+ name?: string;
35
+ };
36
+ }
37
+
38
+ interface SessionHistoryItem {
39
+ id: string;
40
+ startTime: number;
41
+ endTime: number;
42
+ itemCount: number;
43
+ screenshotCount: number;
44
+ sourceName: string;
45
+ firstThumbnail?: string;
46
+ folder: string;
47
+ transcriptionPreview?: string;
48
+ }
49
+
50
+ function extractPreviewFromMarkdown(content: string): string | undefined {
51
+ const blockMatch = content.match(/#### Feedback\s*\n> ([\s\S]*?)(?:\n\n|\n---|$)/);
52
+ const fallbackLine = content.split('\n').find((line) => line.startsWith('> '));
53
+ const rawPreview = blockMatch?.[1] || fallbackLine?.replace(/^>\s*/, '');
54
+
55
+ if (!rawPreview) {
56
+ return undefined;
57
+ }
58
+
59
+ const singleLine = rawPreview.replace(/\n>\s*/g, ' ').replace(/\s+/g, ' ').trim();
60
+ return singleLine.slice(0, 220);
61
+ }
62
+
63
+ async function resolveSessionThumbnail(sessionDir: string): Promise<string | undefined> {
64
+ const screenshotsDir = join(sessionDir, 'screenshots');
65
+
66
+ try {
67
+ const files = await fs.readdir(screenshotsDir);
68
+ const firstImage = files
69
+ .filter((file) => /\.(png|jpe?g|webp)$/i.test(file))
70
+ .sort()[0];
71
+
72
+ if (!firstImage) {
73
+ return undefined;
74
+ }
75
+
76
+ return join(screenshotsDir, firstImage);
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ async function buildSessionHistoryItem(
83
+ dir: string,
84
+ metadata: ListedSessionMetadata
85
+ ): Promise<SessionHistoryItem> {
86
+ const markdownPath = join(dir, 'feedback-report.md');
87
+
88
+ let transcriptionPreview: string | undefined;
89
+ try {
90
+ const markdown = await fs.readFile(markdownPath, 'utf-8');
91
+ transcriptionPreview = extractPreviewFromMarkdown(markdown);
92
+ } catch {
93
+ transcriptionPreview = undefined;
94
+ }
95
+
96
+ return {
97
+ id: metadata.sessionId,
98
+ startTime: metadata.startTime,
99
+ endTime: metadata.endTime || metadata.startTime,
100
+ itemCount: metadata.itemCount || 0,
101
+ screenshotCount: metadata.screenshotCount || 0,
102
+ sourceName: metadata.source?.name || 'Feedback Session',
103
+ firstThumbnail: await resolveSessionThumbnail(dir),
104
+ folder: dir,
105
+ transcriptionPreview,
106
+ };
107
+ }
108
+
109
+ export async function listSessionHistoryItems(): Promise<SessionHistoryItem[]> {
110
+ const sessions = await fileManager.listSessions();
111
+ const items = await Promise.all(
112
+ sessions.map(({ dir, metadata }) =>
113
+ buildSessionHistoryItem(dir, metadata as ListedSessionMetadata)
114
+ )
115
+ );
116
+ return items.sort((a, b) => b.startTime - a.startTime);
117
+ }
118
+
119
+ async function getSessionHistoryItem(sessionId: string): Promise<SessionHistoryItem | null> {
120
+ const sessions = await listSessionHistoryItems();
121
+ return sessions.find((session) => session.id === sessionId) || null;
122
+ }
123
+
124
+ async function exportSessionFolders(sessionIds: string[]): Promise<string> {
125
+ const sessions = await listSessionHistoryItems();
126
+ const selected = sessions.filter((session) => sessionIds.includes(session.id));
127
+
128
+ if (!selected.length) {
129
+ throw new Error('No sessions found to export.');
130
+ }
131
+
132
+ const exportRoot = join(fileManager.getOutputDirectory(), 'exports');
133
+ const bundleDir = join(exportRoot, `bundle-${Date.now()}`);
134
+ await fs.mkdir(bundleDir, { recursive: true });
135
+
136
+ for (const session of selected) {
137
+ const destination = join(bundleDir, basename(session.folder));
138
+ await fs.cp(session.folder, destination, { recursive: true });
139
+ }
140
+
141
+ return bundleDir;
142
+ }
143
+
144
+ // =============================================================================
145
+ // IPC Registration
146
+ // =============================================================================
147
+
148
+ export function registerOutputHandlers(ctx: IpcContext): void {
149
+ const { getSettingsManager } = ctx;
150
+
151
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_SAVE, async (): Promise<SaveResult> => {
152
+ try {
153
+ const session = sessionController.getSession();
154
+ if (!session) {
155
+ return { success: false, error: 'No session to save' };
156
+ }
157
+
158
+ const settingsManager = getSettingsManager();
159
+ const { document } = settingsManager
160
+ ? await aiProcessSession(session, {
161
+ settingsManager,
162
+ projectName: session.metadata?.sourceName || 'Feedback Session',
163
+ screenshotDir: './screenshots',
164
+ })
165
+ : {
166
+ document: generateDocumentForFileManager(session, {
167
+ projectName: session.metadata?.sourceName || 'Feedback Session',
168
+ screenshotDir: './screenshots',
169
+ }),
170
+ };
171
+
172
+ const result = await fileManager.saveSession(session, document);
173
+ return {
174
+ success: result.success,
175
+ path: result.sessionDir,
176
+ error: result.error,
177
+ };
178
+ } catch (error) {
179
+ console.error('[Main] Failed to save session:', error);
180
+ return { success: false, error: (error as Error).message };
181
+ }
182
+ });
183
+
184
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_COPY_CLIPBOARD, async (): Promise<boolean> => {
185
+ try {
186
+ const session = sessionController.getSession();
187
+ if (!session) {
188
+ console.warn('[Main] No session to copy');
189
+ return false;
190
+ }
191
+
192
+ return await outputManager.copySessionSummary(session);
193
+ } catch (error) {
194
+ console.error('[Main] Failed to copy to clipboard:', error);
195
+ return false;
196
+ }
197
+ });
198
+
199
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_OPEN_FOLDER, async (_, sessionDir?: string) => {
200
+ try {
201
+ const dir = sessionDir || fileManager.getOutputDirectory();
202
+ await shell.openPath(dir);
203
+ return { success: true };
204
+ } catch (error) {
205
+ console.error('[Main] Failed to open folder:', error);
206
+ return { success: false, error: (error as Error).message };
207
+ }
208
+ });
209
+
210
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_LIST_SESSIONS, async () => {
211
+ try {
212
+ return await listSessionHistoryItems();
213
+ } catch (error) {
214
+ console.error('[Main] Failed to list sessions:', error);
215
+ return [];
216
+ }
217
+ });
218
+
219
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_GET_SESSION_METADATA, async (_, sessionId: string) => {
220
+ try {
221
+ return await getSessionHistoryItem(sessionId);
222
+ } catch (error) {
223
+ console.error('[Main] Failed to get session metadata:', error);
224
+ return null;
225
+ }
226
+ });
227
+
228
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_DELETE_SESSION, async (_, sessionId: string) => {
229
+ try {
230
+ const session = await getSessionHistoryItem(sessionId);
231
+ if (!session) {
232
+ return { success: false, error: 'Session not found' };
233
+ }
234
+
235
+ await fs.rm(session.folder, { recursive: true, force: true });
236
+ return { success: true };
237
+ } catch (error) {
238
+ console.error('[Main] Failed to delete session:', error);
239
+ return { success: false, error: (error as Error).message };
240
+ }
241
+ });
242
+
243
+ ipcMain.handle(IPC_CHANNELS.OUTPUT_DELETE_SESSIONS, async (_, sessionIds: string[]) => {
244
+ const deleted: string[] = [];
245
+ const failed: string[] = [];
246
+
247
+ for (const sessionId of sessionIds) {
248
+ try {
249
+ const session = await getSessionHistoryItem(sessionId);
250
+ if (!session) {
251
+ failed.push(sessionId);
252
+ continue;
253
+ }
254
+
255
+ await fs.rm(session.folder, { recursive: true, force: true });
256
+ deleted.push(sessionId);
257
+ } catch {
258
+ failed.push(sessionId);
259
+ }
260
+ }
261
+
262
+ return {
263
+ success: failed.length === 0,
264
+ deleted,
265
+ failed,
266
+ };
267
+ });
268
+
269
+ ipcMain.handle(
270
+ IPC_CHANNELS.OUTPUT_EXPORT_SESSION,
271
+ async (_, sessionId: string, format: 'markdown' | 'json' | 'pdf' = 'markdown') => {
272
+ try {
273
+ console.log(`[Main] Exporting session ${sessionId} as ${format}`);
274
+ const exportPath = await exportSessionFolders([sessionId]);
275
+ return { success: true, path: exportPath };
276
+ } catch (error) {
277
+ console.error('[Main] Failed to export session:', error);
278
+ return { success: false, error: (error as Error).message };
279
+ }
280
+ }
281
+ );
282
+
283
+ ipcMain.handle(
284
+ IPC_CHANNELS.OUTPUT_EXPORT_SESSIONS,
285
+ async (_, sessionIds: string[], format: 'markdown' | 'json' | 'pdf' = 'markdown') => {
286
+ try {
287
+ console.log(`[Main] Exporting ${sessionIds.length} sessions as ${format}`);
288
+ const exportPath = await exportSessionFolders(sessionIds);
289
+ return { success: true, path: exportPath };
290
+ } catch (error) {
291
+ console.error('[Main] Failed to export sessions:', error);
292
+ return { success: false, error: (error as Error).message };
293
+ }
294
+ }
295
+ );
296
+
297
+ // Legacy clipboard handler
298
+ ipcMain.handle(IPC_CHANNELS.COPY_TO_CLIPBOARD, async (_, text: string) => {
299
+ const success = await clipboardService.copyWithNotification(text);
300
+ return { success };
301
+ });
302
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Session IPC Handlers
3
+ *
4
+ * Registers IPC handlers for session lifecycle operations:
5
+ * start, stop, pause, resume, cancel, status queries.
6
+ */
7
+
8
+ import { ipcMain } from 'electron';
9
+ import { sessionController } from '../SessionController';
10
+ import { IPC_CHANNELS, type SessionStatusPayload, type SessionPayload } from '../../shared/types';
11
+ import type { IpcContext, SessionActions } from './types';
12
+
13
+ export function registerSessionHandlers(ctx: IpcContext, actions: SessionActions): void {
14
+ ipcMain.handle(IPC_CHANNELS.SESSION_START, async (_, sourceId?: string, sourceName?: string) => {
15
+ console.log('[Main] Starting session');
16
+ return actions.startSession(sourceId, sourceName);
17
+ });
18
+
19
+ ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
20
+ console.log('[Main] Stopping session');
21
+ return actions.stopSession();
22
+ });
23
+
24
+ ipcMain.handle(IPC_CHANNELS.SESSION_PAUSE, async () => {
25
+ console.log('[Main] Pausing session');
26
+ return actions.pauseSession();
27
+ });
28
+
29
+ ipcMain.handle(IPC_CHANNELS.SESSION_RESUME, async () => {
30
+ console.log('[Main] Resuming session');
31
+ return actions.resumeSession();
32
+ });
33
+
34
+ ipcMain.handle(IPC_CHANNELS.SESSION_CANCEL, async () => {
35
+ console.log('[Main] Cancelling session');
36
+ return actions.cancelSession();
37
+ });
38
+
39
+ ipcMain.handle(IPC_CHANNELS.SESSION_GET_STATUS, (): SessionStatusPayload => {
40
+ return sessionController.getStatus();
41
+ });
42
+
43
+ ipcMain.handle(IPC_CHANNELS.SESSION_GET_CURRENT, (): SessionPayload | null => {
44
+ const session = sessionController.getSession();
45
+ return session ? actions.serializeSession(session) : null;
46
+ });
47
+
48
+ // Legacy session handlers (for backwards compatibility)
49
+ ipcMain.handle(IPC_CHANNELS.START_SESSION, async (_, sourceId?: string) => {
50
+ return actions.startSession(sourceId);
51
+ });
52
+
53
+ ipcMain.handle(IPC_CHANNELS.STOP_SESSION, async () => {
54
+ return actions.stopSession();
55
+ });
56
+ }