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,277 @@
1
+ /**
2
+ * Window IPC Handlers
3
+ *
4
+ * Registers IPC handlers for window control, popover management,
5
+ * app version, Windows taskbar, transcription tiers, and Whisper model management.
6
+ */
7
+
8
+ import { ipcMain, app } from 'electron';
9
+ import { tierManager } from '../transcription/TierManager';
10
+ import { modelDownloadManager } from '../transcription/ModelDownloadManager';
11
+ import type { WhisperModel } from '../transcription/types';
12
+ import { POPOVER_SIZES } from '../windows';
13
+ import {
14
+ IPC_CHANNELS,
15
+ type TranscriptionTier as UiTranscriptionTier,
16
+ type TranscriptionTierStatus,
17
+ } from '../../shared/types';
18
+ import type { IpcContext } from './types';
19
+
20
+ export function registerWindowHandlers(ctx: IpcContext): void {
21
+ const { getMainWindow, getPopover, getWindowsTaskbar } = ctx;
22
+
23
+ // -------------------------------------------------------------------------
24
+ // App Version
25
+ // -------------------------------------------------------------------------
26
+
27
+ ipcMain.handle(IPC_CHANNELS.APP_VERSION, () => {
28
+ return app.getVersion();
29
+ });
30
+
31
+ // -------------------------------------------------------------------------
32
+ // Window Control
33
+ // -------------------------------------------------------------------------
34
+
35
+ ipcMain.handle(IPC_CHANNELS.WINDOW_MINIMIZE, () => {
36
+ getMainWindow()?.minimize();
37
+ return { success: true };
38
+ });
39
+
40
+ ipcMain.handle(IPC_CHANNELS.WINDOW_HIDE, () => {
41
+ const popover = getPopover();
42
+ if (popover) {
43
+ popover.hide();
44
+ } else {
45
+ getMainWindow()?.hide();
46
+ }
47
+ return { success: true };
48
+ });
49
+
50
+ ipcMain.handle(IPC_CHANNELS.WINDOW_CLOSE, () => {
51
+ getMainWindow()?.close();
52
+ return { success: true };
53
+ });
54
+
55
+ // -------------------------------------------------------------------------
56
+ // Popover Control (Menu Bar Mode)
57
+ // -------------------------------------------------------------------------
58
+
59
+ ipcMain.handle(IPC_CHANNELS.POPOVER_RESIZE, (_, width: number, height: number) => {
60
+ const popover = getPopover();
61
+ if (popover) {
62
+ popover.resize(width, height);
63
+ return { success: true };
64
+ }
65
+ return { success: false, error: 'Popover not initialized' };
66
+ });
67
+
68
+ ipcMain.handle(IPC_CHANNELS.POPOVER_RESIZE_TO_STATE, (_, state: string) => {
69
+ const popover = getPopover();
70
+ if (popover && state in POPOVER_SIZES) {
71
+ popover.resizeToState(state as keyof typeof POPOVER_SIZES);
72
+ return { success: true };
73
+ }
74
+ return { success: false, error: 'Popover not initialized or invalid state' };
75
+ });
76
+
77
+ ipcMain.handle(IPC_CHANNELS.POPOVER_SHOW, () => {
78
+ getPopover()?.show();
79
+ return { success: true };
80
+ });
81
+
82
+ ipcMain.handle(IPC_CHANNELS.POPOVER_HIDE, () => {
83
+ getPopover()?.hide();
84
+ return { success: true };
85
+ });
86
+
87
+ ipcMain.handle(IPC_CHANNELS.POPOVER_TOGGLE, () => {
88
+ getPopover()?.toggle();
89
+ return { success: true };
90
+ });
91
+
92
+ // -------------------------------------------------------------------------
93
+ // Windows Taskbar (Windows-specific)
94
+ // -------------------------------------------------------------------------
95
+
96
+ ipcMain.handle(
97
+ IPC_CHANNELS.TASKBAR_SET_PROGRESS,
98
+ (_, progress: number) => {
99
+ getWindowsTaskbar()?.setProgress(progress);
100
+ return { success: true };
101
+ }
102
+ );
103
+
104
+ ipcMain.handle(
105
+ IPC_CHANNELS.TASKBAR_FLASH_FRAME,
106
+ (_, count?: number) => {
107
+ getWindowsTaskbar()?.flashFrame(count);
108
+ return { success: true };
109
+ }
110
+ );
111
+
112
+ ipcMain.handle(
113
+ IPC_CHANNELS.TASKBAR_SET_OVERLAY,
114
+ (_, state: 'recording' | 'processing' | 'none') => {
115
+ getWindowsTaskbar()?.setOverlayIcon(state);
116
+ return { success: true };
117
+ }
118
+ );
119
+
120
+ // -------------------------------------------------------------------------
121
+ // Transcription Tier Control
122
+ // -------------------------------------------------------------------------
123
+
124
+ ipcMain.handle(
125
+ IPC_CHANNELS.TRANSCRIPTION_GET_TIER_STATUSES,
126
+ async (): Promise<TranscriptionTierStatus[]> => {
127
+ const statuses = await tierManager.getTierStatuses();
128
+
129
+ return statuses.map((status) => {
130
+ if (tierManager.tierProvidesTranscription(status.tier)) {
131
+ return status;
132
+ }
133
+
134
+ return {
135
+ ...status,
136
+ available: false,
137
+ reason: 'Not supported for narrated feedback reports',
138
+ };
139
+ });
140
+ }
141
+ );
142
+
143
+ ipcMain.handle(
144
+ IPC_CHANNELS.TRANSCRIPTION_GET_CURRENT_TIER,
145
+ async (): Promise<UiTranscriptionTier | null> => {
146
+ const preferred = tierManager.getPreferredTier();
147
+ if (preferred !== 'auto') {
148
+ return preferred;
149
+ }
150
+
151
+ const active = tierManager.getCurrentTier();
152
+ if (active) {
153
+ return active;
154
+ }
155
+
156
+ const best = await tierManager.selectBestTier();
157
+ if (tierManager.tierProvidesTranscription(best)) {
158
+ return best;
159
+ }
160
+
161
+ return null;
162
+ }
163
+ );
164
+
165
+ ipcMain.handle(
166
+ IPC_CHANNELS.TRANSCRIPTION_SET_TIER,
167
+ (_, tier: UiTranscriptionTier): { success: boolean; error?: string } => {
168
+ try {
169
+ const validTiers = new Set(['auto', 'whisper', 'timer-only']);
170
+ if (!validTiers.has(tier)) {
171
+ return { success: false, error: `Tier "${tier}" is no longer supported.` };
172
+ }
173
+ tierManager.setPreferredTier(tier as 'auto' | 'whisper' | 'timer-only');
174
+ return { success: true };
175
+ } catch (error) {
176
+ return {
177
+ success: false,
178
+ error: error instanceof Error ? error.message : 'Failed to set transcription tier.',
179
+ };
180
+ }
181
+ }
182
+ );
183
+
184
+ // -------------------------------------------------------------------------
185
+ // Whisper Model Channels
186
+ // -------------------------------------------------------------------------
187
+
188
+ ipcMain.handle(IPC_CHANNELS.WHISPER_CHECK_MODEL, () => {
189
+ const hasAnyModel = modelDownloadManager.hasAnyModel();
190
+ const downloadedModels: string[] = [];
191
+ const models: WhisperModel[] = ['tiny', 'base', 'small', 'medium', 'large'];
192
+
193
+ for (const model of models) {
194
+ if (modelDownloadManager.isModelDownloaded(model)) {
195
+ downloadedModels.push(model);
196
+ }
197
+ }
198
+
199
+ const defaultModel = hasAnyModel ? modelDownloadManager.getDefaultModel() : null;
200
+ const recommendedModel = 'tiny';
201
+ const recommendedInfo = modelDownloadManager.getModelInfo('tiny');
202
+
203
+ return {
204
+ hasAnyModel,
205
+ defaultModel,
206
+ downloadedModels,
207
+ recommendedModel,
208
+ recommendedModelSizeMB: recommendedInfo.sizeMB,
209
+ };
210
+ });
211
+
212
+ ipcMain.handle(IPC_CHANNELS.WHISPER_HAS_TRANSCRIPTION_CAPABILITY, async () => {
213
+ return tierManager.hasTranscriptionCapability();
214
+ });
215
+
216
+ ipcMain.handle(IPC_CHANNELS.WHISPER_GET_AVAILABLE_MODELS, () => {
217
+ const models = modelDownloadManager.getAvailableModels();
218
+ return models.map((info) => ({
219
+ name: info.name,
220
+ filename: info.filename,
221
+ sizeMB: info.sizeMB,
222
+ ramRequired: info.ramRequired,
223
+ quality: info.quality,
224
+ isDownloaded: modelDownloadManager.isModelDownloaded(info.name as WhisperModel),
225
+ }));
226
+ });
227
+
228
+ ipcMain.handle(IPC_CHANNELS.WHISPER_DOWNLOAD_MODEL, async (_, model: WhisperModel) => {
229
+ try {
230
+ const unsubProgress = modelDownloadManager.onProgress((progress) => {
231
+ getMainWindow()?.webContents.send(IPC_CHANNELS.WHISPER_DOWNLOAD_PROGRESS, {
232
+ model: progress.model,
233
+ downloadedBytes: progress.downloadedBytes,
234
+ totalBytes: progress.totalBytes,
235
+ percent: progress.percent,
236
+ speedBps: progress.speedBps,
237
+ estimatedSecondsRemaining: progress.estimatedSecondsRemaining,
238
+ });
239
+ });
240
+
241
+ const unsubComplete = modelDownloadManager.onComplete((result) => {
242
+ getMainWindow()?.webContents.send(IPC_CHANNELS.WHISPER_DOWNLOAD_COMPLETE, {
243
+ model: result.model,
244
+ path: result.path,
245
+ });
246
+ unsubProgress();
247
+ unsubComplete();
248
+ unsubError();
249
+ });
250
+
251
+ const unsubError = modelDownloadManager.onError((error, errorModel) => {
252
+ getMainWindow()?.webContents.send(IPC_CHANNELS.WHISPER_DOWNLOAD_ERROR, {
253
+ model: errorModel,
254
+ error: error.message,
255
+ });
256
+ unsubProgress();
257
+ unsubComplete();
258
+ unsubError();
259
+ });
260
+
261
+ const result = await modelDownloadManager.downloadModel(model);
262
+
263
+ return { success: result.success };
264
+ } catch (error) {
265
+ console.error('[Main] Failed to download Whisper model:', error);
266
+ return {
267
+ success: false,
268
+ error: error instanceof Error ? error.message : 'Unknown error'
269
+ };
270
+ }
271
+ });
272
+
273
+ ipcMain.handle(IPC_CHANNELS.WHISPER_CANCEL_DOWNLOAD, (_, model: WhisperModel) => {
274
+ modelDownloadManager.cancelDownload(model);
275
+ return { success: true };
276
+ });
277
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * ClipboardService - Auto-copy on save with native notifications
3
+ *
4
+ * Features:
5
+ * - Copy generated summary to clipboard automatically
6
+ * - Toast notification confirms copy
7
+ * - Summary truncation to <1500 chars
8
+ * - Include path to full report
9
+ * - Support both full and compact modes
10
+ */
11
+
12
+ import { clipboard, Notification, app, BrowserWindow } from 'electron';
13
+ import path from 'path';
14
+ import { Session, FeedbackItem } from '../SessionController';
15
+
16
+ // =============================================================================
17
+ // Types
18
+ // =============================================================================
19
+
20
+ export interface ClipboardService {
21
+ copy(content: string): Promise<boolean>;
22
+ copyWithNotification(content: string, title?: string): Promise<boolean>;
23
+ estimateSize(content: string): number;
24
+ generateClipboardSummary(session: Session, options?: SummaryOptions): string;
25
+ }
26
+
27
+ export interface SummaryOptions {
28
+ mode: 'full' | 'compact';
29
+ maxLength: number;
30
+ includeReportPath: boolean;
31
+ reportPath?: string;
32
+ }
33
+
34
+ export const DEFAULT_SUMMARY_OPTIONS: SummaryOptions = {
35
+ mode: 'compact',
36
+ maxLength: 1500,
37
+ includeReportPath: true,
38
+ };
39
+
40
+ // =============================================================================
41
+ // ClipboardService Implementation
42
+ // =============================================================================
43
+
44
+ class ClipboardServiceImpl implements ClipboardService {
45
+ private lastNotificationTime = 0;
46
+ private readonly NOTIFICATION_DEBOUNCE_MS = 1000;
47
+
48
+ /**
49
+ * Copy content to system clipboard
50
+ */
51
+ async copy(content: string): Promise<boolean> {
52
+ try {
53
+ clipboard.writeText(content);
54
+ console.log(`[Clipboard] Copied ${content.length} characters`);
55
+ return true;
56
+ } catch (error) {
57
+ console.error('[Clipboard] Failed to copy:', error);
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Copy content to clipboard and show native notification
64
+ */
65
+ async copyWithNotification(content: string, title?: string): Promise<boolean> {
66
+ const success = await this.copy(content);
67
+
68
+ // Debounce notifications
69
+ const now = Date.now();
70
+ if (now - this.lastNotificationTime < this.NOTIFICATION_DEBOUNCE_MS) {
71
+ return success;
72
+ }
73
+ this.lastNotificationTime = now;
74
+
75
+ if (success) {
76
+ this.showNotification(
77
+ title || 'markupr',
78
+ 'Summary copied to clipboard!',
79
+ 'Paste into your AI coding assistant.'
80
+ );
81
+ } else {
82
+ this.showNotification(
83
+ 'markupr',
84
+ 'Failed to copy',
85
+ 'Please try again or copy manually.'
86
+ );
87
+ }
88
+
89
+ return success;
90
+ }
91
+
92
+ /**
93
+ * Estimate byte size for UTF-8 content
94
+ */
95
+ estimateSize(content: string): number {
96
+ // Calculate UTF-8 byte size
97
+ return Buffer.byteLength(content, 'utf8');
98
+ }
99
+
100
+ /**
101
+ * Generate a clipboard-friendly summary from a session
102
+ * Follows llms.txt format for AI consumption
103
+ */
104
+ generateClipboardSummary(
105
+ session: Session,
106
+ options: Partial<SummaryOptions> = {}
107
+ ): string {
108
+ const opts: SummaryOptions = { ...DEFAULT_SUMMARY_OPTIONS, ...options };
109
+
110
+ const duration = session.endTime
111
+ ? Math.round((session.endTime - session.startTime) / 1000)
112
+ : Math.round((Date.now() - session.startTime) / 1000);
113
+
114
+ const feedbackItems = session.feedbackItems;
115
+
116
+ // Build summary based on mode
117
+ let summary: string;
118
+
119
+ if (opts.mode === 'full') {
120
+ summary = this.generateFullSummary(feedbackItems, duration);
121
+ } else {
122
+ summary = this.generateCompactSummary(feedbackItems, duration);
123
+ }
124
+
125
+ // Add report path if available
126
+ if (opts.includeReportPath && opts.reportPath) {
127
+ summary += `\n\n---\nFull report: ${opts.reportPath}`;
128
+ }
129
+
130
+ // Truncate if needed
131
+ if (summary.length > opts.maxLength) {
132
+ summary = this.truncateSummary(summary, opts.maxLength);
133
+ }
134
+
135
+ return summary;
136
+ }
137
+
138
+ /**
139
+ * Generate full summary with all feedback items
140
+ */
141
+ private generateFullSummary(items: FeedbackItem[], durationSec: number): string {
142
+ const lines: string[] = [
143
+ '# Feedback Session',
144
+ '',
145
+ `**Duration:** ${this.formatDuration(durationSec)}`,
146
+ `**Items:** ${items.length}`,
147
+ '',
148
+ '---',
149
+ '',
150
+ ];
151
+
152
+ for (const item of items) {
153
+ const time = new Date(item.timestamp).toLocaleTimeString('en-US', {
154
+ hour: '2-digit',
155
+ minute: '2-digit',
156
+ second: '2-digit',
157
+ });
158
+
159
+ lines.push(`## [${time}]`);
160
+ lines.push('');
161
+ lines.push(item.text);
162
+
163
+ if (item.screenshot) {
164
+ lines.push('');
165
+ lines.push(`_[Screenshot captured: ${item.screenshot.width}x${item.screenshot.height}]_`);
166
+ }
167
+
168
+ lines.push('');
169
+ }
170
+
171
+ return lines.join('\n');
172
+ }
173
+
174
+ /**
175
+ * Generate compact summary optimized for clipboard
176
+ */
177
+ private generateCompactSummary(items: FeedbackItem[], durationSec: number): string {
178
+ const lines: string[] = [
179
+ '# Feedback Summary',
180
+ '',
181
+ `Duration: ${this.formatDuration(durationSec)} | ${items.length} items | ${items.filter((i) => i.screenshot).length} screenshots`,
182
+ '',
183
+ ];
184
+
185
+ // Group similar items and extract key points
186
+ const keyPoints = this.extractKeyPoints(items);
187
+
188
+ if (keyPoints.length > 0) {
189
+ lines.push('## Key Points');
190
+ lines.push('');
191
+
192
+ for (const point of keyPoints) {
193
+ lines.push(`- ${point}`);
194
+ }
195
+
196
+ lines.push('');
197
+ }
198
+
199
+ // Add timeline summary
200
+ lines.push('## Timeline');
201
+ lines.push('');
202
+
203
+ for (const item of items) {
204
+ const time = new Date(item.timestamp).toLocaleTimeString('en-US', {
205
+ hour: '2-digit',
206
+ minute: '2-digit',
207
+ });
208
+
209
+ // Truncate text for compact view
210
+ const text =
211
+ item.text.length > 100 ? item.text.substring(0, 97) + '...' : item.text;
212
+
213
+ const screenshotIndicator = item.screenshot ? ' [img]' : '';
214
+ lines.push(`- **${time}:** ${text}${screenshotIndicator}`);
215
+ }
216
+
217
+ return lines.join('\n');
218
+ }
219
+
220
+ /**
221
+ * Extract key points from feedback items
222
+ * Simple heuristics for now, could be enhanced with NLP
223
+ */
224
+ private extractKeyPoints(items: FeedbackItem[]): string[] {
225
+ const keyPoints: string[] = [];
226
+
227
+ // Filter out very short or low-confidence items
228
+ const significantItems = items.filter(
229
+ (item) => item.text.length > 20 && item.confidence > 0.7
230
+ );
231
+
232
+ // Extract unique key phrases
233
+ const seen = new Set<string>();
234
+
235
+ for (const item of significantItems) {
236
+ // Look for action words and important phrases
237
+ const text = item.text.toLowerCase();
238
+
239
+ // Skip if too similar to existing points
240
+ const normalized = text.substring(0, 50);
241
+ if (seen.has(normalized)) {
242
+ continue;
243
+ }
244
+ seen.add(normalized);
245
+
246
+ // Capitalize first letter and add
247
+ const point = item.text.charAt(0).toUpperCase() + item.text.slice(1);
248
+ keyPoints.push(point);
249
+
250
+ // Limit to 5 key points
251
+ if (keyPoints.length >= 5) {
252
+ break;
253
+ }
254
+ }
255
+
256
+ return keyPoints;
257
+ }
258
+
259
+ /**
260
+ * Truncate summary while keeping it readable
261
+ */
262
+ private truncateSummary(summary: string, maxLength: number): string {
263
+ if (summary.length <= maxLength) {
264
+ return summary;
265
+ }
266
+
267
+ // Find a good break point (end of line or sentence)
268
+ const targetLength = maxLength - 50; // Leave room for truncation notice
269
+ let breakPoint = summary.lastIndexOf('\n', targetLength);
270
+
271
+ if (breakPoint === -1 || breakPoint < targetLength * 0.7) {
272
+ // Try sentence boundary
273
+ breakPoint = summary.lastIndexOf('. ', targetLength);
274
+
275
+ if (breakPoint === -1 || breakPoint < targetLength * 0.5) {
276
+ breakPoint = targetLength;
277
+ } else {
278
+ breakPoint += 1; // Include the period
279
+ }
280
+ }
281
+
282
+ return (
283
+ summary.substring(0, breakPoint) +
284
+ '\n\n_[Truncated - see full report for details]_'
285
+ );
286
+ }
287
+
288
+ /**
289
+ * Format duration in human-readable form
290
+ */
291
+ private formatDuration(seconds: number): string {
292
+ if (seconds < 60) {
293
+ return `${seconds}s`;
294
+ }
295
+
296
+ const minutes = Math.floor(seconds / 60);
297
+ const remainingSeconds = seconds % 60;
298
+
299
+ if (minutes < 60) {
300
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
301
+ }
302
+
303
+ const hours = Math.floor(minutes / 60);
304
+ const remainingMinutes = minutes % 60;
305
+
306
+ return `${hours}h ${remainingMinutes}m`;
307
+ }
308
+
309
+ /**
310
+ * Show native notification
311
+ */
312
+ private showNotification(title: string, body: string, subtitle?: string): void {
313
+ // Check if notifications are supported
314
+ if (!Notification.isSupported()) {
315
+ console.log(`[Notification] (unsupported) ${title}: ${body}`);
316
+ return;
317
+ }
318
+
319
+ const notification = new Notification({
320
+ title,
321
+ body,
322
+ subtitle,
323
+ silent: false,
324
+ icon: this.getIconPath(),
325
+ timeoutType: 'default', // Auto-dismiss
326
+ });
327
+
328
+ notification.on('click', () => {
329
+ // Focus the app when notification is clicked
330
+ const windows = BrowserWindow.getAllWindows();
331
+ if (windows.length > 0) {
332
+ windows[0].focus();
333
+ }
334
+ console.log('[Notification] Clicked - focusing app');
335
+ });
336
+
337
+ notification.on('close', () => {
338
+ console.log('[Notification] Dismissed');
339
+ });
340
+
341
+ notification.show();
342
+ }
343
+
344
+ /**
345
+ * Get app icon path for notification
346
+ */
347
+ private getIconPath(): string | undefined {
348
+ // macOS uses the app icon automatically
349
+ if (process.platform === 'darwin') {
350
+ return undefined;
351
+ }
352
+
353
+ // For Windows/Linux, try to find the icon
354
+ // This would be in the app resources after packaging
355
+ try {
356
+ const iconPath = path.join(app.getAppPath(), 'assets', 'icon.png');
357
+ return iconPath;
358
+ } catch {
359
+ return undefined;
360
+ }
361
+ }
362
+ }
363
+
364
+ // =============================================================================
365
+ // Singleton Export
366
+ // =============================================================================
367
+
368
+ export const clipboardService = new ClipboardServiceImpl();
369
+ export default ClipboardService;