@@ -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,54 @@ 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+ event . preventDefault ( ) ;
84+
85+ // If the app isn't ready yet, queue the URL for later processing
86+ if ( ! app . isReady ( ) ) {
87+ pendingDeepLinkUrl = url ;
88+ return ;
89+ }
90+
91+ const deepLinkService = get < DeepLinkService > ( MAIN_TOKENS . DeepLinkService ) ;
92+ deepLinkService . handleUrl ( url ) ;
93+
94+ // Focus the main window when receiving a deep link
95+ if ( mainWindow ) {
96+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
97+ mainWindow . focus ( ) ;
98+ }
99+ } ) ;
100+
101+ // Handle deep link URLs on Windows/Linux (second instance sends URL via command line)
102+ app . on ( "second-instance" , ( _event , commandLine ) => {
103+ const url = commandLine . find ( ( arg ) => arg . startsWith ( "array://" ) ) ;
104+ if ( url ) {
105+ const deepLinkService = get < DeepLinkService > ( MAIN_TOKENS . DeepLinkService ) ;
106+ deepLinkService . handleUrl ( url ) ;
107+ }
108+
109+ if ( mainWindow ) {
110+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
111+ mainWindow . focus ( ) ;
112+ }
113+ } ) ;
114+
64115function ensureClaudeConfigDir ( ) : void {
65116 const existing = process . env . CLAUDE_CONFIG_DIR ;
66117 if ( existing ) return ;
@@ -219,6 +270,17 @@ app.whenReady().then(() => {
219270 createWindow ( ) ;
220271 ensureClaudeConfigDir ( ) ;
221272
273+ // Initialize deep link service and register protocol
274+ const deepLinkService = get < DeepLinkService > ( MAIN_TOKENS . DeepLinkService ) ;
275+ deepLinkService . registerProtocol ( ) ;
276+
277+ // Register OAuth callback handler for deep links
278+ const oauthService = get < OAuthService > ( MAIN_TOKENS . OAuthService ) ;
279+ deepLinkService . registerHandler (
280+ "callback" ,
281+ oauthService . getDeepLinkHandler ( ) ,
282+ ) ;
283+
222284 // Initialize dock badge service for notification badges
223285 dockBadgeService . initialize ( ( ) => mainWindow ) ;
224286
@@ -230,6 +292,21 @@ app.whenReady().then(() => {
230292 getOrRefreshApps ( ) . catch ( ( ) => {
231293 // Silently fail, will retry on first use
232294 } ) ;
295+
296+ // Handle case where app was launched by a deep link
297+ if ( process . platform === "darwin" ) {
298+ // On macOS, the open-url event may have fired before app was ready
299+ if ( pendingDeepLinkUrl ) {
300+ deepLinkService . handleUrl ( pendingDeepLinkUrl ) ;
301+ pendingDeepLinkUrl = null ;
302+ }
303+ } else {
304+ // On Windows/Linux, the URL comes via command line arguments
305+ const deepLinkUrl = process . argv . find ( ( arg ) => arg . startsWith ( "array://" ) ) ;
306+ if ( deepLinkUrl ) {
307+ deepLinkService . handleUrl ( deepLinkUrl ) ;
308+ }
309+ }
233310} ) ;
234311
235312app . on ( "window-all-closed" , async ( ) => {
@@ -260,7 +337,6 @@ registerAutoUpdater(() => mainWindow);
260337ipcMain . handle ( "app:get-version" , ( ) => app . getVersion ( ) ) ;
261338
262339// Register IPC handlers via services
263- registerOAuthHandlers ( ) ;
264340registerGitIpc ( ) ;
265341registerAgentIpc ( taskControllers , ( ) => mainWindow ) ;
266342registerFsIpc ( ) ;
0 commit comments