Skip to content

Recover account Macs deleted before iOS recovery support - #8757

Closed
azooz2003-bit wants to merge 2 commits into
mainfrom
task-ios-account-recovery-any-version
Closed

azooz2003-bit wants to merge 2 commits into
mainfrom
task-ios-account-recovery-any-version

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 23, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

CMUX Internal only exposed account recovery when the current install had a local forgotten-Mac marker. Macs deleted before recovery support, or in another install scope, had no action even though the signed-in account broker could still discover them.

Fix

  • expose explicit account recovery whenever signed-in personal-account Iroh discovery is available
  • keep the forgotten marker for passive rediscovery suppression and deleted-state copy only
  • discover live same-account Macs on explicit recovery, then authenticate device ID and instance tag before persistence
  • explain the account recovery path before deletion and on empty/device-tree states
  • localize new English and Japanese copy

Verification

  • regression test committed before fix
  • swift test --filter IrohZeroTouchDiscoveryTests (18 passed)
  • testMarkerlessAccountRecoveryIsVisibleAndActionable XCUITest (passed)
  • tagged macOS and isolated iOS Simulator builds succeeded

The full iOS test plan remains blocked by the existing unrelated WorkspaceMacSelectionTests compile error for missing macTitlePickerSelection; the focused UI target passes when isolated.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Enable account-based Mac recovery on iOS even without a local deletion marker by scanning live same‑account Iroh devices, improving recovery for Macs deleted before recovery support or across installs.

  • New Features

    • Added recoverIrohMacFromAccount() to MobileShellComposite to scan same‑account Iroh and authenticate device ID + instance tag before persisting; prioritizes forgotten Macs, then unpaired; returns MobileAccountComputerRecoveryResult and handles scope changes and in‑progress scans.
    • Introduced accountComputerRecoveryMode to drive UI availability and copy; keep forgotten markers for passive suppression and messaging.
    • Updated UI with AccountComputerRecoveryButton and footer that adapt to mode; shown in Disconnected and Device Tree when personalIrohDiscovery exists; added EN/JA strings and new accessibility id MobileAccountComputerRecoveryButton.
    • Empty state no longer auto‑opens Add Computer when account recovery is available.
  • Refactors

    • Replaced deleted‑only flow with generalized account recovery: removed recoverForgottenIrohMacFromAccount() and isRecoveringDeletedComputer; added isRecoveringAccountComputer.
    • Renamed DeletedComputerRecovery* views to AccountComputerRecovery*; removed MobileShellComposite+ForgottenMacRecovery.swift and added MobileShellComposite+AccountMacRecovery.swift.
    • Expanded tests to cover markerless recovery, mixed routes, in‑progress protection, and UI visibility.

Written for commit 7db653e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added account computer recovery to find and reconnect available computers.
    • Recovery now supports both finding an account computer and restoring a previously deleted computer.
    • Added recovery status, progress, failure messaging, and accessibility support.
    • Recovery remains available when no deletion marker is present.
  • Bug Fixes

    • Prevented recovery from continuing after sign-out or account changes.
    • Improved handling of failed recovery attempts and concurrent recovery actions.
  • Tests

    • Added coverage for successful, unavailable, failed, concurrent, and account-change recovery scenarios.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds account-scoped Iroh Mac recovery for iOS, replacing the deleted-computer recovery API and UI. Recovery now supports markerless discovery, scoped candidate filtering, concurrent-run protection, mode-specific localized messaging, and updated unit, UI, and end-to-end tests.

Changes

Account computer recovery

Layer / File(s) Summary
Recovery contract and discovery flow
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AccountMacRecovery.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Defines recovery modes and results, selects account recovery availability, discovers and filters Iroh candidates, reconnects eligible Macs, and replaces the recovery state flag.
Mode-driven recovery controls
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AccountComputerRecoveryButton.swift, ios/cmux/Resources/Localizable.xcstrings
Replaces deleted-computer controls with mode-specific account recovery buttons, footers, status text, failure messages, and accessibility identifiers.
Recovery surface integration
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift
Wires account recovery into the device tree and disconnected workspace, including visibility and add-device presentation behavior.
Recovery behavior validation
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift, Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/DisconnectedWorkspaceShellRecoveryTests.swift, ios/cmuxUITests/cmuxUITests.swift
Covers markerless and forgotten-device recovery, failure, concurrency, stale scopes, UI visibility, and displayed recovery guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AccountComputerRecoveryButton
  participant MobileShellComposite
  participant MobileIrohMacDiscovering
  participant IrohMac
  User->>AccountComputerRecoveryButton: Tap account recovery
  AccountComputerRecoveryButton->>MobileShellComposite: recoverIrohMacFromAccount()
  MobileShellComposite->>MobileIrohMacDiscovering: discoverLiveMacs()
  MobileIrohMacDiscovering-->>MobileShellComposite: Eligible live Macs
  MobileShellComposite->>IrohMac: Connect and persist pairing
  IrohMac-->>MobileShellComposite: Recovery result
  MobileShellComposite-->>AccountComputerRecoveryButton: recovered, notFound, or staleScope
