@@ -77,8 +77,10 @@ class ContextManager: ObservableObject {
7777 @Published var contexts : [ Context ] = [ ]
7878 @Published var activeContext : Context ?
7979
80- // Mapping from context UUID to Chrome window ID
81- private var chromeWindowIDs : [ UUID : Int ] = [ : ]
80+ // Mapping from context UUID to Chrome window IDs
81+ private var chromeWindowIDs : [ UUID : [ Int ] ] = [ : ]
82+ // Mapping from context UUID to Safari window IDs
83+ private var safariWindowIDs : [ UUID : [ Int ] ] = [ : ]
8284
8385 private let appName = Bundle . main. bundleIdentifier ?? " Flitro "
8486 private let appSupportURL = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) . first!
@@ -243,6 +245,8 @@ class ContextManager: ObservableObject {
243245
244246 // Close any Chrome window opened for this context (if tracked)
245247 closeChromeWindowForContext ( context. id)
248+ // Close any Safari windows opened for this context (if tracked)
249+ closeSafariWindowsForContext ( context. id)
246250 }
247251
248252 private func isSystemApp( _ bundleIdentifier: String ) -> Bool {
@@ -295,29 +299,41 @@ class ContextManager: ObservableObject {
295299 }
296300
297301 private func launchContextBrowserTabs( _ context: Context ) {
298- // Determine if Chrome is the default browser
299- let isChromeDefault = getDefaultBrowser ( ) == " chrome "
300- // Group tabs by browser, including 'default' tabs in Chrome if Chrome is default
301- let chromeTabs : [ BrowserTab ]
302- let otherTabs : [ BrowserTab ]
303- if isChromeDefault {
304- chromeTabs = context. browserTabs. filter { tab in
305- let b = tab. browser. lowercased ( )
306- return b == " chrome " || b == " default "
307- }
308- otherTabs = context. browserTabs. filter { tab in
309- let b = tab. browser. lowercased ( )
310- return b != " chrome " && b != " default "
311- }
312- } else {
313- chromeTabs = context. browserTabs. filter { $0. browser. lowercased ( ) == " chrome " }
314- otherTabs = context. browserTabs. filter { $0. browser. lowercased ( ) != " chrome " }
302+ // Group tabs by explicit browser type
303+ var chromeTabs = context. browserTabs. filter { $0. browser. lowercased ( ) == " chrome " }
304+ var safariTabs = context. browserTabs. filter { $0. browser. lowercased ( ) == " safari " }
305+ var firefoxTabs = context. browserTabs. filter { $0. browser. lowercased ( ) == " firefox " }
306+ var otherTabs = context. browserTabs. filter { ![ " chrome " , " safari " , " firefox " , " default " ] . contains ( $0. browser. lowercased ( ) ) }
307+ let defaultTabs = context. browserTabs. filter { $0. browser. lowercased ( ) == " default " }
308+
309+ // Assign default tabs to the detected default browser
310+ let defaultBrowser = getDefaultBrowser ( )
311+ switch defaultBrowser {
312+ case " chrome " :
313+ chromeTabs += defaultTabs
314+ case " safari " :
315+ safariTabs += defaultTabs
316+ case " firefox " :
317+ firefoxTabs += defaultTabs
318+ default :
319+ otherTabs += defaultTabs
315320 }
321+
316322 // Open Chrome tabs using Apple Events
317323 if !chromeTabs. isEmpty {
318324 print ( " Launching Chrome tabs: \( chromeTabs) " )
319325 _ = launchChromeTabsWithAppleEvents ( chromeTabs, for: context. id)
320326 }
327+ // Open Safari tabs using Apple Events
328+ if !safariTabs. isEmpty {
329+ print ( " Launching Safari tabs: \( safariTabs) " )
330+ _ = launchSafariTabsWithAppleEvents ( safariTabs, for: context. id)
331+ }
332+ // Open Firefox tabs
333+ if !firefoxTabs. isEmpty {
334+ print ( " Launching Firefox tabs: \( firefoxTabs) " )
335+ _ = launchFirefoxTabs ( firefoxTabs, for: context. id)
336+ }
321337 // Open other tabs using the existing method
322338 for tab in otherTabs {
323339 openBrowserTab ( tab)
@@ -334,23 +350,97 @@ class ContextManager: ObservableObject {
334350 }
335351 }
336352
337- private func launchSafariTabs( _ tabs: [ BrowserTab ] , for contextId: UUID ) -> Bool {
353+ /// Launch Safari tabs using Apple Events (AppleScript), creating a new window and opening all tabs
354+ private func launchSafariTabsWithAppleEvents( _ tabs: [ BrowserTab ] , for contextId: UUID ) -> Bool {
338355 guard !tabs. isEmpty else { return false }
339- let workspace = NSWorkspace . shared
340- guard let safariURL = workspace. urlForApplication ( withBundleIdentifier: " com.apple.Safari " ) else {
341- return false
356+ let safariIsRunning = NSRunningApplication . runningApplications ( withBundleIdentifier: " com.apple.Safari " ) . count > 0
357+ let urls = tabs. map { $0. url } . filter { !$0. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty }
358+ guard !urls. isEmpty else { return false }
359+ let urlList = urls. map { " \" \( $0) \" " } . joined ( separator: " , " )
360+ let script : String
361+ if urls. count == 1 {
362+ if safariIsRunning {
363+ script = """
364+ tell application \" Safari \"
365+ activate
366+ make new document
367+ set URL of current tab of front window to \( urlList)
368+ set winId to id of front window
369+ return winId
370+ end tell
371+ """
372+ } else {
373+ script = """
374+ tell application \" Safari \"
375+ activate
376+ delay 0.5
377+ try
378+ close window 1
379+ end try
380+ make new document
381+ set URL of current tab of front window to \( urlList)
382+ set winId to id of front window
383+ return winId
384+ end tell
385+ """
386+ }
387+ } else {
388+ if safariIsRunning {
389+ script = """
390+ tell application \" Safari \"
391+ activate
392+ make new document
393+ set tabUrls to { \( urlList) }
394+ set URL of current tab of front window to (item 1 of tabUrls)
395+ repeat with i from 2 to count of tabUrls
396+ tell front window to set newTab to make new tab at end of tabs
397+ set URL of newTab to (item i of tabUrls)
398+ end repeat
399+ set winId to id of front window
400+ return winId
401+ end tell
402+ """
403+ } else {
404+ script = """
405+ tell application \" Safari \"
406+ activate
407+ delay 0.5
408+ try
409+ close window 1
410+ end try
411+ make new document
412+ set tabUrls to { \( urlList) }
413+ set URL of current tab of front window to (item 1 of tabUrls)
414+ repeat with i from 2 to count of tabUrls
415+ tell front window to set newTab to make new tab at end of tabs
416+ set URL of newTab to (item i of tabUrls)
417+ end repeat
418+ set winId to id of front window
419+ return winId
420+ end tell
421+ """
422+ }
342423 }
343- let config = NSWorkspace . OpenConfiguration ( )
344- for tab in tabs {
345- if let url = URL ( string: tab. url) {
346- workspace. open ( [ url] , withApplicationAt: safariURL, configuration: config) { app, error in
347- if let error = error {
348- print ( " Failed to open URL in Safari: \( error) " )
349- }
424+ print ( script)
425+ if let appleScript = NSAppleScript ( source: script) {
426+ var error : NSDictionary ? = nil
427+ let result = appleScript. executeAndReturnError ( & error)
428+ if let error = error {
429+ print ( " AppleScript error (Safari): \( error) " )
430+ return false
431+ }
432+ let winId = result. int32Value
433+ if winId != 0 {
434+ if safariWindowIDs [ contextId] != nil {
435+ safariWindowIDs [ contextId] ? . append ( Int ( winId) )
436+ } else {
437+ safariWindowIDs [ contextId] = [ Int ( winId) ]
350438 }
439+ return true
351440 }
441+ return true // fallback, even if winId is 0
352442 }
353- return true
443+ return false
354444 }
355445
356446 private func launchChromeTabs( _ tabs: [ BrowserTab ] , for contextId: UUID ) -> Bool {
@@ -425,7 +515,11 @@ class ContextManager: ObservableObject {
425515 let result = appleScript. executeAndReturnError ( & error)
426516 let winId = result. int32Value
427517 if winId != 0 {
428- chromeWindowIDs [ contextId] = Int ( winId)
518+ if chromeWindowIDs [ contextId] != nil {
519+ chromeWindowIDs [ contextId] ? . append ( Int ( winId) )
520+ } else {
521+ chromeWindowIDs [ contextId] = [ Int ( winId) ]
522+ }
429523 return true
430524 } else if let error = error {
431525 print ( " AppleScript error: \( error) " )
@@ -434,26 +528,56 @@ class ContextManager: ObservableObject {
434528 return false
435529 }
436530
437- /// Close the Chrome window associated with a context (by window ID )
531+ /// Close all Chrome windows associated with a context (by window IDs )
438532 func closeChromeWindowForContext( _ contextId: UUID ) {
439- guard let winId = chromeWindowIDs [ contextId] else { return }
533+ guard let winIds = chromeWindowIDs [ contextId] , !winIds. isEmpty else { return }
534+ let winIdList = winIds. map { String ( $0) } . joined ( separator: " , " )
440535 let script = """
441536 tell application \" Google Chrome \"
442- if (exists window id \( winId) ) then
443- close window id \( winId)
444- end if
537+ repeat with wid in { \( winIdList) }
538+ if (exists window id wid) then
539+ try
540+ close window id wid
541+ end try
542+ end if
543+ end repeat
445544 end tell
446545 """
447546 if let appleScript = NSAppleScript ( source: script) {
448547 var error : NSDictionary ? = nil
449548 appleScript. executeAndReturnError ( & error)
450549 if let error = error {
451- print ( " Failed to close Chrome window: \( error) " )
550+ print ( " Failed to close Chrome window(s) : \( error) " )
452551 }
453552 }
454553 chromeWindowIDs. removeValue ( forKey: contextId)
455554 }
456555
556+ /// Close the Safari windows associated with a context (by window IDs)
557+ private func closeSafariWindowsForContext( _ contextId: UUID ) {
558+ guard let winIds = safariWindowIDs [ contextId] , !winIds. isEmpty else { return }
559+ let winIdList = winIds. map { String ( $0) } . joined ( separator: " , " )
560+ let script = """
561+ tell application \" Safari \"
562+ repeat with wid in { \( winIdList) }
563+ if (exists window id wid) then
564+ try
565+ close window id wid
566+ end try
567+ end if
568+ end repeat
569+ end tell
570+ """
571+ if let appleScript = NSAppleScript ( source: script) {
572+ var error : NSDictionary ? = nil
573+ appleScript. executeAndReturnError ( & error)
574+ if let error = error {
575+ print ( " Failed to close Safari window(s): \( error) " )
576+ }
577+ }
578+ safariWindowIDs. removeValue ( forKey: contextId)
579+ }
580+
457581 private func launchContextTerminals( _ context: Context ) {
458582 for session in context. terminalSessions {
459583 let commandToRun : String
0 commit comments