From 523387442555881bfb4e16ab4e7f6c20fb24eddb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 11 Mar 2026 18:32:23 -0700 Subject: [PATCH] ok --- Sources/Panels/BrowserPanel.swift | 105 ++++++++++++++++++ Sources/TerminalController.swift | 11 +- Sources/Workspace.swift | 57 ++++++++++ cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 70 ++++++++++++ 4 files changed, 233 insertions(+), 10 deletions(-) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 5c2d7cd8..adda9205 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2710,6 +2710,111 @@ final class BrowserPanel: Panel, ObservableObject { } } +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 + 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() + webView = replacement + webViewInstanceID = UUID() + shouldRenderWebView = false + bindWebView(replacement) + refreshNavigationAvailability() + +#if DEBUG + dlog( + "browser.contextReset.end panel=\(id.uuidString.prefix(5)) " + + "reason=\(reason) instance=\(webViewInstanceID.uuidString.prefix(6))" + ) +#endif + } +} + func resolveBrowserNavigableURL(_ input: String) -> URL? { let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 44fd70cf..6a708ae2 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -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 } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 1ceca0b4..3f9e25ff 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -1686,6 +1686,63 @@ 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) + + guard let tabId = surfaceIdFromPanelId(browserPanel.id), + let existing = bonsplitController.tab(tabId) else { + continue + } + + let nextTitle = browserPanel.displayTitle + if panelTitles[browserPanel.id] != nextTitle { + panelTitles[browserPanel.id] = nextTitle + } + + let resolvedTitle = resolvedPanelTitle(panelId: browserPanel.id, fallback: nextTitle) + let titleUpdate: String? = existing.title == resolvedTitle ? nil : resolvedTitle + let faviconUpdate: Data?? = existing.iconImageData == nil ? nil : .some(nil) + let loadingUpdate: Bool? = existing.isLoading ? false : nil + + guard titleUpdate != nil || faviconUpdate != nil || loadingUpdate != nil else { + continue + } + + bonsplitController.updateTab( + tabId, + title: titleUpdate, + iconImageData: faviconUpdate, + hasCustomTitle: panelCustomTitles[browserPanel.id] != nil, + isLoading: loadingUpdate + ) + } + } + @discardableResult func updatePanelTitle(panelId: UUID, title: String) -> Bool { let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 3db83f10..68513559 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2430,6 +2430,76 @@ 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.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.metadataBlocks.isEmpty) + XCTAssertNotNil(workspace.progress) + XCTAssertNotNil(workspace.gitBranch) + XCTAssertNotNil(workspace.pullRequest) + XCTAssertEqual(workspace.listeningPorts, [3000]) + + 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) + } + } @MainActor