Loading

Possibly related PRs

  • manaflow-ai/cmux#8683: Replaces the earlier forgotten/deleted Iroh Mac recovery flow with account-computer recovery.
  • manaflow-ai/cmux#8712: Refactors the same iOS recovery surfaces and recovery action wiring.

Suggested reviewers: lawrencecchen

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: account-level Mac recovery for previously deleted or markerless installs.
Description check ✅ Passed The description covers the problem, fix, and verification, though it omits the template's Demo Video, Review Trigger, and Checklist sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No new Swift 6 actor-isolation violations found; the new store APIs stay on @MainActor, new enums are top-level Sendable values, and UI changes are within SwiftUI view code.
Cmux Swift Blocking Runtime ✅ Passed The PR adds no new blocking waits, sleeps, syncs, or locks in production Swift; the only sleep remains preexisting, and test-only waits are allowed.
Cmux Browser Automation Off-Main ✅ Passed PR only changes iOS account-recovery UI/tests; it does not touch TerminalController or ControlCommandExecutionPolicy, and no browser.* socket automation code was modified.
Cmux Expensive Synchronous Load ✅ Passed The diff adds only async account-recovery UI/flow; no RestorableAgentSessionIndex.load()/JSONL/transcript-style sync load was introduced on a main-actor interactive path.
Cmux Cache Substitution Correctness ✅ Passed The new cache use is UI-only; the recovery path still reads fresh discovery/store data and has cold/stale scope checks plus reloads.
Cmux No Hacky Sleeps ✅ Passed The diff touches only Swift sources, tests, and localization; no covered non-Swift runtime/build scripts changed, so the no-hacky-sleeps rule isn’t triggered.
Cmux Algorithmic Complexity ✅ Passed The new recovery path uses linear passes plus a fixed 4-candidate cap; the UI changes only add O(1) mode checks and no nested rescans over scalable collections.
Cmux Swift Concurrency ✅ Passed The new Task in AccountComputerRecoveryButton is stateful and cancelled on disappear; no new DispatchQueue, Combine, or completion-handler async patterns were introduced.
Cmux Swift @Concurrent ✅ Passed New recovery stays on @MainActor like existing discovery; UI calls it via Task{@MainActor}; no nonisolated async or invalid @concurrent added.
Cmux Swift Package Boundaries ✅ Passed The new recovery logic lives in CmuxMobileShell/CmuxMobileShellUI package targets, with tests in package targets; no new app-target source logic was added.
Cmux Swiftpm Lockfiles ✅ Passed Diff only changes Swift source/resources; no Package.swift, Package.resolved, project.pbxproj, or cmux-owned .gitignore changes, so the lockfile policy isn’t violated.
Cmux Swift Logging ✅ Passed PASS: The diff adds no print/debugPrint/dump/NSLog calls or new Logger declarations in the changed recovery/UI code; the existing mobileShellLog predates the PR.
Cmux User-Facing Error Privacy ✅ Passed Changed alerts/copy use generic cmux/account language; no upstream vendor/provider names, raw errors, IDs, or secrets appear in user-facing text.
Cmux Full Internationalization ✅ Passed New recovery UI text uses L10n.string(String(localized:)) everywhere, and the four new catalog keys are translated for both supported locales (en/ja).
Cmux Swiftui State Layout ✅ Passed No new rule-flagged SwiftUI patterns were introduced; list rows still use snapshots/closures and the recovery UI keeps state local.
Cmux Architecture Rethink ✅ Passed No banned timing/lock/observer patterns were introduced; MobileShellComposite owns the recovery state, and UI reuses one shared button/closure path.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Changed files only add recovery views/tests; no NSWindow/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes, so the auxiliary-window shortcut rule isn’t implicated.
Cmux Source Artifacts ✅ Passed All changed paths are source, tests, localization, or a source-file removal; no logs, caches, screenshots, build output, or scratch artifacts appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Diff only replaces the recovery feature with production callers; no new DEBUG/test-only seams or widened test accessors were added in Sources.
Cmux No Ambient Global State ✅ Passed No new file-scope mutable state or singleton API; recovery behavior lives on MobileShellComposite/view structs, and the new enums are caseful types, not namespace-only shells.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-ios-account-recovery-any-version

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AccountComputerRecoveryButton.swift (1)

