Merge pull request #2405 from manaflow-ai/issue-2388-sidebar-layout-regression
Fix duplicate sidebar git metadata publishes
This commit is contained in:
commit
6e6a2c95b5
3 changed files with 206 additions and 17 deletions
|
|
@ -11163,7 +11163,7 @@ enum SidebarTrailingAccessoryWidthPolicy {
|
|||
// Reactive workspace state inside the row must not rely on parent diffs alone:
|
||||
// `.equatable()` can otherwise leave sidebar badges/details stale until an
|
||||
// unrelated parent change sneaks through. Keep the workspace reference plain
|
||||
// and bridge its objectWillChange into local state instead.
|
||||
// and bridge only sidebar-visible workspace changes into local state.
|
||||
// Do NOT add @EnvironmentObject or new @Binding without updating ==.
|
||||
// Do NOT remove .equatable() from the ForEach call site in VerticalTabsSidebar.
|
||||
private struct TabItemView: View, Equatable {
|
||||
|
|
@ -11795,7 +11795,7 @@ private struct TabItemView: View, Equatable {
|
|||
}
|
||||
}
|
||||
.onReceive(
|
||||
tab.objectWillChange
|
||||
tab.sidebarObservationPublisher
|
||||
.receive(on: RunLoop.main)
|
||||
// Prompt-time sidebar telemetry can arrive as a short burst
|
||||
// (pwd, branch, PR, shell state). Coalesce that burst so the
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ func cmuxInheritedSurfaceConfig(
|
|||
return config
|
||||
}
|
||||
|
||||
struct SidebarStatusEntry {
|
||||
struct SidebarStatusEntry: Equatable {
|
||||
let key: String
|
||||
let value: String
|
||||
let icon: String?
|
||||
|
|
@ -143,7 +143,7 @@ struct SidebarStatusEntry {
|
|||
}
|
||||
}
|
||||
|
||||
struct SidebarMetadataBlock {
|
||||
struct SidebarMetadataBlock: Equatable {
|
||||
let key: String
|
||||
let markdown: String
|
||||
let priority: Int
|
||||
|
|
@ -4896,23 +4896,31 @@ enum SidebarLogLevel: String {
|
|||
case error
|
||||
}
|
||||
|
||||
struct SidebarLogEntry {
|
||||
struct SidebarLogEntry: Equatable {
|
||||
let message: String
|
||||
let level: SidebarLogLevel
|
||||
let source: String?
|
||||
let timestamp: Date
|
||||
}
|
||||
|
||||
struct SidebarProgressState {
|
||||
struct SidebarProgressState: Equatable {
|
||||
let value: Double
|
||||
let label: String?
|
||||
}
|
||||
|
||||
struct SidebarGitBranchState {
|
||||
struct SidebarGitBranchState: Equatable {
|
||||
let branch: String
|
||||
let isDirty: Bool
|
||||
}
|
||||
|
||||
private struct SidebarPanelObservationState: Equatable {
|
||||
let panelIds: [UUID]
|
||||
|
||||
init(panels: [UUID: any Panel]) {
|
||||
panelIds = panels.keys.sorted { $0.uuidString < $1.uuidString }
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkspaceRemoteConnectionState: String {
|
||||
case disconnected
|
||||
case connecting
|
||||
|
|
@ -5583,6 +5591,47 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
var agentPIDs: [String: pid_t] = [:]
|
||||
private var restoredTerminalScrollbackByPanelId: [UUID: String] = [:]
|
||||
|
||||
private func sidebarObservationSignal<Value: Equatable>(
|
||||
_ publisher: Published<Value>.Publisher
|
||||
) -> AnyPublisher<Void, Never> {
|
||||
publisher
|
||||
.dropFirst()
|
||||
.removeDuplicates()
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
lazy var sidebarObservationPublisher: AnyPublisher<Void, Never> = {
|
||||
let publishers: [AnyPublisher<Void, Never>] = [
|
||||
sidebarObservationSignal($title),
|
||||
sidebarObservationSignal($isPinned),
|
||||
sidebarObservationSignal($customColor),
|
||||
sidebarObservationSignal($currentDirectory),
|
||||
$panels
|
||||
.map(SidebarPanelObservationState.init)
|
||||
.dropFirst()
|
||||
.removeDuplicates()
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher(),
|
||||
sidebarObservationSignal($panelDirectories),
|
||||
sidebarObservationSignal($statusEntries),
|
||||
sidebarObservationSignal($metadataBlocks),
|
||||
sidebarObservationSignal($logEntries),
|
||||
sidebarObservationSignal($progress),
|
||||
sidebarObservationSignal($gitBranch),
|
||||
sidebarObservationSignal($panelGitBranches),
|
||||
sidebarObservationSignal($pullRequest),
|
||||
sidebarObservationSignal($panelPullRequests),
|
||||
sidebarObservationSignal($remoteConfiguration),
|
||||
sidebarObservationSignal($remoteConnectionState),
|
||||
sidebarObservationSignal($remoteConnectionDetail),
|
||||
sidebarObservationSignal($activeRemoteTerminalSessionCount),
|
||||
sidebarObservationSignal($listeningPorts),
|
||||
]
|
||||
|
||||
return Publishers.MergeMany(publishers).eraseToAnyPublisher()
|
||||
}()
|
||||
|
||||
private static func isProxyOnlyRemoteError(_ detail: String) -> Bool {
|
||||
let lowered = detail.lowercased()
|
||||
return lowered.contains("remote proxy")
|
||||
|
|
@ -6449,22 +6498,32 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
panelGitBranches[panelId] = state
|
||||
}
|
||||
if branchChanged {
|
||||
panelPullRequests.removeValue(forKey: panelId)
|
||||
if panelId == focusedPanelId {
|
||||
if panelPullRequests[panelId] != nil {
|
||||
panelPullRequests.removeValue(forKey: panelId)
|
||||
}
|
||||
if panelId == focusedPanelId, pullRequest != nil {
|
||||
pullRequest = nil
|
||||
}
|
||||
}
|
||||
if panelId == focusedPanelId {
|
||||
if panelId == focusedPanelId, gitBranch != state {
|
||||
gitBranch = state
|
||||
}
|
||||
}
|
||||
|
||||
func clearPanelGitBranch(panelId: UUID) {
|
||||
panelGitBranches.removeValue(forKey: panelId)
|
||||
panelPullRequests.removeValue(forKey: panelId)
|
||||
if panelGitBranches[panelId] != nil {
|
||||
panelGitBranches.removeValue(forKey: panelId)
|
||||
}
|
||||
if panelPullRequests[panelId] != nil {
|
||||
panelPullRequests.removeValue(forKey: panelId)
|
||||
}
|
||||
if panelId == focusedPanelId {
|
||||
gitBranch = nil
|
||||
pullRequest = nil
|
||||
if gitBranch != nil {
|
||||
gitBranch = nil
|
||||
}
|
||||
if pullRequest != nil {
|
||||
pullRequest = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6520,14 +6579,16 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
if existing != state {
|
||||
panelPullRequests[panelId] = state
|
||||
}
|
||||
if panelId == focusedPanelId {
|
||||
if panelId == focusedPanelId, pullRequest != state {
|
||||
pullRequest = state
|
||||
}
|
||||
}
|
||||
|
||||
func clearPanelPullRequest(panelId: UUID) {
|
||||
panelPullRequests.removeValue(forKey: panelId)
|
||||
if panelId == focusedPanelId {
|
||||
if panelPullRequests[panelId] != nil {
|
||||
panelPullRequests.removeValue(forKey: panelId)
|
||||
}
|
||||
if panelId == focusedPanelId, pullRequest != nil {
|
||||
pullRequest = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import WebKit
|
|||
import ObjectiveC.runtime
|
||||
import Bonsplit
|
||||
import UserNotifications
|
||||
import Combine
|
||||
|
||||
#if canImport(cmux_DEV)
|
||||
@testable import cmux_DEV
|
||||
|
|
@ -2111,6 +2112,133 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
|
|||
XCTAssertEqual(ordered.map(\.isDirty), [false, true])
|
||||
}
|
||||
|
||||
func testUpdatingFocusedPanelGitBranchWithSameStateDoesNotRepublishWorkspace() {
|
||||
let workspace = Workspace()
|
||||
guard let panelId = workspace.focusedPanelId else {
|
||||
XCTFail("Expected initial focused panel")
|
||||
return
|
||||
}
|
||||
|
||||
var publishCount = 0
|
||||
let cancellable = workspace.objectWillChange.sink { _ in
|
||||
publishCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
|
||||
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
|
||||
let baselinePublishCount = publishCount
|
||||
|
||||
XCTAssertGreaterThan(
|
||||
baselinePublishCount,
|
||||
0,
|
||||
"Expected the first focused branch update to publish workspace changes"
|
||||
)
|
||||
|
||||
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
|
||||
|
||||
XCTAssertEqual(
|
||||
publishCount,
|
||||
baselinePublishCount,
|
||||
"Expected identical focused branch refreshes to avoid extra workspace publishes"
|
||||
)
|
||||
}
|
||||
|
||||
func testUpdatingFocusedPanelPullRequestWithSameStateDoesNotRepublishWorkspace() {
|
||||
let workspace = Workspace()
|
||||
guard let panelId = workspace.focusedPanelId else {
|
||||
XCTFail("Expected initial focused panel")
|
||||
return
|
||||
}
|
||||
|
||||
workspace.updatePanelGitBranch(panelId: panelId, branch: "feature/sidebar-pr", isDirty: false)
|
||||
|
||||
var publishCount = 0
|
||||
let cancellable = workspace.objectWillChange.sink { _ in
|
||||
publishCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
|
||||
let pullRequestURL = URL(string: "https://github.com/manaflow-ai/cmux/pull/2388")!
|
||||
workspace.updatePanelPullRequest(
|
||||
panelId: panelId,
|
||||
number: 2388,
|
||||
label: "PR",
|
||||
url: pullRequestURL,
|
||||
status: .open,
|
||||
branch: "feature/sidebar-pr"
|
||||
)
|
||||
let baselinePublishCount = publishCount
|
||||
|
||||
XCTAssertGreaterThan(
|
||||
baselinePublishCount,
|
||||
0,
|
||||
"Expected the first focused pull request update to publish workspace changes"
|
||||
)
|
||||
|
||||
workspace.updatePanelPullRequest(
|
||||
panelId: panelId,
|
||||
number: 2388,
|
||||
label: "PR",
|
||||
url: pullRequestURL,
|
||||
status: .open,
|
||||
branch: "feature/sidebar-pr"
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
publishCount,
|
||||
baselinePublishCount,
|
||||
"Expected identical focused pull request refreshes to avoid extra workspace publishes"
|
||||
)
|
||||
}
|
||||
|
||||
func testSidebarObservationPublisherEmitsForFocusedGitBranchChangesOnlyOncePerState() {
|
||||
let workspace = Workspace()
|
||||
guard let panelId = workspace.focusedPanelId else {
|
||||
XCTFail("Expected initial focused panel")
|
||||
return
|
||||
}
|
||||
|
||||
var publishCount = 0
|
||||
let cancellable = workspace.sidebarObservationPublisher.sink {
|
||||
publishCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
|
||||
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
|
||||
let baselinePublishCount = publishCount
|
||||
XCTAssertGreaterThan(
|
||||
baselinePublishCount,
|
||||
0,
|
||||
"Expected focused git branch updates to invalidate sidebar rows"
|
||||
)
|
||||
|
||||
workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false)
|
||||
XCTAssertEqual(
|
||||
publishCount,
|
||||
baselinePublishCount,
|
||||
"Expected identical git metadata refreshes to be ignored by sidebar rows"
|
||||
)
|
||||
}
|
||||
|
||||
func testSidebarObservationPublisherIgnoresRemoteHeartbeatOnlyChanges() {
|
||||
let workspace = Workspace()
|
||||
|
||||
var publishCount = 0
|
||||
let cancellable = workspace.sidebarObservationPublisher.sink {
|
||||
publishCount += 1
|
||||
}
|
||||
defer { cancellable.cancel() }
|
||||
|
||||
workspace.remoteHeartbeatCount = 1
|
||||
workspace.remoteLastHeartbeatAt = Date()
|
||||
|
||||
XCTAssertEqual(
|
||||
publishCount,
|
||||
0,
|
||||
"Expected non-visible remote heartbeat updates to avoid invalidating sidebar rows"
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testSidebarPullRequestsTrackFocusedPanelOnly() {
|
||||
let workspace = Workspace()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue