Skip to content

iOS: control Mac sleep & caffeinate from cmux mobile - #6902

Closed
austinywang wants to merge 23 commits into
mainfrom
issue-6482-cmux-mobile-should-control-mac-sleep
Closed

austinywang wants to merge 23 commits into
mainfrom
issue-6482-cmux-mobile-should-control-mac-sleep

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6482

What

Lets the cmux mobile app control the connected Mac's power state, covering the three controls the issue asks for:

  • Sleep the Mac from the phone.
  • Disable active keep-awake, including caffeinate.
  • Show whether the Mac is currently being kept awake by cmux or related processes (and by which ones).

The controls live in the per-computer detail screen (Computers → a Mac → Mac Power), shown only for the Mac the phone is actively connected to and only when that Mac advertises the capability.

How

CmuxMacPower (new macOS package, unit-tested):

  • MacPowerCommandRunning seam (+ SystemMacPowerCommandRunner over Process) so the logic is testable without touching the real machine.
  • MacKeepAwakeStatusParser — parses pmset -g assertions into a structured status (who holds which sleep-prevention assertion; flags for cmux / caffeinate / system vs display).
  • MacPowerController — sleepSystem() (AppleScript System Events → sleep, the supported no-root path; cmux already ships the apple-events entitlement + usage string), keepAwakeStatus(), and disableKeepAwake() (pkill -x caffeinate, then re-read).
  • 16 Swift Testing cases covering the parser and the controller (with a fake runner).

Mobile host RPC (macOS app):

  • mac.power.status, mac.power.sleep, mac.power.keep_awake.disable in mobileHostHandleRPC, advertised via the new mac.power.control.v1 capability.
  • These ride the existing same-account data-plane trust boundary (the one that already gates terminal.input), so no extra gate is added.

iOS:

  • supportsMacPowerControl gate + macPowerStatus / sleepMac / disableMacKeepAwake client methods (Decodable status mirror).
  • "Mac Power" section in MacComputerDetailView with a sleep confirmation and a live keep-awake summary. A sleep request that drops the connection is treated as success (the Mac slept and the link died); an explicit RPC error surfaces an Automation-permission hint. All strings localized (en/ja).

Notes

  • A sleep request normally drops the phone↔Mac connection as the Mac sleeps; the client distinguishes that (success) from an explicit refusal (Automation not granted).
  • GUI keep-awake apps (Amphetamine, etc.) that hold assertions are surfaced in the status but not force-killed; only caffeinate is terminated.

🤖 Generated with Claude Code


Summary by cubic

Adds Mac power controls to the cmux mobile app so you can sleep the Mac, stop caffeinate, and see who’s keeping it awake, with clear errors when status can’t be read or sleep is refused. Ships CmuxMacPower, mobile RPCs, and an iOS “Mac Power” section to satisfy #6482.

  • New Features

    • iOS: Adds “Mac Power” in per‑computer detail (foreground only, gated by mac.power.control.v1) with a keep‑awake summary and holders, Sleep with confirm, Disable keep‑awake, and Refresh. Auto‑loads when the capability arrives, hardens busy state, ensures stable row identity without PIDs (with a regression test), and shows “Couldn't read Mac power status.” when unavailable. Sleep outcomes classify explicit refusal vs delivery failures (timeouts/disconnects are failures).
    • macOS: CmuxMacPower provides a pmset -g assertions parser (handles system‑wide counts when no owning‑process lines exist) and MacPowerController to sleep via System Events, read status, and disable keep‑awake by signaling only verified caffeinate PIDs (revalidated via ps). Includes a command runner with timeouts, SIGTERM→SIGKILL handling, and a safeguard to avoid sending SIGKILL to a reused PID after timeouts. Unit tests cover the parser, parentheses regression, timeouts/PID reuse, and PID revalidation.
    • Mobile RPCs: mac.power.status, mac.power.sleep, and mac.power.keep_awake.disable, advertised via mac.power.control.v1. RPCs return explicit errors for status unavailability and sleep refusal so iOS can show the right message.
  • Migration

    • Sleeping the Mac requires granting cmux Automation access to System Events in System Settings › Privacy & Security › Automation.

Written for commit 2b8d8a6. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added Mac power controls in the app, including viewing keep-awake status, disabling keep-awake, and putting the Mac to sleep.
    • The Mac detail view now shows a dedicated power section with clearer status, confirmation prompts, and live updates.
    • Added support for reporting which apps or processes are keeping the Mac awake.
  • Bug Fixes

    • Improved handling of sleep and keep-awake actions with clearer failure and refusal messages.
    • Added support for more reliable detection of Mac power status across different system states.

Adds Mac power control to the phone's per-computer detail screen so a
paired phone can sleep the Mac, disable active keep-awake (caffeinate),
and see whether the Mac is currently being kept awake and by which
processes. Resolves the three controls in #6482.

- New CmuxMacPower package (macOS): a command-runner seam, a
  `pmset -g assertions` parser, and MacPowerController (sleep via
  osascript→System Events, keep-awake status, disable via
  `pkill -x caffeinate`), all behind an injected runner with Swift
  Testing unit tests.