67-80: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not present .unavailable as a recovery action.

.unavailable currently renders “Find Account Computer”; recovery then returns .notFound when discovery is unavailable and shows a misleading “No account computer was found” alert. Hide/disable this section for .unavailable rather than mapping it to the find-account copy.

Also applies to: 91-117, 128-163

🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AccountComputerRecoveryButton.swift`
around lines 67 - 80, Update the recovery UI and action flow around
recoverAccountComputer and its related rendering sections so Account Computer
recovery is hidden or disabled when the state is .unavailable. Do not map
.unavailable to the “Find Account Computer” copy or invoke recovery; preserve
the existing behavior for discoverable and other supported states.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AccountComputerRecoveryButton.swift`:
- Around line 67-80: Update the recovery UI and action flow around
recoverAccountComputer and its related rendering sections so Account Computer
recovery is hidden or disabled when the state is .unavailable. Do not map
.unavailable to the “Find Account Computer” copy or invoke recovery; preserve
the existing behavior for discoverable and other supported states.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0e309d2d-1b54-4d98-9f7c-0f4b744b4350

📥 Commits

Reviewing files that changed from the base of the PR and between fc093e0 and 7db653e.

📒 Files selected for processing (10)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AccountMacRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AccountComputerRecoveryButton.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/DisconnectedWorkspaceShellRecoveryTests.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxUITests/cmuxUITests.swift
💤 Files with no reviewable changes (1)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR broadens the iOS account Mac recovery surface: where recovery was previously only exposed when a local "forgotten Mac" deletion marker existed, it is now available whenever the signed-in personal-account Iroh discovery service is present. The deleted-marker path is preserved for copy differentiation and passive-discovery suppression; the new markerless path lets a user find any same-account Mac that was deleted before recovery support was introduced or paired under a different install scope.

  • New recoverIrohMacFromAccount method replaces the narrower recoverForgottenIrohMacFromAccount; it prioritises forgotten candidates, then tries unpaired live Macs, with scope-staleness guards throughout.
  • accountComputerRecoveryMode computed property drives UI presentation — .recoverDeletedComputer when a marker exists, .findAccountComputer otherwise — wiring into both DeviceTreeView and DisconnectedWorkspaceShellView.
  • New and updated strings (findAccount, findingAccount, findAccountFailedMessage, findAccountFooter) are added to Localizable.xcstrings with both English and Japanese translations.

Confidence Score: 4/5

Safe to merge after fixing the mismatched failure alert title in the find-account flow.

The core recovery logic, scope guards, deduplication, and i18n additions are all correct and well-tested. The one concrete defect is that failureTitle in AccountComputerRecoveryButton is not mode-switched, so the findAccountComputer path surfaces a Couldnt recover computer dialog title that contradicts the Find Account Computer label the user just tapped.

