Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
05c8643
test: require explicit iOS pairing opt-in
azooz2003-bit Sep 11, 2026
63132cd
fix: require explicit iOS pairing opt-in
azooz2003-bit Sep 11, 2026
d7f413a
test: preserve control socket with fresh wrappers
azooz2003-bit Sep 11, 2026
5483e2f
fix: keep current control socket alive
azooz2003-bit Sep 11, 2026
93340cd
feat: surface iOS pairing opt-in during setup
azooz2003-bit Sep 11, 2026
fc8c4c7
fix: keep pairing guidance visible on final page
azooz2003-bit Sep 11, 2026
73856eb
fix: explain pairing in empty mobile state
azooz2003-bit Sep 11, 2026
e616736
fix: put iOS pairing first in Mobile settings
azooz2003-bit Sep 11, 2026
a76d2b1
feat: make iOS pairing requirement visible
azooz2003-bit Sep 11, 2026
beb3fdd
fix: place pairing runtime gate on host service
azooz2003-bit Sep 11, 2026
149326e
fix: keep onboarding swipes responsive
azooz2003-bit Sep 11, 2026
786a609
fix: emphasize mac pairing toggle in whats new
azooz2003-bit Sep 11, 2026
2b030bb
debug: trace mobile attach startup milestones
azooz2003-bit Sep 11, 2026
0a39285
fix: remove stale tailscale whats new keys
azooz2003-bit Sep 11, 2026
a95c6fe
fix: emphasize pairing setting in whats new
azooz2003-bit Sep 12, 2026
bf77833
fix: keep pairing notice on its own whats new page
azooz2003-bit Sep 13, 2026
e4eb64e
fix: show mac pairing setting in whats new
azooz2003-bit Sep 13, 2026
885e0e8
test: preserve whats new during catalog rollout
azooz2003-bit Sep 13, 2026
5acb24e
fix: keep whats new visible during catalog rollout
azooz2003-bit Sep 13, 2026
c7d22d5
test: focus pairing whats new on opt-in
azooz2003-bit Sep 14, 2026
99b2558
fix: streamline pairing whats new page
azooz2003-bit Sep 14, 2026
9575106
test: make pairing whats new hierarchy explicit
azooz2003-bit Sep 14, 2026
2a64bba
fix: simplify pairing whats new hierarchy
azooz2003-bit Sep 14, 2026
ed3377b
fix: remove retired Mac update whats new title
azooz2003-bit Sep 14, 2026
d82622a
fix: tighten pairing announcement layout
azooz2003-bit Sep 14, 2026
f99fcbe
test: preserve separate whats new updates and read state
azooz2003-bit Sep 15, 2026
b8af003
fix: restore previous update and focus pairing screenshot
azooz2003-bit Sep 15, 2026
6b054be
test: locate whats new content by accessible labels
azooz2003-bit Sep 15, 2026
4ad7a9c
test: cover fitted Whats New swipe pages and appearance
azooz2003-bit Sep 15, 2026
8d9e9d4
test: capture Mac pairing setting in both appearances
azooz2003-bit Sep 15, 2026
9aae3f0
test: correct capture queries and retain visual evidence
azooz2003-bit Sep 15, 2026
b3de579
Correct pairing screenshot setup and sheet selectors
azooz2003-bit Sep 15, 2026
06305ff
Fit Whats New sheets to each page and match Mac screenshot appearance
azooz2003-bit Sep 15, 2026
e334836
Measure the final footnote when checking the older update fit
azooz2003-bit Sep 15, 2026
be081a4
Retain passing swipe recordings and use an instance for pairing copy
azooz2003-bit Sep 15, 2026
8a19be9
Check rendered pairing state when capturing Mac settings
azooz2003-bit Sep 15, 2026
83f798c
Detect clipped pairing title in Settings update detail
azooz2003-bit Sep 15, 2026
8e2b84b
Animate fitted sheet selection and preserve full update titles
azooz2003-bit Sep 15, 2026
45ec602
fix: package pairing screenshots for devices
azooz2003-bit Sep 15, 2026
7a33d1a
Add developer replay for a range of What's New updates
azooz2003-bit Sep 15, 2026
1e94dc3
Show pairing update before Mac discovery
azooz2003-bit Sep 15, 2026
5f8b4e2
Keep pairing update visible with stale catalog
azooz2003-bit Sep 15, 2026
77dac59
Sharpen pairing settings screenshots
azooz2003-bit Sep 15, 2026
dc32363
Use framed pairing settings screenshots
azooz2003-bit Sep 15, 2026
18bbbb8
Refine pairing update screenshot and title
azooz2003-bit Sep 16, 2026
654428d
Add light pairing settings screenshot
azooz2003-bit Sep 16, 2026
f93e694
Merge origin/main into task-explicit-ios-pairing-opt-in
azooz2003-bit Sep 16, 2026
09a7299
Restore debug transport compatibility after merge
azooz2003-bit Sep 16, 2026
8d6a9e8
Restore legacy pairing runtime dependency
azooz2003-bit Sep 16, 2026
3acccbe
Include legacy pairing runtime sources
azooz2003-bit Sep 16, 2026
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
9 changes: 9 additions & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,15 @@ jobs:
if-no-files-found: warn
retention-days: 30

- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results
path: ${{ env.CMUX_DERIVED_DATA_PATH }}/Logs/Test/*.xcresult
if-no-files-found: warn
retention-days: 7

- name: Publish test summary
if: always()
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,14 @@ public enum DiagnosticAppEventKind: Int, Sendable, Codable, CaseIterable {
/// The transport that actually carries the foreground connection, recorded
/// on connect and on every active-route change. `c`: ``DiagnosticTransportKind``.
case foregroundTransportSelected = 662
/// Whether a DEBUG launch supplied an injected dogfood attach route.
/// `c`: boolean.
case dogfoodAttachEnvironmentObserved = 663
/// Whether authentication bootstrap completed with an authenticated user.
/// `c`: boolean.
case authBootstrapCompleted = 664
/// A DEBUG launch attach route was admitted by the startup coordinator.
case dogfoodAttachStarted = 665
}

/// The user's configured connection method, mirrored from the settings picker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ struct CMUXMobileRootView: View {
#endif
}

private var shouldShowWhatsNewPreview: Bool {
#if os(iOS) && DEBUG
return UITestConfig.whatsNewPreviewEnabled
#else
return false
#endif
}

private var shouldShowOnboardingPreview: Bool {
#if os(iOS) && DEBUG
return UITestConfig.onboardingPreviewEnabled
Expand Down Expand Up @@ -249,6 +257,14 @@ struct CMUXMobileRootView: View {
#endif
}

@ViewBuilder private var whatsNewPreview: some View {
#if os(iOS) && DEBUG
MobileWhatsNewPreviewView()
#else
EmptyView()
#endif
}

var body: some View {
rootContent
#if os(iOS)
Expand Down Expand Up @@ -276,6 +292,12 @@ struct CMUXMobileRootView: View {
.animation(.snappy(duration: 0.18), value: store.phase)
.onAppear {
syncShellAuthentication(isAuthenticated)
#if os(iOS)
diagnosticLog?.recordAppEvent(
.dogfoodAttachEnvironmentObserved,
count: hasInjectedAttachLaunchRoute ? 1 : 0
)
#endif
store.resumeForegroundRefresh()
#if os(iOS)
pushCoordinator.bind(store: store)
Expand Down Expand Up @@ -495,6 +517,8 @@ struct CMUXMobileRootView: View {
macSurfaceGalleryPreview
} else if shouldShowHiddenComputersPreview {
hiddenComputersPreview
} else if shouldShowWhatsNewPreview {
whatsNewPreview
} else if shouldShowOnboardingPreview {
onboardingPreview
} else if shouldShowOnboarding {
Expand Down Expand Up @@ -1105,6 +1129,10 @@ struct CMUXMobileRootView: View {
#endif
await authManager.awaitBootstrapped()
guard !Task.isCancelled else { return }
diagnosticLog?.recordAppEvent(
.authBootstrapCompleted,
count: authManager.isAuthenticated ? 1 : 0
)
if authManager.isAuthenticated {
guard prepareResolvedAccountScope() != nil else { return }
}
Expand Down Expand Up @@ -1420,6 +1448,7 @@ struct CMUXMobileRootView: View {
let attachURL = UITestConfig.dogfoodAttachURL ?? UITestConfig.attachURL else {
return false
}
diagnosticLog?.recordAppEvent(.dogfoodAttachStarted)
return startupConnectionCoordinator.startInjectedAttach(
attachURL: attachURL,
prepare: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#if os(iOS) && DEBUG
import CmuxMobileSupport
import Foundation
import SwiftUI

/// A frozen range keeps a catalog refresh from changing an open replay.
struct MobileWhatsNewReplay: Identifiable {
let id = UUID()
let pages: [MobileWhatsNewPage]

init?(pages: [MobileWhatsNewPage], firstID: String, lastID: String) {
guard let first = pages.firstIndex(where: { $0.listID == firstID }),
let last = pages.firstIndex(where: { $0.listID == lastID }) else { return nil }
self.pages = Array(pages[min(first, last)...max(first, last)])
}
}

struct MobileWhatsNewDebugView: View {
let pages: [MobileWhatsNewPage]
let allowedWebHosts: Set<String>
@State private var firstID: String
@State private var lastID: String
@State private var replay: MobileWhatsNewReplay?

init(pages: [MobileWhatsNewPage], allowedWebHosts: Set<String>) {
self.pages = pages
self.allowedWebHosts = allowedWebHosts
_firstID = State(initialValue: pages.first?.listID ?? "")
_lastID = State(initialValue: pages.last?.listID ?? "")
}

private var selection: MobileWhatsNewReplay? {
MobileWhatsNewReplay(pages: pages, firstID: firstID, lastID: lastID)
}

var body: some View {
Form {
if pages.isEmpty {
Text(L10n.string("mobile.whatsNew.debug.empty", defaultValue: "No updates available"))
} else {
Section {
Picker(
L10n.string("mobile.whatsNew.debug.first", defaultValue: "First Update"),
selection: $firstID
) {
ForEach(pages, id: \.listID) { page in
Text(page.id).tag(page.listID)
}
}
.accessibilityIdentifier("MobileWhatsNewReplayFirst")

Picker(
L10n.string("mobile.whatsNew.debug.last", defaultValue: "Last Update"),
selection: $lastID
) {
ForEach(pages, id: \.listID) { page in
Text(page.id).tag(page.listID)
}
}
.accessibilityIdentifier("MobileWhatsNewReplayLast")
}
.pickerStyle(.menu)

Section {
ForEach(selection?.pages ?? [], id: \.listID) { page in
VStack(alignment: .leading, spacing: 4) {
Text(page.title)
Text(page.id)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}

Section {
Button {
replay = selection
} label: {
Label(
L10n.string("mobile.whatsNew.debug.show", defaultValue: "Show Sheets"),
systemImage: "play.rectangle"
)
}
.disabled(selection == nil)
.accessibilityIdentifier("MobileWhatsNewReplayShow")
}
}
}
.navigationTitle(L10n.string("mobile.whatsNew.debug.title", defaultValue: "Replay What's New"))
.navigationBarTitleDisplayMode(.inline)
.accessibilityIdentifier("MobileWhatsNewDebugView")
.sheet(item: $replay) { replay in
MobileWhatsNewSheet(
pages: replay.pages,
allowedWebHosts: allowedWebHosts,
dismiss: { self.replay = nil }
)
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#if os(iOS) && DEBUG
import SwiftUI
import Foundation

/// Deterministic What's New host for screenshot checks. It renders the same
/// native sheet view the app presents after update, but does not require a
/// signed-in or paired app session.
public struct MobileWhatsNewPreviewView: View {
@State private var showsSheet = true
@State private var center = MobileWhatsNewCenter(
apiBaseURL: "https://cmux.test",
buildType: .beta,
defaults: UserDefaults(suiteName: "MobileWhatsNewPreview-\(UUID().uuidString)")!,
loader: { _ in throw URLError(.notConnectedToInternet) }
)

public init() {}

public var body: some View {
NavigationStack {
MobileWhatsNewListView()
}
.environment(center)
.sheet(isPresented: $showsSheet) {
MobileWhatsNewSheet(
pages: MobileWhatsNewCatalog.entries,
allowedWebHosts: [],
dismiss: { showsSheet = false }
)
.preferredColorScheme(
ProcessInfo.processInfo.environment["CMUX_UITEST_WHATS_NEW_APPEARANCE"] == "dark"
? .dark : .light
)
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ struct DeviceTreeView: View {
if connectionMethodStore?.method == .tailscale {
return MobilePairingScannerSheet.emptyStateGuidanceText
}
return showAddDevice != nil
let description = showAddDevice != nil
? L10n.string(
"mobile.v2.connections.empty",
defaultValue: "On your Mac, turn on Enable iOS pairing in cmux Settings. Select the same team on both devices and keep cmux running. Only Macs you own or have permission to connect to appear here."
Expand All @@ -212,6 +212,7 @@ struct DeviceTreeView: View {
"mobile.v2.devices.emptyDescription",
defaultValue: "On your Mac, turn on Enable iOS pairing in cmux Settings. Select the same team on both devices and keep cmux running. Only Macs you own or have permission to connect to appear here."
)
return "\(description) \(MobilePairingCopy().emptyWorkspaceMessage)"
}

private func hideComputer(_ computer: MacComputerSnapshot) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ struct DisconnectedWorkspaceShellView: View {
ToolbarItem(placement: .topBarLeading) {
Button(action: showComputers) {
MobileDevicesToolbarLabel(
gateWarningPairingIDs: store?.macVersionUpdateRequiredPairingIDs ?? [],
computerPairingIDs: savedComputerPairingIDs
gateWarningDeviceIDs: store?.macVersionUpdateRequiredDeviceIDs ?? [],
computerDeviceIDs: savedComputerDeviceIDs
)
}
.accessibilityLabel(L10n.string(
Expand Down Expand Up @@ -164,8 +164,11 @@ struct DisconnectedWorkspaceShellView: View {

/// The Computers sheet includes both shown and hidden rows, so both sets
/// participate in the toolbar warning scope while this screen is open.
private var savedComputerPairingIDs: Set<String> {
Set(savedComputers.map(\.id) + (store?.hiddenComputers.map(\.id) ?? []))
private var savedComputerDeviceIDs: Set<String> {
Set(
savedComputers.map(\.deviceId)
+ (store?.hiddenComputers.map(\.macDeviceID) ?? [])
)
}

@ViewBuilder
Expand Down Expand Up @@ -290,9 +293,9 @@ struct DisconnectedWorkspaceShellView: View {
}
#endif
return L10n.string(
"mobile.v2.devices.emptyDescription",
defaultValue: "On your Mac, turn on Enable iOS pairing in cmux Settings. Select the same team on both devices and keep cmux running. Only Macs you own or have permission to connect to appear here."
)
"mobile.devices.emptyDescription",
defaultValue: "For Iroh to find a Mac, run cmux 0.64.20 or later on the Mac, sign in to cmux on both devices with the same account, and keep cmux running on the Mac while both devices are online. If any requirement is missing, the Mac will not appear automatically. To use Tailscale instead, open Settings, tap Connection Method, and choose Tailscale Only."
) + " " + MobilePairingCopy().emptyWorkspaceMessage
}

/// Reconnect this row's computer. `switchToMac` promotes a live secondary
Expand Down Expand Up @@ -390,8 +393,8 @@ struct DisconnectedWorkspaceShellView: View {
Text(
savedMacs.isEmpty
? L10n.string(
"mobile.v2.devices.emptyDescription",
defaultValue: "On your Mac, turn on Enable iOS pairing in cmux Settings. Select the same team on both devices and keep cmux running. Only Macs you own or have permission to connect to appear here."
"mobile.devices.emptyDescription",
defaultValue: "For Iroh to find a Mac, run cmux 0.64.20 or later on the Mac, sign in to cmux on both devices with the same account, and keep cmux running on the Mac while both devices are online. If any requirement is missing, the Mac will not appear automatically. To use Tailscale instead, open Settings, tap Connection Method, and choose Tailscale Only."
)
: savedMacDescription
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#if os(iOS)
import CmuxMobileSupport

struct MobilePairingCopy {
var enableOnMacShort: String {
L10n.string(
"mobile.pairing.enableOnMac.short",
defaultValue: "Enable iOS pairing in cmux Settings > Mobile on your Mac."
)
}

var enableOnMac: String {
L10n.string(
"mobile.pairing.enableOnMac",
defaultValue: "Before pairing, open cmux Settings > Mobile on the Mac and turn on Enable iOS pairing. This Mac stays hidden from iOS while it is off."
)
}

var emptyWorkspaceMessage: String {
L10n.string(
"mobile.workspaces.empty.message",
defaultValue: "Enable iOS pairing in cmux Settings > Mobile on your Mac, sign in to the same cmux account on both devices, and keep cmux running. Your Mac and its workspaces will appear here after pairing is enabled."
)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,14 @@ extension MobilePairingScannerSheet {

/// Tailscale setup guidance for an empty computer list, including the route back to Auto-Connect.
static var emptyStateGuidanceText: String {
L10n.string(
let guidance = L10n.string(
"mobile.tailscalePairing.emptyDescription",
defaultValue: """
Install Tailscale on both devices and use the same Tailscale network. Open Tailscale \
Pairing on the Mac and scan its QR here, or enter the Mac's numeric Tailscale IP and port. \
To use Auto-Connect instead, open Settings, tap Connection Method, and choose Auto-Connect.
"""
)
return "\(guidance) \(MobilePairingCopy().emptyWorkspaceMessage)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ struct MobileSettingsView: View {

var body: some View {
@Bindable var displaySettings = displaySettings
#if DEBUG
let whatsNewPages = whatsNewCenter?.archivePages ?? MobileWhatsNewCatalog.channelVisibleEntries()
let whatsNewHosts = whatsNewCenter?.allowedWebHosts ?? []
#endif
return NavigationStack {
Form {
MobileSettingsAccountSection(signOut: signOut)
Expand Down Expand Up @@ -273,6 +277,15 @@ struct MobileSettingsView: View {

#if DEBUG
Section(L10n.string("mobile.settings.developer", defaultValue: "Developer")) {
NavigationLink {
MobileWhatsNewDebugView(pages: whatsNewPages, allowedWebHosts: whatsNewHosts)
} label: {
Label(
L10n.string("mobile.whatsNew.debug.title", defaultValue: "Replay What's New"),
systemImage: "rectangle.stack"
)
}
.accessibilityIdentifier("MobileSettingsReplayWhatsNew")
Button {
showingToastGallery = true
} label: {
Expand Down
Loading
Loading