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
@@ -0,0 +1,63 @@
/// A strictly numeric dotted Mac marketing version used by the mobile update advisor.
///
/// Valid versions contain one to three nonnegative decimal components. Missing trailing
/// components compare as zero, so `0.65` and `0.65.0` are equal.
public struct MobileMacAppVersion: Comparable, Equatable, Sendable, CustomStringConvertible {
/// The one to three numeric components parsed from the marketing version.
public let components: [Int]

/// Creates a version from a strictly numeric dotted marketing-version string.
///
/// - Parameter string: A version containing one to three ASCII-decimal components.
/// - Returns: A parsed version, or `nil` for an empty, malformed, signed, suffixed, or oversized version.
public init?(parsing string: String) {
let substrings = string.split(separator: ".", omittingEmptySubsequences: false)
guard (1 ... 3).contains(substrings.count) else { return nil }

var parsedComponents: [Int] = []
parsedComponents.reserveCapacity(substrings.count)
for substring in substrings {
guard !substring.isEmpty,
substring.utf8.allSatisfy({ (48 ... 57).contains($0) }),
let component = Int(substring)
else {
return nil
}
parsedComponents.append(component)
}

components = parsedComponents
}

/// The canonical dotted representation of the parsed numeric components.
public var description: String {
components.map(String.init).joined(separator: ".")
}

/// Returns whether two versions have equal numeric components after adding trailing zeroes.
///
/// - Parameters:
/// - lhs: The first version to compare.
/// - rhs: The second version to compare.
/// - Returns: `true` when both versions represent the same numeric version.
public static func == (lhs: Self, rhs: Self) -> Bool {
!(lhs < rhs) && !(rhs < lhs)
}

/// Returns whether the first version numerically precedes the second version.
///
/// - Parameters:
/// - lhs: The first version to compare.
/// - rhs: The second version to compare.
/// - Returns: `true` when `lhs` is numerically older than `rhs`.
public static func < (lhs: Self, rhs: Self) -> Bool {
for index in 0 ..< 3 {
let left = index < lhs.components.count ? lhs.components[index] : 0
let right = index < rhs.components.count ? rhs.components[index] : 0
if left != right {
return left < right
}
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/// A Mac host capability and the mobile-visible feature it enables.
public struct MobileMacUpdateCapabilityRequirement: Sendable, Equatable {
/// The stable capability identifier advertised by a Mac host.
public let capability: String

/// The mobile-visible feature enabled by the capability.
public let feature: MobileMacUpdateFeature

/// The first released Mac marketing version that advertises the capability.
///
/// A `nil` value means the capability has not shipped in a Mac release, so the
/// update advisor never claims that updating unlocks it.
public let firstReleasedMacVersion: MobileMacAppVersion?

/// Creates a capability requirement for the update advisor.
///
/// - Parameters:
/// - capability: The stable capability identifier advertised by a Mac host.
/// - feature: The mobile-visible feature enabled by the capability.
/// - firstReleasedMacVersion: The first released Mac version containing the capability, or `nil` if unreleased.
public init(
capability: String,
feature: MobileMacUpdateFeature,
firstReleasedMacVersion: MobileMacAppVersion?
) {
self.capability = capability
self.feature = feature
self.firstReleasedMacVersion = firstReleasedMacVersion
}

/// The capability release registry known to this iOS build.
///
/// Update an unreleased entry when its capability first ships in a Mac release.
// lint:allow singleton — `standard` is the spec-required immutable declaration registry, not runtime state.
public static let standard: [MobileMacUpdateCapabilityRequirement] = [
.init(
capability: "workspace.actions.v1",
feature: .workspaceActions,
firstReleasedMacVersion: MobileMacAppVersion(parsing: "0.64.15")
),
.init(
capability: "workspace.read_state.v1",
feature: .workspaceReadState,
firstReleasedMacVersion: MobileMacAppVersion(parsing: "0.64.16")
),
.init(
capability: "workspace.close.v1",
feature: .workspaceClose,
firstReleasedMacVersion: MobileMacAppVersion(parsing: "0.64.16")
),
.init(
capability: "workspace.groups.v1",
feature: .workspaceGroups,
firstReleasedMacVersion: MobileMacAppVersion(parsing: "0.64.16")
),
.init(capability: "workspace.move.v1", feature: .workspaceMove, firstReleasedMacVersion: nil),
.init(capability: "workspace.group_actions.v1", feature: .workspaceGroupActions, firstReleasedMacVersion: nil),
.init(capability: "workspace.create_in_group.v1", feature: .workspaceCreateInGroup, firstReleasedMacVersion: nil),
.init(capability: "workspace.group_create.v1", feature: .workspaceGroupCreate, firstReleasedMacVersion: nil),
]
Comment on lines +35 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether MobileMacUpdateAdvisorTests already validates the standard registry.
rg -n 'standard' Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacUpdateAdvisorTests.swift

Repository: manaflow-ai/cmux

Length of output: 329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Requirement source =="
sed -n '1,140p' Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacUpdateCapabilityRequirement.swift

echo
echo "== Version parser =="
rg -n 'init\\(parsing:|struct MobileMacAppVersion|enum MobileMacAppVersion|firstReleasedMacVersion' Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell -S

echo
echo "== Existing advisor tests around standard registry =="
sed -n '1,240p' Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacUpdateAdvisorTests.swift

Repository: manaflow-ai/cmux

Length of output: 3241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '110,180p' Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacUpdateAdvisorTests.swift

Repository: manaflow-ai/cmux

Length of output: 1623


Add a registry invariant test for released capabilities

MobileMacAppVersion(parsing:) can return nil, and the current advisor test only covers the .workspaceGroups hint path. Add a test that asserts every standard entry with a declared release version still has a non-nil firstReleasedMacVersion, so a typo in one of the literals fails fast instead of silently making that capability look unreleased.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacUpdateCapabilityRequirement.swift`
around lines 35 - 60, Add a registry invariant test covering
MobileMacUpdateCapabilityRequirement.standard that filters entries with a
declared release version and asserts each has a non-nil firstReleasedMacVersion.
Ensure the test exercises every released capability, including the
workspaceGroups path, so malformed MobileMacAppVersion(parsing:) literals fail
immediately.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// A mobile-visible feature gated on a Mac host capability.
///
/// Raw values are stable identifiers used in dismissal signatures and analytics.
public enum MobileMacUpdateFeature: String, CaseIterable, Sendable {
/// Renaming and pinning workspaces.
case workspaceActions

/// Marking workspaces as read or unread.
case workspaceReadState

/// Closing workspaces.
case workspaceClose

/// Organizing workspaces into groups.
case workspaceGroups

/// Reordering workspaces.
case workspaceMove

/// Moving and grouping workspaces.
case workspaceGroupActions

/// Creating workspaces inside groups.
case workspaceCreateInGroup

/// Creating workspace groups.
case workspaceGroupCreate
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/// A truthful recommendation that a released Mac update unlocks mobile-visible features.
public struct MobileMacUpdateHint: Equatable, Sendable {
/// The missing features that a released Mac update unlocks, in registry order without duplicates.
public let features: [MobileMacUpdateFeature]

/// The minimum Mac marketing version that unlocks every listed feature.
public let minimumMacVersion: MobileMacAppVersion

/// The connected Mac's current marketing version, explicit or inferred.
public let macAppVersion: MobileMacAppVersion

/// Whether ``macAppVersion`` was inferred from the advertised capability set
/// rather than reported by the Mac. Released Macs older than 0.64.16 predate
/// the `mac_app_version` status field (and pre-0.64.17 attach tickets carry
/// no version either), so the versions this registry targets can only be
/// established by inference. UI copy must not state an inferred version as
/// the Mac's current version.
public let isVersionInferred: Bool

/// The stable missing-capability identifiers used to build the dismissal signature.
private let missingCapabilities: [String]

/// Creates a Mac update hint from contributing capability requirements.
///
/// - Parameters:
/// - features: The unique features to present, in registry order.
/// - minimumMacVersion: The minimum Mac version that unlocks the features.
/// - macAppVersion: The connected Mac's current version, explicit or inferred.
/// - isVersionInferred: Whether the version was derived from advertised capabilities.
/// - missingCapabilities: The contributing stable capability identifiers.
init(
features: [MobileMacUpdateFeature],
minimumMacVersion: MobileMacAppVersion,
macAppVersion: MobileMacAppVersion,
isVersionInferred: Bool,
missingCapabilities: [String]
) {
self.features = features
self.minimumMacVersion = minimumMacVersion
self.macAppVersion = macAppVersion
self.isVersionInferred = isVersionInferred
self.missingCapabilities = missingCapabilities
}

/// A stable signature that re-arms dismissal when the capability gap or target version changes.
public var dismissalSignature: String {
"\(Set(missingCapabilities).sorted().joined(separator: ","))>=\(minimumMacVersion)"
}

/// Builds a truthful update hint for missing capabilities available in a newer released Mac version.
///
/// The Mac's version comes from `mobile.host.status` or the attach ticket
/// when present. Both fields postdate the releases the standard registry
/// targets (status gained `mac_app_version` in 0.64.16, tickets in
/// 0.64.17), so when neither is available the version is inferred as the
/// newest `firstReleasedMacVersion` among registry capabilities the host
/// DOES advertise: a released Mac advertising a capability that first
/// shipped in X is at least X, and a released Mac at X missing a
/// capability that shipped in Y > X is older than Y. A host advertising
/// no registered capability yields no inference and no hint.
///
/// - Parameters:
/// - hostCapabilities: Capabilities from a successfully decoded `mobile.host.status` response.
/// - versionString: The connected Mac's reported marketing version, when available.
/// - requirements: The capability release registry known to the iOS build.
/// - Returns: A hint when at least one missing capability shipped after the Mac version, otherwise `nil`.
public init?(
hostCapabilities: Set<String>,
macAppVersion versionString: String?,
requirements: [MobileMacUpdateCapabilityRequirement] = MobileMacUpdateCapabilityRequirement.standard
) {
let explicitVersion = versionString.flatMap { MobileMacAppVersion(parsing: $0) }
// An unparseable non-empty version (nightly/prerelease marker) stays
// conservative: it is an explicit report we cannot compare, so no
// inference and no hint, rather than second-guessing a custom build.
if let versionString, !versionString.isEmpty, explicitVersion == nil {
return nil
}
let inferredVersion = explicitVersion == nil
? requirements
.filter { requirement in
requirement.firstReleasedMacVersion != nil
&& hostCapabilities.contains(requirement.capability)
}
.compactMap(\.firstReleasedMacVersion)
.max()
: nil
guard let macAppVersion = explicitVersion ?? inferredVersion else {
return nil
}

let contributors = requirements.filter { requirement in
guard let releaseVersion = requirement.firstReleasedMacVersion else { return false }
return !hostCapabilities.contains(requirement.capability) && macAppVersion < releaseVersion
}
guard !contributors.isEmpty,
let minimumMacVersion = contributors.compactMap(\.firstReleasedMacVersion).max()
else {
return nil
}

var seenFeatures: Set<MobileMacUpdateFeature> = []
self.init(
features: contributors.compactMap { requirement in
seenFeatures.insert(requirement.feature).inserted ? requirement.feature : nil
},
minimumMacVersion: minimumMacVersion,
macAppVersion: macAppVersion,
isVersionInferred: explicitVersion == nil,
missingCapabilities: contributors.map(\.capability)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
public import Foundation

/// Persists per-Mac update-hint dismissal signatures in injected user defaults.
public struct MobileMacUpdateHintDismissalStore {
/// The key prefix for per-Mac dismissal signatures.
private static let keyPrefix = "cmux.mobile.macUpdateHint.dismissed."

/// The injected defaults domain used for persistence.
private let defaults: UserDefaults

/// Creates a dismissal store backed by the supplied defaults domain.
///
/// - Parameter defaults: The defaults domain to use; production callers default to `UserDefaults.standard`.
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}

/// Returns whether a Mac dismissed the exact capability-gap signature.
///
/// - Parameters:
/// - macDeviceID: The stable identifier of the connected Mac.
/// - signature: The current hint's dismissal signature.
/// - Returns: `true` only when the stored signature exactly matches `signature`.
public func isDismissed(macDeviceID: String, signature: String) -> Bool {
defaults.string(forKey: Self.key(for: macDeviceID)) == signature
}

/// Persists dismissal of an exact capability gap for one Mac.
///
/// - Parameters:
/// - macDeviceID: The stable identifier of the connected Mac.
/// - signature: The current hint's dismissal signature.
public func dismiss(macDeviceID: String, signature: String) {
defaults.set(signature, forKey: Self.key(for: macDeviceID))
}

/// Builds the persistence key for a Mac device identifier.
///
/// - Parameter macDeviceID: The stable identifier of the connected Mac.
/// - Returns: The defaults key scoped to that Mac.
private static func key(for macDeviceID: String) -> String {
keyPrefix + macDeviceID
}
}
Loading
Loading