AccountComputerRecoveryButton.swift needs failureTitle mode-switched; DisconnectedWorkspaceShellView.swift has a semantically inverted nil-store check in showsAccountComputerRecoveryAction.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AccountMacRecovery.swift New file replacing the deleted-only recovery path with a broader account recovery path; logic is sound — forgotten IDs are tried first, then unpaired candidates, with scope guards throughout.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift Deleted file; old recoverForgottenIrohMacFromAccount superseded by the wider recoverIrohMacFromAccount in the new AccountMacRecovery extension.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Renames isRecoveringDeletedComputer to isRecoveringAccountComputer and updates doc-comment; clean property rename with no logic change.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AccountComputerRecoveryButton.swift Renames and expands the recovery button/footer to support both findAccountComputer and recoverDeletedComputer modes; failureTitle is not mode-switched and always shows Couldnt recover computer even in the find flow.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift Switches from hasRecoverableDeletedComputers to accountComputerRecoveryMode for recovery visibility; semantic nil-guard inversion in showsAccountComputerRecoveryAction is currently harmless but fragile.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift Updates device tree to show the recovery section whenever accountComputerRecoveryMode != .unavailable; straightforward rename with no logic issues.
Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/DisconnectedWorkspaceShellRecoveryTests.swift Adds new test for markerless account recovery; a dead hasRecoverableDeletedComputers = true setup line in failedPairedMacLoadStillOffersAccountRecovery is confusing but not incorrect.
ios/cmux/Resources/Localizable.xcstrings Adds four new string keys with both en and ja translations; localization coverage is complete for the new strings.
ios/cmuxUITests/cmuxUITests.swift Adds testMarkerlessAccountRecoveryIsVisibleAndActionable XCUITest verifying the new find-account-computer button label, footer, and failure alert end-to-end.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User taps recovery button] --> B{accountComputerRecoveryMode}
    B -->|unavailable| Z[Button hidden]
    B -->|recoverDeletedComputer| C[recoverIrohMacFromAccount]
    B -->|findAccountComputer| C
    C --> D{isRecoveringAccountComputer?}
    D -->|yes| E[returns alreadyInProgress]
    D -->|no| F[Set flag = true]
    F --> G[Snapshot scope + forgottenIDs + knownIDs]
    G --> H[personalIrohDiscovery.discoverLiveMacs]
    H --> I{scope still current?}
    I -->|no| J[returns staleScope]
    I -->|yes| K[accountIrohRecoveryCandidates forgotten first then unpaired]
    K --> L[For each candidate connectAccountDiscoveredIrohMac]
    L --> M{connected?}
    M -->|yes| N[loadPairedMacs + loadRegistryDevices returns recovered]
    M -->|no| L
    L -->|exhausted| O[returns notFound show alert]
Loading

Reviews (1): Last reviewed commit: "fix(ios): recover account Macs without l..." | Re-trigger Greptile

Comment on lines 120 to 125
@@ -108,24 +125,42 @@ struct DeletedComputerRecoveryButton: View {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Failure alert title mismatches the .findAccountComputer flow

failureTitle is not mode-switched and always returns "Couldn't recover computer" (mobile.computers.recoverFailedTitle). When the user taps "Find Account Computer" and no Mac is found, the failure dialog reads "Couldn't recover computer" — a description that only fits the .recoverDeletedComputer path. failureMessage is already correctly mode-switched; failureTitle needs the same treatment, with a new "mobile.computers.findAccountFailedTitle" catalog key (e.g., "Couldn't find computer") for the .findAccountComputer / .unavailable branch.

Comment on lines +144 to 146
var showsAccountComputerRecoveryAction: Bool {
store?.accountComputerRecoveryMode != .unavailable
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 nil store makes showsAccountComputerRecoveryAction return true

store?.accountComputerRecoveryMode != .unavailable evaluates to true when store is nil, because Swift compares Optional.none as not-equal to any .some value. The old code (store?.hasRecoverableDeletedComputers == true) correctly returned false for a nil store. The current guards (if showsAccountComputerRecoveryAction, let store and the guard let store else { return false } in shouldAutoPresentAddDeviceAfterLoadingSavedMacs) prevent any visible bug today, but the semantic inversion could silently produce wrong behavior if a future caller checks showsAccountComputerRecoveryAction before binding store. The safer pattern is store?.accountComputerRecoveryMode.map { $0 != .unavailable } ?? false.

Comment on lines +56 to 70
@Test func failedPairedMacLoadStillOffersAccountRecovery() async throws {
let store = try await shellStore(
pairedMacStore: FailingLoadPairedMacStore(),
personalIrohDiscovery: EmptyAccountIrohDiscovery()
)
store.hasRecoverableDeletedComputers = true

await store.loadPairedMacs()
let view = disconnectedView(store: store)

#expect(store.pairedMacLoadState == .failed)
#expect(!view.showsDeletedComputerRecoveryAction)
#expect(store.accountComputerRecoveryMode == .findAccountComputer)
#expect(view.showsAccountComputerRecoveryAction)
#expect(!view.shouldAutoPresentAddDeviceAfterLoadingSavedMacs)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Dead-code setup obscures test intent

store.hasRecoverableDeletedComputers = true is set before await store.loadPairedMacs(), but the FailingLoadPairedMacStore causes that load to fail and reset hasRecoverableDeletedComputers to false. The subsequent assertion accountComputerRecoveryMode == .findAccountComputer proves the flag was reset. The true assignment has no effect on the observable outcome and misleads a reader into thinking the flag survives a failed load — which is the opposite of what the test verifies. Removing the dead-code line (or moving it after loadPairedMacs to make the scenario deliberate) would clarify the intent.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants