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.
- package/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,1693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markupr - Main Process Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This is the orchestration heart of markupr. It:
|
|
5
|
+
* - Initializes all services in the correct order
|
|
6
|
+
* - Wires up the complete session lifecycle
|
|
7
|
+
* - Manages IPC communication with renderer
|
|
8
|
+
* - Handles hotkey registration and tray management
|
|
9
|
+
* - Coordinates graceful shutdown
|
|
10
|
+
*
|
|
11
|
+
* Service Integration Order:
|
|
12
|
+
* 1. Error handler (for crash recovery)
|
|
13
|
+
* 2. Settings (needed for API keys and config)
|
|
14
|
+
* 3. Secure settings + API key availability check
|
|
15
|
+
* 4. Window creation
|
|
16
|
+
* 5. Session controller initialization
|
|
17
|
+
* 6. Transcription service configuration
|
|
18
|
+
* 7. Tray manager initialization
|
|
19
|
+
* 8. Hotkey registration
|
|
20
|
+
* 9. IPC handler setup
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
app,
|
|
25
|
+
BrowserWindow,
|
|
26
|
+
desktopCapturer,
|
|
27
|
+
screen,
|
|
28
|
+
shell,
|
|
29
|
+
Notification,
|
|
30
|
+
} from 'electron';
|
|
31
|
+
import * as fs from 'fs/promises';
|
|
32
|
+
import { join, dirname, basename, extname } from 'path';
|
|
33
|
+
import { fileURLToPath } from 'url';
|
|
34
|
+
|
|
35
|
+
// Hide dock icon on macOS for pure menu bar experience
|
|
36
|
+
// IMPORTANT: Must be called before app.whenReady()
|
|
37
|
+
if (process.platform === 'darwin') {
|
|
38
|
+
app.dock.hide();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Ensure runtime app identity uses the shipped product name.
|
|
42
|
+
app.setName('markupr');
|
|
43
|
+
|
|
44
|
+
// ESM compatibility - __dirname doesn't exist in ESM
|
|
45
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
46
|
+
const __dirname = dirname(__filename);
|
|
47
|
+
import {
|
|
48
|
+
IPC_CHANNELS,
|
|
49
|
+
type PermissionType,
|
|
50
|
+
type SessionState,
|
|
51
|
+
type SessionPayload,
|
|
52
|
+
type TrayState,
|
|
53
|
+
} from '../shared/types';
|
|
54
|
+
import { hotkeyManager, type HotkeyAction } from './HotkeyManager';
|
|
55
|
+
import { sessionController, type Session } from './SessionController';
|
|
56
|
+
import { trayManager } from './TrayManager';
|
|
57
|
+
import { audioCapture } from './audio/AudioCapture';
|
|
58
|
+
import { SettingsManager } from './settings';
|
|
59
|
+
import { fileManager, clipboardService, generateDocumentForFileManager, adaptSessionForMarkdown } from './output';
|
|
60
|
+
import { processSession as aiProcessSession } from './ai';
|
|
61
|
+
import { modelDownloadManager } from './transcription/ModelDownloadManager';
|
|
62
|
+
import { errorHandler } from './ErrorHandler';
|
|
63
|
+
import { autoUpdaterManager } from './AutoUpdater';
|
|
64
|
+
import { crashRecovery, type RecoverableFeedbackItem } from './CrashRecovery';
|
|
65
|
+
import {
|
|
66
|
+
postProcessor,
|
|
67
|
+
type PostProcessResult,
|
|
68
|
+
type PostProcessProgress,
|
|
69
|
+
type TranscriptSegment,
|
|
70
|
+
} from './pipeline';
|
|
71
|
+
import { menuManager } from './MenuManager';
|
|
72
|
+
import { WindowsTaskbar, createWindowsTaskbar } from './platform';
|
|
73
|
+
import { PopoverManager, POPOVER_SIZES } from './windows';
|
|
74
|
+
import { permissionManager } from './PermissionManager';
|
|
75
|
+
import {
|
|
76
|
+
registerAllHandlers,
|
|
77
|
+
extensionFromMimeType,
|
|
78
|
+
finalizeScreenRecording,
|
|
79
|
+
getScreenRecordingSnapshot,
|
|
80
|
+
deleteFinalizedRecording,
|
|
81
|
+
getActiveScreenRecordings,
|
|
82
|
+
getFinalizedScreenRecordings,
|
|
83
|
+
} from './ipc';
|
|
84
|
+
import {
|
|
85
|
+
extractAiFrameHintsFromMarkdown,
|
|
86
|
+
appendExtractedFramesToReport,
|
|
87
|
+
syncExtractedFrameMetadata,
|
|
88
|
+
syncExtractedFrameSummary,
|
|
89
|
+
writeProcessingTrace,
|
|
90
|
+
} from './output/MarkdownPatcher';
|
|
91
|
+
|
|
92
|
+
// Guard against stdio EIO crashes when the parent terminal/PTY closes.
|
|
93
|
+
type ConsoleMethod = (...args: unknown[]) => void;
|
|
94
|
+
|
|
95
|
+
function wrapConsoleMethod(method: ConsoleMethod): ConsoleMethod {
|
|
96
|
+
return (...args: unknown[]) => {
|
|
97
|
+
try {
|
|
98
|
+
method(...args);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof Error && error.message.includes('EIO')) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isIgnorableStdioError(error: unknown): boolean {
|
|
109
|
+
if (!error) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof error === 'string') {
|
|
114
|
+
return error.toUpperCase().includes('EIO');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
if (error.message.toUpperCase().includes('EIO')) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const withCode = error as Error & { code?: string };
|
|
122
|
+
return withCode.code?.toUpperCase() === 'EIO';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof error === 'object' && 'code' in error) {
|
|
126
|
+
const code = (error as { code?: unknown }).code;
|
|
127
|
+
if (typeof code === 'string' && code.toUpperCase() === 'EIO') {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log = wrapConsoleMethod(console.log.bind(console));
|
|
136
|
+
console.info = wrapConsoleMethod(console.info.bind(console));
|
|
137
|
+
console.warn = wrapConsoleMethod(console.warn.bind(console));
|
|
138
|
+
console.error = wrapConsoleMethod(console.error.bind(console));
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Module State
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
let mainWindow: BrowserWindow | null = null;
|
|
145
|
+
let popover: PopoverManager | null = null;
|
|
146
|
+
let settingsManager: SettingsManager | null = null;
|
|
147
|
+
let isQuitting = false;
|
|
148
|
+
let hasCompletedOnboarding = false;
|
|
149
|
+
const rendererRecoveryAttempts = new WeakMap<BrowserWindow, number>();
|
|
150
|
+
let teardownAudioTelemetry: Array<() => void> = [];
|
|
151
|
+
let teardownSettingsListeners: Array<() => void> = [];
|
|
152
|
+
|
|
153
|
+
// Windows taskbar integration (Windows only)
|
|
154
|
+
let windowsTaskbar: WindowsTaskbar | null = null;
|
|
155
|
+
|
|
156
|
+
const DEV_RENDERER_URL = 'http://localhost:5173';
|
|
157
|
+
const DEV_RENDERER_LOAD_RETRIES = 10;
|
|
158
|
+
|
|
159
|
+
function sleep(ms: number): Promise<void> {
|
|
160
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function attachRendererDiagnostics(window: BrowserWindow, label: string): void {
|
|
164
|
+
window.on('unresponsive', () => {
|
|
165
|
+
console.error(`[Main] ${label} window became unresponsive`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
window.on('responsive', () => {
|
|
169
|
+
console.log(`[Main] ${label} window responsive again`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
window.webContents.on('render-process-gone', (_event, details) => {
|
|
173
|
+
console.error(`[Main] ${label} renderer process gone`, details);
|
|
174
|
+
|
|
175
|
+
const attempts = rendererRecoveryAttempts.get(window) ?? 0;
|
|
176
|
+
if (attempts >= 3) {
|
|
177
|
+
console.error(`[Main] ${label} renderer recovery skipped after ${attempts} failed attempts`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const nextAttempt = attempts + 1;
|
|
182
|
+
rendererRecoveryAttempts.set(window, nextAttempt);
|
|
183
|
+
|
|
184
|
+
const retryDelayMs = 300 * nextAttempt;
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
if (window.isDestroyed()) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void loadRendererIntoWindow(window, `${label} (recovery #${nextAttempt})`)
|
|
191
|
+
.then(() => {
|
|
192
|
+
rendererRecoveryAttempts.set(window, 0);
|
|
193
|
+
console.log(`[Main] ${label} renderer recovered on attempt ${nextAttempt}`);
|
|
194
|
+
})
|
|
195
|
+
.catch((error) => {
|
|
196
|
+
console.error(`[Main] ${label} renderer recovery attempt ${nextAttempt} failed`, error);
|
|
197
|
+
});
|
|
198
|
+
}, retryDelayMs);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
window.webContents.on(
|
|
202
|
+
'did-fail-load',
|
|
203
|
+
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
204
|
+
if (!isMainFrame) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.error(
|
|
208
|
+
`[Main] ${label} failed to load renderer (${errorCode}): ${errorDescription} (${validatedURL})`,
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function wireAudioTelemetry(): void {
|
|
215
|
+
teardownAudioTelemetry.forEach((teardown) => teardown());
|
|
216
|
+
teardownAudioTelemetry = [];
|
|
217
|
+
|
|
218
|
+
const sendAudioLevel = (level: number) => {
|
|
219
|
+
mainWindow?.webContents.send(IPC_CHANNELS.AUDIO_LEVEL, level);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const sendVoiceActivity = (active: boolean) => {
|
|
223
|
+
mainWindow?.webContents.send(IPC_CHANNELS.AUDIO_VOICE_ACTIVITY, active);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
teardownAudioTelemetry.push(
|
|
227
|
+
audioCapture.onAudioLevel(sendAudioLevel),
|
|
228
|
+
audioCapture.onVoiceActivity(sendVoiceActivity),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function loadRendererIntoWindow(window: BrowserWindow, label: string): Promise<void> {
|
|
233
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
234
|
+
await window.loadFile(join(__dirname, '../renderer/index.html'));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let lastError: unknown = null;
|
|
239
|
+
for (let attempt = 1; attempt <= DEV_RENDERER_LOAD_RETRIES; attempt++) {
|
|
240
|
+
try {
|
|
241
|
+
await window.loadURL(DEV_RENDERER_URL);
|
|
242
|
+
window.webContents.openDevTools({ mode: 'detach' });
|
|
243
|
+
return;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
lastError = error;
|
|
246
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
247
|
+
console.warn(
|
|
248
|
+
`[Main] ${label} renderer load attempt ${attempt}/${DEV_RENDERER_LOAD_RETRIES} failed: ${errorMessage}`,
|
|
249
|
+
);
|
|
250
|
+
await sleep(250 * attempt);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const finalMessage =
|
|
255
|
+
lastError instanceof Error ? lastError.message : 'Unknown renderer load failure';
|
|
256
|
+
await window.loadURL(
|
|
257
|
+
`data:text/html;charset=utf-8,${encodeURIComponent(`
|
|
258
|
+
<html>
|
|
259
|
+
<body style="margin:0;padding:20px;background:#121212;color:#f5f5f5;font-family:-apple-system,system-ui,sans-serif;">
|
|
260
|
+
<h2 style="margin:0 0 12px 0;">markupr failed to load</h2>
|
|
261
|
+
<p style="margin:0 0 8px 0;">Dev renderer did not become reachable at ${DEV_RENDERER_URL}.</p>
|
|
262
|
+
<p style="margin:0;color:#b3b3b3;">${finalMessage}</p>
|
|
263
|
+
</body>
|
|
264
|
+
</html>
|
|
265
|
+
`)}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// Window Management
|
|
271
|
+
// =============================================================================
|
|
272
|
+
|
|
273
|
+
function createWindow(): void {
|
|
274
|
+
// Resolve preload path - works in both dev and production
|
|
275
|
+
const preloadPath = join(app.getAppPath(), 'dist', 'preload', 'index.mjs');
|
|
276
|
+
console.log('[Main] Preload path:', preloadPath);
|
|
277
|
+
|
|
278
|
+
mainWindow = new BrowserWindow({
|
|
279
|
+
width: 400,
|
|
280
|
+
height: 300,
|
|
281
|
+
minWidth: 320,
|
|
282
|
+
minHeight: 200,
|
|
283
|
+
resizable: true,
|
|
284
|
+
frame: false,
|
|
285
|
+
transparent: true,
|
|
286
|
+
alwaysOnTop: true,
|
|
287
|
+
skipTaskbar: false,
|
|
288
|
+
show: false, // Don't show until ready
|
|
289
|
+
webPreferences: {
|
|
290
|
+
preload: preloadPath,
|
|
291
|
+
nodeIntegration: false,
|
|
292
|
+
contextIsolation: true,
|
|
293
|
+
sandbox: false, // Required for preload to work with contextBridge
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
attachRendererDiagnostics(mainWindow, 'Main');
|
|
298
|
+
void loadRendererIntoWindow(mainWindow, 'Main');
|
|
299
|
+
|
|
300
|
+
// Show window when ready
|
|
301
|
+
mainWindow.once('ready-to-show', () => {
|
|
302
|
+
mainWindow?.show();
|
|
303
|
+
console.log('[Main] Window ready to show');
|
|
304
|
+
|
|
305
|
+
// Check if onboarding needed
|
|
306
|
+
if (!hasCompletedOnboarding) {
|
|
307
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SHOW_ONBOARDING);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Handle window close - hide instead of quit on macOS
|
|
312
|
+
mainWindow.on('close', (event) => {
|
|
313
|
+
if (!isQuitting && process.platform === 'darwin') {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
mainWindow?.hide();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
mainWindow.on('closed', () => {
|
|
321
|
+
mainWindow = null;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Handle external links
|
|
325
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
326
|
+
shell.openExternal(url);
|
|
327
|
+
return { action: 'deny' };
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Set main window on session controller
|
|
331
|
+
sessionController.setMainWindow(mainWindow);
|
|
332
|
+
|
|
333
|
+
console.log('[Main] Window created');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Show the main window (from tray or dock click)
|
|
338
|
+
* In menu bar mode, shows the popover instead
|
|
339
|
+
*/
|
|
340
|
+
function showWindow(): void {
|
|
341
|
+
// In menu bar mode, show the popover
|
|
342
|
+
if (popover) {
|
|
343
|
+
popover.show();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Fallback for non-popover mode
|
|
348
|
+
if (!mainWindow) {
|
|
349
|
+
createWindow();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (mainWindow.isMinimized()) {
|
|
354
|
+
mainWindow.restore();
|
|
355
|
+
}
|
|
356
|
+
mainWindow.show();
|
|
357
|
+
mainWindow.focus();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// =============================================================================
|
|
361
|
+
// Session State Handling
|
|
362
|
+
// =============================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Map SessionController state to TrayManager state
|
|
366
|
+
*/
|
|
367
|
+
function mapToTrayState(state: SessionState): TrayState {
|
|
368
|
+
switch (state) {
|
|
369
|
+
case 'idle':
|
|
370
|
+
return 'idle';
|
|
371
|
+
case 'recording':
|
|
372
|
+
return 'recording';
|
|
373
|
+
case 'processing':
|
|
374
|
+
return 'processing';
|
|
375
|
+
case 'complete':
|
|
376
|
+
return 'idle';
|
|
377
|
+
default:
|
|
378
|
+
return 'idle';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Handle session state changes - update tray, Windows taskbar, and notify renderer
|
|
384
|
+
*/
|
|
385
|
+
function handleSessionStateChange(state: SessionState, session: Session | null): void {
|
|
386
|
+
console.log(`[Main] Session state changed: ${state}`);
|
|
387
|
+
|
|
388
|
+
// Update tray icon
|
|
389
|
+
trayManager.setState(mapToTrayState(state));
|
|
390
|
+
if (state === 'recording' && sessionController.isSessionPaused()) {
|
|
391
|
+
trayManager.setTooltip('markupr - Paused (Cmd+Shift+P to resume)');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const keepVisibleOnBlur =
|
|
395
|
+
state === 'starting' ||
|
|
396
|
+
state === 'recording' ||
|
|
397
|
+
state === 'stopping' ||
|
|
398
|
+
state === 'processing';
|
|
399
|
+
popover?.setKeepVisibleOnBlur(keepVisibleOnBlur);
|
|
400
|
+
|
|
401
|
+
if (popover && (state === 'recording' || state === 'stopping' || state === 'processing')) {
|
|
402
|
+
const hudState = state === 'recording' ? 'recording' : 'processing';
|
|
403
|
+
popover.resizeToState(hudState);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (state === 'recording' && popover && !popover.isVisible()) {
|
|
407
|
+
popover.show();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Update Windows taskbar (if on Windows)
|
|
411
|
+
windowsTaskbar?.updateSessionState(state);
|
|
412
|
+
|
|
413
|
+
// Notify renderer
|
|
414
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SESSION_STATE_CHANGED, {
|
|
415
|
+
state,
|
|
416
|
+
session: session ? serializeSession(session) : null,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Also send status update
|
|
420
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SESSION_STATUS, sessionController.getStatus());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Handle new feedback item
|
|
425
|
+
*/
|
|
426
|
+
function handleFeedbackItem(item: {
|
|
427
|
+
id: string;
|
|
428
|
+
timestamp: number;
|
|
429
|
+
text: string;
|
|
430
|
+
confidence: number;
|
|
431
|
+
}): void {
|
|
432
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SESSION_FEEDBACK_ITEM, {
|
|
433
|
+
id: item.id,
|
|
434
|
+
timestamp: item.timestamp,
|
|
435
|
+
text: item.text,
|
|
436
|
+
confidence: item.confidence,
|
|
437
|
+
hasScreenshot: false,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Update crash recovery with new feedback item
|
|
441
|
+
const session = sessionController.getSession();
|
|
442
|
+
if (session) {
|
|
443
|
+
const recoverableFeedbackItems: RecoverableFeedbackItem[] = session.feedbackItems.map((fi) => ({
|
|
444
|
+
id: fi.id,
|
|
445
|
+
timestamp: fi.timestamp,
|
|
446
|
+
text: fi.text,
|
|
447
|
+
confidence: fi.confidence,
|
|
448
|
+
hasScreenshot: false,
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
crashRecovery.updateSession({
|
|
452
|
+
feedbackItems: recoverableFeedbackItems,
|
|
453
|
+
screenshotCount: sessionController.getStatus().screenshotCount,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Handle session errors
|
|
460
|
+
*/
|
|
461
|
+
function handleSessionError(error: Error): void {
|
|
462
|
+
console.error('[Main] Session error:', error);
|
|
463
|
+
|
|
464
|
+
// Update tray to error state
|
|
465
|
+
trayManager.setState('error');
|
|
466
|
+
trayManager.setTooltip(`markupr - Error: ${error.message}`);
|
|
467
|
+
|
|
468
|
+
// Notify renderer
|
|
469
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SESSION_ERROR, {
|
|
470
|
+
message: error.message,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Show notification
|
|
474
|
+
showErrorNotification('Recording Error', error.message);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// =============================================================================
|
|
478
|
+
// Tray Handling
|
|
479
|
+
// =============================================================================
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Handle tray icon click - toggle popover for menu bar mode
|
|
483
|
+
*/
|
|
484
|
+
function handleTrayClick(): void {
|
|
485
|
+
// In menu bar mode, tray click toggles the popover
|
|
486
|
+
if (popover) {
|
|
487
|
+
popover.toggle();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Fallback for non-popover mode
|
|
492
|
+
const currentState = sessionController.getState();
|
|
493
|
+
|
|
494
|
+
if (currentState === 'idle') {
|
|
495
|
+
// Show window to start a new session
|
|
496
|
+
showWindow();
|
|
497
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SHOW_WINDOW_SELECTOR);
|
|
498
|
+
} else if (currentState === 'recording') {
|
|
499
|
+
// Stop recording
|
|
500
|
+
stopSession();
|
|
501
|
+
} else {
|
|
502
|
+
// Just show the window
|
|
503
|
+
showWindow();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Handle settings click from tray menu
|
|
509
|
+
*/
|
|
510
|
+
function handleSettingsClick(): void {
|
|
511
|
+
showWindow();
|
|
512
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SHOW_SETTINGS);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Handle menu bar actions from MenuManager
|
|
517
|
+
*/
|
|
518
|
+
function handleMenuAction(action: string, data?: unknown): void {
|
|
519
|
+
console.log(`[Main] Menu action: ${action}`, data);
|
|
520
|
+
|
|
521
|
+
switch (action) {
|
|
522
|
+
case 'toggle-recording':
|
|
523
|
+
handleToggleRecording();
|
|
524
|
+
break;
|
|
525
|
+
case 'show-settings':
|
|
526
|
+
handleSettingsClick();
|
|
527
|
+
break;
|
|
528
|
+
case 'show-history':
|
|
529
|
+
showWindow();
|
|
530
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SHOW_HISTORY);
|
|
531
|
+
break;
|
|
532
|
+
case 'show-export':
|
|
533
|
+
showWindow();
|
|
534
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SHOW_EXPORT);
|
|
535
|
+
break;
|
|
536
|
+
case 'show-shortcuts':
|
|
537
|
+
showWindow();
|
|
538
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SHOW_SHORTCUTS);
|
|
539
|
+
break;
|
|
540
|
+
case 'check-updates':
|
|
541
|
+
autoUpdaterManager.checkForUpdates();
|
|
542
|
+
break;
|
|
543
|
+
case 'open-session':
|
|
544
|
+
showWindow();
|
|
545
|
+
mainWindow?.webContents.send(IPC_CHANNELS.OPEN_SESSION_DIALOG);
|
|
546
|
+
break;
|
|
547
|
+
case 'open-session-path':
|
|
548
|
+
if (data && typeof data === 'object' && 'path' in data) {
|
|
549
|
+
showWindow();
|
|
550
|
+
mainWindow?.webContents.send(IPC_CHANNELS.OPEN_SESSION, (data as { path: string }).path);
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
default:
|
|
554
|
+
console.warn(`[Main] Unknown menu action: ${action}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// =============================================================================
|
|
559
|
+
// Notifications
|
|
560
|
+
// =============================================================================
|
|
561
|
+
|
|
562
|
+
function showSuccessNotification(title: string, body: string): void {
|
|
563
|
+
if (Notification.isSupported()) {
|
|
564
|
+
const notification = new Notification({ title, body, silent: false });
|
|
565
|
+
notification.show();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function showErrorNotification(title: string, body: string): void {
|
|
570
|
+
if (body.toUpperCase().includes('EIO')) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (Notification.isSupported()) {
|
|
575
|
+
const notification = new Notification({ title, body, silent: false, urgency: 'critical' });
|
|
576
|
+
notification.show();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// =============================================================================
|
|
581
|
+
// Hotkey Management
|
|
582
|
+
// =============================================================================
|
|
583
|
+
|
|
584
|
+
function initializeHotkeys(): void {
|
|
585
|
+
const results = hotkeyManager.initialize();
|
|
586
|
+
|
|
587
|
+
for (const result of results) {
|
|
588
|
+
if (result.success) {
|
|
589
|
+
console.log(`[Main] Hotkey registered: ${result.action} -> ${result.accelerator}`);
|
|
590
|
+
if (result.fallbackUsed) {
|
|
591
|
+
console.log(`[Main] (Used fallback: ${result.fallbackUsed})`);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
console.error(`[Main] Failed to register hotkey for ${result.action}: ${result.error}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
hotkeyManager.onHotkey((action: HotkeyAction) => {
|
|
599
|
+
handleHotkeyAction(action);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function handleHotkeyAction(action: HotkeyAction): void {
|
|
604
|
+
console.log(`[Main] Hotkey triggered: ${action}`);
|
|
605
|
+
|
|
606
|
+
switch (action) {
|
|
607
|
+
case 'toggleRecording':
|
|
608
|
+
void handleToggleRecording();
|
|
609
|
+
break;
|
|
610
|
+
|
|
611
|
+
case 'manualScreenshot':
|
|
612
|
+
void handleManualScreenshot();
|
|
613
|
+
break;
|
|
614
|
+
|
|
615
|
+
case 'pauseResume':
|
|
616
|
+
void handlePauseResume();
|
|
617
|
+
break;
|
|
618
|
+
|
|
619
|
+
default:
|
|
620
|
+
console.warn(`[Main] Unknown hotkey action: ${action}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function handleToggleRecording(): Promise<void> {
|
|
625
|
+
const currentState = sessionController.getState();
|
|
626
|
+
|
|
627
|
+
if (currentState === 'recording') {
|
|
628
|
+
// Stop recording
|
|
629
|
+
await stopSession();
|
|
630
|
+
} else if (currentState === 'idle') {
|
|
631
|
+
const result = await startSession();
|
|
632
|
+
if (!result.success && result.error) {
|
|
633
|
+
showErrorNotification('Unable to Start Recording', result.error);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function handlePauseResume(): Promise<void> {
|
|
639
|
+
if (sessionController.getState() !== 'recording') {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (sessionController.isSessionPaused()) {
|
|
644
|
+
resumeSession();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
pauseSession();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function handleManualScreenshot(): Promise<void> {
|
|
652
|
+
const cue = sessionController.registerCaptureCue('manual');
|
|
653
|
+
if (!cue) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
crashRecovery.updateSession({
|
|
658
|
+
screenshotCount: cue.count,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function pauseSession(): { success: boolean; error?: string } {
|
|
663
|
+
if (sessionController.getState() !== 'recording') {
|
|
664
|
+
return { success: false, error: 'No recording session is active.' };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const paused = sessionController.pause();
|
|
668
|
+
if (!paused) {
|
|
669
|
+
return { success: false, error: 'Session is already paused.' };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
trayManager.setTooltip('markupr - Paused (Cmd+Shift+P to resume)');
|
|
673
|
+
return { success: true };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function resumeSession(): { success: boolean; error?: string } {
|
|
677
|
+
if (sessionController.getState() !== 'recording') {
|
|
678
|
+
return { success: false, error: 'No recording session is active.' };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const resumed = sessionController.resume();
|
|
682
|
+
if (!resumed) {
|
|
683
|
+
return { success: false, error: 'Session is not paused.' };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
trayManager.setTooltip('markupr - Recording... (Cmd+Shift+F to stop)');
|
|
687
|
+
return { success: true };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// =============================================================================
|
|
691
|
+
// Session Control
|
|
692
|
+
// =============================================================================
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Resolve the default capture source for zero-friction recording start.
|
|
696
|
+
* Prefers the primary display to match what users are actively looking at.
|
|
697
|
+
*/
|
|
698
|
+
async function resolveDefaultCaptureSource(): Promise<{ sourceId: string; sourceName: string }> {
|
|
699
|
+
const sources = await desktopCapturer.getSources({
|
|
700
|
+
types: ['screen'],
|
|
701
|
+
thumbnailSize: { width: 1, height: 1 },
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
if (!sources.length) {
|
|
705
|
+
throw new Error('No screen capture source is available.');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const primaryDisplayId = String(screen.getPrimaryDisplay().id);
|
|
709
|
+
const preferredSource = sources.find((source) => source.display_id === primaryDisplayId);
|
|
710
|
+
const fallbackSource = sources.find((source) => source.id.startsWith('screen')) || sources[0];
|
|
711
|
+
const selected = preferredSource || fallbackSource;
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
sourceId: selected.id,
|
|
715
|
+
sourceName: selected.name || 'Main Display',
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function buildPostProcessTranscriptSegments(session: Session): TranscriptSegment[] {
|
|
720
|
+
const sessionStartSec = session.startTime / 1000;
|
|
721
|
+
const events = session.transcriptBuffer
|
|
722
|
+
.filter((event) => event.text.trim().length > 0 && event.isFinal)
|
|
723
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
724
|
+
|
|
725
|
+
if (events.length === 0) {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return events.map((event, index) => {
|
|
730
|
+
const startTime = Math.max(0, event.timestamp - sessionStartSec);
|
|
731
|
+
const nextTimestampSec =
|
|
732
|
+
index < events.length - 1
|
|
733
|
+
? Math.max(startTime + 0.35, events[index + 1].timestamp - sessionStartSec)
|
|
734
|
+
: startTime + Math.min(3, Math.max(1, event.text.trim().split(/\s+/).length * 0.35));
|
|
735
|
+
const endTime = Math.max(startTime + 0.35, nextTimestampSec);
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
text: event.text.trim(),
|
|
739
|
+
startTime,
|
|
740
|
+
endTime,
|
|
741
|
+
confidence: Number.isFinite(event.confidence) ? event.confidence : 0.8,
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function copyReportPathToClipboard(path: string): Promise<boolean> {
|
|
747
|
+
const maxAttempts = 3;
|
|
748
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
749
|
+
const copied = await clipboardService.copy(path);
|
|
750
|
+
if (copied) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (attempt < maxAttempts) {
|
|
755
|
+
await sleep(120 * attempt);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function attachRecordingToSessionOutput(
|
|
763
|
+
sessionId: string,
|
|
764
|
+
sessionDir: string,
|
|
765
|
+
markdownPath: string
|
|
766
|
+
): Promise<{ path: string; mimeType: string; bytesWritten: number; startTime?: number } | undefined> {
|
|
767
|
+
const artifact = await finalizeScreenRecording(sessionId);
|
|
768
|
+
if (!artifact || artifact.bytesWritten <= 0) {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const extension = extname(artifact.tempPath) || extensionFromMimeType(artifact.mimeType);
|
|
773
|
+
const finalPath = join(sessionDir, `session-recording${extension}`);
|
|
774
|
+
|
|
775
|
+
try {
|
|
776
|
+
await fs.copyFile(artifact.tempPath, finalPath);
|
|
777
|
+
|
|
778
|
+
// Append recording link to the report for agent context replay.
|
|
779
|
+
let markdown = await fs.readFile(markdownPath, 'utf-8');
|
|
780
|
+
if (!markdown.includes('## Session Recording')) {
|
|
781
|
+
markdown += `\n## Session Recording\n- [Open full recording](./${basename(finalPath)})\n`;
|
|
782
|
+
await fs.writeFile(markdownPath, markdown, 'utf-8');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
path: finalPath,
|
|
787
|
+
mimeType: artifact.mimeType,
|
|
788
|
+
bytesWritten: artifact.bytesWritten,
|
|
789
|
+
startTime: artifact.startTime,
|
|
790
|
+
};
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.warn('[Main] Failed to attach session recording to output:', error);
|
|
793
|
+
return undefined;
|
|
794
|
+
} finally {
|
|
795
|
+
deleteFinalizedRecording(sessionId);
|
|
796
|
+
await fs.unlink(artifact.tempPath).catch(() => {
|
|
797
|
+
// Best-effort cleanup for temp artifacts.
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function attachAudioToSessionOutput(
|
|
803
|
+
sessionDir: string,
|
|
804
|
+
markdownPath: string
|
|
805
|
+
): Promise<{ path: string; bytesWritten: number; durationMs: number; mimeType: string } | undefined> {
|
|
806
|
+
const basePath = join(sessionDir, 'session-audio');
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const exported = await sessionController.exportCapturedAudio(basePath);
|
|
810
|
+
if (!exported || exported.bytesWritten <= 0) {
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let markdown = await fs.readFile(markdownPath, 'utf-8');
|
|
815
|
+
if (!markdown.includes('## Session Audio')) {
|
|
816
|
+
markdown += `\n## Session Audio\n- [Open narration audio](./${basename(exported.path)})\n`;
|
|
817
|
+
await fs.writeFile(markdownPath, markdown, 'utf-8');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
path: exported.path,
|
|
822
|
+
bytesWritten: exported.bytesWritten,
|
|
823
|
+
durationMs: exported.durationMs,
|
|
824
|
+
mimeType: exported.mimeType,
|
|
825
|
+
};
|
|
826
|
+
} catch (error) {
|
|
827
|
+
console.warn('[Main] Failed to attach session audio to output:', error);
|
|
828
|
+
return undefined;
|
|
829
|
+
} finally {
|
|
830
|
+
sessionController.clearCapturedAudio();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Start a recording session.
|
|
836
|
+
*/
|
|
837
|
+
async function startSession(sourceId?: string, sourceName?: string): Promise<{
|
|
838
|
+
success: boolean;
|
|
839
|
+
sessionId?: string;
|
|
840
|
+
error?: string;
|
|
841
|
+
}> {
|
|
842
|
+
try {
|
|
843
|
+
const [hasMicrophonePermission, hasScreenPermission] = await Promise.all([
|
|
844
|
+
checkPermission('microphone'),
|
|
845
|
+
checkPermission('screen'),
|
|
846
|
+
]);
|
|
847
|
+
|
|
848
|
+
if (!hasMicrophonePermission) {
|
|
849
|
+
await requestPermission('microphone');
|
|
850
|
+
}
|
|
851
|
+
if (!hasScreenPermission) {
|
|
852
|
+
await requestPermission('screen');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const [microphoneGranted, screenGranted] = await Promise.all([
|
|
856
|
+
checkPermission('microphone'),
|
|
857
|
+
checkPermission('screen'),
|
|
858
|
+
]);
|
|
859
|
+
|
|
860
|
+
if (!microphoneGranted || !screenGranted) {
|
|
861
|
+
return {
|
|
862
|
+
success: false,
|
|
863
|
+
error:
|
|
864
|
+
'Microphone and screen recording permissions are required. Enable both in macOS System Settings, then retry.',
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
let resolvedSourceId = sourceId;
|
|
869
|
+
let resolvedSourceName = sourceName;
|
|
870
|
+
|
|
871
|
+
if (!resolvedSourceId) {
|
|
872
|
+
const defaultSource = await resolveDefaultCaptureSource();
|
|
873
|
+
resolvedSourceId = defaultSource.sourceId;
|
|
874
|
+
resolvedSourceName = defaultSource.sourceName;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Start the session
|
|
878
|
+
await sessionController.start(resolvedSourceId, resolvedSourceName);
|
|
879
|
+
|
|
880
|
+
const session = sessionController.getSession();
|
|
881
|
+
|
|
882
|
+
// Start crash recovery tracking
|
|
883
|
+
if (session) {
|
|
884
|
+
crashRecovery.startTracking({
|
|
885
|
+
id: session.id,
|
|
886
|
+
startTime: session.startTime,
|
|
887
|
+
lastSaveTime: Date.now(),
|
|
888
|
+
feedbackItems: [],
|
|
889
|
+
transcriptionBuffer: '',
|
|
890
|
+
sourceId: session.sourceId,
|
|
891
|
+
sourceName: resolvedSourceName || 'Unknown Source',
|
|
892
|
+
screenshotCount: 0,
|
|
893
|
+
metadata: {
|
|
894
|
+
appVersion: app.getVersion(),
|
|
895
|
+
platform: process.platform,
|
|
896
|
+
sessionDurationMs: 0,
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
success: true,
|
|
903
|
+
sessionId: session?.id,
|
|
904
|
+
};
|
|
905
|
+
} catch (error) {
|
|
906
|
+
console.error('[Main] Failed to start session:', error);
|
|
907
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
908
|
+
return {
|
|
909
|
+
success: false,
|
|
910
|
+
error: message,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Stop the current recording session and generate output
|
|
917
|
+
*/
|
|
918
|
+
async function stopSession(): Promise<{
|
|
919
|
+
success: boolean;
|
|
920
|
+
session?: SessionPayload;
|
|
921
|
+
reportPath?: string;
|
|
922
|
+
error?: string;
|
|
923
|
+
}> {
|
|
924
|
+
let stoppedSessionId: string | null = null;
|
|
925
|
+
const stopStartedAt = Date.now();
|
|
926
|
+
let aiDurationMs = 0;
|
|
927
|
+
let saveDurationMs = 0;
|
|
928
|
+
let postProcessDurationMs = 0;
|
|
929
|
+
let aiFrameHintCount = 0;
|
|
930
|
+
let stopPhaseTicker: NodeJS.Timeout | null = null;
|
|
931
|
+
let stopPhasePercent = 6;
|
|
932
|
+
|
|
933
|
+
const cleanupRecordingArtifacts = async (sessionId: string): Promise<void> => {
|
|
934
|
+
const artifact = await finalizeScreenRecording(sessionId).catch(() => null);
|
|
935
|
+
if (!artifact?.tempPath) {
|
|
936
|
+
deleteFinalizedRecording(sessionId);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
await fs.unlink(artifact.tempPath).catch(() => {
|
|
941
|
+
// Best-effort cleanup of orphaned temp recordings.
|
|
942
|
+
});
|
|
943
|
+
deleteFinalizedRecording(sessionId);
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const emitProcessingProgress = (percent: number, step: string): void => {
|
|
947
|
+
const boundedPercent = Math.max(0, Math.min(100, Math.round(percent)));
|
|
948
|
+
mainWindow?.webContents.send(IPC_CHANNELS.PROCESSING_PROGRESS, {
|
|
949
|
+
percent: boundedPercent,
|
|
950
|
+
step,
|
|
951
|
+
});
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const stopStopPhaseTicker = (): void => {
|
|
955
|
+
if (stopPhaseTicker) {
|
|
956
|
+
clearInterval(stopPhaseTicker);
|
|
957
|
+
stopPhaseTicker = null;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const startStopPhaseTicker = (): void => {
|
|
962
|
+
stopStopPhaseTicker();
|
|
963
|
+
emitProcessingProgress(stopPhasePercent, 'preparing');
|
|
964
|
+
stopPhaseTicker = setInterval(() => {
|
|
965
|
+
stopPhasePercent = Math.min(32, stopPhasePercent + 1);
|
|
966
|
+
emitProcessingProgress(stopPhasePercent, 'preparing');
|
|
967
|
+
windowsTaskbar?.setProgress(Math.max(0.06, stopPhasePercent / 100));
|
|
968
|
+
if (stopPhasePercent >= 32) {
|
|
969
|
+
stopStopPhaseTicker();
|
|
970
|
+
}
|
|
971
|
+
}, 420);
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
try {
|
|
975
|
+
// Set Windows taskbar to processing state with indeterminate progress
|
|
976
|
+
windowsTaskbar?.setProgress(-1);
|
|
977
|
+
startStopPhaseTicker();
|
|
978
|
+
|
|
979
|
+
// Stop the session and get results
|
|
980
|
+
console.log('[Main:stopSession] Step 1/6: Stopping session controller...');
|
|
981
|
+
const session = await sessionController.stop();
|
|
982
|
+
stopStopPhaseTicker();
|
|
983
|
+
|
|
984
|
+
// Stop crash recovery tracking
|
|
985
|
+
crashRecovery.stopTracking();
|
|
986
|
+
|
|
987
|
+
if (!session) {
|
|
988
|
+
windowsTaskbar?.clearProgress();
|
|
989
|
+
return {
|
|
990
|
+
success: false,
|
|
991
|
+
error: 'No active session to stop',
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
stoppedSessionId = session.id;
|
|
995
|
+
console.log(
|
|
996
|
+
`[Main:stopSession] Session stopped: ${session.id}, ` +
|
|
997
|
+
`${session.feedbackItems.length} feedback items, ` +
|
|
998
|
+
`${session.transcriptBuffer.length} transcript events`
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const recordingProbe = getScreenRecordingSnapshot(session.id);
|
|
1002
|
+
const hasTranscript = session.transcriptBuffer.some((entry) => entry.text.trim().length > 0);
|
|
1003
|
+
const hasRecording = !!recordingProbe && recordingProbe.bytesWritten > 0;
|
|
1004
|
+
const recordingExtension = hasRecording
|
|
1005
|
+
? extname(recordingProbe?.tempPath ?? '') || extensionFromMimeType(recordingProbe?.mimeType)
|
|
1006
|
+
: '.webm';
|
|
1007
|
+
const recordingFilename = `session-recording${recordingExtension}`;
|
|
1008
|
+
|
|
1009
|
+
if (!hasTranscript && !hasRecording) {
|
|
1010
|
+
await cleanupRecordingArtifacts(session.id);
|
|
1011
|
+
sessionController.clearCapturedAudio();
|
|
1012
|
+
windowsTaskbar?.clearProgress();
|
|
1013
|
+
return {
|
|
1014
|
+
success: false,
|
|
1015
|
+
error:
|
|
1016
|
+
'No capture data was collected (no transcript or recording). Check microphone/screen capture access and retry.',
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
emitProcessingProgress(10, 'preparing');
|
|
1021
|
+
|
|
1022
|
+
// Update progress: generating document (33%)
|
|
1023
|
+
windowsTaskbar?.setProgress(0.33);
|
|
1024
|
+
emitProcessingProgress(24, 'analyzing');
|
|
1025
|
+
|
|
1026
|
+
// Generate output document -- uses AI pipeline if an Anthropic key is configured,
|
|
1027
|
+
// otherwise falls back to the free-tier rule-based generator.
|
|
1028
|
+
console.log(
|
|
1029
|
+
`[Main:stopSession] Step 2/6: Running AI analysis pipeline ` +
|
|
1030
|
+
`(settingsManager ${settingsManager ? 'available' : 'NOT available'}, ` +
|
|
1031
|
+
`hasTranscript=${hasTranscript}, hasRecording=${hasRecording})...`
|
|
1032
|
+
);
|
|
1033
|
+
const aiStartedAt = Date.now();
|
|
1034
|
+
let aiTier: 'free' | 'byok' | 'premium' = 'free';
|
|
1035
|
+
let aiEnhanced = false;
|
|
1036
|
+
let aiFallbackReason: string | undefined;
|
|
1037
|
+
const { document } = settingsManager
|
|
1038
|
+
? await aiProcessSession(session, {
|
|
1039
|
+
settingsManager,
|
|
1040
|
+
projectName: session.metadata?.sourceName || 'Feedback Session',
|
|
1041
|
+
screenshotDir: './screenshots',
|
|
1042
|
+
hasRecording,
|
|
1043
|
+
recordingFilename,
|
|
1044
|
+
}).then((result) => {
|
|
1045
|
+
aiTier = result.pipelineOutput.tier;
|
|
1046
|
+
aiEnhanced = result.pipelineOutput.aiEnhanced;
|
|
1047
|
+
aiFallbackReason = result.pipelineOutput.fallbackReason;
|
|
1048
|
+
return result;
|
|
1049
|
+
})
|
|
1050
|
+
: {
|
|
1051
|
+
document: generateDocumentForFileManager(session, {
|
|
1052
|
+
projectName: session.metadata?.sourceName || 'Feedback Session',
|
|
1053
|
+
screenshotDir: './screenshots',
|
|
1054
|
+
}),
|
|
1055
|
+
};
|
|
1056
|
+
aiDurationMs = Date.now() - aiStartedAt;
|
|
1057
|
+
console.log(
|
|
1058
|
+
`[Main:stopSession] Step 2/6 complete: AI analysis took ${aiDurationMs}ms ` +
|
|
1059
|
+
`(tier=${aiTier}, aiEnhanced=${aiEnhanced}${aiFallbackReason ? `, fallback=${aiFallbackReason}` : ''})`
|
|
1060
|
+
);
|
|
1061
|
+
emitProcessingProgress(44, 'analyzing');
|
|
1062
|
+
|
|
1063
|
+
// Update progress: saving to file system (66%)
|
|
1064
|
+
windowsTaskbar?.setProgress(0.66);
|
|
1065
|
+
emitProcessingProgress(56, 'saving');
|
|
1066
|
+
|
|
1067
|
+
// Save to file system
|
|
1068
|
+
console.log('[Main:stopSession] Step 3/6: Saving session to file system...');
|
|
1069
|
+
const saveStartedAt = Date.now();
|
|
1070
|
+
const saveResult = await fileManager.saveSession(session, document);
|
|
1071
|
+
saveDurationMs = Date.now() - saveStartedAt;
|
|
1072
|
+
console.log(`[Main:stopSession] Step 3/6 complete: save took ${saveDurationMs}ms`);
|
|
1073
|
+
if (!saveResult.success) {
|
|
1074
|
+
await cleanupRecordingArtifacts(session.id);
|
|
1075
|
+
sessionController.clearCapturedAudio();
|
|
1076
|
+
windowsTaskbar?.clearProgress();
|
|
1077
|
+
return {
|
|
1078
|
+
success: false,
|
|
1079
|
+
error: saveResult.error || 'Unable to save session report.',
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
emitProcessingProgress(64, 'saving');
|
|
1083
|
+
|
|
1084
|
+
console.log('[Main:stopSession] Step 4/6: Attaching recording and audio artifacts...');
|
|
1085
|
+
const recordingArtifact = await attachRecordingToSessionOutput(
|
|
1086
|
+
session.id,
|
|
1087
|
+
saveResult.sessionDir,
|
|
1088
|
+
saveResult.markdownPath
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
const audioArtifact = await attachAudioToSessionOutput(
|
|
1092
|
+
saveResult.sessionDir,
|
|
1093
|
+
saveResult.markdownPath
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
if (recordingArtifact) {
|
|
1097
|
+
sessionController.setSessionMetadata({
|
|
1098
|
+
recordingPath: recordingArtifact.path,
|
|
1099
|
+
recordingMimeType: recordingArtifact.mimeType,
|
|
1100
|
+
recordingBytes: recordingArtifact.bytesWritten,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (audioArtifact) {
|
|
1104
|
+
sessionController.setSessionMetadata({
|
|
1105
|
+
audioPath: audioArtifact.path,
|
|
1106
|
+
audioBytes: audioArtifact.bytesWritten,
|
|
1107
|
+
audioDurationMs: audioArtifact.durationMs,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
console.log(
|
|
1111
|
+
`[Main:stopSession] Step 4/6 complete: recording=${recordingArtifact ? `${recordingArtifact.bytesWritten}B` : 'none'}, ` +
|
|
1112
|
+
`audio=${audioArtifact ? `${audioArtifact.bytesWritten}B, ${audioArtifact.durationMs}ms` : 'none'}`
|
|
1113
|
+
);
|
|
1114
|
+
emitProcessingProgress(71, 'preparing');
|
|
1115
|
+
|
|
1116
|
+
// ------------------------------------------------------------------
|
|
1117
|
+
// Post-Processing Pipeline
|
|
1118
|
+
// ------------------------------------------------------------------
|
|
1119
|
+
// Run the post-processor if we have audio and/or video artifacts.
|
|
1120
|
+
// Progress and completion events are sent to the renderer via IPC.
|
|
1121
|
+
let postProcessResult: PostProcessResult | null = null;
|
|
1122
|
+
const providedTranscriptSegments = buildPostProcessTranscriptSegments(session);
|
|
1123
|
+
const aiMomentHints = extractAiFrameHintsFromMarkdown(
|
|
1124
|
+
document.content,
|
|
1125
|
+
providedTranscriptSegments
|
|
1126
|
+
);
|
|
1127
|
+
aiFrameHintCount = aiMomentHints.length;
|
|
1128
|
+
|
|
1129
|
+
console.log(
|
|
1130
|
+
`[Main:stopSession] Step 5/6: Post-processing pipeline ` +
|
|
1131
|
+
`(${providedTranscriptSegments.length} pre-provided segments, ` +
|
|
1132
|
+
`${aiMomentHints.length} AI frame hints, ` +
|
|
1133
|
+
`hasAudio=${!!audioArtifact}, hasRecording=${!!recordingArtifact})...`
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
if (audioArtifact || recordingArtifact) {
|
|
1137
|
+
const postProcessStartedAt = Date.now();
|
|
1138
|
+
emitProcessingProgress(74, 'transcribing');
|
|
1139
|
+
try {
|
|
1140
|
+
postProcessResult = await postProcessor.process({
|
|
1141
|
+
videoPath: recordingArtifact?.path ?? '',
|
|
1142
|
+
audioPath: audioArtifact?.path ?? '',
|
|
1143
|
+
sessionDir: saveResult.sessionDir,
|
|
1144
|
+
aiMomentHints,
|
|
1145
|
+
transcriptSegments:
|
|
1146
|
+
providedTranscriptSegments.length > 0
|
|
1147
|
+
? providedTranscriptSegments
|
|
1148
|
+
: undefined,
|
|
1149
|
+
onProgress: (progress: PostProcessProgress) => {
|
|
1150
|
+
const bounded = Math.max(0, Math.min(100, progress.percent));
|
|
1151
|
+
// Map pipeline-local progress into the global stop-session range.
|
|
1152
|
+
const mappedPercent = 72 + bounded * 0.2; // 72% -> 92%
|
|
1153
|
+
emitProcessingProgress(mappedPercent, progress.step);
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
console.log(
|
|
1158
|
+
`[Main:stopSession] Step 5/6 complete: post-processing took ${Date.now() - postProcessStartedAt}ms, ` +
|
|
1159
|
+
`${postProcessResult?.transcriptSegments.length ?? 0} segments, ` +
|
|
1160
|
+
`${postProcessResult?.extractedFrames.length ?? 0} frames extracted`
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
// Notify renderer that post-processing is complete
|
|
1164
|
+
mainWindow?.webContents.send(IPC_CHANNELS.PROCESSING_COMPLETE, postProcessResult);
|
|
1165
|
+
} catch (postProcessError) {
|
|
1166
|
+
console.warn('[Main:stopSession] Step 5/6 FAILED: Post-processing pipeline error, continuing with basic output:', postProcessError);
|
|
1167
|
+
// Non-fatal: we still have the basic markdown report from the AI/rule-based pipeline
|
|
1168
|
+
} finally {
|
|
1169
|
+
postProcessDurationMs = Date.now() - postProcessStartedAt;
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
console.log('[Main:stopSession] Step 5/6 skipped: no audio or recording artifacts available');
|
|
1173
|
+
}
|
|
1174
|
+
emitProcessingProgress(93, 'generating-report');
|
|
1175
|
+
|
|
1176
|
+
console.log('[Main:stopSession] Step 6/6: Finalizing report and copying to clipboard...');
|
|
1177
|
+
if (postProcessResult?.extractedFrames?.length) {
|
|
1178
|
+
await appendExtractedFramesToReport(
|
|
1179
|
+
saveResult.markdownPath,
|
|
1180
|
+
postProcessResult.extractedFrames
|
|
1181
|
+
).catch((error) => {
|
|
1182
|
+
console.warn('[Main] Failed to append extracted frame links to report:', error);
|
|
1183
|
+
});
|
|
1184
|
+
await syncExtractedFrameMetadata(
|
|
1185
|
+
saveResult.sessionDir,
|
|
1186
|
+
postProcessResult.extractedFrames.length
|
|
1187
|
+
);
|
|
1188
|
+
await syncExtractedFrameSummary(
|
|
1189
|
+
saveResult.sessionDir,
|
|
1190
|
+
postProcessResult.extractedFrames.length
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const markdownForPayload = await fs
|
|
1195
|
+
.readFile(saveResult.markdownPath, 'utf-8')
|
|
1196
|
+
.catch(() => document.content);
|
|
1197
|
+
|
|
1198
|
+
// Update progress: copying to clipboard (90%)
|
|
1199
|
+
windowsTaskbar?.setProgress(0.9);
|
|
1200
|
+
emitProcessingProgress(96, 'complete');
|
|
1201
|
+
|
|
1202
|
+
// Copy markdown report path to clipboard (the bridge into AI agents)
|
|
1203
|
+
const clipboardCopied = await copyReportPathToClipboard(saveResult.markdownPath);
|
|
1204
|
+
|
|
1205
|
+
// Complete progress and flash taskbar
|
|
1206
|
+
windowsTaskbar?.setProgress(1);
|
|
1207
|
+
windowsTaskbar?.flashFrame(3);
|
|
1208
|
+
emitProcessingProgress(99, 'complete');
|
|
1209
|
+
|
|
1210
|
+
// Clear progress after a brief delay
|
|
1211
|
+
setTimeout(() => {
|
|
1212
|
+
windowsTaskbar?.clearProgress();
|
|
1213
|
+
}, 1000);
|
|
1214
|
+
|
|
1215
|
+
// Build the review session for the SessionReview component
|
|
1216
|
+
const reviewSession = adaptSessionForMarkdown(session);
|
|
1217
|
+
|
|
1218
|
+
await writeProcessingTrace(saveResult.sessionDir, {
|
|
1219
|
+
reportPath: saveResult.markdownPath,
|
|
1220
|
+
totalMs: Date.now() - stopStartedAt,
|
|
1221
|
+
aiMs: aiDurationMs,
|
|
1222
|
+
saveMs: saveDurationMs,
|
|
1223
|
+
postProcessMs: postProcessDurationMs,
|
|
1224
|
+
audioBytes: audioArtifact?.bytesWritten ?? 0,
|
|
1225
|
+
recordingBytes: recordingArtifact?.bytesWritten ?? 0,
|
|
1226
|
+
transcriptBufferEvents: session.transcriptBuffer.length,
|
|
1227
|
+
providedTranscriptSegments: providedTranscriptSegments.length,
|
|
1228
|
+
aiFrameHints: aiFrameHintCount,
|
|
1229
|
+
postProcessSegments: postProcessResult?.transcriptSegments.length ?? 0,
|
|
1230
|
+
extractedFrames: postProcessResult?.extractedFrames.length ?? 0,
|
|
1231
|
+
aiTier,
|
|
1232
|
+
aiEnhanced,
|
|
1233
|
+
aiFallbackReason,
|
|
1234
|
+
completedAt: new Date().toISOString(),
|
|
1235
|
+
}).catch((error) => {
|
|
1236
|
+
console.warn('[Main] Failed to write processing trace:', error);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
const totalDurationMs = Date.now() - stopStartedAt;
|
|
1240
|
+
console.log(
|
|
1241
|
+
`[Main:stopSession] All steps complete in ${totalDurationMs}ms ` +
|
|
1242
|
+
`(AI: ${aiDurationMs}ms, save: ${saveDurationMs}ms, postProcess: ${postProcessDurationMs}ms). ` +
|
|
1243
|
+
`Report: ${saveResult.markdownPath}`
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
// Notify renderer only after final post-processing/trace bookkeeping is finished.
|
|
1247
|
+
mainWindow?.webContents.send(IPC_CHANNELS.SESSION_COMPLETE, serializeSession(session));
|
|
1248
|
+
mainWindow?.webContents.send(IPC_CHANNELS.OUTPUT_READY, {
|
|
1249
|
+
markdown: markdownForPayload,
|
|
1250
|
+
sessionId: session.id,
|
|
1251
|
+
path: saveResult.markdownPath,
|
|
1252
|
+
reportPath: saveResult.markdownPath,
|
|
1253
|
+
sessionDir: saveResult.sessionDir,
|
|
1254
|
+
recordingPath: recordingArtifact?.path,
|
|
1255
|
+
audioPath: audioArtifact?.path,
|
|
1256
|
+
audioDurationMs: audioArtifact?.durationMs,
|
|
1257
|
+
videoStartTime: recordingArtifact?.startTime,
|
|
1258
|
+
reviewSession,
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// Show completion notification only after trace/write pipeline is done.
|
|
1262
|
+
showSuccessNotification(
|
|
1263
|
+
'Feedback Captured!',
|
|
1264
|
+
clipboardCopied
|
|
1265
|
+
? `${session.feedbackItems.length} items saved. Report path copied to clipboard.`
|
|
1266
|
+
: `${session.feedbackItems.length} items saved. Clipboard copy failed, use Copy Path in the app.`
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
return {
|
|
1270
|
+
success: true,
|
|
1271
|
+
session: serializeSession(session),
|
|
1272
|
+
reportPath: saveResult.markdownPath,
|
|
1273
|
+
};
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
if (stoppedSessionId) {
|
|
1276
|
+
await cleanupRecordingArtifacts(stoppedSessionId);
|
|
1277
|
+
}
|
|
1278
|
+
stopStopPhaseTicker();
|
|
1279
|
+
sessionController.clearCapturedAudio();
|
|
1280
|
+
console.error('[Main] Failed to stop session:', error);
|
|
1281
|
+
windowsTaskbar?.clearProgress();
|
|
1282
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1283
|
+
return {
|
|
1284
|
+
success: false,
|
|
1285
|
+
error: message,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Cancel session without saving
|
|
1292
|
+
*/
|
|
1293
|
+
function cancelSession(): { success: boolean } {
|
|
1294
|
+
const currentSessionId = sessionController.getSession()?.id;
|
|
1295
|
+
sessionController.cancel();
|
|
1296
|
+
crashRecovery.stopTracking();
|
|
1297
|
+
|
|
1298
|
+
if (currentSessionId) {
|
|
1299
|
+
void finalizeScreenRecording(currentSessionId).then(async (artifact) => {
|
|
1300
|
+
if (artifact?.tempPath) {
|
|
1301
|
+
await fs.unlink(artifact.tempPath).catch(() => {
|
|
1302
|
+
// Best-effort cleanup for canceled session recordings.
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
deleteFinalizedRecording(currentSessionId);
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return { success: true };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// =============================================================================
|
|
1313
|
+
// IPC Handlers Setup (delegated to src/main/ipc/ modules)
|
|
1314
|
+
// =============================================================================
|
|
1315
|
+
|
|
1316
|
+
function setupIPC(): void {
|
|
1317
|
+
registerAllHandlers(
|
|
1318
|
+
{
|
|
1319
|
+
getMainWindow: () => mainWindow,
|
|
1320
|
+
getPopover: () => popover,
|
|
1321
|
+
getSettingsManager: () => settingsManager,
|
|
1322
|
+
getWindowsTaskbar: () => windowsTaskbar,
|
|
1323
|
+
getHasCompletedOnboarding: () => hasCompletedOnboarding,
|
|
1324
|
+
setHasCompletedOnboarding: (value: boolean) => { hasCompletedOnboarding = value; },
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
startSession,
|
|
1328
|
+
stopSession,
|
|
1329
|
+
pauseSession,
|
|
1330
|
+
resumeSession,
|
|
1331
|
+
cancelSession,
|
|
1332
|
+
serializeSession,
|
|
1333
|
+
checkPermission,
|
|
1334
|
+
requestPermission,
|
|
1335
|
+
},
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// =============================================================================
|
|
1340
|
+
// Permission Helpers
|
|
1341
|
+
// =============================================================================
|
|
1342
|
+
|
|
1343
|
+
async function checkPermission(type: PermissionType): Promise<boolean> {
|
|
1344
|
+
return permissionManager.isGranted(type);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
async function requestPermission(type: PermissionType): Promise<boolean> {
|
|
1348
|
+
return permissionManager.requestPermission(type);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Check all permissions on startup and show dialog if any are missing
|
|
1353
|
+
* This runs after the window is ready to ensure dialogs are properly parented
|
|
1354
|
+
*/
|
|
1355
|
+
async function checkStartupPermissions(): Promise<void> {
|
|
1356
|
+
if (process.platform !== 'darwin') {
|
|
1357
|
+
// Only macOS has these system-level permissions
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const initial = await permissionManager.checkAllPermissions();
|
|
1362
|
+
|
|
1363
|
+
// On first launch, proactively trigger macOS permission prompts for not-determined states.
|
|
1364
|
+
if (initial.state.microphone === 'not-determined') {
|
|
1365
|
+
await requestPermission('microphone');
|
|
1366
|
+
}
|
|
1367
|
+
if (initial.state.screen === 'not-determined') {
|
|
1368
|
+
await requestPermission('screen');
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const result = await permissionManager.checkAllPermissions();
|
|
1372
|
+
|
|
1373
|
+
if (!result.allGranted && result.missing.length > 0) {
|
|
1374
|
+
// Log which permissions are missing
|
|
1375
|
+
errorHandler.log('warn', 'Missing required permissions on startup', {
|
|
1376
|
+
component: 'Main',
|
|
1377
|
+
operation: 'checkStartupPermissions',
|
|
1378
|
+
data: {
|
|
1379
|
+
missing: result.missing,
|
|
1380
|
+
state: result.state,
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// Show guidance dialog for users who already finished onboarding.
|
|
1385
|
+
// New users will continue through onboarding guidance.
|
|
1386
|
+
if (hasCompletedOnboarding) {
|
|
1387
|
+
// Avoid blocking startup on a modal dialog; show guidance asynchronously.
|
|
1388
|
+
setTimeout(() => {
|
|
1389
|
+
void permissionManager.showStartupPermissionDialog(result.missing).catch((error) => {
|
|
1390
|
+
console.warn('[Main] Startup permission dialog failed:', error);
|
|
1391
|
+
});
|
|
1392
|
+
}, 500);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// =============================================================================
|
|
1398
|
+
// Session Serialization Helper
|
|
1399
|
+
// =============================================================================
|
|
1400
|
+
|
|
1401
|
+
function serializeSession(session: Session): SessionPayload {
|
|
1402
|
+
return {
|
|
1403
|
+
id: session.id,
|
|
1404
|
+
startTime: session.startTime,
|
|
1405
|
+
endTime: session.endTime,
|
|
1406
|
+
state: session.state,
|
|
1407
|
+
sourceId: session.sourceId,
|
|
1408
|
+
feedbackItems: session.feedbackItems.map((item) => ({
|
|
1409
|
+
id: item.id,
|
|
1410
|
+
timestamp: item.timestamp,
|
|
1411
|
+
text: item.text,
|
|
1412
|
+
confidence: item.confidence,
|
|
1413
|
+
hasScreenshot: false, // Screenshots are now extracted in post-processing
|
|
1414
|
+
})),
|
|
1415
|
+
metadata: session.metadata,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// =============================================================================
|
|
1420
|
+
// App Lifecycle
|
|
1421
|
+
// =============================================================================
|
|
1422
|
+
|
|
1423
|
+
// Prevent multiple instances
|
|
1424
|
+
const gotTheLock = app.requestSingleInstanceLock();
|
|
1425
|
+
|
|
1426
|
+
if (!gotTheLock) {
|
|
1427
|
+
console.log('[Main] Another instance is running, quitting...');
|
|
1428
|
+
app.quit();
|
|
1429
|
+
} else {
|
|
1430
|
+
app.on('second-instance', () => {
|
|
1431
|
+
showWindow();
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
app.whenReady().then(async () => {
|
|
1436
|
+
console.log('[Main] App ready, starting initialization...');
|
|
1437
|
+
|
|
1438
|
+
// 1. Initialize error handler first (for crash recovery)
|
|
1439
|
+
await errorHandler.initialize();
|
|
1440
|
+
|
|
1441
|
+
// 1b. Initialize crash recovery manager
|
|
1442
|
+
await crashRecovery.initialize();
|
|
1443
|
+
console.log('[Main] Crash recovery initialized');
|
|
1444
|
+
|
|
1445
|
+
// 2. Initialize settings manager
|
|
1446
|
+
settingsManager = new SettingsManager();
|
|
1447
|
+
console.log('[Main] Settings loaded');
|
|
1448
|
+
|
|
1449
|
+
teardownSettingsListeners.forEach((teardown) => teardown());
|
|
1450
|
+
teardownSettingsListeners = [];
|
|
1451
|
+
teardownSettingsListeners.push(
|
|
1452
|
+
settingsManager.onChange((key, newValue) => {
|
|
1453
|
+
if (key === 'checkForUpdates') {
|
|
1454
|
+
autoUpdaterManager.setAutoCheckEnabled(Boolean(newValue));
|
|
1455
|
+
}
|
|
1456
|
+
}),
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
// 3. Determine onboarding readiness from persisted flag or BYOK keys + transcription path
|
|
1460
|
+
const [hasOpenAiKey, hasAnthropicKey] = await Promise.all([
|
|
1461
|
+
settingsManager.hasApiKey('openai'),
|
|
1462
|
+
settingsManager.hasApiKey('anthropic'),
|
|
1463
|
+
]);
|
|
1464
|
+
const hasLocalWhisperModel = modelDownloadManager.hasAnyModel();
|
|
1465
|
+
const hasTranscriptionPath = hasOpenAiKey || hasLocalWhisperModel;
|
|
1466
|
+
hasCompletedOnboarding = settingsManager.get('hasCompletedOnboarding') || (hasAnthropicKey && hasTranscriptionPath);
|
|
1467
|
+
|
|
1468
|
+
// 5. Initialize session controller
|
|
1469
|
+
await sessionController.initialize();
|
|
1470
|
+
console.log('[Main] Session controller initialized');
|
|
1471
|
+
|
|
1472
|
+
// 7. Initialize tray manager FIRST (needed for popover positioning)
|
|
1473
|
+
trayManager.initialize();
|
|
1474
|
+
console.log('[Main] Tray manager initialized');
|
|
1475
|
+
|
|
1476
|
+
// 8. Create popover window (menu bar native UI)
|
|
1477
|
+
const tray = trayManager.getTray();
|
|
1478
|
+
if (tray) {
|
|
1479
|
+
popover = new PopoverManager({
|
|
1480
|
+
width: POPOVER_SIZES.idle.width,
|
|
1481
|
+
height: POPOVER_SIZES.idle.height,
|
|
1482
|
+
tray: tray,
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
const popoverWindow = popover.create();
|
|
1486
|
+
mainWindow = popoverWindow; // Assign to mainWindow for compatibility
|
|
1487
|
+
sessionController.setMainWindow(popoverWindow);
|
|
1488
|
+
|
|
1489
|
+
attachRendererDiagnostics(popoverWindow, 'Popover');
|
|
1490
|
+
void loadRendererIntoWindow(popoverWindow, 'Popover');
|
|
1491
|
+
|
|
1492
|
+
// Check if onboarding needed after window is ready
|
|
1493
|
+
popoverWindow.once('ready-to-show', () => {
|
|
1494
|
+
console.log('[Main] Popover ready to show');
|
|
1495
|
+
if (!hasCompletedOnboarding) {
|
|
1496
|
+
popoverWindow.webContents.send(IPC_CHANNELS.SHOW_ONBOARDING);
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
console.log('[Main] Popover window created');
|
|
1501
|
+
} else {
|
|
1502
|
+
// Fallback to regular window if tray not available
|
|
1503
|
+
createWindow();
|
|
1504
|
+
console.log('[Main] Fallback: Regular window created (no tray)');
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
wireAudioTelemetry();
|
|
1508
|
+
|
|
1509
|
+
// Set error handler main window
|
|
1510
|
+
errorHandler.setMainWindow(mainWindow!);
|
|
1511
|
+
|
|
1512
|
+
// Set crash recovery main window
|
|
1513
|
+
crashRecovery.setMainWindow(mainWindow!);
|
|
1514
|
+
|
|
1515
|
+
// Set permission manager main window
|
|
1516
|
+
permissionManager.setMainWindow(mainWindow!);
|
|
1517
|
+
|
|
1518
|
+
// 9. Wire up tray click to toggle popover
|
|
1519
|
+
trayManager.onClick(handleTrayClick);
|
|
1520
|
+
trayManager.onSettingsClick(handleSettingsClick);
|
|
1521
|
+
|
|
1522
|
+
// 8b. Initialize menu manager (native macOS menu bar)
|
|
1523
|
+
menuManager.initialize(mainWindow!);
|
|
1524
|
+
menuManager.onAction((action, data) => {
|
|
1525
|
+
handleMenuAction(action, data);
|
|
1526
|
+
});
|
|
1527
|
+
// Load recent sessions into menu
|
|
1528
|
+
const recentSessions = sessionController.getRecentSessions();
|
|
1529
|
+
menuManager.setRecentSessions(
|
|
1530
|
+
recentSessions.map((s) => ({
|
|
1531
|
+
id: s.id,
|
|
1532
|
+
name: s.metadata?.sourceName || 'Feedback Session',
|
|
1533
|
+
path: s.id, // We use ID as path for now
|
|
1534
|
+
date: new Date(s.startTime),
|
|
1535
|
+
}))
|
|
1536
|
+
);
|
|
1537
|
+
console.log('[Main] Menu manager initialized');
|
|
1538
|
+
|
|
1539
|
+
// 8c. Initialize Windows taskbar (Windows only)
|
|
1540
|
+
if (process.platform === 'win32') {
|
|
1541
|
+
windowsTaskbar = createWindowsTaskbar(mainWindow!);
|
|
1542
|
+
windowsTaskbar.setActionCallbacks({
|
|
1543
|
+
onRecord: () => handleToggleRecording(),
|
|
1544
|
+
onStop: () => stopSession(),
|
|
1545
|
+
onScreenshot: () => handleManualScreenshot(),
|
|
1546
|
+
onSettings: () => handleSettingsClick(),
|
|
1547
|
+
});
|
|
1548
|
+
windowsTaskbar.initialize();
|
|
1549
|
+
// Update jump list with recent sessions
|
|
1550
|
+
windowsTaskbar.updateRecentSessions(
|
|
1551
|
+
recentSessions.map((s) => ({
|
|
1552
|
+
id: s.id,
|
|
1553
|
+
name: s.metadata?.sourceName || 'Feedback Session',
|
|
1554
|
+
path: s.id,
|
|
1555
|
+
date: new Date(s.startTime),
|
|
1556
|
+
}))
|
|
1557
|
+
);
|
|
1558
|
+
console.log('[Main] Windows taskbar initialized');
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// 9. Initialize hotkeys
|
|
1562
|
+
initializeHotkeys();
|
|
1563
|
+
|
|
1564
|
+
// 10. Setup IPC handlers
|
|
1565
|
+
setupIPC();
|
|
1566
|
+
|
|
1567
|
+
// 11. Configure session controller event callbacks
|
|
1568
|
+
sessionController.setEventCallbacks({
|
|
1569
|
+
onStateChange: handleSessionStateChange,
|
|
1570
|
+
onFeedbackItem: handleFeedbackItem,
|
|
1571
|
+
onError: handleSessionError,
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
// 12. Initialize auto-updater (only in production)
|
|
1575
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
1576
|
+
autoUpdaterManager.setAutoCheckEnabled(settingsManager.get('checkForUpdates'));
|
|
1577
|
+
autoUpdaterManager.initialize(mainWindow!);
|
|
1578
|
+
console.log('[Main] Auto-updater initialized');
|
|
1579
|
+
} else {
|
|
1580
|
+
console.log('[Main] Auto-updater skipped (development mode)');
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// 13. Check permissions on startup (macOS only)
|
|
1584
|
+
// Delay slightly to ensure window is fully ready
|
|
1585
|
+
setTimeout(async () => {
|
|
1586
|
+
await checkStartupPermissions();
|
|
1587
|
+
console.log('[Main] Startup permission check complete');
|
|
1588
|
+
}, 1000);
|
|
1589
|
+
|
|
1590
|
+
// Handle macOS dock click (fallback, dock is hidden in menu bar mode)
|
|
1591
|
+
app.on('activate', () => {
|
|
1592
|
+
// In menu bar mode, show the popover
|
|
1593
|
+
if (popover) {
|
|
1594
|
+
popover.show();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Fallback for non-popover mode
|
|
1599
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
1600
|
+
createWindow();
|
|
1601
|
+
} else {
|
|
1602
|
+
showWindow();
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
console.log('[Main] markupr initialization complete');
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// Handle all windows closed
|
|
1610
|
+
app.on('window-all-closed', () => {
|
|
1611
|
+
// On macOS, keep the app running in the tray
|
|
1612
|
+
if (process.platform !== 'darwin') {
|
|
1613
|
+
app.quit();
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// Handle before quit
|
|
1618
|
+
app.on('before-quit', () => {
|
|
1619
|
+
isQuitting = true;
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// Handle app quit
|
|
1623
|
+
app.on('will-quit', async () => {
|
|
1624
|
+
console.log('[Main] App quitting, cleaning up...');
|
|
1625
|
+
|
|
1626
|
+
// Stop any active session
|
|
1627
|
+
if (sessionController.getState() === 'recording') {
|
|
1628
|
+
sessionController.cancel();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Best-effort cleanup of temporary recording artifacts.
|
|
1632
|
+
for (const [sessionId] of getActiveScreenRecordings()) {
|
|
1633
|
+
const artifact = await finalizeScreenRecording(sessionId).catch(() => null);
|
|
1634
|
+
if (artifact?.tempPath) {
|
|
1635
|
+
await fs.unlink(artifact.tempPath).catch(() => {});
|
|
1636
|
+
}
|
|
1637
|
+
deleteFinalizedRecording(sessionId);
|
|
1638
|
+
}
|
|
1639
|
+
for (const artifact of getFinalizedScreenRecordings().values()) {
|
|
1640
|
+
await fs.unlink(artifact.tempPath).catch(() => {});
|
|
1641
|
+
}
|
|
1642
|
+
getFinalizedScreenRecordings().clear();
|
|
1643
|
+
|
|
1644
|
+
// Cleanup services
|
|
1645
|
+
teardownAudioTelemetry.forEach((teardown) => teardown());
|
|
1646
|
+
teardownAudioTelemetry = [];
|
|
1647
|
+
teardownSettingsListeners.forEach((teardown) => teardown());
|
|
1648
|
+
teardownSettingsListeners = [];
|
|
1649
|
+
hotkeyManager.unregisterAll();
|
|
1650
|
+
popover?.destroy();
|
|
1651
|
+
trayManager.destroy();
|
|
1652
|
+
menuManager.destroy();
|
|
1653
|
+
windowsTaskbar?.destroy();
|
|
1654
|
+
sessionController.destroy();
|
|
1655
|
+
autoUpdaterManager.destroy();
|
|
1656
|
+
crashRecovery.destroy();
|
|
1657
|
+
|
|
1658
|
+
// Clean up error handler
|
|
1659
|
+
await errorHandler.destroy();
|
|
1660
|
+
|
|
1661
|
+
console.log('[Main] Cleanup complete');
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
// Handle uncaught exceptions
|
|
1665
|
+
process.on('uncaughtException', (error) => {
|
|
1666
|
+
if (isIgnorableStdioError(error)) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
console.error('[Main] Uncaught exception:', error);
|
|
1671
|
+
try {
|
|
1672
|
+
showErrorNotification('markupr Error', error.message);
|
|
1673
|
+
} catch {
|
|
1674
|
+
// Ignore notification errors
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// Handle unhandled promise rejections
|
|
1679
|
+
process.on('unhandledRejection', (reason) => {
|
|
1680
|
+
if (isIgnorableStdioError(reason)) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
console.error('[Main] Unhandled rejection:', reason);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
// Export for testing
|
|
1688
|
+
export {
|
|
1689
|
+
createWindow,
|
|
1690
|
+
startSession,
|
|
1691
|
+
stopSession,
|
|
1692
|
+
showWindow,
|
|
1693
|
+
};
|