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,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings IPC Handlers
|
|
3
|
+
*
|
|
4
|
+
* Registers IPC handlers for settings read/write, API key management,
|
|
5
|
+
* permissions, hotkeys, and crash recovery configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ipcMain, dialog, app } from 'electron';
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { sessionController } from '../SessionController';
|
|
12
|
+
import { hotkeyManager } from '../HotkeyManager';
|
|
13
|
+
import { crashRecovery } from '../CrashRecovery';
|
|
14
|
+
import {
|
|
15
|
+
IPC_CHANNELS,
|
|
16
|
+
DEFAULT_SETTINGS,
|
|
17
|
+
type AppSettings,
|
|
18
|
+
type HotkeyConfig,
|
|
19
|
+
type PermissionType,
|
|
20
|
+
type PermissionStatus,
|
|
21
|
+
type ApiKeyValidationResult,
|
|
22
|
+
} from '../../shared/types';
|
|
23
|
+
import type { IpcContext, SessionActions } from './types';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// API Key Validation
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
type ApiKeyProvider = 'openai' | 'anthropic';
|
|
30
|
+
|
|
31
|
+
async function validateProviderApiKey(
|
|
32
|
+
service: ApiKeyProvider,
|
|
33
|
+
key: string,
|
|
34
|
+
): Promise<ApiKeyValidationResult> {
|
|
35
|
+
const trimmedKey = typeof key === 'string' ? key.trim() : '';
|
|
36
|
+
|
|
37
|
+
if (trimmedKey.length < 10) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
error: 'Please enter a valid API key.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeoutHandle = setTimeout(() => controller.abort(), 12000);
|
|
46
|
+
|
|
47
|
+
const requestConfig = service === 'openai'
|
|
48
|
+
? {
|
|
49
|
+
url: 'https://api.openai.com/v1/audio/transcriptions',
|
|
50
|
+
method: 'POST' as const,
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${trimmedKey}`,
|
|
53
|
+
} as Record<string, string>,
|
|
54
|
+
body: new FormData() as BodyInit,
|
|
55
|
+
}
|
|
56
|
+
: {
|
|
57
|
+
url: 'https://api.anthropic.com/v1/models?limit=1',
|
|
58
|
+
method: 'GET' as const,
|
|
59
|
+
headers: {
|
|
60
|
+
'x-api-key': trimmedKey,
|
|
61
|
+
'anthropic-version': '2023-06-01',
|
|
62
|
+
} as Record<string, string>,
|
|
63
|
+
body: undefined as BodyInit | undefined,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(requestConfig.url, {
|
|
68
|
+
method: requestConfig.method,
|
|
69
|
+
headers: requestConfig.headers,
|
|
70
|
+
body: requestConfig.body,
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (service === 'openai' && response.status === 400) {
|
|
75
|
+
return { valid: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (response.ok) {
|
|
79
|
+
return { valid: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (service === 'openai' && (response.status === 401 || response.status === 403)) {
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
status: response.status,
|
|
86
|
+
error:
|
|
87
|
+
response.status === 401
|
|
88
|
+
? 'Invalid OpenAI API key. Please check and try again.'
|
|
89
|
+
: 'OpenAI key is valid but missing required permissions. Enable model/audio access for this project key and try again.',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (service === 'anthropic' && (response.status === 401 || response.status === 403)) {
|
|
94
|
+
return {
|
|
95
|
+
valid: false,
|
|
96
|
+
status: response.status,
|
|
97
|
+
error: 'Invalid Anthropic API key. Please check and try again.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const providerLabel = service === 'openai' ? 'OpenAI' : 'Anthropic';
|
|
102
|
+
return {
|
|
103
|
+
valid: false,
|
|
104
|
+
status: response.status,
|
|
105
|
+
error: `${providerLabel} API error (${response.status}). Please try again.`,
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: 'Request timed out. Please check your connection and try again.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
error: 'Unable to reach API service. Check internet/VPN/firewall and try again.',
|
|
118
|
+
};
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timeoutHandle);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// IPC Registration
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
export function registerSettingsHandlers(ctx: IpcContext, actions: SessionActions): void {
|
|
129
|
+
const { getMainWindow, getSettingsManager, setHasCompletedOnboarding } = ctx;
|
|
130
|
+
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
// Settings Channels
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET, (_, key: keyof AppSettings) => {
|
|
136
|
+
return getSettingsManager()?.get(key) ?? DEFAULT_SETTINGS[key];
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_ALL, (): AppSettings => {
|
|
140
|
+
return getSettingsManager()?.getAll() ?? { ...DEFAULT_SETTINGS };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
ipcMain.handle(
|
|
144
|
+
IPC_CHANNELS.SETTINGS_SET,
|
|
145
|
+
(_, key: keyof AppSettings, value: unknown): AppSettings => {
|
|
146
|
+
const updates = { [key]: value } as Partial<AppSettings>;
|
|
147
|
+
return getSettingsManager()?.update(updates) ?? { ...DEFAULT_SETTINGS, ...updates };
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_SELECT_DIRECTORY, async (): Promise<string | null> => {
|
|
152
|
+
const mainWindow = getMainWindow();
|
|
153
|
+
const options: Electron.OpenDialogOptions = {
|
|
154
|
+
title: 'Select Feedback Output Folder',
|
|
155
|
+
buttonLabel: 'Use Folder',
|
|
156
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
157
|
+
};
|
|
158
|
+
const result = mainWindow
|
|
159
|
+
? await dialog.showOpenDialog(mainWindow, options)
|
|
160
|
+
: await dialog.showOpenDialog(options);
|
|
161
|
+
|
|
162
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const selected = result.filePaths[0];
|
|
167
|
+
getSettingsManager()?.update({ outputDirectory: selected });
|
|
168
|
+
return selected;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_CLEAR_ALL_DATA, async (): Promise<void> => {
|
|
172
|
+
const settingsManager = getSettingsManager();
|
|
173
|
+
if (!settingsManager) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const outputDirectory = settingsManager.get('outputDirectory');
|
|
178
|
+
await fs.rm(outputDirectory, { recursive: true, force: true }).catch(() => {});
|
|
179
|
+
|
|
180
|
+
await settingsManager.deleteApiKey('openai').catch(() => {});
|
|
181
|
+
await settingsManager.deleteApiKey('anthropic').catch(() => {});
|
|
182
|
+
|
|
183
|
+
settingsManager.reset();
|
|
184
|
+
crashRecovery.discardIncompleteSession();
|
|
185
|
+
crashRecovery.clearCrashLogs();
|
|
186
|
+
sessionController.reset();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_EXPORT, async (): Promise<void> => {
|
|
190
|
+
const settingsManager = getSettingsManager();
|
|
191
|
+
if (!settingsManager) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const mainWindow = getMainWindow();
|
|
196
|
+
const options: Electron.SaveDialogOptions = {
|
|
197
|
+
title: 'Export markupr Settings',
|
|
198
|
+
defaultPath: join(app.getPath('documents'), 'markupr-settings.json'),
|
|
199
|
+
filters: [{ name: 'JSON', extensions: ['json'] }],
|
|
200
|
+
};
|
|
201
|
+
const result = mainWindow
|
|
202
|
+
? await dialog.showSaveDialog(mainWindow, options)
|
|
203
|
+
: await dialog.showSaveDialog(options);
|
|
204
|
+
|
|
205
|
+
if (result.canceled || !result.filePath) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const payload = JSON.stringify(settingsManager.getAll(), null, 2);
|
|
210
|
+
await fs.writeFile(result.filePath, payload, 'utf-8');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
ipcMain.handle(IPC_CHANNELS.SETTINGS_IMPORT, async (): Promise<AppSettings | null> => {
|
|
214
|
+
const settingsManager = getSettingsManager();
|
|
215
|
+
if (!settingsManager) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const mainWindow = getMainWindow();
|
|
220
|
+
const options: Electron.OpenDialogOptions = {
|
|
221
|
+
title: 'Import markupr Settings',
|
|
222
|
+
properties: ['openFile'],
|
|
223
|
+
filters: [{ name: 'JSON', extensions: ['json'] }],
|
|
224
|
+
};
|
|
225
|
+
const result = mainWindow
|
|
226
|
+
? await dialog.showOpenDialog(mainWindow, options)
|
|
227
|
+
: await dialog.showOpenDialog(options);
|
|
228
|
+
|
|
229
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const raw = await fs.readFile(result.filePaths[0], 'utf-8');
|
|
234
|
+
const parsed: unknown = JSON.parse(raw);
|
|
235
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
236
|
+
throw new Error('Invalid settings file format.');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const entries = Object.entries(parsed as Record<string, unknown>);
|
|
240
|
+
const allowedKeys = new Set(Object.keys(DEFAULT_SETTINGS));
|
|
241
|
+
const sanitized: Partial<AppSettings> = {};
|
|
242
|
+
|
|
243
|
+
for (const [key, value] of entries) {
|
|
244
|
+
if (!allowedKeys.has(key)) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
(sanitized as Record<string, unknown>)[key] = value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return settingsManager.update(sanitized);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Legacy settings handlers
|
|
254
|
+
ipcMain.handle(IPC_CHANNELS.GET_SETTINGS, () => {
|
|
255
|
+
return getSettingsManager()?.getAll() ?? { ...DEFAULT_SETTINGS };
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
ipcMain.handle(IPC_CHANNELS.SET_SETTINGS, (_, newSettings: Partial<AppSettings>) => {
|
|
259
|
+
const settings = getSettingsManager()?.update(newSettings) ?? {
|
|
260
|
+
...DEFAULT_SETTINGS,
|
|
261
|
+
...newSettings,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (newSettings.hotkeys) {
|
|
265
|
+
const results = hotkeyManager.updateConfig(newSettings.hotkeys);
|
|
266
|
+
console.log('[Main] Hotkeys updated:', results);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (newSettings.hasCompletedOnboarding) {
|
|
270
|
+
setHasCompletedOnboarding(true);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return settings;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
// API Key Channels (Secure Storage)
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
ipcMain.handle(
|
|
281
|
+
IPC_CHANNELS.SETTINGS_GET_API_KEY,
|
|
282
|
+
async (_, service: string): Promise<string | null> => {
|
|
283
|
+
return getSettingsManager()?.getApiKey(service) ?? null;
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
ipcMain.handle(
|
|
288
|
+
IPC_CHANNELS.SETTINGS_SET_API_KEY,
|
|
289
|
+
async (_, service: string, key: string): Promise<boolean> => {
|
|
290
|
+
const settingsManager = getSettingsManager();
|
|
291
|
+
if (!settingsManager) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
await settingsManager.setApiKey(service, key);
|
|
297
|
+
|
|
298
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
299
|
+
const persisted = await settingsManager.getApiKey(service);
|
|
300
|
+
if (persisted && persisted.trim().length > 0) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (attempt < 2) {
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (key.trim().length > 0) {
|
|
310
|
+
console.warn(
|
|
311
|
+
`[Main] ${service} API key write verification timed out; accepting write success.`
|
|
312
|
+
);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return false;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error(`[Main] Failed to store ${service} API key:`, error);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
ipcMain.handle(
|
|
325
|
+
IPC_CHANNELS.SETTINGS_DELETE_API_KEY,
|
|
326
|
+
async (_, service: string): Promise<boolean> => {
|
|
327
|
+
const settingsManager = getSettingsManager();
|
|
328
|
+
if (!settingsManager) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await settingsManager.deleteApiKey(service);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
ipcMain.handle(
|
|
338
|
+
IPC_CHANNELS.SETTINGS_HAS_API_KEY,
|
|
339
|
+
async (_, service: string): Promise<boolean> => {
|
|
340
|
+
return getSettingsManager()?.hasApiKey(service) ?? false;
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
ipcMain.handle(
|
|
345
|
+
IPC_CHANNELS.SETTINGS_TEST_API_KEY,
|
|
346
|
+
async (_, service: ApiKeyProvider, key: string): Promise<ApiKeyValidationResult> => {
|
|
347
|
+
if (service !== 'openai' && service !== 'anthropic') {
|
|
348
|
+
return {
|
|
349
|
+
valid: false,
|
|
350
|
+
error: 'Unsupported API provider.',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
return await validateProviderApiKey(service, key);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const detail = error instanceof Error ? error.message : 'Unknown validation error';
|
|
358
|
+
console.error(`[Main] API key validation failed for ${service}:`, error);
|
|
359
|
+
return {
|
|
360
|
+
valid: false,
|
|
361
|
+
error: `Unable to validate ${service} API key (${detail}).`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// -------------------------------------------------------------------------
|
|
368
|
+
// Permissions Channels
|
|
369
|
+
// -------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
ipcMain.handle(
|
|
372
|
+
IPC_CHANNELS.PERMISSIONS_CHECK,
|
|
373
|
+
async (_, type: PermissionType): Promise<boolean> => {
|
|
374
|
+
return actions.checkPermission(type);
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
ipcMain.handle(
|
|
379
|
+
IPC_CHANNELS.PERMISSIONS_REQUEST,
|
|
380
|
+
async (_, type: PermissionType): Promise<boolean> => {
|
|
381
|
+
return actions.requestPermission(type);
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
ipcMain.handle(IPC_CHANNELS.PERMISSIONS_GET_ALL, async (): Promise<PermissionStatus> => {
|
|
386
|
+
return {
|
|
387
|
+
microphone: await actions.checkPermission('microphone'),
|
|
388
|
+
screen: await actions.checkPermission('screen'),
|
|
389
|
+
accessibility: await actions.checkPermission('accessibility'),
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// -------------------------------------------------------------------------
|
|
394
|
+
// Hotkey Channels
|
|
395
|
+
// -------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
ipcMain.handle(IPC_CHANNELS.HOTKEY_CONFIG, (): HotkeyConfig => {
|
|
398
|
+
return hotkeyManager.getConfig();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
ipcMain.handle(
|
|
402
|
+
IPC_CHANNELS.HOTKEY_UPDATE,
|
|
403
|
+
(_, newConfig: Partial<HotkeyConfig>) => {
|
|
404
|
+
const results = hotkeyManager.updateConfig(newConfig);
|
|
405
|
+
getSettingsManager()?.update({ hotkeys: hotkeyManager.getConfig() });
|
|
406
|
+
return { config: hotkeyManager.getConfig(), results };
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// -------------------------------------------------------------------------
|
|
411
|
+
// Crash Recovery Channels
|
|
412
|
+
// -------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
ipcMain.handle(IPC_CHANNELS.CRASH_RECOVERY_CHECK, () => {
|
|
415
|
+
const session = crashRecovery.getIncompleteSession();
|
|
416
|
+
return {
|
|
417
|
+
hasIncomplete: !!session,
|
|
418
|
+
session: session,
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
ipcMain.handle(IPC_CHANNELS.CRASH_RECOVERY_RECOVER, (_, sessionId: string) => {
|
|
423
|
+
const session = crashRecovery.getIncompleteSession();
|
|
424
|
+
if (!session || session.id !== sessionId) {
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
error: 'Session not found or ID mismatch',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
crashRecovery.discardIncompleteSession();
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
success: true,
|
|
435
|
+
session: {
|
|
436
|
+
id: session.id,
|
|
437
|
+
feedbackItems: session.feedbackItems,
|
|
438
|
+
startTime: session.startTime,
|
|
439
|
+
sourceName: session.sourceName,
|
|
440
|
+
screenshotCount: session.screenshotCount,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
ipcMain.handle(IPC_CHANNELS.CRASH_RECOVERY_DISCARD, () => {
|
|
446
|
+
crashRecovery.discardIncompleteSession();
|
|
447
|
+
return { success: true };
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
ipcMain.handle(IPC_CHANNELS.CRASH_RECOVERY_GET_LOGS, (_, limit?: number) => {
|
|
451
|
+
return crashRecovery.getCrashLogs(limit);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
ipcMain.handle(IPC_CHANNELS.CRASH_RECOVERY_CLEAR_LOGS, () => {
|
|
455
|
+
crashRecovery.clearCrashLogs();
|
|
456
|
+
return { success: true };
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
ipcMain.handle(
|
|
460
|
+
IPC_CHANNELS.CRASH_RECOVERY_UPDATE_SETTINGS,
|
|
461
|
+
(_, settings: Partial<{
|
|
462
|
+
enableAutoSave: boolean;
|
|
463
|
+
autoSaveIntervalMs: number;
|
|
464
|
+
enableCrashReporting: boolean;
|
|
465
|
+
maxCrashLogs: number;
|
|
466
|
+
}>) => {
|
|
467
|
+
crashRecovery.updateSettings(settings);
|
|
468
|
+
return { success: true };
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC Handler Dependency Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the context interface that IPC handler modules receive
|
|
5
|
+
* from the main process entry point. Getters are used for mutable state
|
|
6
|
+
* that changes after initialization.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { BrowserWindow } from 'electron';
|
|
10
|
+
import type { SettingsManager } from '../settings';
|
|
11
|
+
import type { PopoverManager } from '../windows';
|
|
12
|
+
import type { WindowsTaskbar } from '../platform';
|
|
13
|
+
import type { Session } from '../SessionController';
|
|
14
|
+
import type {
|
|
15
|
+
SessionPayload,
|
|
16
|
+
PermissionType,
|
|
17
|
+
} from '../../shared/types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shared context passed to all IPC handler modules.
|
|
21
|
+
* Singleton services are imported directly by each module;
|
|
22
|
+
* only mutable/lazy state needs to flow through here.
|
|
23
|
+
*/
|
|
24
|
+
export interface IpcContext {
|
|
25
|
+
getMainWindow: () => BrowserWindow | null;
|
|
26
|
+
getPopover: () => PopoverManager | null;
|
|
27
|
+
getSettingsManager: () => SettingsManager | null;
|
|
28
|
+
getWindowsTaskbar: () => WindowsTaskbar | null;
|
|
29
|
+
getHasCompletedOnboarding: () => boolean;
|
|
30
|
+
setHasCompletedOnboarding: (value: boolean) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Session-related action functions defined in the main entry point.
|
|
35
|
+
* These encapsulate complex multi-service orchestration and are
|
|
36
|
+
* passed to handler modules rather than imported.
|
|
37
|
+
*/
|
|
38
|
+
export interface SessionActions {
|
|
39
|
+
startSession: (sourceId?: string, sourceName?: string) => Promise<{
|
|
40
|
+
success: boolean;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
}>;
|
|
44
|
+
stopSession: () => Promise<{
|
|
45
|
+
success: boolean;
|
|
46
|
+
session?: SessionPayload;
|
|
47
|
+
reportPath?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|
|
50
|
+
pauseSession: () => { success: boolean; error?: string };
|
|
51
|
+
resumeSession: () => { success: boolean; error?: string };
|
|
52
|
+
cancelSession: () => { success: boolean };
|
|
53
|
+
serializeSession: (session: Session) => SessionPayload;
|
|
54
|
+
checkPermission: (type: PermissionType) => Promise<boolean>;
|
|
55
|
+
requestPermission: (type: PermissionType) => Promise<boolean>;
|
|
56
|
+
}
|