Skip to content

Add Open at Login toggle to General settings#450

Merged
FuJacob merged 2 commits into
mainfrom
jafu/launch-at-login
May 31, 2026
Merged

Add Open at Login toggle to General settings#450
FuJacob merged 2 commits into
mainfrom
jafu/launch-at-login

Conversation

@FuJacob
Copy link
Copy Markdown
Owner

@FuJacob FuJacob commented May 31, 2026

Summary

Adds an Open at Login toggle so Cotabby can start automatically when the user logs in. The SMAppService-backed LaunchAtLoginService already existed and was wired through the app's dependency graph (constructed in CotabbyAppEnvironment, retained in AppDelegate, passed into SettingsContainerView), but it was never surfaced in any settings pane, so there was no way to actually turn launch-at-login on. This exposes it in the General pane's Status section.

  • GeneralPaneView observes LaunchAtLoginService and binds the toggle to its state.isEnabled / setEnabled.
  • The toggle is disabled only when macOS reports the login item as unavailable (e.g. the app isn't in /Applications), and the row description surfaces the OS status detail (approval required / move-to-Applications) or any registration error, so the row reflects real OS state rather than just the switch position.
  • SettingsContainerView threads the already-available service into the pane.
  • Added pure unit tests for LaunchAtLoginState's presentation flags.

Validation

swiftlint lint --strict --quiet
# exit 0

xcodegen generate && git diff --quiet -- Cotabby.xcodeproj/project.pbxproj
# regenerated to register the new test file (committed)

xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build
# ** BUILD SUCCEEDED **

xcodebuild test -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' \
  CODE_SIGNING_ALLOWED=NO -only-testing:CotabbyTests/LaunchAtLoginStateTests
# ** TEST SUCCEEDED **  Executed 3 tests, with 0 failures

The toggle's live registration behavior depends on a signed build installed in /Applications (SMAppService requires the app's real code identity), so the end-to-end enable/approve flow is best confirmed manually in a release-style build; the state/presentation logic is covered by tests.

Linked issues

Risk / rollout notes

  • UI-only wiring of an existing service; no new system permissions or entitlements (SMAppService relies on the app's code identity, and the app is non-sandboxed with macOS 14 deployment target, well above SMAppService's macOS 13 requirement).
  • Cotabby.xcodeproj/project.pbxproj is regenerated via xcodegen generate to register the new test file (project.yml is the source of truth).

Greptile Summary

This PR surfaces the already-wired LaunchAtLoginService in the General settings pane by adding an Open at Login toggle to the Status section, and simultaneously fixes a stale-error-message bug identified in a prior review by splitting refresh() into two methods.

  • LaunchAtLoginService gains a private reloadState() for post-mutation re-reads (preserves a just-captured error) while the public refresh() — called when Settings opens — clears lastErrorMessage first, so old failure strings no longer persist after an out-of-band fix.
  • GeneralPaneView adds the toggle bound to launchAtLoginService.state.isEnabled / setEnabled, disabled when canToggle is false, with launchAtLoginDescription correctly preferring state.detail over lastErrorMessage and suppressing both when the item is enabled.
  • Three pure unit tests cover all LaunchAtLoginState cases for isEnabled, canToggle, and detail.

Confidence Score: 5/5

Safe to merge — UI-only wiring of an existing service with no new system permissions, and the stale-error fix from the prior review is correctly addressed.

The change introduces no new entitlements or system calls beyond what SMAppService already required. The refresh()/reloadState() split is a minimal, targeted fix for the previously identified stale-error scenario, and the description logic in launchAtLoginDescription correctly handles all four LaunchAtLoginState cases. Unit tests cover all state presentation flags. No edge cases appear unhandled.

No files require special attention.

Important Files Changed

Filename Overview
Cotabby/Services/Utilities/LaunchAtLoginService.swift Adds refresh()/reloadState() split to address the previously flagged stale-error-message bug; setEnabled now uses the private reloadState() to preserve freshly captured errors while refresh() (called on Settings open) clears them first.
Cotabby/UI/Settings/Panes/GeneralPaneView.swift Adds the Open at Login toggle to the Status section; launchAtLoginDescription correctly prioritises state.detail over lastErrorMessage, and suppresses both when the item is enabled.
Cotabby/UI/Settings/SettingsContainerView.swift One-line threading of the already-available launchAtLoginService into GeneralPaneView; no new logic introduced.
CotabbyTests/LaunchAtLoginStateTests.swift Covers all four LaunchAtLoginState cases for isEnabled, canToggle, and detail; tests are pure and accurate against the implementation.

Sequence Diagram

sequenceDiagram
    participant User
    participant GeneralPaneView
    participant LaunchAtLoginService
    participant SMAppService

    User->>GeneralPaneView: Opens Settings window
    GeneralPaneView->>LaunchAtLoginService: refresh()
    LaunchAtLoginService->>LaunchAtLoginService: "lastErrorMessage = nil"
    LaunchAtLoginService->>SMAppService: read .status
    SMAppService-->>LaunchAtLoginService: status
    LaunchAtLoginService->>LaunchAtLoginService: "state = map(status)"
    LaunchAtLoginService-->>GeneralPaneView: "@Published state updates toggle + description"

    User->>GeneralPaneView: Flips toggle ON
    GeneralPaneView->>LaunchAtLoginService: setEnabled(true)
    LaunchAtLoginService->>SMAppService: register()
    alt success
        SMAppService-->>LaunchAtLoginService: (no throw)
        LaunchAtLoginService->>LaunchAtLoginService: "lastErrorMessage = nil"
    else failure
        SMAppService-->>LaunchAtLoginService: throws error
        LaunchAtLoginService->>LaunchAtLoginService: "lastErrorMessage = error.localizedDescription"
    end
    LaunchAtLoginService->>SMAppService: read .status (reloadState)
    SMAppService-->>LaunchAtLoginService: status
    LaunchAtLoginService->>LaunchAtLoginService: "state = map(status)"
    LaunchAtLoginService-->>GeneralPaneView: "@Published state + lastErrorMessage update UI"
Loading

Reviews (2): Last reviewed commit: "Clear stale Open at Login error on out-o..." | Re-trigger Greptile

The LaunchAtLoginService (SMAppService-backed) was already built and wired
through the app, but never surfaced in the UI, so users had no way to enable
launch-at-login. Add an Open at Login toggle to the General pane's Status
section.

- GeneralPaneView observes LaunchAtLoginService and binds the toggle to its
  state/setEnabled. The toggle is disabled when macOS reports the login item
  as unavailable, and the row description surfaces the OS status detail
  (approval needed / move to Applications) or any registration error.
- SettingsContainerView threads the already-available service into the pane.
- Pure tests for LaunchAtLoginState presentation flags.
Comment thread Cotabby/UI/Settings/Panes/GeneralPaneView.swift Outdated
refresh() now clears lastErrorMessage before re-reading the login-item status, so a one-time registration failure (such as a first launch outside /Applications) no longer lingers as the row's subtext after the user fixes the cause and macOS reports the item enabled or cleanly disabled.

setEnabled() re-reads OS state via a new private reloadState() instead of refresh(), so an error it just captured survives the post-mutation read and can still explain why the toggle did not take effect.

GeneralPaneView's launchAtLoginDescription returns the plain explanation when the item is enabled, so a working toggle is never described by leftover detail or error text.

Resolves the Greptile P1 review comment on GeneralPaneView.swift.
@FuJacob FuJacob merged commit e8b17b3 into main May 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant