@@ -14,7 +14,11 @@ import {
1414import { createIPCHandler } from "trpc-electron/main" ;
1515import "./lib/logger" ;
1616import { ANALYTICS_EVENTS } from "../types/analytics.js" ;
17+ import { get } from "./di/container.js" ;
18+ import { MAIN_TOKENS } from "./di/tokens.js" ;
19+ import type { DeepLinkService } from "./services/deep-link/service.js" ;
1720import { dockBadgeService } from "./services/dockBadge.js" ;
21+ import type { OAuthService } from "./services/oauth/service.js" ;
1822import {
1923 cleanupAgentSessions ,
2024 registerAgentIpc ,
@@ -34,7 +38,6 @@ import {
3438 getOrRefreshApps ,
3539 registerExternalAppsIpc ,
3640} from "./services/externalApps.js" ;
37- import { registerOAuthHandlers } from "./services/oauth.js" ;
3841import {
3942 initializePostHog ,
4043 shutdownPostHog ,
@@ -61,6 +64,55 @@ dns.setDefaultResultOrder("ipv4first");
6164// Set app name to ensure consistent userData path across platforms
6265app . setName ( "Array" ) ;
6366
67+ // Single instance lock must be acquired FIRST before any other app setup
68+ // This ensures deep links go to the existing instance, not a new one
69+ // In development, we need to pass the same args that setAsDefaultProtocolClient uses
70+ const additionalData = process . defaultApp ? { argv : process . argv } : undefined ;
71+ const gotTheLock = app . requestSingleInstanceLock ( additionalData ) ;
72+ if ( ! gotTheLock ) {
73+ app . quit ( ) ;
74+ // Must exit immediately to prevent any further initialization
75+ process . exit ( 0 ) ;
76+ }
77+
78+ // Queue to hold deep link URLs received before app is ready
79+ let pendingDeepLinkUrl : string | null = null ;
80+
81+ // Handle deep link URLs on macOS - must be registered before app is ready
82+ app . on ( "open-url" , ( event , url ) => {
83+ console . log ( "open-url" , url ) ;
84+ event . preventDefault ( ) ;
85+
86+ // If the app isn't ready yet, queue the URL for later processing
87+ if ( ! app . isReady ( ) ) {
88+ pendingDeepLinkUrl = url ;
89+ return ;
90+ }
91+
92+ const deepLinkService = get < DeepLinkService > ( MAIN_TOKENS . DeepLinkService ) ;
93+ deepLinkService . handleUrl ( url ) ;
94+
95+ // Focus the main window when receiving a deep link
96+ if ( mainWindow ) {
97+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
98+ mainWindow . focus ( ) ;
99+ }
100+ } ) ;
101+
102+ // Handle deep link URLs on Windows/Linux (second instance sends URL via command line)
103+ app . on ( "second-instance" , ( _event , commandLine ) => {
104+ const url = commandLine . find ( ( arg ) => arg . startsWith ( "array://" ) ) ;
105+ if ( url ) {
106+ const deepLinkService = get < DeepLinkService > ( MAIN_TOKENS . DeepLinkService ) ;
107+ deepLinkService . handleUrl ( url ) ;
108+ }
109+
110+ if ( mainWindow ) {
111+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
112+ mainWindow . focus ( ) ;
113+ }
114+ } ) ;
115+
64116function ensureClaudeConfigDir ( ) : void {
65117 const existing = process . env . CLAUDE_CONFIG_DIR ;
66118 if ( existing ) return ;
@@ -219,6 +271,14 @@ app.whenReady().then(() => {
219271 createWindow ( ) ;
220272 ensureClaudeConfigDir ( ) ;
221273
274+ // Initialize deep link service and register protocol
275+ const deepLinkService = get < DeepLinkService > ( MAIN_TOKENS . DeepLinkService ) ;
276+ deepLinkService . registerProtocol ( ) ;
277+
278+ // Register OAuth callback handler for deep links
279+ const oauthService = get < OAuthService > ( MAIN_TOKENS . OAuthService ) ;
280+ deepLinkService . registerHandler ( "callback" , oauthService . getDeepLinkHandler ( ) ) ;
281+
222282 // Initialize dock badge service for notification badges
223283 dockBadgeService . initialize ( ( ) => mainWindow ) ;
224284
@@ -230,6 +290,21 @@ app.whenReady().then(() => {
230290 getOrRefreshApps ( ) . catch ( ( ) => {
231291 // Silently fail, will retry on first use
232292 } ) ;
293+
294+ // Handle case where app was launched by a deep link
295+ if ( process . platform === "darwin" ) {
296+ // On macOS, the open-url event may have fired before app was ready
297+ if ( pendingDeepLinkUrl ) {
298+ deepLinkService . handleUrl ( pendingDeepLinkUrl ) ;
299+ pendingDeepLinkUrl = null ;
300+ }
301+ } else {
302+ // On Windows/Linux, the URL comes via command line arguments
303+ const deepLinkUrl = process . argv . find ( ( arg ) => arg . startsWith ( "array://" ) ) ;
304+ if ( deepLinkUrl ) {
305+ deepLinkService . handleUrl ( deepLinkUrl ) ;
306+ }
307+ }
233308} ) ;
234309
235310app . on ( "window-all-closed" , async ( ) => {
@@ -260,7 +335,6 @@ registerAutoUpdater(() => mainWindow);
260335ipcMain . handle ( "app:get-version" , ( ) => app . getVersion ( ) ) ;
261336
262337// Register IPC handlers via services
263- registerOAuthHandlers ( ) ;
264338registerGitIpc ( ) ;
265339registerAgentIpc ( taskControllers , ( ) => mainWindow ) ;
266340registerFsIpc ( ) ;
0 commit comments