Fix sidebar live refresh for branch and PR state (#2331)
* Add regression coverage for sidebar live refresh * Refresh sidebar git metadata on active workspaces
This commit is contained in:
parent
e419fd9164
commit
94cc865e83
2 changed files with 93 additions and 24 deletions
|
|
@ -699,6 +699,7 @@ class TabManager: ObservableObject {
|
|||
private static var nextPortOrdinal: Int = 0
|
||||
private static let initialWorkspaceGitProbeDelays: [TimeInterval] = [0, 0.5, 1.5, 3.0, 6.0, 10.0]
|
||||
private static let workspaceGitMetadataPollInterval: TimeInterval = 30
|
||||
private static let selectedWorkspaceGitMetadataPollInterval: TimeInterval = 5
|
||||
private nonisolated static let workspacePullRequestProbeTimeout: TimeInterval = 5.0
|
||||
@Published var selectedTabId: UUID? {
|
||||
willSet {
|
||||
|
|
@ -820,6 +821,7 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
private var agentPIDSweepTimer: DispatchSourceTimer?
|
||||
private var workspaceGitMetadataPollTimer: DispatchSourceTimer?
|
||||
private var selectedWorkspaceGitMetadataPollTimer: DispatchSourceTimer?
|
||||
#if DEBUG
|
||||
private var debugWorkspaceSwitchCounter: UInt64 = 0
|
||||
private var debugWorkspaceSwitchId: UInt64 = 0
|
||||
|
|
@ -867,6 +869,7 @@ class TabManager: ObservableObject {
|
|||
|
||||
startAgentPIDSweepTimer()
|
||||
startWorkspaceGitMetadataPollTimer()
|
||||
startSelectedWorkspaceGitMetadataPollTimer()
|
||||
#if DEBUG
|
||||
setupUITestFocusShortcutsIfNeeded()
|
||||
setupSplitCloseRightUITestIfNeeded()
|
||||
|
|
@ -879,6 +882,7 @@ class TabManager: ObservableObject {
|
|||
workspaceCycleCooldownTask?.cancel()
|
||||
agentPIDSweepTimer?.cancel()
|
||||
workspaceGitMetadataPollTimer?.cancel()
|
||||
selectedWorkspaceGitMetadataPollTimer?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Agent PID Sweep
|
||||
|
|
@ -918,6 +922,24 @@ class TabManager: ObservableObject {
|
|||
workspaceGitMetadataPollTimer = timer
|
||||
}
|
||||
|
||||
/// Refresh the selected workspace more aggressively so branch checkouts and
|
||||
/// newly created PRs show up in the sidebar without waiting for the slower
|
||||
/// background sweep across every tracked workspace.
|
||||
private func startSelectedWorkspaceGitMetadataPollTimer() {
|
||||
let timer = DispatchSource.makeTimerSource(queue: .global(qos: .utility))
|
||||
let interval = Self.selectedWorkspaceGitMetadataPollInterval
|
||||
timer.schedule(deadline: .now() + interval, repeating: interval)
|
||||
timer.setEventHandler { [weak self] in
|
||||
guard let self else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.refreshSelectedWorkspaceGitMetadata()
|
||||
}
|
||||
}
|
||||
timer.resume()
|
||||
selectedWorkspaceGitMetadataPollTimer = timer
|
||||
}
|
||||
|
||||
private func refreshTrackedWorkspaceGitMetadata() {
|
||||
let activeProbeKeys = Set(workspaceGitProbeGenerationByKey.keys)
|
||||
|
||||
|
|
@ -935,6 +957,26 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func refreshSelectedWorkspaceGitMetadata() {
|
||||
guard let workspace = selectedWorkspace,
|
||||
let focusedPanelId = workspace.focusedPanelId else {
|
||||
return
|
||||
}
|
||||
|
||||
let activeProbeKeys = Set(workspaceGitProbeGenerationByKey.keys)
|
||||
let candidatePanelIds = trackedWorkspaceGitMetadataPollCandidatePanelIds(
|
||||
in: workspace,
|
||||
activeProbeKeys: activeProbeKeys
|
||||
)
|
||||
guard candidatePanelIds.contains(focusedPanelId) else { return }
|
||||
|
||||
scheduleWorkspaceGitMetadataRefreshIfPossible(
|
||||
workspaceId: workspace.id,
|
||||
panelId: focusedPanelId,
|
||||
reason: "selectedPeriodicPoll"
|
||||
)
|
||||
}
|
||||
|
||||
func refreshTrackedWorkspaceGitMetadataForTesting() {
|
||||
refreshTrackedWorkspaceGitMetadata()
|
||||
}
|
||||
|
|
@ -965,28 +1007,10 @@ class TabManager: ObservableObject {
|
|||
|
||||
return Set(candidatePanelIds.filter { panelId in
|
||||
let probeKey = WorkspaceGitProbeKey(workspaceId: workspace.id, panelId: panelId)
|
||||
guard !activeProbeKeys.contains(probeKey) else { return false }
|
||||
return shouldPollTrackedWorkspaceGitMetadata(in: workspace, panelId: panelId)
|
||||
return !activeProbeKeys.contains(probeKey)
|
||||
})
|
||||
}
|
||||
|
||||
private func shouldPollTrackedWorkspaceGitMetadata(in workspace: Workspace, panelId: UUID) -> Bool {
|
||||
guard let branch = trackedWorkspaceGitBranch(in: workspace, panelId: panelId) else {
|
||||
return true
|
||||
}
|
||||
return !Self.shouldSkipWorkspacePullRequestLookup(branch: branch)
|
||||
}
|
||||
|
||||
private func trackedWorkspaceGitBranch(in workspace: Workspace, panelId: UUID) -> String? {
|
||||
if let branch = workspace.panelGitBranches[panelId]?.branch {
|
||||
return branch
|
||||
}
|
||||
if workspace.focusedPanelId == panelId {
|
||||
return workspace.gitBranch?.branch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func sweepStaleAgentPIDs() {
|
||||
for tab in tabs {
|
||||
var keysToRemove: [String] = []
|
||||
|
|
|
|||
|
|
@ -404,7 +404,7 @@ final class TabManagerPullRequestProbeTests: XCTestCase {
|
|||
XCTAssertFalse(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "release/master-fix"))
|
||||
}
|
||||
|
||||
func testTrackedWorkspaceGitMetadataPollCandidatesSkipMainAndMasterPanelsOnly() throws {
|
||||
func testTrackedWorkspaceGitMetadataPollCandidatesIncludeMainAndMasterPanels() throws {
|
||||
let manager = TabManager()
|
||||
guard let workspace = manager.selectedWorkspace,
|
||||
let mainPanelId = workspace.focusedPanelId else {
|
||||
|
|
@ -435,11 +435,11 @@ final class TabManagerPullRequestProbeTests: XCTestCase {
|
|||
|
||||
XCTAssertEqual(
|
||||
manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id),
|
||||
Set([featurePanel.id, mainlinePanel.id])
|
||||
Set([mainPanelId, masterPanel.id, featurePanel.id, mainlinePanel.id])
|
||||
)
|
||||
}
|
||||
|
||||
func testTrackedWorkspaceGitMetadataPollCandidatesSkipFocusedFallbackOnMainOnly() {
|
||||
func testTrackedWorkspaceGitMetadataPollCandidatesIncludeFocusedFallbackOnMain() {
|
||||
let manager = TabManager()
|
||||
guard let workspace = manager.selectedWorkspace,
|
||||
let panelId = workspace.focusedPanelId else {
|
||||
|
|
@ -448,8 +448,9 @@ final class TabManagerPullRequestProbeTests: XCTestCase {
|
|||
}
|
||||
|
||||
workspace.gitBranch = SidebarGitBranchState(branch: "main", isDirty: false)
|
||||
XCTAssertTrue(
|
||||
manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id).isEmpty
|
||||
XCTAssertEqual(
|
||||
manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id),
|
||||
Set([panelId])
|
||||
)
|
||||
|
||||
workspace.gitBranch = SidebarGitBranchState(branch: "feature/sidebar-pr", isDirty: false)
|
||||
|
|
@ -459,6 +460,50 @@ final class TabManagerPullRequestProbeTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func testPeriodicWorkspaceGitMetadataRefreshUpdatesMainWorkspaceAfterCheckoutToFeatureBranch() throws {
|
||||
let fileManager = FileManager.default
|
||||
let repoURL = fileManager.temporaryDirectory.appendingPathComponent("cmux-git-main-refresh-\(UUID().uuidString)")
|
||||
try fileManager.createDirectory(at: repoURL, withIntermediateDirectories: true)
|
||||
defer { try? fileManager.removeItem(at: repoURL) }
|
||||
|
||||
try runGit(["init", "-b", "main"], in: repoURL)
|
||||
try runGit(["config", "user.name", "cmux tests"], in: repoURL)
|
||||
try runGit(["config", "user.email", "cmux@example.invalid"], in: repoURL)
|
||||
try "seed\n".write(
|
||||
to: repoURL.appendingPathComponent("README.md"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
try runGit(["add", "README.md"], in: repoURL)
|
||||
try runGit(["commit", "-m", "Initial commit"], in: repoURL)
|
||||
|
||||
let manager = TabManager()
|
||||
guard let workspace = manager.selectedWorkspace,
|
||||
let panelId = workspace.focusedPanelId else {
|
||||
XCTFail("Expected selected workspace with focused panel")
|
||||
return
|
||||
}
|
||||
|
||||
workspace.updatePanelDirectory(panelId: panelId, directory: repoURL.path)
|
||||
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
|
||||
|
||||
XCTAssertEqual(
|
||||
manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id),
|
||||
Set([panelId])
|
||||
)
|
||||
|
||||
try runGit(["checkout", "-b", "feature/sidebar-live-refresh"], in: repoURL)
|
||||
|
||||
manager.refreshTrackedWorkspaceGitMetadataForTesting()
|
||||
|
||||
XCTAssertTrue(
|
||||
waitForCondition {
|
||||
workspace.panelGitBranches[panelId]?.branch == "feature/sidebar-live-refresh"
|
||||
}
|
||||
)
|
||||
XCTAssertEqual(workspace.gitBranch?.branch, "feature/sidebar-live-refresh")
|
||||
}
|
||||
|
||||
func testResolvedCommandPathFallsBackOutsideAppPATH() throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDir = fileManager.temporaryDirectory.appendingPathComponent(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue