Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
THTM0002 /* TerminalThemeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = THTM0001 /* TerminalThemeStore.swift */; };
THTM0003 /* TerminalThemeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = THTM0001 /* TerminalThemeStore.swift */; };
REND0002A1B2C3D4E5F60719 /* RendererRealization.swift in Sources */ = {isa = PBXBuildFile; fileRef = REND0001A1B2C3D4E5F60719 /* RendererRealization.swift */; };
WKTR0002A1B2C3D4E5F60719 /* WebKitSubviewTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = WKTR0001A1B2C3D4E5F60719 /* WebKitSubviewTransfer.swift */; };
A5FF0007 /* SettingDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FF0017 /* SettingDefinition.swift */; };
A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; };
NRSP0084A1B2C3D4E5F60719 /* WorkspaceSidebarModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRSP0083A1B2C3D4E5F60719 /* WorkspaceSidebarModels.swift */; };
Expand Down Expand Up @@ -439,6 +440,7 @@
NRPA00010 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
THTM0001 /* TerminalThemeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalThemeStore.swift; sourceTree = "<group>"; };
REND0001A1B2C3D4E5F60719 /* RendererRealization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RendererRealization.swift; sourceTree = "<group>"; };
WKTR0001A1B2C3D4E5F60719 /* WebKitSubviewTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitSubviewTransfer.swift; sourceTree = "<group>"; };
A5FF0017 /* SettingDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingDefinition.swift; sourceTree = "<group>"; };
A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
NRSP0083A1B2C3D4E5F60719 /* WorkspaceSidebarModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceSidebarModels.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -912,6 +914,7 @@
NRPA00010 /* SettingsView.swift */,
THTM0001 /* TerminalThemeStore.swift */,
REND0001A1B2C3D4E5F60719 /* RendererRealization.swift */,
WKTR0001A1B2C3D4E5F60719 /* WebKitSubviewTransfer.swift */,
A5FF0017 /* SettingDefinition.swift */,
A5001012 /* ContentView.swift */,
B10A1CE5 /* RenderableSystemSymbol.swift */,
Expand Down Expand Up @@ -1474,6 +1477,7 @@
NRPA00009 /* SettingsView.swift in Sources */,
THTM0002 /* TerminalThemeStore.swift in Sources */,
REND0002A1B2C3D4E5F60719 /* RendererRealization.swift in Sources */,
WKTR0002A1B2C3D4E5F60719 /* WebKitSubviewTransfer.swift in Sources */,
A5FF0007 /* SettingDefinition.swift in Sources */,
A5001002 /* ContentView.swift in Sources */,
B10A1CE6 /* RenderableSystemSymbol.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
</dict>
</dict>
<key>SUAutomaticallyUpdate</key>
<false/>
<true/>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUFeedURL</key>
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1659,7 +1659,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser
SessionPersistenceStore.rotateIntoHistory()
guard SessionRestorePolicy.shouldAttemptRestore() else { return }
Self.removeLegacyPersistedWindowGeometry()
startupSessionSnapshot = SessionPersistenceStore.load()
startupSessionSnapshot = SessionPersistenceStore.loadWithHistoryFallback()
}

private func persistedWindowGeometry(
Expand Down
108 changes: 20 additions & 88 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,50 +514,6 @@ final class WindowBrowserPortal: HostedViewPortalRegistry {
}) ?? reference
}

private func directTransferChild(of container: NSView, containing descendant: NSView) -> NSView? {
var current: NSView? = descendant
var directChild: NSView?
while let view = current, view !== container {
directChild = view
current = view.superview
}
guard current === container else { return nil }
return directChild
}

private func relatedWebKitTransferSubviews(
from sourceSuperview: NSView,
primaryWebView: WKWebView
) -> [NSView] {
var relatedSubviews: [NSView] = []
var seen = Set<ObjectIdentifier>()

func append(_ candidate: NSView?) {
guard let candidate, candidate !== sourceSuperview else { return }
let id = ObjectIdentifier(candidate)
guard seen.insert(id).inserted else { return }
relatedSubviews.append(candidate)
}

append(directTransferChild(of: sourceSuperview, containing: primaryWebView) ?? primaryWebView)

if let inspectorFrontend = primaryWebView.programaInspectorFrontendWebView() {
append(directTransferChild(of: sourceSuperview, containing: inspectorFrontend) ?? inspectorFrontend)
}

for view in sourceSuperview.subviews {
if view === primaryWebView { continue }
let className = String(describing: type(of: view))
guard className.contains("WK") else { continue }
if InspectorDock.isInspectorView(view) && !InspectorDock.isVisibleCandidate(view) {
continue
}
append(view)
}

return relatedSubviews
}

private func appendHostedWebKitSubviews(
in root: NSView,
to result: inout [WKWebView],
Expand Down Expand Up @@ -826,46 +782,6 @@ final class WindowBrowserPortal: HostedViewPortalRegistry {
}
}

private func moveWebKitRelatedSubviewsIfNeeded(
from sourceSuperview: NSView,
to containerView: WindowBrowserSlotView,
primaryWebView: WKWebView,
reason: String
) {
guard sourceSuperview !== containerView else { return }
// When Web Inspector is docked, WebKit can inject companion WK* subviews
// next to the primary WKWebView. Move those with the web view so inspector
// UI state does not get orphaned in the old host during split churn.
let relatedSubviews = relatedWebKitTransferSubviews(
from: sourceSuperview,
primaryWebView: primaryWebView
)
guard !relatedSubviews.isEmpty else { return }
#if DEBUG
dlog(
"browser.portal.reparent.batch reason=\(reason) source=\(browserPortalDebugToken(sourceSuperview)) " +
"container=\(browserPortalDebugToken(containerView)) count=\(relatedSubviews.count) " +
"sourceType=\(String(describing: type(of: sourceSuperview))) targetType=\(String(describing: type(of: containerView))) " +
"sourceFlipped=\(sourceSuperview.isFlipped ? 1 : 0) targetFlipped=\(containerView.isFlipped ? 1 : 0) " +
"sourceBounds=\(browserPortalDebugFrame(sourceSuperview.bounds)) targetBounds=\(browserPortalDebugFrame(containerView.bounds))"
)
#endif
for view in relatedSubviews {
let frameInWindow = sourceSuperview.convert(view.frame, to: nil)
let className = String(describing: type(of: view))
view.removeFromSuperview()
containerView.addSubview(view, positioned: .above, relativeTo: nil)
let convertedFrame = containerView.convert(frameInWindow, from: nil)
view.frame = convertedFrame
#if DEBUG
dlog(
"browser.portal.reparent.batch.item reason=\(reason) class=\(className) " +
"view=\(browserPortalDebugToken(view)) frameInWindow=\(browserPortalDebugFrame(frameInWindow)) " +
"converted=\(browserPortalDebugFrame(convertedFrame))"
)
#endif
}
}

func detachWebView(withId webViewId: ObjectIdentifier) {
cancelPendingHostedWebViewRefreshes(for: webViewId)
Expand Down Expand Up @@ -1138,12 +1054,22 @@ final class WindowBrowserPortal: HostedViewPortalRegistry {
)
#endif
if let sourceSuperview = webView.superview {
moveWebKitRelatedSubviewsIfNeeded(
// When Web Inspector is docked, WebKit can inject companion WK*
// subviews next to the primary WKWebView. Move those with the web
// view so inspector UI state does not get orphaned in the old
// host during split churn. Shared with WebViewRepresentable's
// local-inline reparenting; see WebKitSubviewTransfer's doc
// comment for the unified fast-path/window-relative contract.
WebKitSubviewTransfer.move(
from: sourceSuperview,
to: containerView,
primaryWebView: webView,
reason: "bind.attachContainer"
)
) { message in
#if DEBUG
dlog(message)
#endif
}
} else {
containerView.addSubview(webView, positioned: .above, relativeTo: nil)
}
Expand Down Expand Up @@ -1459,12 +1385,18 @@ final class WindowBrowserPortal: HostedViewPortalRegistry {
)
#endif
if let sourceSuperview = webView.superview {
moveWebKitRelatedSubviewsIfNeeded(
// See WebKitSubviewTransfer's doc comment for the unified
// fast-path/window-relative reparenting contract.
WebKitSubviewTransfer.move(
from: sourceSuperview,
to: containerView,
primaryWebView: webView,
reason: "sync.attachContainer"
)
) { message in
#if DEBUG
dlog(message)
#endif
}
} else {
containerView.addSubview(webView, positioned: .above, relativeTo: nil)
}
Expand Down
17 changes: 17 additions & 0 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,12 @@ final class BrowserPanel: Panel, ObservableObject {
guard let self, self.isCurrentWebView(webView, instanceID: boundWebViewInstanceID) else { return }
self.hasPromptedPasskeyHandoffForCurrentNavigation = false
}
navigationDelegate.didCommit = { [weak self] webView in
guard let self, self.isCurrentWebView(webView, instanceID: boundWebViewInstanceID) else { return }
// Invalidate element refs (@eN) allocated on the previous page (M6a) — this is the
// single choke point for a committed main-frame navigation.
TerminalController.shared.v2BrowserBumpNavigationGeneration(forSurface: self.id)
}
navigationDelegate.didFinish = { [weak self] webView in
Task { @MainActor [weak self] in
guard let self, self.isCurrentWebView(webView, instanceID: boundWebViewInstanceID) else { return }
Expand Down Expand Up @@ -2163,12 +2169,23 @@ final class BrowserPanel: Panel, ObservableObject {
)
}

/// BY DESIGN (audit 2026-08-20, M4 — decided 2026-08-20): callers of this
/// bypass fall into two classes, both deliberate. Back/forward/reload skip
/// the insecure-HTTP prompt because the user already accepted the page.
/// Session/profile restore and content-process-crash replacement ALSO skip
/// it — prompting N times at launch for tabs the user left open was judged
/// hostile UX. The trade: a plaintext http:// tab left open reloads over
/// plaintext on relaunch with no prompt. Restore-class bypasses are logged
/// to the release diagnostics channel below so the behavior is auditable.
func navigateWithoutInsecureHTTPPrompt(
request: URLRequest,
recordTypedNavigation: Bool,
preserveRestoredSessionHistory: Bool = false
) {
guard let url = request.url else { return }
if preserveRestoredSessionHistory, browserShouldBlockInsecureHTTPURL(url) {
dilog("browser.restore", "insecure_http_reload_without_prompt host=\(url.host ?? "-")")
}
if usesRemoteWorkspaceProxy, remoteProxyEndpoint == nil {
pendingRemoteNavigation = PendingRemoteNavigation(
request: request,
Expand Down
8 changes: 8 additions & 0 deletions Sources/Panels/BrowserPanelWebDelegates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ func browserNavigationShouldFallbackNilTargetToNewTab(

class BrowserNavigationDelegate: NSObject, WKNavigationDelegate {
var didStartProvisionalNavigation: ((WKWebView) -> Void)?
/// Fires once a main-frame navigation commits and the web view begins showing new content
/// (WKNavigationDelegate contract — this is the single choke point for "the page changed").
/// Wired to invalidate stale browser-automation element refs (M6a).
var didCommit: ((WKWebView) -> Void)?
var didFinish: ((WKWebView) -> Void)?
var didFailNavigation: ((WKWebView, String) -> Void)?
var didTerminateWebContentProcess: ((WKWebView) -> Void)?
Expand All @@ -83,6 +87,10 @@ class BrowserNavigationDelegate: NSObject, WKNavigationDelegate {
didStartProvisionalNavigation?(webView)
}

func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
didCommit?(webView)
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
didFinish?(webView)
}
Expand Down
Loading
Loading