- Mobile host RPC: mac.power.status / mac.power.sleep /
  mac.power.keep_awake.disable, advertised by the new
  mac.power.control.v1 capability. These ride the existing same-account
  data-plane trust boundary (the one that already gates terminal.input),
  so they need no extra gate.
- iOS client methods + a "Mac Power" section in MacComputerDetailView,
  with a sleep confirmation dialog and a live keep-awake summary. A
  sleep request that drops the connection is treated as success (the Mac
  slept); an explicit RPC error surfaces an Automation-permission hint.
  All strings localized (en/ja).

Fixes #6482

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 5, 2026 1:39am
cmux-staging Building Building Preview, Comment Jul 5, 2026 1:39am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new macOS Swift package (CmuxMacPower) providing pmset-based keep-awake status parsing, sleep, and caffeinate-disable controls; wires these into host RPC dispatch and capability advertisement; extends the iOS mobile shell client with typed models, error classification, and RPC routing; and adds a Mac Power section to the Mac detail view with related localizations and tests.

Changes

Mac power control

Layer / File(s) Summary
Keep-awake status parsing
Packages/macOS/CmuxMacPower/Package.swift, .../MacPowerAssertionHolder.swift, .../MacKeepAwakeDisableOutcome.swift, .../MacKeepAwakeStatus.swift, .../MacKeepAwakeStatusParser.swift, Tests/.../MacKeepAwakeStatusParserTests.swift
New package manifest defines the CmuxMacPower library/test targets; adds MacPowerAssertionHolder, MacKeepAwakeDisableOutcome, and MacKeepAwakeStatus (with jsonObject serialization); implements MacKeepAwakeStatusParser.parse(pmsetAssertions:) to derive status/holders from pmset output, validated by a comprehensive test suite.
Command runner and power controller
.../MacPowerProcessLauncher.swift, .../MacPowerProcessRunState.swift, .../MacPowerCommandRunning.swift, .../SystemMacPowerCommandRunner.swift, .../MacPowerController.swift, Tests/.../FakeMacPowerCommandRunner*.swift, Tests/.../MacPowerControllerTests.swift
Adds MacPowerProcessLauncher/MacPowerProcessRunState for async process execution with timeout/SIGKILL handling; defines the MacPowerCommandRunning protocol and SystemMacPowerCommandRunner; implements MacPowerController with keepAwakeStatus(), sleepSystem(), and disableKeepAwake(); adds fake runner and controller tests.
Host capability and RPC wiring
Sources/Mobile/MobileHostService+Capabilities.swift, Sources/TerminalController.swift, cmuxTests/MobileHostAuthorizationTests.swift, cmux.xcodeproj/project.pbxproj, cmux.xcworkspace/contents.xcworkspacedata
Advertises mac.power.control.v1 capability; adds v2MacPowerStatus/Sleep/DisableKeepAwake methods routed via mobileHostHandleRPC; adds capability test assertion; wires the CmuxMacPower package into the Xcode project and workspace.
Mobile shell power client
.../MobileShellComposite.swift, .../MobileShellComposite+MacPower.swift, .../MobileMacPowerStatus.swift, .../MobileMacPowerHolder.swift, .../MobileMacSleepResult.swift, .../MobileMacSleepErrorClassifier.swift, Tests/.../MobileMacPowerHolderTests.swift, Tests/.../MobileMacSleepResultTests.swift
Adds supportsMacPowerControl capability gate; implements macPowerStatus, sleepMac, disableMacKeepAwake with RPC client routing; adds decodable MobileMacPowerStatus/MobileMacPowerHolder, MobileMacSleepResult enum, and MobileMacSleepErrorClassifier; adds related tests.
Mac detail view controls
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift, ios/cmux/Resources/Localizable.xcstrings, Resources/Localizable.xcstrings
Adds Mac power state, conditional section rendering, sleep confirmation dialog, load/disable/sleep async handlers, and localized keep-awake/sleep/error strings (English and Japanese).

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

Sequence Diagram(s)

sequenceDiagram
  participant MacComputerDetailView
  participant MobileShellComposite
  participant TerminalController
  participant MacPowerController
  participant pmset/osascript/kill

  MacComputerDetailView->>MobileShellComposite: sleepMac(macDeviceID)
  MobileShellComposite->>TerminalController: RPC mac.power.sleep
  TerminalController->>MacPowerController: sleepSystem()
  MacPowerController->>pmset/osascript/kill: osascript sleep command
  pmset/osascript/kill-->>MacPowerController: exit status
  MacPowerController-->>TerminalController: Bool success
  TerminalController-->>MobileShellComposite: ok / sleep_failed
  MobileShellComposite-->>MacComputerDetailView: MobileMacSleepResult (requested/refused/failed)
