Merge pull request #1215 from manaflow-ai/issue-1208-browser-pane-stale-content
Fix stale browser pane content after drag splits
This commit is contained in:
commit
6272f5082e
6 changed files with 316 additions and 28 deletions
|
|
@ -2851,9 +2851,12 @@ final class WindowBrowserPortal: NSObject {
|
|||
}
|
||||
return
|
||||
}
|
||||
let previousTransientRecoveryReason = entry.transientRecoveryReason
|
||||
func hideContainerView(reason: String) {
|
||||
containerView.setPaneTopChromeHeight(0)
|
||||
containerView.setSearchOverlay(nil)
|
||||
containerView.setPaneDropContext(nil)
|
||||
containerView.setPortalDragDropZone(nil)
|
||||
containerView.setDropZoneOverlay(zone: nil)
|
||||
if !containerView.isHidden, webView.superview === containerView {
|
||||
webView.browserPortalNotifyHidden(reason: reason)
|
||||
|
|
@ -2884,6 +2887,8 @@ final class WindowBrowserPortal: NSObject {
|
|||
"reason=\(reason) frame=\(browserPortalDebugFrame(containerView.frame))"
|
||||
)
|
||||
#endif
|
||||
containerView.setPaneDropContext(nil)
|
||||
containerView.setPortalDragDropZone(nil)
|
||||
containerView.setDropZoneOverlay(zone: nil)
|
||||
return true
|
||||
}
|
||||
|
|
@ -3027,6 +3032,9 @@ final class WindowBrowserPortal: NSObject {
|
|||
"reason=hostBoundsNotReady frame=\(browserPortalDebugFrame(containerView.frame))"
|
||||
)
|
||||
#endif
|
||||
containerView.setPaneDropContext(nil)
|
||||
containerView.setPortalDragDropZone(nil)
|
||||
containerView.setDropZoneOverlay(zone: nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
|
@ -3089,6 +3097,10 @@ final class WindowBrowserPortal: NSObject {
|
|||
shouldHide &&
|
||||
entry.visibleInUI &&
|
||||
!containerView.isHidden
|
||||
let recoveredFromTransientGeometry =
|
||||
previousTransientRecoveryReason != nil &&
|
||||
transientRecoveryReason == nil &&
|
||||
!shouldHide
|
||||
#if DEBUG
|
||||
let frameWasClamped = hasFiniteFrame && !Self.rectApproximatelyEqual(frameInHost, targetFrame)
|
||||
if frameWasClamped {
|
||||
|
|
@ -3131,6 +3143,7 @@ final class WindowBrowserPortal: NSObject {
|
|||
if hasExistingVisibleFrame {
|
||||
containerView.setDropZoneOverlay(zone: nil)
|
||||
containerView.setPaneDropContext(nil)
|
||||
containerView.setPortalDragDropZone(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -3240,10 +3253,16 @@ final class WindowBrowserPortal: NSObject {
|
|||
}
|
||||
containerView.setPaneTopChromeHeight(shouldHide ? 0 : entry.paneTopChromeHeight)
|
||||
containerView.setSearchOverlay(shouldHide ? nil : entry.searchOverlay)
|
||||
containerView.setPaneDropContext(containerView.isHidden ? nil : entry.paneDropContext)
|
||||
containerView.setDropZoneOverlay(zone: containerView.isHidden ? nil : entry.dropZone)
|
||||
if revealedForDisplay {
|
||||
refreshReasons.append("reveal")
|
||||
}
|
||||
if recoveredFromTransientGeometry {
|
||||
// Drag/reparent churn can recover to the same visible frame we preserved.
|
||||
// Force a redraw so WebKit doesn't keep stale tiles until a later resize/focus.
|
||||
refreshReasons.append("transientRecovery")
|
||||
}
|
||||
if forcePresentationRefresh {
|
||||
refreshReasons.append("anchor")
|
||||
}
|
||||
|
|
@ -3254,7 +3273,7 @@ final class WindowBrowserPortal: NSObject {
|
|||
containerOwnsWebView &&
|
||||
hostView.reapplyHostedInspectorDividerIfNeeded(in: containerView, reason: "portal.sync")
|
||||
if !shouldHide, containerOwnsWebView, !refreshReasons.isEmpty {
|
||||
if hostedInspectorAdjustedDuringSync {
|
||||
if hostedInspectorAdjustedDuringSync && !recoveredFromTransientGeometry {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.portal.refresh.skip web=\(browserPortalDebugToken(webView)) " +
|
||||
|
|
|
|||
|
|
@ -2005,6 +2005,12 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
setupObservers(for: webView)
|
||||
}
|
||||
|
||||
private func isCurrentWebView(_ candidate: WKWebView, instanceID: UUID? = nil) -> Bool {
|
||||
guard candidate === webView else { return false }
|
||||
guard let instanceID else { return true }
|
||||
return instanceID == webViewInstanceID
|
||||
}
|
||||
|
||||
init(workspaceId: UUID, initialURL: URL? = nil, bypassInsecureHTTPHostOnce: String? = nil) {
|
||||
self.id = UUID()
|
||||
self.workspaceId = workspaceId
|
||||
|
|
@ -2020,15 +2026,16 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
navDelegate.didFinish = { webView in
|
||||
BrowserHistoryStore.shared.recordVisit(url: webView.url, title: webView.title)
|
||||
Task { @MainActor [weak self] in
|
||||
self?.refreshFavicon(from: webView)
|
||||
self?.applyBrowserThemeModeIfNeeded()
|
||||
guard let self, self.isCurrentWebView(webView) else { return }
|
||||
self.refreshFavicon(from: webView)
|
||||
self.applyBrowserThemeModeIfNeeded()
|
||||
// Keep find-in-page open through load completion and refresh matches for the new DOM.
|
||||
self?.restoreFindStateAfterNavigation(replaySearch: true)
|
||||
self.restoreFindStateAfterNavigation(replaySearch: true)
|
||||
}
|
||||
}
|
||||
navDelegate.didFailNavigation = { [weak self] _, failedURL in
|
||||
navDelegate.didFailNavigation = { [weak self] failedWebView, failedURL in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
guard let self, self.isCurrentWebView(failedWebView) else { return }
|
||||
// Clear stale title/favicon from the previous page so the tab
|
||||
// shows the failed URL instead of the old page's branding.
|
||||
self.pageTitle = failedURL.isEmpty ? "" : failedURL
|
||||
|
|
@ -2162,10 +2169,13 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
}
|
||||
|
||||
private func setupObservers(for webView: WKWebView) {
|
||||
let observedWebViewInstanceID = webViewInstanceID
|
||||
|
||||
// URL changes
|
||||
let urlObserver = webView.observe(\.url, options: [.new]) { [weak self] webView, _ in
|
||||
Task { @MainActor in
|
||||
self?.currentURL = webView.url
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return }
|
||||
self.currentURL = webView.url
|
||||
}
|
||||
}
|
||||
webViewObservers.append(urlObserver)
|
||||
|
|
@ -2173,12 +2183,13 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
// Title changes
|
||||
let titleObserver = webView.observe(\.title, options: [.new]) { [weak self] webView, _ in
|
||||
Task { @MainActor in
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return }
|
||||
// Keep showing the last non-empty title while the new navigation is loading.
|
||||
// WebKit often clears title to nil/"" during reload/navigation, which causes
|
||||
// a distracting tab-title flash (e.g. to host/URL). Only accept non-empty titles.
|
||||
let trimmed = (webView.title ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
self?.pageTitle = trimmed
|
||||
self.pageTitle = trimmed
|
||||
}
|
||||
}
|
||||
webViewObservers.append(titleObserver)
|
||||
|
|
@ -2186,7 +2197,8 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
// Loading state
|
||||
let loadingObserver = webView.observe(\.isLoading, options: [.new]) { [weak self] webView, _ in
|
||||
Task { @MainActor in
|
||||
self?.handleWebViewLoadingChanged(webView.isLoading)
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return }
|
||||
self.handleWebViewLoadingChanged(webView.isLoading)
|
||||
}
|
||||
}
|
||||
webViewObservers.append(loadingObserver)
|
||||
|
|
@ -2194,7 +2206,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
// Can go back
|
||||
let backObserver = webView.observe(\.canGoBack, options: [.new]) { [weak self] webView, _ in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return }
|
||||
self.nativeCanGoBack = webView.canGoBack
|
||||
self.refreshNavigationAvailability()
|
||||
}
|
||||
|
|
@ -2204,7 +2216,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
// Can go forward
|
||||
let forwardObserver = webView.observe(\.canGoForward, options: [.new]) { [weak self] webView, _ in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return }
|
||||
self.nativeCanGoForward = webView.canGoForward
|
||||
self.refreshNavigationAvailability()
|
||||
}
|
||||
|
|
@ -2214,7 +2226,8 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
// Progress
|
||||
let progressObserver = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
|
||||
Task { @MainActor in
|
||||
self?.estimatedProgress = webView.estimatedProgress
|
||||
guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return }
|
||||
self.estimatedProgress = webView.estimatedProgress
|
||||
}
|
||||
}
|
||||
webViewObservers.append(progressObserver)
|
||||
|
|
@ -2250,6 +2263,9 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
|
||||
webViewObservers.removeAll()
|
||||
webViewCancellables.removeAll()
|
||||
faviconTask?.cancel()
|
||||
faviconTask = nil
|
||||
faviconRefreshGeneration &+= 1
|
||||
BrowserWindowPortalRegistry.detach(webView: terminatedWebView)
|
||||
terminatedWebView.stopLoading()
|
||||
terminatedWebView.navigationDelegate = nil
|
||||
|
|
@ -2260,11 +2276,12 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
|
||||
let replacement = Self.makeWebView()
|
||||
replacement.pageZoom = desiredZoom
|
||||
webView = replacement
|
||||
webViewInstanceID = UUID()
|
||||
webView = replacement
|
||||
shouldRenderWebView = wasRenderable
|
||||
|
||||
bindWebView(replacement)
|
||||
applyBrowserThemeModeIfNeeded()
|
||||
|
||||
if !history.backHistoryURLStrings.isEmpty || !history.forwardHistoryURLStrings.isEmpty {
|
||||
restoreSessionNavigationHistory(
|
||||
|
|
@ -2360,9 +2377,11 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
guard let scheme = pageURL.scheme?.lowercased(), scheme == "http" || scheme == "https" else { return }
|
||||
faviconRefreshGeneration &+= 1
|
||||
let refreshGeneration = faviconRefreshGeneration
|
||||
let refreshWebViewInstanceID = webViewInstanceID
|
||||
|
||||
faviconTask = Task { @MainActor [weak self, weak webView] in
|
||||
guard let self, let webView else { return }
|
||||
guard self.isCurrentWebView(webView, instanceID: refreshWebViewInstanceID) else { return }
|
||||
guard self.isCurrentFaviconRefresh(generation: refreshGeneration) else { return }
|
||||
|
||||
// Try to discover the best icon URL from the document.
|
||||
|
|
@ -2397,6 +2416,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
discoveredURL = u
|
||||
}
|
||||
}
|
||||
guard self.isCurrentWebView(webView, instanceID: refreshWebViewInstanceID) else { return }
|
||||
guard self.isCurrentFaviconRefresh(generation: refreshGeneration) else { return }
|
||||
|
||||
let fallbackURL = URL(string: "/favicon.ico", relativeTo: pageURL)
|
||||
|
|
@ -2422,6 +2442,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
} catch {
|
||||
return
|
||||
}
|
||||
guard self.isCurrentWebView(webView, instanceID: refreshWebViewInstanceID) else { return }
|
||||
guard self.isCurrentFaviconRefresh(generation: refreshGeneration) else { return }
|
||||
|
||||
guard let http = response as? HTTPURLResponse,
|
||||
|
|
@ -2701,12 +2722,121 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
if let detachedDeveloperToolsWindowCloseObserver {
|
||||
NotificationCenter.default.removeObserver(detachedDeveloperToolsWindowCloseObserver)
|
||||
}
|
||||
webViewObservers.removeAll()
|
||||
webViewCancellables.removeAll()
|
||||
let webView = webView
|
||||
Task { @MainActor in
|
||||
BrowserWindowPortalRegistry.detach(webView: webView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowserPanel {
|
||||
private var needsWorkspaceContextReset: Bool {
|
||||
shouldRenderWebView ||
|
||||
currentURL != nil ||
|
||||
!pageTitle.isEmpty ||
|
||||
faviconPNGData != nil ||
|
||||
searchState != nil ||
|
||||
nativeCanGoBack ||
|
||||
nativeCanGoForward ||
|
||||
restoredHistoryCurrentURL != nil ||
|
||||
!restoredBackHistoryStack.isEmpty ||
|
||||
!restoredForwardHistoryStack.isEmpty ||
|
||||
estimatedProgress > 0 ||
|
||||
isLoading ||
|
||||
isDownloading ||
|
||||
activeDownloadCount != 0 ||
|
||||
preferredDeveloperToolsVisible ||
|
||||
webView.superview != nil
|
||||
}
|
||||
|
||||
func resetForWorkspaceContextChange(reason: String) {
|
||||
guard needsWorkspaceContextReset else {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.contextReset.skip panel=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) render=\(shouldRenderWebView ? 1 : 0)"
|
||||
)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.contextReset.begin panel=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) render=\(shouldRenderWebView ? 1 : 0) " +
|
||||
"url=\(preferredURLStringForOmnibar() ?? "nil")"
|
||||
)
|
||||
#endif
|
||||
|
||||
_ = hideDeveloperTools()
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
preferredDeveloperToolsVisible = false
|
||||
preferredDeveloperToolsPresentation = .unknown
|
||||
forceDeveloperToolsRefreshOnNextAttach = false
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
developerToolsRestoreRetryAttempt = 0
|
||||
preferredAttachedDeveloperToolsWidth = nil
|
||||
preferredAttachedDeveloperToolsWidthFraction = nil
|
||||
|
||||
loadingEndWorkItem?.cancel()
|
||||
loadingEndWorkItem = nil
|
||||
faviconTask?.cancel()
|
||||
faviconTask = nil
|
||||
faviconRefreshGeneration &+= 1
|
||||
loadingGeneration &+= 1
|
||||
activeDownloadCount = 0
|
||||
isDownloading = false
|
||||
isLoading = false
|
||||
estimatedProgress = 0
|
||||
nativeCanGoBack = false
|
||||
nativeCanGoForward = false
|
||||
navigationDelegate?.lastAttemptedURL = nil
|
||||
abandonRestoredSessionHistoryIfNeeded()
|
||||
|
||||
pendingAddressBarFocusRequestId = nil
|
||||
preferredFocusIntent = .addressBar
|
||||
suppressOmnibarAutofocusUntil = nil
|
||||
suppressWebViewFocusUntil = nil
|
||||
endSuppressWebViewFocusForAddressBar()
|
||||
invalidateAddressBarPageFocusRestoreAttempts()
|
||||
invalidateSearchFocusRequests(reason: "contextReset")
|
||||
searchState = nil
|
||||
|
||||
pageTitle = ""
|
||||
currentURL = nil
|
||||
faviconPNGData = nil
|
||||
lastFaviconURLString = nil
|
||||
activePortalHostLease = nil
|
||||
pendingDistinctPortalHostReplacementPaneId = nil
|
||||
lockedPortalHost = nil
|
||||
|
||||
let oldWebView = webView
|
||||
webViewObservers.removeAll()
|
||||
webViewCancellables.removeAll()
|
||||
BrowserWindowPortalRegistry.detach(webView: oldWebView)
|
||||
oldWebView.stopLoading()
|
||||
oldWebView.navigationDelegate = nil
|
||||
oldWebView.uiDelegate = nil
|
||||
if let oldCmuxWebView = oldWebView as? CmuxWebView {
|
||||
oldCmuxWebView.onContextMenuDownloadStateChanged = nil
|
||||
}
|
||||
|
||||
let replacement = Self.makeWebView()
|
||||
webViewInstanceID = UUID()
|
||||
webView = replacement
|
||||
shouldRenderWebView = false
|
||||
bindWebView(replacement)
|
||||
applyBrowserThemeModeIfNeeded()
|
||||
refreshNavigationAvailability()
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.contextReset.end panel=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) instance=\(webViewInstanceID.uuidString.prefix(6))"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5523,12 +5523,11 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
}
|
||||
|
||||
// SwiftUI can transiently dismantle/rebuild the browser host view during split
|
||||
// rearrangement. Do not detach the portal-hosted WKWebView here; explicit detach
|
||||
// still happens on real web view replacement and panel teardown.
|
||||
// rearrangement. Do not detach the portal-hosted WKWebView or clear its pane-drop
|
||||
// context here; explicit teardown still happens on real web view replacement and
|
||||
// panel teardown, and preserving this state lets internal tab drags re-enter the
|
||||
// browser pane while SwiftUI churns underneath.
|
||||
BrowserWindowPortalRegistry.updateDropZoneOverlay(for: webView, zone: nil)
|
||||
BrowserWindowPortalRegistry.updatePaneTopChromeHeight(for: webView, height: 0)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(for: webView, context: nil)
|
||||
BrowserWindowPortalRegistry.updateSearchOverlay(for: webView, configuration: nil)
|
||||
coordinator.lastPortalHostId = nil
|
||||
coordinator.lastSynchronizedHostGeometryRevision = 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13813,16 +13813,7 @@ class TerminalController {
|
|||
result = "ERROR: Tab not found"
|
||||
return
|
||||
}
|
||||
tab.statusEntries.removeAll()
|
||||
tab.logEntries.removeAll()
|
||||
tab.progress = nil
|
||||
tab.gitBranch = nil
|
||||
tab.panelGitBranches.removeAll()
|
||||
tab.pullRequest = nil
|
||||
tab.panelPullRequests.removeAll()
|
||||
tab.surfaceListeningPorts.removeAll()
|
||||
tab.listeningPorts.removeAll()
|
||||
tab.metadataBlocks.removeAll()
|
||||
tab.resetSidebarContext(reason: "reset_sidebar")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1686,6 +1686,57 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func resetSidebarContext(reason: String = "unspecified") {
|
||||
statusEntries.removeAll()
|
||||
logEntries.removeAll()
|
||||
progress = nil
|
||||
gitBranch = nil
|
||||
panelGitBranches.removeAll()
|
||||
pullRequest = nil
|
||||
panelPullRequests.removeAll()
|
||||
surfaceListeningPorts.removeAll()
|
||||
listeningPorts.removeAll()
|
||||
metadataBlocks.removeAll()
|
||||
resetBrowserPanelsForContextChange(reason: reason)
|
||||
}
|
||||
|
||||
func resetBrowserPanelsForContextChange(reason: String) {
|
||||
let browserPanels = panels.values.compactMap { $0 as? BrowserPanel }
|
||||
guard !browserPanels.isEmpty else { return }
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"workspace.contextReset.browserPanels workspace=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) count=\(browserPanels.count)"
|
||||
)
|
||||
#endif
|
||||
|
||||
for browserPanel in browserPanels {
|
||||
browserPanel.resetForWorkspaceContextChange(reason: reason)
|
||||
let nextTitle = browserPanel.displayTitle
|
||||
_ = updatePanelTitle(panelId: browserPanel.id, title: nextTitle)
|
||||
|
||||
guard let tabId = surfaceIdFromPanelId(browserPanel.id),
|
||||
let existing = bonsplitController.tab(tabId) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let faviconUpdate: Data?? = existing.iconImageData == nil ? nil : .some(nil)
|
||||
let loadingUpdate: Bool? = existing.isLoading ? false : nil
|
||||
|
||||
guard faviconUpdate != nil || loadingUpdate != nil else {
|
||||
continue
|
||||
}
|
||||
|
||||
bonsplitController.updateTab(
|
||||
tabId,
|
||||
iconImageData: faviconUpdate,
|
||||
hasCustomTitle: panelCustomTitles[browserPanel.id] != nil,
|
||||
isLoading: loadingUpdate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func updatePanelTitle(panelId: UUID, title: String) -> Bool {
|
||||
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
|
@ -4839,6 +4890,21 @@ extension Workspace: BonsplitDelegate {
|
|||
"originalKinds=[\(paneKindSummary(originalPane))] newKinds=[\(paneKindSummary(newPane))]"
|
||||
)
|
||||
#endif
|
||||
let rearmBrowserPortalHostReplacement: (PaneID, String) -> Void = { paneId, reason in
|
||||
for tab in controller.tabs(inPane: paneId) {
|
||||
guard let panelId = self.panelIdFromSurfaceId(tab.id),
|
||||
let browserPanel = self.browserPanel(for: panelId) else {
|
||||
continue
|
||||
}
|
||||
browserPanel.preparePortalHostReplacementForNextDistinctClaim(
|
||||
inPane: paneId,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
}
|
||||
rearmBrowserPortalHostReplacement(originalPane, "workspace.didSplit.original")
|
||||
rearmBrowserPortalHostReplacement(newPane, "workspace.didSplit.new")
|
||||
|
||||
// Only auto-create a terminal if the split came from bonsplit UI.
|
||||
// Programmatic splits via newTerminalSplit() set isProgrammaticSplit and handle their own panels.
|
||||
guard !isProgrammaticSplit else {
|
||||
|
|
|
|||
|
|
@ -2430,6 +2430,89 @@ final class BrowserSessionHistoryRestoreTests: XCTestCase {
|
|||
|
||||
XCTAssertFalse(panel.shouldRenderWebView)
|
||||
}
|
||||
|
||||
func testResetSidebarContextClearsBrowserPanelsIntoNewTabState() throws {
|
||||
let workspace = Workspace()
|
||||
let paneId = try XCTUnwrap(workspace.bonsplitController.allPaneIds.first)
|
||||
let contextPanelId = try XCTUnwrap(workspace.focusedPanelId)
|
||||
let browser = try XCTUnwrap(
|
||||
workspace.newBrowserSurface(
|
||||
inPane: paneId,
|
||||
url: URL(string: "https://example.com"),
|
||||
focus: false
|
||||
)
|
||||
)
|
||||
|
||||
browser.restoreSessionNavigationHistory(
|
||||
backHistoryURLStrings: ["https://example.com/prev"],
|
||||
forwardHistoryURLStrings: ["https://example.com/next"],
|
||||
currentURLString: "https://example.com/current"
|
||||
)
|
||||
browser.startFind()
|
||||
|
||||
workspace.statusEntries["task"] = SidebarStatusEntry(key: "task", value: "Issue #1208")
|
||||
workspace.metadataBlocks["notes"] = SidebarMetadataBlock(
|
||||
key: "notes",
|
||||
markdown: "test",
|
||||
priority: 0,
|
||||
timestamp: Date()
|
||||
)
|
||||
workspace.progress = SidebarProgressState(value: 0.5, label: "Loading")
|
||||
workspace.updatePanelGitBranch(panelId: contextPanelId, branch: "issue-1208", isDirty: false)
|
||||
workspace.updatePanelPullRequest(
|
||||
panelId: contextPanelId,
|
||||
number: 1208,
|
||||
label: "PR",
|
||||
url: try XCTUnwrap(URL(string: "https://example.com/pull/1208")),
|
||||
status: .open
|
||||
)
|
||||
workspace.logEntries.append(
|
||||
SidebarLogEntry(
|
||||
message: "Issue #1208",
|
||||
level: .info,
|
||||
source: "test",
|
||||
timestamp: Date()
|
||||
)
|
||||
)
|
||||
workspace.surfaceListeningPorts[contextPanelId] = [3000]
|
||||
workspace.recomputeListeningPorts()
|
||||
|
||||
XCTAssertTrue(browser.shouldRenderWebView)
|
||||
XCTAssertNotNil(browser.preferredURLStringForOmnibar())
|
||||
XCTAssertTrue(browser.canGoBack)
|
||||
XCTAssertTrue(browser.canGoForward)
|
||||
XCTAssertNotNil(browser.searchState)
|
||||
XCTAssertFalse(workspace.statusEntries.isEmpty)
|
||||
XCTAssertFalse(workspace.logEntries.isEmpty)
|
||||
XCTAssertFalse(workspace.metadataBlocks.isEmpty)
|
||||
XCTAssertNotNil(workspace.progress)
|
||||
XCTAssertNotNil(workspace.gitBranch)
|
||||
XCTAssertNotNil(workspace.pullRequest)
|
||||
XCTAssertEqual(workspace.listeningPorts, [3000])
|
||||
|
||||
let priorWebView = browser.webView
|
||||
let priorInstanceID = browser.webViewInstanceID
|
||||
workspace.resetSidebarContext(reason: "test")
|
||||
|
||||
XCTAssertTrue(workspace.statusEntries.isEmpty)
|
||||
XCTAssertTrue(workspace.logEntries.isEmpty)
|
||||
XCTAssertTrue(workspace.metadataBlocks.isEmpty)
|
||||
XCTAssertNil(workspace.progress)
|
||||
XCTAssertNil(workspace.gitBranch)
|
||||
XCTAssertTrue(workspace.panelGitBranches.isEmpty)
|
||||
XCTAssertNil(workspace.pullRequest)
|
||||
XCTAssertTrue(workspace.panelPullRequests.isEmpty)
|
||||
XCTAssertTrue(workspace.surfaceListeningPorts.isEmpty)
|
||||
XCTAssertTrue(workspace.listeningPorts.isEmpty)
|
||||
XCTAssertFalse(browser.shouldRenderWebView)
|
||||
XCTAssertNil(browser.preferredURLStringForOmnibar())
|
||||
XCTAssertFalse(browser.canGoBack)
|
||||
XCTAssertFalse(browser.canGoForward)
|
||||
XCTAssertNil(browser.searchState)
|
||||
XCTAssertFalse(browser.webView === priorWebView)
|
||||
XCTAssertNotEqual(browser.webViewInstanceID, priorInstanceID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue