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
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ final class ConfigStore: ObservableObject {

func revert(_ keys: [String]) {
for k in keys { configSet(&working, k, configGet(original, k)) }
// Discarding the Listen edit ends "the user cleared it": follow the
// running core's address again.
if keys.contains("listen") {
listenCleared = false
adoptRunningListenIfBlank()
}
revision += 1
}

Expand Down
30 changes: 29 additions & 1 deletion native/macos/MCPProxy/MCPProxy/Views/ClientsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ struct ClientsView: View {
tab = 0
focusedClientID = id
pendingExpandID = id
// Clear the clients filter only when it would hide the target;
// the `.onChange` reload keeps `pendingExpandID`, so the row expands.
if Self.filterHidesTarget(filter: clientsFilter, targetID: id,
filteredIDs: nil, roster: appState.clients) {
clientsFilter = ScopeFilter()
}
expandPendingClient(dropIfAbsent: false)
case .connect(let id)?:
tab = 0
Expand All @@ -142,6 +148,21 @@ struct ClientsView: View {
}
}

/// Whether the active clients filter hides the client a `.clientDetail`
/// route targets (a pure decision, so it is unit-testable without a view).
/// `filteredIDs` is the filtered list once loaded (nil before); `roster` is
/// the unfiltered app-wide client list. An unknown target is never "hidden":
/// the request is dropped as before and the filter is left alone.
static func filterHidesTarget(filter: ScopeFilter, targetID: String,
filteredIDs: [String]?, roster: [ClientPresenceRecord]) -> Bool {
guard filter.profile != nil || filter.client != nil else { return false }
guard let target = roster.first(where: { $0.id == targetID }) else { return false }
if let client = filter.client, client != targetID { return true }
if let filteredIDs { return !filteredIDs.contains(targetID) }
if let profile = filter.profile { return target.profile != profile }
return false
}

/// Expand the row a `.clientDetail` route asked for, once it is loaded.
/// After a full load the request is dropped when the client is gone.
private func expandPendingClient(dropIfAbsent: Bool) {
Expand Down Expand Up @@ -488,7 +509,14 @@ struct ClientsView: View {
let response = try await loadedClients
clients = response.clients
warnings = response.warnings ?? []
expandPendingClient(dropIfAbsent: true)
if let id = pendingExpandID,
Self.filterHidesTarget(filter: clientsFilter, targetID: id,
filteredIDs: clients.map(\.id), roster: appState.clients) {
// Filtered out but known: widen the view (the reload re-expands it).
clientsFilter = ScopeFilter()
} else {
expandPendingClient(dropIfAbsent: true)
}
if profile == nil && client == nil {
appState.clients = response.clients
appState.clientWarnings = warnings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,11 @@ final class ConnectClientModel: ObservableObject {
profiles = updated
if !profile.isEmpty, !updated.contains(where: { $0.name == profile }) {
profile = ""
lockedChoice = false
lockedChoice = true
// Back to "untouched": the write then sends `.unspecified`, which
// keeps the client's existing binding instead of forcing
// All servers / switchable over it.
bindingTouched = false
invalidatePreview()
}
}
Expand Down
59 changes: 59 additions & 0 deletions native/macos/MCPProxy/MCPProxyTests/ClientsRoutingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ClientsRoutingTests.swift
// MCPProxyTests
//
// A `.clientDetail` route clears the active clients filter only when that
// filter would hide the target client (wave-2 item 1451-4).

import XCTest
@testable import MCPProxy

final class ClientsRoutingTests: XCTestCase {
private func record(_ id: String, profile: String?) -> ClientPresenceRecord {
var r = ClientPresenceRecord(
id: id, displayName: id, kind: "supported", icon: nil, state: "connected_seen",
installed: true, connected: true, connectionUnverified: nil, configPath: nil,
displayPath: nil, lastSeen: nil, activeSessions: 0, calls24h: 0, reloadHint: nil)
r.profile = profile
return r
}

private var roster: [ClientPresenceRecord] {
[record("cursor", profile: "ro"), record("zed", profile: "rw")]
}

private func hides(_ filter: ScopeFilter, _ id: String, filtered: [String]? = nil) -> Bool {
ClientsView.filterHidesTarget(filter: filter, targetID: id, filteredIDs: filtered, roster: roster)
}

func testFilterOnAnotherClientHidesTheTarget() {
var f = ScopeFilter(); f.client = "zed"
XCTAssertTrue(hides(f, "cursor"))
}

func testFilterOnTheTargetItselfDoesNotHideIt() {
var f = ScopeFilter(); f.client = "cursor"
XCTAssertFalse(hides(f, "cursor"))
}

func testProfileFilterThatIncludesTheTargetIsKept() {
var f = ScopeFilter(); f.profile = "ro"
XCTAssertFalse(hides(f, "cursor"))
XCTAssertFalse(hides(f, "cursor", filtered: ["cursor"]))
}

func testProfileFilterThatExcludesTheTargetIsCleared() {
var f = ScopeFilter(); f.profile = "ro"
XCTAssertTrue(hides(f, "zed"))
XCTAssertTrue(hides(f, "zed", filtered: ["cursor"]))
}

func testUnknownTargetIsNotHidden() {
var f = ScopeFilter(); f.profile = "ro"
XCTAssertFalse(hides(f, "ghost", filtered: []))
}

func testNoFilterNeverHides() {
XCTAssertFalse(hides(ScopeFilter(), "cursor"))
XCTAssertFalse(hides(ScopeFilter(), "zed", filtered: []))
}
}
11 changes: 11 additions & 0 deletions native/macos/MCPProxy/MCPProxyTests/ConnectClientModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,17 @@ final class ConnectClientPresentationTests: XCTestCase {
XCTAssertEqual(model.mode, .locked)
}

func testRefreshProfilesFallbackSendsUnspecifiedSoTheExistingBindingIsKept() async {
let ro = ProfileView(name: "ro", title: "Read-only", servers: ["github"], managementTools: false)
let model = await selectedModel(FakeConnectSource(), profiles: [ro])
model.chooseProfile("ro")
XCTAssertTrue(model.bindingTouched)
model.refreshProfiles([])
XCTAssertEqual(model.profile, "")
XCTAssertEqual(model.binding, .unspecified,
"a vanished profile must not turn into an explicit All servers write")
}

func testRefreshProfilesFallsBackToAllServersWhenTheSelectionIsGone() async {
let ro = ProfileView(name: "work-ro", title: "Work Read-only", servers: ["github"], managementTools: false)
let model = await selectedModel(FakeConnectSource(), profiles: [ro])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ final class SettingsEffectiveStateTests: XCTestCase {
"a field the user emptied stays empty")
}

func testDiscardingAClearedListenReadoptsTheRunningAddress() async throws {
let store = makeStore(
configJSON: #"{"quarantine_enabled":true}"#,
statusJSON: #"{"running":true,"listen_addr":"127.0.0.1:18666"}"#)
await store.load()
store.setValue("listen", "")
store.revert(["listen"])
store.applyStatus(try status(#"{"running":true,"listen_addr":"127.0.0.1:19777"}"#))
XCTAssertEqual(store.stringBinding("listen").wrappedValue, "127.0.0.1:19777")
XCTAssertFalse(store.isDirty("listen"))
}

func testTypedListenIsNotMarkedCleanWhenTheCoreRestartsOntoIt() async {
let store = makeStore(
configJSON: #"{"quarantine_enabled":true}"#,
Expand Down
Loading