Skip to content

Commit e15fede

Browse files
eddiesanjuanclaude
andcommitted
fix: guard all window.markupr accesses to prevent crash when preload fails
On fresh installs or after upgrading from older versions, the Electron preload script can fail to load, leaving window.markupr undefined. This caused an immediate crash: "Cannot read properties of undefined (reading 'whisper')" because RecordingContext, UIContext, and ModelDownloadDialog all access window.markupr synchronously on mount without null checks. Added optional chaining guards to all critical mount-time useEffects so the app degrades gracefully instead of crashing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c925d91 commit e15fede

File tree

3 files changed

+21
-9
lines changed

3 files changed

+21
-9
lines changed

src/renderer/components/ModelDownloadDialog.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function useModelDownload(): UseModelDownloadResult {
7878
const [error, setError] = useState<string | null>(null);
7979

8080
useEffect(() => {
81+
if (!window.markupr?.whisper) return;
8182
// Subscribe to download events
8283
const unsubProgress = window.markupr.whisper.onDownloadProgress((p) => {
8384
setProgress(p);
@@ -105,15 +106,15 @@ function useModelDownload(): UseModelDownloadResult {
105106
setError(null);
106107
setProgress(null);
107108

108-
const result = await window.markupr.whisper.downloadModel(model);
109-
if (!result.success && result.error) {
109+
const result = await window.markupr?.whisper?.downloadModel(model);
110+
if (!result?.success && result?.error) {
110111
setError(result.error);
111112
setIsDownloading(false);
112113
}
113114
}, []);
114115

115116
const cancelDownload = useCallback((model: string) => {
116-
window.markupr.whisper.cancelDownload(model);
117+
window.markupr?.whisper?.cancelDownload(model);
117118
setIsDownloading(false);
118119
setProgress(null);
119120
}, []);
@@ -145,6 +146,7 @@ export const ModelDownloadDialog: React.FC<ModelDownloadDialogProps> = ({
145146

146147
// Load available models on mount
147148
useEffect(() => {
149+
if (!window.markupr?.whisper) return;
148150
const loadModels = async () => {
149151
const availableModels = await window.markupr.whisper.getAvailableModels();
150152
setModels(availableModels);
@@ -163,6 +165,7 @@ export const ModelDownloadDialog: React.FC<ModelDownloadDialogProps> = ({
163165

164166
// Listen for download complete to transition state
165167
useEffect(() => {
168+
if (!window.markupr?.whisper) return;
166169
const unsubComplete = window.markupr.whisper.onDownloadComplete(() => {
167170
setState('complete');
168171
});
@@ -494,6 +497,10 @@ export function useModelCheck(): ModelCheckResult {
494497

495498
const checkModel = async () => {
496499
try {
500+
if (!window.markupr?.whisper) {
501+
setNeedsDownload(true);
502+
return;
503+
}
497504
// Check if we have any transcription capability (OpenAI or Whisper)
498505
const hasCapability = await withTimeout(
499506
window.markupr.whisper.hasTranscriptionCapability(),

src/renderer/contexts/RecordingContext.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export const RecordingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
173173
// Transcription capability check
174174
// ---------------------------------------------------------------------------
175175
useEffect(() => {
176+
if (!window.markupr?.whisper) return;
176177
window.markupr.whisper
177178
.hasTranscriptionCapability()
178179
.then((ready) => setHasTranscriptionCapability(ready))
@@ -289,6 +290,7 @@ export const RecordingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
289290
// Session IPC listeners
290291
// ---------------------------------------------------------------------------
291292
useEffect(() => {
293+
if (!window.markupr?.session) return;
292294
let mounted = true;
293295
const recorder = screenRecorderRef.current;
294296

@@ -425,6 +427,7 @@ export const RecordingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
425427
// Audio level + voice activity listeners
426428
// ---------------------------------------------------------------------------
427429
useEffect(() => {
430+
if (!window.markupr?.audio) return;
428431
const unsubLevel = window.markupr.audio.onLevel((level) => {
429432
setAudioLevel(level);
430433
});
@@ -490,8 +493,8 @@ export const RecordingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
490493
if (!result.success) {
491494
setState('error');
492495
setErrorMessage(result.error || 'Unable to start session.');
493-
window.markupr.whisper
494-
.hasTranscriptionCapability()
496+
window.markupr?.whisper
497+
?.hasTranscriptionCapability()
495498
.then((ready) => setHasTranscriptionCapability(ready))
496499
.catch(() => {});
497500
} else {

src/renderer/contexts/UIContext.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
109109
const [hasRequiredByokKeys, setHasRequiredByokKeys] = useState<boolean | null>(null);
110110

111111
useEffect(() => {
112+
if (!window.markupr?.settings) return;
112113
let mounted = true;
113114
const loadInitialSettings = async () => {
114115
try {
@@ -140,7 +141,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
140141
// Navigation event listeners (from main process menu/tray)
141142
// ---------------------------------------------------------------------------
142143
useEffect(() => {
143-
const nav = window.markupr.navigation;
144+
const nav = window.markupr?.navigation;
144145
if (!nav) return;
145146

146147
const unsubSettings = nav.onShowSettings(() => setCurrentView('settings'));
@@ -172,6 +173,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
172173
// Popover resize on state/view change
173174
// ---------------------------------------------------------------------------
174175
useEffect(() => {
176+
if (!window.markupr?.popover) return;
175177
if (currentView !== 'main') {
176178
const { width, height } = mapOverlaySize(currentView);
177179
window.markupr.popover.resize(width, height).catch(() => {});
@@ -248,9 +250,9 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
248250

249251
const handleOnboardingComplete = useCallback(() => {
250252
setShowOnboarding(false);
251-
window.markupr.setSettings({ hasCompletedOnboarding: true }).catch(() => {});
252-
window.markupr.whisper
253-
.hasTranscriptionCapability()
253+
window.markupr?.setSettings({ hasCompletedOnboarding: true }).catch(() => {});
254+
window.markupr?.whisper
255+
?.hasTranscriptionCapability()
254256
.then(() => {})
255257
.catch(() => {});
256258
}, []);

0 commit comments

Comments
 (0)