experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(external-url): delay URL processing to ensure app activation completes

Problem:
- Clicking URLs from external apps while Peek was in background caused the first
click to focus Peek but not open the URL
- Second click would work correctly because app was already focused
- Only affected macOS open-url event handler

Root cause:
- macOS fires 'open-url' event during app activation, not after
- The event handler fired before app was fully focused
- URL processing happened too early, before the app could properly handle it

Solution:
- Added 100ms delay for macOS 'open-url' events from OS
- Ensures app activation completes before processing URL
- Only applies to macOS + OS source (not CLI or second-instance)
- Non-macOS platforms process immediately (no delay)

Test:
- Added test that simulates external URL event (open-url from OS)
- Verifies URL opens correctly on first trigger
- Tests the external:open-url pubsub path

+77 -7
+20 -7
backend/electron/main.ts
··· 1057 1057 return; 1058 1058 } 1059 1059 1060 - // Note: Using trackingSource/trackingSourceId because preload.js overwrites msg.source 1061 - publish(getSystemAddress(), scopes.GLOBAL, 'external:open-url', { 1062 - url, 1063 - trackingSource: 'external', 1064 - trackingSourceId: sourceId, 1065 - timestamp: Date.now() 1066 - }); 1060 + // On macOS, open-url fires during app activation, which can happen before 1061 + // the app is fully focused. Wait a tick to ensure activation completes. 1062 + // This fixes the issue where first click from external app focuses Peek 1063 + // but doesn't open the URL (second click works because app is already focused). 1064 + const processUrl = () => { 1065 + // Note: Using trackingSource/trackingSourceId because preload.js overwrites msg.source 1066 + publish(getSystemAddress(), scopes.GLOBAL, 'external:open-url', { 1067 + url, 1068 + trackingSource: 'external', 1069 + trackingSourceId: sourceId, 1070 + timestamp: Date.now() 1071 + }); 1072 + }; 1073 + 1074 + // Small delay to ensure app activation completes before processing URL 1075 + if (process.platform === 'darwin' && sourceId === 'os') { 1076 + setTimeout(processUrl, 100); 1077 + } else { 1078 + processUrl(); 1079 + } 1067 1080 } 1068 1081 1069 1082 /**
+57
tests/desktop/smoke.spec.ts
··· 784 784 }, openResult.id); 785 785 } 786 786 }); 787 + 788 + test('external URL handler opens URL on first click (simulates OS open-url event)', async () => { 789 + // This test simulates clicking a URL from an external app (like clicking a link 790 + // in another app when Peek is set as default browser). 791 + // Tests the fix for: first click focuses app but doesn't open URL, second click works. 792 + 793 + await waitForExtensionsReady(sharedBgWindow, 15000); 794 + 795 + // Get initial window count 796 + const initialList = await sharedBgWindow.evaluate(async () => { 797 + return await (window as any).app.window.list(); 798 + }); 799 + const initialCount = initialList.windows?.length || 0; 800 + 801 + // Simulate external URL open event (what happens when clicking URL from another app) 802 + // This bypasses the normal window.open flow and tests the handleExternalUrl path 803 + const testUrl = 'https://example.com/external-test'; 804 + 805 + // Trigger external:open-url event directly (simulates what handleExternalUrl does) 806 + await sharedBgWindow.evaluate(async (url: string) => { 807 + const api = (window as any).app; 808 + // Publish the same event that handleExternalUrl publishes 809 + await api.publish('external:open-url', { 810 + url, 811 + trackingSource: 'external', 812 + trackingSourceId: 'os', 813 + timestamp: Date.now() 814 + }); 815 + }, testUrl); 816 + 817 + // Wait for window to be created (give it time to process the event) 818 + await sleep(500); 819 + 820 + // Verify URL was opened 821 + const finalList = await sharedBgWindow.evaluate(async () => { 822 + return await (window as any).app.window.list(); 823 + }); 824 + 825 + expect(finalList.success).toBe(true); 826 + const finalCount = finalList.windows?.length || 0; 827 + 828 + // Should have created a new window 829 + expect(finalCount).toBeGreaterThan(initialCount); 830 + 831 + // Find the window with our test URL 832 + const externalWindow = finalList.windows?.find((w: any) => 833 + w.url && (w.url.includes(testUrl) || w.url.includes(encodeURIComponent(testUrl))) 834 + ); 835 + expect(externalWindow).toBeTruthy(); 836 + 837 + // Clean up 838 + if (externalWindow) { 839 + await sharedBgWindow.evaluate(async (id: number) => { 840 + await (window as any).app.window.close(id); 841 + }, externalWindow.id); 842 + } 843 + }); 787 844 }); 788 845 789 846 // ============================================================================