Loading
sequenceDiagram
  participant MacComputerDetailView
  participant MobileShellComposite
  participant TerminalController
  participant MacPowerController
  participant pmset/osascript/kill

  MacComputerDetailView->>MobileShellComposite: disableMacKeepAwake(macDeviceID)
  MobileShellComposite->>TerminalController: RPC mac.power.keep_awake.disable
  TerminalController->>MacPowerController: disableKeepAwake()
  MacPowerController->>pmset/osascript/kill: ps check + kill caffeinate PIDs
  MacPowerController->>pmset/osascript/kill: re-read pmset assertions
  pmset/osascript/kill-->>MacPowerController: refreshed status
  MacPowerController-->>TerminalController: MacKeepAwakeDisableOutcome
  TerminalController-->>MobileShellComposite: terminated_caffeinate + status json
  MobileShellComposite-->>MacComputerDetailView: MobileMacPowerStatus
Loading

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error New production MacPowerProcessLauncher adds DispatchSource timers for command timeout and SIGTERM→SIGKILL grace; the rule forbids timer-based coordination in shipped Swift code. Replace the timeout/kill grace timers with a cancellation-aware scheduler or explicit process-exit signal path owned by the actor; keep any timing-only scaffolding in tests.
Cmux Full Internationalization ❌ Error Resources/Localizable.xcstrings adds mobile.macPower.* keys with only en/ja, but the catalog already supports 20 locales. Add translated entries for every existing locale in Resources/Localizable.xcstrings for mobile.macPower.sleepFailed and mobile.macPower.statusUnavailable.
Description check ⚠️ Warning The description has good content, but it doesn't follow the required template and omits Summary, Testing, Demo Video, Review Trigger, and Checklist sections. Rewrite it using the repository template: add Summary, Testing, Demo Video (or N/A), Review Trigger, and Checklist sections, and include what was tested and verified.
✅ Passed checks (22 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement all requested controls: sleep, disable keep-awake including caffeinate, and show current keep-awake status.
Out of Scope Changes check ✅ Passed The added packages, RPCs, UI, tests, and localization all support the Mac power-control feature, with no clear unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed No new actor-isolation regression: added models are standalone value types, and the new async UI/controller calls stay on existing @MainActor types.
Cmux Browser Automation Off-Main ✅ Passed No changed file touches browser automation, TerminalController, or ControlCommandExecutionPolicy; this PR only adds Mac power control, so the rule is not applicable.
Cmux Expensive Synchronous Load ✅ Passed No agent-history or broad-sync load was added; the new Mac power UI only awaits async RPCs, and macOS work runs through async Process/Task paths off-main.
Cmux Cache Substitution Correctness ✅ Passed No authoritative fresh read was replaced by a cache in any persistence/history/undo/snapshot path; the only cached power state is transient UI state, which is allowed.
Cmux No Hacky Sleeps ✅ Passed The diff only touches Swift sources/tests; there are no TS/JS/shell/build runtime changes or fixed-delay hacks in scope.
Cmux Algorithmic Complexity ✅ Passed The new Mac power paths are linear-time: single-pass pmset parsing, one-pass holder dedupe, and targeted per-PID process checks; no nested rescans or hot-path sorting.
Cmux Swift Concurrency ✅ Passed PASS: New Dispatch/Task usage is confined to Process/DispatchSource and SwiftUI button/.task boundaries; no new Combine or uncontrolled app-state async was added.
Cmux Swift @Concurrent ✅ Passed PASS: The new async work is either @MainActor UI-bound or nonisolated value-type async; no changed file adds invalid @concurrent or a missing hop in the touched code.
Cmux Swift File And Package Boundaries ✅ Passed Core Mac-power logic lives in new small CmuxMacPower package; app-target changes are thin RPC/UI glue, and oversized files only got modest edits.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore ignores Package.resolved; CmuxMacPower has no external deps, and the Xcode change is a local package ref, so no lockfile diff was required.
Cmux Swift Logging ✅ Passed No banned print/debugPrint/dump/NSLog calls were added; the new macPower logger is nonisolated private let and logs only error strings, with no secret/personal-data logging.
Cmux User-Facing Error Privacy ✅ Passed New error/alert strings stay generic and don’t leak raw upstream messages, IDs, or secrets; the only specifics are a macOS permission path and affected process names.
Cmux Swiftui State Layout ✅ Passed The new MacComputerDetailView only adds local @State and lifecycle tasks; no ObservableObject/@published, GeometryReader, lazy-row store refs, or render-time body mutations.
Cmux Architecture Rethink ✅ Passed MacPowerController owns the new Mac-power logic; UI/RPC layers are thin bridges, with no new polling/locks/observers or split ownership introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone NSWindow/Panel/WindowGroup changes were introduced; the Mac Power UI is an in-view section with confirmation dialogs, and no cmuxAuxiliaryWindowIdentifiers changes were added.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test files under Packages/macOS/CmuxMacPower; no logs, caches, build output, temp dirs, or other artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new DEBUG/test-only production hooks were added; the new mac power types are production feature APIs or general DI seams, not test-only accessors.
Cmux No Ambient Global State ✅ Passed No new ambient global state: added behavior lives on injectable types, with only a type-scoped capability property and private file-scope helpers/constants.
Title check ✅ Passed The title clearly summarizes the main change: adding Mac sleep and caffeinate control for the iOS cmux app.
✨ 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 issue-6482-cmux-mobile-should-control-mac-sleep

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.

cmux and others added 3 commits June 26, 2026 04:38
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Key the auto-load .task on both isForeground and supportsMacPowerControl so
the first keep-awake status fetch fires when the host capability handshake
flips the section on while the detail view is already open (was keyed to
isForeground alone, leaving the section stuck on 'Checking…').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-should-control-mac-sleep

# Conflicts:
#	.github/swift-file-length-budget.tsv
@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Mac power control to the cmux mobile app — sleep the Mac, disable caffeinate, and show keep-awake status — backed by a new CmuxMacPower Swift package, three mobile RPCs, and an iOS "Mac Power" section in the computer detail view.

  • CmuxMacPower: A stateless, injectable MacPowerController struct wraps pmset -g assertions parsing, AppleScript sleep via System Events, and a PID-revalidating caffeinate terminator; 16 unit tests cover the parser and controller with a fake runner.
  • macOS RPCs: mac.power.status, mac.power.sleep, and mac.power.keep_awake.disable are wired into TerminalController's mobile RPC switch and advertised via the new mac.power.control.v1 capability.
  • iOS UI: A "Mac Power" section appears only for the foreground-connected Mac when the capability is present; powerLoadKey keys the auto-load .task on both the foreground flag and the capability so the first fetch fires correctly even if the capability arrives after the view is open.

Confidence Score: 4/5

Safe to merge after addressing the blocking-read issue in the process launcher.

The macPowerReadToEnd() blocking Darwin.read() loop runs inside Task.detached, occupying a Swift cooperative thread pool thread for the lifetime of each child process. For the commands in use (pmset, ps, osascript) this is milliseconds in practice, but it violates the rule against blocking cooperative threads and could stall other async work under load. All other parts of the change — the actor-protected run-state, PID-revalidating caffeinate terminator, iOS busy-count management, composite id for Identifiable holders, and complete en/ja localization — look correct and well-tested.

Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerProcessLauncher.swift — the stdout reader should move off the cooperative pool onto a background GCD queue.

Important Files Changed

Filename Overview
Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerProcessLauncher.swift New process launcher uses Task.detached to run a blocking Darwin.read() loop, occupying a Swift cooperative thread pool thread for the duration of each child process's stdout output.
Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift New power controller: stateless Sendable struct with injectable runner; keepAwakeStatus returns nil on pmset failure; disableKeepAwake revalidates PIDs via ps before signaling; looks correct.
Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatusParser.swift New parser struct with private file-scope helpers; parses pmset -g assertions into structured status with system-wide fallback counts; file-scope privates are pure helpers, not ambient globals.
Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerProcessRunState.swift Actor-protected completion state correctly guards against double-resume across the three racing event sources (output reader, termination handler, timeout).
Sources/TerminalController.swift Adds three mobile RPC handlers (mac.power.status/sleep/keep_awake.disable) to the existing mobileHostHandleRPC switch; each maps to a fresh MacPowerController call; error codes are structured and don't expose internal details.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift Adds Mac Power section gated by isForeground && supportsMacPowerControl; powerLoadKey correctly keys the .task on both gates so capability arrival after view open triggers the first fetch; busy counter uses a count (not a flag) to handle overlapping ops.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacPower.swift New Mac power RPC client methods; macPowerClient resolver correctly mirrors the foreground-connection routing; file-scope nonisolated private Logger is a private constant, not ambient global state.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacPowerHolder.swift Identifiable id is now a composite string of pid, processName, assertionTypes, and detail — avoids duplicate-id collisions for missing-pid holders; addresses previous review finding.
Packages/macOS/CmuxMacPower/Package.swift New local Swift package with no external dependencies; Swift 6 language mode enabled; no Package.resolved needed since there are no remote dependencies to lock.
ios/cmux/Resources/Localizable.xcstrings All new iOS UI strings (13 keys) include both en and ja translations; macOS host xcstrings also complete for both locales.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant iOS as iOS (MacComputerDetailView)
    participant Shell as MobileShellComposite
    participant RPC as TerminalController (Mac)
    participant Ctrl as MacPowerController
    participant OS as macOS (pmset / osascript / kill)

    iOS->>Shell: macPowerStatus(macDeviceID:)
    Shell->>RPC: mac.power.status RPC
    RPC->>Ctrl: keepAwakeStatus()
    Ctrl->>OS: /usr/bin/pmset -g assertions
    OS-->>Ctrl: output (or nil on timeout)
    Ctrl-->>RPC: MacKeepAwakeStatus?
    RPC-->>Shell: JSON status (or status_unavailable error)
    Shell-->>iOS: MobileMacPowerStatus?

    iOS->>Shell: sleepMac(macDeviceID:)
    Shell->>RPC: mac.power.sleep RPC
    RPC->>Ctrl: sleepSystem()
    Ctrl->>OS: osascript tell System Events to sleep
    OS-->>Ctrl: exit 0 or non-zero if Automation denied
    RPC-->>Shell: ok / sleep_failed error
    Shell-->>iOS: MobileMacSleepResult

    iOS->>Shell: disableMacKeepAwake(macDeviceID:)
    Shell->>RPC: mac.power.keep_awake.disable RPC
    RPC->>Ctrl: disableKeepAwake()
    Ctrl->>OS: pmset status before
    Ctrl->>OS: ps revalidate PID
    Ctrl->>OS: kill SIGTERM
    Ctrl->>OS: pmset status after
    RPC-->>Shell: terminated_caffeinate + status
    Shell-->>iOS: MobileMacPowerStatus?
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant iOS as iOS (MacComputerDetailView)
    participant Shell as MobileShellComposite
    participant RPC as TerminalController (Mac)
    participant Ctrl as MacPowerController
    participant OS as macOS (pmset / osascript / kill)

    iOS->>Shell: macPowerStatus(macDeviceID:)
    Shell->>RPC: mac.power.status RPC
    RPC->>Ctrl: keepAwakeStatus()
    Ctrl->>OS: /usr/bin/pmset -g assertions
    OS-->>Ctrl: output (or nil on timeout)
    Ctrl-->>RPC: MacKeepAwakeStatus?
    RPC-->>Shell: JSON status (or status_unavailable error)
    Shell-->>iOS: MobileMacPowerStatus?

    iOS->>Shell: sleepMac(macDeviceID:)
    Shell->>RPC: mac.power.sleep RPC
    RPC->>Ctrl: sleepSystem()
    Ctrl->>OS: osascript tell System Events to sleep
    OS-->>Ctrl: exit 0 or non-zero if Automation denied
    RPC-->>Shell: ok / sleep_failed error
    Shell-->>iOS: MobileMacSleepResult

    iOS->>Shell: disableMacKeepAwake(macDeviceID:)
    Shell->>RPC: mac.power.keep_awake.disable RPC
    RPC->>Ctrl: disableKeepAwake()
    Ctrl->>OS: pmset status before
    Ctrl->>OS: ps revalidate PID
    Ctrl->>OS: kill SIGTERM
    Ctrl->>OS: pmset status after
    RPC-->>Shell: terminated_caffeinate + status
    Shell-->>iOS: MobileMacPowerStatus?
Loading

Reviews (17): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

/// merge multiple lines for the same pid, and derive the booleans from the set
/// of assertion types seen. The system-wide aggregate counts are intentionally
/// ignored because they cannot attribute "who" is keeping the Mac awake.
public enum MacKeepAwakeStatusParser {

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 Caseless enum used as a static-function namespace

MacKeepAwakeStatusParser is a caseless enum whose entire API is static methods and static lets — exactly the shape the cmux no-ambient-global-state rule explicitly flags. Caseless enums cannot be instantiated, so the type cannot be injected, subclassed, or mocked independently of the static state. The canonical fix is to convert it to a struct (or fold parse(_:) into MacPowerController directly, since the controller is already the only caller).

Rule Used: Flag new ambient global state in production Swift:... (source)

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed: parser is now MacKeepAwakeStatus.parse(pmsetAssertions:) with private file-scoped helpers; no caseless namespace enum remains.

— Claude Code

Comment on lines +17 to +21
public func keepAwakeStatus() async -> MacKeepAwakeStatus {
guard let output = await runner.capture("/usr/bin/pmset", ["-g", "assertions"]) else {
return .idle
}
return MacKeepAwakeStatusParser.parse(output)

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 pmset failure silently returns "Sleeps normally" (false negative)

When runner.capture returns nil (e.g., the process fails to launch or pmset exits non-zero on a restricted system), keepAwakeStatus() returns .idle — all flags false, empty holders. The macOS RPC then sends {"kept_awake": false, …} to iOS, and the phone renders "Sleeps normally." The user sees a clean status when the actual sleep-prevention state is simply unknown. The reliability rule requires failing closed: on a read failure, the status should be distinguishable from a confirmed-idle state (e.g., return an Optional<MacKeepAwakeStatus> and surface an error banner on iOS), rather than substituting a fake all-clear.

Rule Used: Flag correctness-critical detection/identity deriv... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed: keepAwakeStatus() now returns nil on pmset capture failure or timeout; the RPC maps that to status_unavailable, and iOS surfaces a localized status-unavailable message instead of Sleeps normally.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed earlier: keepAwakeStatus() no longer turns a pmset/capture failure into a confirmed idle status. The controller returns nil for an unreadable status, and the mobile UI surfaces the unavailable/read-failed state instead of showing "Sleeps normally".

— Claude Code

Comment on lines +30 to +33
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
pid = (try container.decodeIfPresent(Int.self, forKey: .pid)) ?? 0
processName = (try container.decodeIfPresent(String.self, forKey: .processName)) ?? ""

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 pid: 0 fallback creates duplicate Identifiable IDs for missing-pid holders

MobileMacPowerHolder.id is pid, but when the pid field is absent from JSON the decoder assigns 0, so every such holder shares id = 0. SwiftUI ForEach treats duplicate IDs as the same element and will silently drop or misrender extra rows. Using processName as a fallback (or a composite of pid + processName) keeps identity unique in practice even if the server ever omits the pid field.

Suggested change
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
pid = (try container.decodeIfPresent(Int.self, forKey: .pid)) ?? 0
processName = (try container.decodeIfPresent(String.self, forKey: .processName)) ?? ""
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
processName = (try container.decodeIfPresent(String.self, forKey: .processName)) ?? ""
pid = (try container.decodeIfPresent(Int.self, forKey: .pid)) ?? processName.hashValue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed: MobileMacPowerHolder.id is now a deterministic composite of pid, process, assertion types, and detail, with regression coverage for missing-pid holders.

— Claude Code

The package-conventions lint forbids caseless-enum namespace types. Replace
the all-static `enum MacKeepAwakeStatusParser` with a
`MacKeepAwakeStatus.parse(pmsetAssertions:)` static factory on the receiver
value type plus file-scoped private helpers — no behavior change, 16 tests
still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

@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.

Actionable comments posted: 4

🤖 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.

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+MacPower.swift:
- Around line 139-161: The sleepMac flow is treating request construction
failures as a successful sleep request because MobileCoreRPCClient.requestData
and client.sendRequest are wrapped in the same do/catch. Split the
request-building step from the send step in
MobileShellComposite+MacPower.sleepMac so only post-send disconnects can map to
.requested, and make any failure from requestData return .failed (or otherwise
not .requested). Keep the existing handling for
disconnectForAuthorizationFailureIfNeeded and
MobileShellConnectionError.rpcError unchanged.
- Around line 5-8: The `sleepMac` flow is conflating request-construction
failures from `requestData(...)` with the expected post-sleep disconnect path in
`sendRequest(...)`, so non-RPC errors are being reported as `.requested`.
Refactor the `sleepMac` implementation to build the request outside the
success/failure translation or add separate error handling so only the expected
disconnect maps to `.requested`, while local request-building failures return
`.failed` instead.

In
`@Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus`+Parse.swift:
- Around line 113-153: macParsePmsetProcessLine currently parses the process
head with the first opening and closing parentheses, which breaks names that
contain embedded parentheses and can corrupt the remainder parsing. Update the
name extraction in macParsePmsetProcessLine to identify the end of the `pid
N(name):` header by matching the literal `): ` sequence instead of the first
`)`, then derive `name` and `remainder` from that boundary so assertion types
and `named:` detail are preserved for helpers like `Google Chrome` and similar
process names.

In
`@Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandRunning.swift`:
- Around line 22-79: The current SystemMacPowerCommandRunner.runSync path still
waits indefinitely on Process.waitUntilExit(), so Mac power RPCs like
v2MacPowerSleep can hang forever if the underlying tool stalls. Add a bounded
timeout inside runSync for both run(_:_:) and capture(_:_:), and if the process
exceeds it, terminate/kill the Process and return failure so sleepSystem() and
its callers can fail cleanly instead of blocking. Keep the change localized to
SystemMacPowerCommandRunner and its runSync helper so the timeout applies
consistently to all command executions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81c972f2-14eb-4ce1-a5bb-6fb0d9a7f4c8

📥 Commits

Reviewing files that changed from the base of the PR and between eeded15 and dee0eec.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (16)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacPower.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift
  • Packages/macOS/CmuxMacPower/Package.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus+Parse.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandRunning.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacKeepAwakeStatusParserTests.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacPowerControllerTests.swift
  • Sources/Mobile/MobileHostService+Capabilities.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmux.xcworkspace/contents.xcworkspacedata
  • cmuxTests/MobileHostAuthorizationTests.swift
  • ios/cmux/Resources/Localizable.xcstrings

Comment thread Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandRunning.swift Outdated

@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/MacComputerDetailView.swift (1)

404-406: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer String.localizedStringWithFormat for the localized format string.

String(format:) doesn't apply locale-aware substitution/positional-argument handling that a translated format may need. For UI-facing localized formats this repo's convention is String.localizedStringWithFormat.

♻️ Proposed change
-        return String(
-            format: L10n.string("mobile.computers.power.byFormat", defaultValue: "Kept awake by %@"),
-            names.joined(separator: ", "))
+        return String.localizedStringWithFormat(
+            L10n.string("mobile.computers.power.byFormat", defaultValue: "Kept awake by %@"),
+            names.joined(separator: ", "))

Based on learnings that localized %@-style format strings taking separate format arguments should use String.localizedStringWithFormat rather than String(format:).

🤖 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/MacComputerDetailView.swift`
around lines 404 - 406, The localized format in MacComputerDetailView’s power-by
label is using String(format:) instead of the repo’s preferred locale-aware
formatter. Update the return in the power-by formatting path to use
String.localizedStringWithFormat with the
L10n.string("mobile.computers.power.byFormat", ...) value and the joined names
argument, so translated %@ formatting is handled correctly.

Source: Learnings

🤖 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/MacComputerDetailView.swift`:
- Around line 404-406: The localized format in MacComputerDetailView’s power-by
label is using String(format:) instead of the repo’s preferred locale-aware
formatter. Update the return in the power-by formatting path to use
String.localizedStringWithFormat with the
L10n.string("mobile.computers.power.byFormat", ...) value and the joined names
argument, so translated %@ formatting is handled correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd8d52f3-6c55-4e19-9246-462e2e14c4a5

📥 Commits

Reviewing files that changed from the base of the PR and between dee0eec and 15fdb0c.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (11)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacPower.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacPowerHolderTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus+Parse.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandRunning.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacKeepAwakeStatusParserTests.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacPowerControllerTests.swift
  • Resources/Localizable.xcstrings
  • Sources/TerminalController.swift
  • ios/cmux/Resources/Localizable.xcstrings

@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.

Actionable comments posted: 3

🤖 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.

Inline comments:
In
`@Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus`+Parse.swift:
- Around line 3-23: The parsing entry point was moved from a method on
MacKeepAwakeStatus to a top-level function, which violates the
no-ambient-global-state guideline for shared production APIs. Move
macParseKeepAwakeStatus back onto MacKeepAwakeStatus as a static (or instance)
parse method, then update all call sites such as MacPowerController and the test
suite to call through MacKeepAwakeStatus instead of the free function.

In
`@Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandProcess.swift`:
- Line 20: The MacPowerCommandProcess setup is discarding all stderr from the
spawned system tools by routing standardError to FileHandle.nullDevice, which
prevents useful diagnostics from pmset, kill, or ps failures. Update
MacPowerCommandProcess so stderr is preserved or captured instead of sending it
to /dev/null, and make sure the caller can access or log that error output when
commands fail.
- Around line 11-86: `macPowerRunProcess` is currently a top-level internal
function being used across files, which violates the guideline against exposing
ambient free-function APIs. Move the behavior into an owning type such as a new
`MacPowerProcessLauncher` or onto `SystemMacPowerCommandRunner` itself, and
update all call sites to use that method instead of the free function. Keep
`macPowerReadToEnd` and `macPowerScheduleSigkill` private helpers, and preserve
the existing process/timeout logic inside the new method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74541fb1-559c-4f5f-8d9a-c7fb779305bb

📥 Commits

Reviewing files that changed from the base of the PR and between 63e360b and 091de61.

📒 Files selected for processing (17)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacPowerHolder.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacPowerStatus.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacSleepResult.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacPower.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeDisableOutcome.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus+Parse.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerAssertionHolder.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandProcess.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandRunning.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerProcessRunState.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/SystemMacPowerCommandRunner.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/FakeMacPowerCommandRunner.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/FakeMacPowerCommandRunnerCall.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacKeepAwakeStatusParserTests.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacPowerControllerTests.swift
💤 Files with no reviewable changes (1)
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandRunning.swift

Comment thread Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatus+Parse.swift Outdated
Comment thread Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandProcess.swift Outdated
Comment thread Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerCommandProcess.swift Outdated

@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 (3)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacPower.swift (1)

10-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

terminated_caffeinate is documented but never decoded.

The doc comment says the payload is { terminated_caffeinate, status }, but MobileMacKeepAwakeDisableResponse only decodes status. If callers/UI ever need to confirm caffeinate was actually terminated (vs. some other keep-awake source), that signal is silently dropped.

Also applies to: 83-84

🤖 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/MobileShellComposite`+MacPower.swift
around lines 10 - 17, The `MobileMacKeepAwakeDisableResponse` model currently
documents a `terminated_caffeinate` field but only decodes `status`, so the
payload signal is being dropped. Update this response type to include and decode
`terminated_caffeinate` alongside `status`, and keep the wrapper/comment aligned
with the actual `mac.power.keep_awake.disable` payload so callers can inspect
whether caffeinate was terminated.
Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift (2)

49-66: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the termination result when the final status reread fails. If the post-kill keepAwakeStatus() call returns nil, this method drops terminatedCaffeinate too and makes a successful disable indistinguishable from an unavailable status read. Return an outcome that can carry an unknown status instead of collapsing the whole call to nil.

🤖 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/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift`
around lines 49 - 66, MacPowerController.disableKeepAwake currently returns nil
when the post-kill keepAwakeStatus() reread fails, which loses the already-known
terminatedCaffeinate result. Update disableKeepAwake() to preserve the
termination outcome even if the final status fetch is unavailable, using
MacKeepAwakeDisableOutcome with an unknown/missing status representation instead
of collapsing the whole method to nil. Use the existing disableKeepAwake(),
keepAwakeStatus(), and MacKeepAwakeDisableOutcome symbols to keep the successful
kill signal result intact.

29-43: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

sleepSystem() needs its own timeout budget. It reuses the shared 10s SystemMacPowerCommandRunner deadline, so the first Automation grant can still be open when the runner kills osascript and reports a failure. Give sleep a separate, longer timeout instead of sharing the fast-status default.

🤖 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/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift`
around lines 29 - 43, sleepSystem() is currently using the shared
SystemMacPowerCommandRunner timeout, which is too short for the first Automation
grant flow; update MacPowerController.sleepSystem() to run with its own longer
timeout budget instead of the default fast-status deadline. Adjust the call site
in sleepSystem() to use a dedicated timeout/configuration path in
SystemMacPowerCommandRunner so osascript can complete when Automation permission
prompts are involved, while leaving the existing short timeout behavior
unchanged for other commands.
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+MacPower.swift:
- Around line 10-17: The `MobileMacKeepAwakeDisableResponse` model currently
documents a `terminated_caffeinate` field but only decodes `status`, so the
payload signal is being dropped. Update this response type to include and decode
`terminated_caffeinate` alongside `status`, and keep the wrapper/comment aligned
with the actual `mac.power.keep_awake.disable` payload so callers can inspect
whether caffeinate was terminated.

In `@Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift`:
- Around line 49-66: MacPowerController.disableKeepAwake currently returns nil
when the post-kill keepAwakeStatus() reread fails, which loses the already-known
terminatedCaffeinate result. Update disableKeepAwake() to preserve the
termination outcome even if the final status fetch is unavailable, using
MacKeepAwakeDisableOutcome with an unknown/missing status representation instead
of collapsing the whole method to nil. Use the existing disableKeepAwake(),
keepAwakeStatus(), and MacKeepAwakeDisableOutcome symbols to keep the successful
kill signal result intact.
- Around line 29-43: sleepSystem() is currently using the shared
SystemMacPowerCommandRunner timeout, which is too short for the first Automation
grant flow; update MacPowerController.sleepSystem() to run with its own longer
timeout budget instead of the default fast-status deadline. Adjust the call site
in sleepSystem() to use a dedicated timeout/configuration path in
SystemMacPowerCommandRunner so osascript can complete when Automation permission
prompts are involved, while leaving the existing short timeout behavior
unchanged for other commands.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 487479e3-ab60-4fb8-9a9d-3604f50a28fd

📥 Commits

Reviewing files that changed from the base of the PR and between 091de61 and ef90e29.

📒 Files selected for processing (9)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacSleepErrorClassifier.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacSleepResult.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacPower.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileMacSleepResultTests.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacKeepAwakeStatusParser.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerController.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/MacPowerProcessLauncher.swift
  • Packages/macOS/CmuxMacPower/Sources/CmuxMacPower/SystemMacPowerCommandRunner.swift
  • Packages/macOS/CmuxMacPower/Tests/CmuxMacPowerTests/MacKeepAwakeStatusParserTests.swift

Comment on lines +1 to +21
internal import CmuxMobileRPC

struct MobileMacSleepErrorClassifier {
func result(forSendError error: any Error) -> MobileMacSleepResult {
guard let connectionError = error as? MobileShellConnectionError else {
return .failed
}
switch connectionError {
case .rpcError:
return .refused
case .invalidResponse,
.connectionClosed,
.requestTimedOut,
.insecureManualRoute,
.attachTicketExpired,
.authorizationFailed,
.accountMismatch:
return .failed
}
}
}

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 Connection-close misclassified as failure after sleep request

The PR description states "A sleep request that drops the connection is treated as success (the Mac slept and the link died)", but MobileMacSleepErrorClassifier maps both .connectionClosed and .requestTimedOut to .failed. In practice tell application "System Events" to sleep causes osascript to return before the Mac actually sleeps, so the success response is usually delivered in time — but there is a real race window where the Mac can go to sleep before the TCP frame carrying the RPC response is flushed. When that race fires, the phone shows "Couldn't reach the Mac to sleep it." even though the Mac successfully went to sleep. .connectionClosed and .requestTimedOut arriving in direct response to a sleep command are the primary evidence of success, not failure, and should return .requested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am leaving this as .failed intentionally. The branch now has MobileMacSleepErrorClassifier tests asserting that connectionClosed and requestTimedOut are delivery failures, while only an explicit successful RPC response maps to .requested and an RPC error maps to .refused. A closed/timed-out transport after send is ambiguous from the phone's side; osascript normally returns before actual sleep, so treating those as success creates false positives. The older PR wording about dropped connections was stale after the sleep-error tightening commits.

— Claude Code

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.

That's a fair correction — if osascript returns before the actual sleep, the response window is reliable enough that a transport failure genuinely is ambiguous (dropped wifi, flaky link, etc.) rather than a sleep signal. The test-backed decision to require an explicit success response makes sense.

Withdrawing the concern.

…e-should-control-mac-sleep

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 2b8d8a62 Deployed Jul 5, 2026 by vercel[bot]
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.

cmux mobile should control Mac sleep and caffeinate mode

3 participants