macOS: fix crash when adding tab from tab overview

This commit is contained in:
Noah Gregory
2026-02-24 18:08:45 -05:00
parent db1e31c7a6
commit dd4e36f921
6 changed files with 79 additions and 10 deletions

View File

@@ -1,3 +1,4 @@
// C imports here are exposed to Swift.
#import "VibrantLayer.h"
#import "ObjCExceptionCatcher.h"

View File

@@ -411,14 +411,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// If we already have a tab group and we want the new tab to open at the end,
// then we use the last window in the tab group as the parent.
if let last = parent.tabGroup?.windows.last {
last.addTabbedWindow(window, ordered: .above)
last.addTabbedWindowSafely(window, ordered: .above)
} else {
fallthrough
}
case "current": fallthrough
default:
parent.addTabbedWindow(window, ordered: .above)
parent.addTabbedWindowSafely(window, ordered: .above)
}
}
@@ -863,7 +863,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
controller.showWindow(nil)
if let firstWindow = firstController.window,
let newWindow = controller.window {
firstWindow.addTabbedWindow(newWindow, ordered: .above)
firstWindow.addTabbedWindowSafely(newWindow, ordered: .above)
}
}
@@ -952,9 +952,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
if tabIndex < tabGroup.windows.count {
// Find the window that is currently at that index
let currentWindow = tabGroup.windows[tabIndex]
currentWindow.addTabbedWindow(window, ordered: .below)
currentWindow.addTabbedWindowSafely(window, ordered: .below)
} else {
tabGroup.windows.last?.addTabbedWindow(window, ordered: .above)
tabGroup.windows.last?.addTabbedWindowSafely(window, ordered: .above)
}
// Make it the key window
@@ -1386,7 +1386,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
if #available(macOS 26, *) {
if window is TitlebarTabsTahoeTerminalWindow {
tabGroup.removeWindow(selectedWindow)
targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
targetWindow.addTabbedWindowSafely(selectedWindow, ordered: action.amount < 0 ? .below : .above)
DispatchQueue.main.async {
selectedWindow.makeKey()
}
@@ -1401,7 +1401,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// Remove and re-add the window in the correct position
tabGroup.removeWindow(selectedWindow)
targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
targetWindow.addTabbedWindowSafely(selectedWindow, ordered: action.amount < 0 ? .below : .above)
// Ensure our window remains selected
selectedWindow.makeKey()

View File

@@ -39,6 +39,24 @@ extension NSWindow {
guard let firstWindow = tabGroup?.windows.first else { return true }
return firstWindow === self
}
/// Wraps `addTabbedWindow` with an Objective-C exception catcher because AppKit can
/// occasionally throw NSExceptions in visual tab picker flows.
@discardableResult
func addTabbedWindowSafely(
_ child: NSWindow,
ordered: NSWindow.OrderingMode
) -> Bool {
var error: NSError?
let success = GhosttyAddTabbedWindowSafely(self, child, ordered.rawValue, &error)
if let error {
let reason = error.localizedDescription
Ghostty.logger.error("addTabbedWindow failed: \(reason)")
}
return success
}
}
/// Native tabbing private API usage. :(

View File

@@ -296,13 +296,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
if tabIndex == 0 {
// We were previously the first tab. Add it before ("below")
// the first window in the tab group currently.
tabGroup.windows.first!.addTabbedWindow(window, ordered: .below)
tabGroup.windows.first!.addTabbedWindowSafely(window, ordered: .below)
} else if tabIndex <= tabGroup.windows.count {
// We were somewhere in the middle
tabGroup.windows[tabIndex - 1].addTabbedWindow(window, ordered: .above)
tabGroup.windows[tabIndex - 1].addTabbedWindowSafely(window, ordered: .above)
} else {
// We were at the end
tabGroup.windows.last!.addTabbedWindow(window, ordered: .below)
tabGroup.windows.last!.addTabbedWindowSafely(window, ordered: .below)
}
}

View File

@@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
/// Minimal Objective-C exception bridge for AppKit tabbing APIs.
FOUNDATION_EXPORT BOOL GhosttyAddTabbedWindowSafely(
id parent,
id child,
NSInteger ordered,
NSError * _Nullable * _Nullable error
);

View File

@@ -0,0 +1,41 @@
#import "ObjCExceptionCatcher.h"
#import <TargetConditionals.h>
#if TARGET_OS_OSX
#import <AppKit/AppKit.h>
#endif
BOOL GhosttyAddTabbedWindowSafely(
id parent,
id child,
NSInteger ordered,
NSError * _Nullable * _Nullable error
) {
#if TARGET_OS_OSX
@try {
[((NSWindow *)parent) addTabbedWindow:(NSWindow *)child ordered:(NSWindowOrderingMode)ordered];
return YES;
} @catch (NSException *exception) {
if (error != NULL) {
NSString *reason = exception.reason ?: @"Unknown Objective-C exception";
*error = [NSError errorWithDomain:@"Ghostty.ObjCException"
code:1
userInfo:@{
NSLocalizedDescriptionKey: reason,
@"exception_name": exception.name,
}];
}
return NO;
}
#else
if (error != NULL) {
*error = [NSError errorWithDomain:@"Ghostty.ObjCException"
code:2
userInfo:@{
NSLocalizedDescriptionKey: @"GhosttyAddTabbedWindowSafely is unavailable on this platform.",
}];
}
return NO;
#endif
}