@@ -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,17 @@ 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 (
281+ "callback" ,
282+ oauthService . getDeepLinkHandler ( ) ,
283+ ) ;
284+
222285 // Initialize dock badge service for notification badges
223286 dockBadgeService . initialize ( ( ) => mainWindow ) ;
224287
@@ -230,6 +293,21 @@ app.whenReady().then(() => {
230293 getOrRefreshApps ( ) . catch ( ( ) => {
231294 // Silently fail, will retry on first use
232295 } ) ;
296+
297+ // Handle case where app was launched by a deep link
298+ if ( process . platform === "darwin" ) {
299+ // On macOS, the open-url event may have fired before app was ready
300+ if ( pendingDeepLinkUrl ) {
301+ deepLinkService . handleUrl ( pendingDeepLinkUrl ) ;
302+ pendingDeepLinkUrl = null ;
303+ }
304+ } else {
305+ // On Windows/Linux, the URL comes via command line arguments
306+ const deepLinkUrl = process . argv . find ( ( arg ) => arg . startsWith ( "array://" ) ) ;
307+ if ( deepLinkUrl ) {
308+ deepLinkService . handleUrl ( deepLinkUrl ) ;
309+ }
310+ }
233311} ) ;
234312
235313app . on ( "window-all-closed" , async ( ) => {
@@ -260,7 +338,6 @@ registerAutoUpdater(() => mainWindow);
260338ipcMain . handle ( "app:get-version" , ( ) => app . getVersion ( ) ) ;
261339
262340// Register IPC handlers via services
263- registerOAuthHandlers ( ) ;
264341registerGitIpc ( ) ;
265342registerAgentIpc ( taskControllers , ( ) => mainWindow ) ;
266343registerFsIpc ( ) ;
0 commit comments