Conversation
Implements Phase 1 Task 1: App Group and Shared State Infrastructure ## Changes Made - **App Group Setup**: Verified existing `group.com.alienator88.Pearcleaner` entitlement - **MenuBarUpdatePublisher**: Created new class for publishing update counts and timestamps to shared UserDefaults - Singleton pattern for centralized access - Methods: `publishUpdateCount(_:)`, `publishLastCheckDate(_:)`, `publishUpdate(count:lastCheck:)` - Error handling for missing App Group with graceful degradation - Read methods for testing and verification - **AppState Extension**: Added `@Published var showMenuBar: Bool` property - Persists to UserDefaults key `"settings.interface.showMenuBar"` - Auto-saves on property change via didSet observer - Defaults to false (menu bar hidden initially) - **Test Documentation**: Created comprehensive test verification document ## Technical Details - Location: `Pearcleaner/Logic/MenuBar/MenuBarUpdatePublisher.swift` - Shared storage keys: - `menubar.updateCount`: Current update count - `menubar.lastCheck`: Last update check timestamp - Uses `printOS()` for debug logging - All methods return Bool for success/failure indication ## Testing See `.agent-os/specs/2025-11-26-menubar-foundation/test-verification/task-1-verification.md` for manual test checklist. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Summary of ChangesHello @jsonify, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request lays the groundwork for integrating a persistent menu bar component into Pearcleaner. It focuses on establishing the necessary infrastructure for shared state management between the main application and the upcoming menu bar feature, ensuring seamless data synchronization and user preference persistence. The changes are part of Phase 1 Task 1 of the Menu Bar Foundation, setting up the core communication channels and initial configuration without yet implementing the menu bar UI itself. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request lays a solid foundation for the new menu bar feature by setting up the App Group and shared state infrastructure. The code is well-structured and accompanied by thorough documentation, which is great to see. I've provided a couple of suggestions to improve maintainability and align with modern Swift practices, mainly concerning the use of UserDefaults.
| // Menu bar visibility (persisted to UserDefaults) | ||
| @Published var showMenuBar: Bool = UserDefaults.standard.bool(forKey: "settings.interface.showMenuBar") { | ||
| didSet { | ||
| UserDefaults.standard.set(showMenuBar, forKey: "settings.interface.showMenuBar") | ||
| } | ||
| } |
There was a problem hiding this comment.
The UserDefaults key "settings.interface.showMenuBar" is hardcoded and duplicated. It's a good practice to define it as a constant to avoid typos and improve maintainability. This also makes it easier to find all usages of this key.
| // Menu bar visibility (persisted to UserDefaults) | |
| @Published var showMenuBar: Bool = UserDefaults.standard.bool(forKey: "settings.interface.showMenuBar") { | |
| didSet { | |
| UserDefaults.standard.set(showMenuBar, forKey: "settings.interface.showMenuBar") | |
| } | |
| } | |
| // Menu bar visibility (persisted to UserDefaults) | |
| private static let showMenuBarKey = "settings.interface.showMenuBar" | |
| @Published var showMenuBar: Bool = UserDefaults.standard.bool(forKey: AppState.showMenuBarKey) { | |
| didSet { | |
| UserDefaults.standard.set(showMenuBar, forKey: AppState.showMenuBarKey) | |
| } | |
| } |
| class MenuBarUpdatePublisher { | ||
| static let shared = MenuBarUpdatePublisher() | ||
|
|
||
| // MARK: - Shared UserDefaults Keys | ||
| private let updateCountKey = "menubar.updateCount" | ||
| private let lastCheckKey = "menubar.lastCheck" | ||
| private let appGroupIdentifier = "group.com.alienator88.Pearcleaner" | ||
|
|
||
| // MARK: - Shared Storage Access | ||
|
|
||
| /// Shared UserDefaults container for App Group communication | ||
| /// Returns nil if App Group is not configured | ||
| private var sharedDefaults: UserDefaults? { | ||
| return UserDefaults(suiteName: appGroupIdentifier) | ||
| } | ||
|
|
||
| // MARK: - Public Methods | ||
|
|
||
| /// Publishes update count to shared storage | ||
| /// - Parameter count: Total number of pending updates | ||
| /// - Returns: Boolean indicating success or failure | ||
| @discardableResult | ||
| func publishUpdateCount(_ count: Int) -> Bool { | ||
| guard let defaults = sharedDefaults else { | ||
| printOS("MenuBarUpdatePublisher: App Group not available, cannot publish update count") | ||
| return false | ||
| } | ||
|
|
||
| defaults.set(count, forKey: updateCountKey) | ||
| defaults.synchronize() | ||
|
|
||
| printOS("MenuBarUpdatePublisher: Published update count: \(count)") | ||
| return true | ||
| } | ||
|
|
||
| /// Publishes last check timestamp to shared storage | ||
| /// - Parameter date: Date when update check completed | ||
| /// - Returns: Boolean indicating success or failure | ||
| @discardableResult | ||
| func publishLastCheckDate(_ date: Date) -> Bool { | ||
| guard let defaults = sharedDefaults else { | ||
| printOS("MenuBarUpdatePublisher: App Group not available, cannot publish last check date") | ||
| return false | ||
| } | ||
|
|
||
| defaults.set(date, forKey: lastCheckKey) | ||
| defaults.synchronize() | ||
|
|
||
| printOS("MenuBarUpdatePublisher: Published last check date: \(date)") | ||
| return true | ||
| } | ||
|
|
||
| /// Publishes both update count and timestamp atomically | ||
| /// - Parameters: | ||
| /// - count: Total number of pending updates | ||
| /// - date: Date when update check completed | ||
| /// - Returns: Boolean indicating success or failure | ||
| @discardableResult | ||
| func publishUpdate(count: Int, lastCheck date: Date) -> Bool { | ||
| guard let defaults = sharedDefaults else { | ||
| printOS("MenuBarUpdatePublisher: App Group not available, cannot publish update") | ||
| return false | ||
| } | ||
|
|
||
| defaults.set(count, forKey: updateCountKey) | ||
| defaults.set(date, forKey: lastCheckKey) | ||
| defaults.synchronize() | ||
|
|
||
| printOS("MenuBarUpdatePublisher: Published count: \(count), date: \(date)") | ||
| return true | ||
| } | ||
|
|
||
| // MARK: - Read Methods (for testing/debugging) | ||
|
|
||
| /// Reads current update count from shared storage | ||
| /// Returns nil if App Group not available or key doesn't exist | ||
| func readUpdateCount() -> Int? { | ||
| guard let defaults = sharedDefaults else { | ||
| return nil | ||
| } | ||
|
|
||
| guard defaults.object(forKey: updateCountKey) != nil else { | ||
| return nil | ||
| } | ||
|
|
||
| return defaults.integer(forKey: updateCountKey) | ||
| } | ||
|
|
||
| /// Reads last check date from shared storage | ||
| /// Returns nil if App Group not available or key doesn't exist | ||
| func readLastCheckDate() -> Date? { | ||
| guard let defaults = sharedDefaults else { | ||
| return nil | ||
| } | ||
|
|
||
| return defaults.object(forKey: lastCheckKey) as? Date | ||
| } | ||
| } |
There was a problem hiding this comment.
This is a solid implementation for publishing state. I have a couple of suggestions to improve it further:
- Use Static Constants for Keys: The
UserDefaultskeys and app group identifier are defined as private instance properties. Making themstaticconstants is more idiomatic and allows other components (like the future menu bar reader) to safely access them (e.g.,MenuBarUpdatePublisher.appGroupIdentifier), preventing stringly-typed errors. - Remove
synchronize()Calls: The calls todefaults.synchronize()are no longer necessary.UserDefaultsautomatically saves data at appropriate times, and explicitly callingsynchronize()can be inefficient. Removing these calls is the modern best practice.
I've combined these changes into a single suggestion for the whole class.
class MenuBarUpdatePublisher {
static let shared = MenuBarUpdatePublisher()
// MARK: - Shared UserDefaults Keys
static let updateCountKey = "menubar.updateCount"
static let lastCheckKey = "menubar.lastCheck"
static let appGroupIdentifier = "group.com.alienator88.Pearcleaner"
// MARK: - Shared Storage Access
/// Shared UserDefaults container for App Group communication
/// Returns nil if App Group is not configured
private var sharedDefaults: UserDefaults? {
return UserDefaults(suiteName: Self.appGroupIdentifier)
}
// MARK: - Public Methods
/// Publishes update count to shared storage
/// - Parameter count: Total number of pending updates
/// - Returns: Boolean indicating success or failure
@discardableResult
func publishUpdateCount(_ count: Int) -> Bool {
guard let defaults = sharedDefaults else {
printOS("MenuBarUpdatePublisher: App Group not available, cannot publish update count")
return false
}
defaults.set(count, forKey: Self.updateCountKey)
printOS("MenuBarUpdatePublisher: Published update count: \(count)")
return true
}
/// Publishes last check timestamp to shared storage
/// - Parameter date: Date when update check completed
/// - Returns: Boolean indicating success or failure
@discardableResult
func publishLastCheckDate(_ date: Date) -> Bool {
guard let defaults = sharedDefaults else {
printOS("MenuBarUpdatePublisher: App Group not available, cannot publish last check date")
return false
}
defaults.set(date, forKey: Self.lastCheckKey)
printOS("MenuBarUpdatePublisher: Published last check date: \(date)")
return true
}
/// Publishes both update count and timestamp atomically
/// - Parameters:
/// - count: Total number of pending updates
/// - date: Date when update check completed
/// - Returns: Boolean indicating success or failure
@discardableResult
func publishUpdate(count: Int, lastCheck date: Date) -> Bool {
guard let defaults = sharedDefaults else {
printOS("MenuBarUpdatePublisher: App Group not available, cannot publish update")
return false
}
defaults.set(count, forKey: Self.updateCountKey)
defaults.set(date, forKey: Self.lastCheckKey)
printOS("MenuBarUpdatePublisher: Published count: \(count), date: \(date)")
return true
}
// MARK: - Read Methods (for testing/debugging)
/// Reads current update count from shared storage
/// Returns nil if App Group not available or key doesn't exist
func readUpdateCount() -> Int? {
guard let defaults = sharedDefaults else {
return nil
}
guard defaults.object(forKey: Self.updateCountKey) != nil else {
return nil
}
return defaults.integer(forKey: Self.updateCountKey)
}
/// Reads last check date from shared storage
/// Returns nil if App Group not available or key doesn't exist
func readLastCheckDate() -> Date? {
guard let defaults = sharedDefaults else {
return nil
}
return defaults.object(forKey: Self.lastCheckKey) as? Date
}
}This commit implements the foundational menu bar components for Phase 1: **Task 2: MenuBarManager Core Logic** - Created MenuBarManager singleton with @mainactor isolation - Implements reactive state management via Combine publishers - Observes UpdateManager for real-time update count and checking state - Provides updateFromSharedDefaults() for App Group synchronization - Implements openMainApp() for main window activation and navigation - Includes error handling and fallback states for missing App Group **Task 3: UpdateManager Integration** - Extended UpdateManager with publishToMenuBar() method - Publishes update count and timestamp after each scan completion - Aggregates updates across all sources (App Store, Homebrew, Sparkle) - Uses MenuBarUpdatePublisher for atomic shared storage writes - Maintains backward compatibility with existing update flow **Task 4: Menu Bar Icon and Visual States** - Created MenuBarIconView with dynamic state rendering - Implements MenuBarIconState enum (idle, checking, updatesAvailable) - Badge overlay with red capsule showing update count (99+ support) - 360° rotation animation during update checks - Tooltip with relative timestamp ("Last checked 2 hours ago") **Bonus: Menu Bar Dropdown Content (Task 5 partial)** - Created MenuBarContent view with update summary and actions - "View Updates" / "Open Pearcleaner" button with smart navigation - "Quit Pearcleaner" option following macOS patterns - Custom MenuButtonStyle for native macOS appearance - Progress indicator during update checks **Implementation Details:** - All code follows existing Pearcleaner patterns and conventions - Uses Combine for reactive state observation - Proper @mainactor isolation for thread safety - Error handling with printOS() logging - RelativeDateTimeFormatter for human-readable timestamps - Preview providers for Xcode Canvas support **Files Modified:** - Pearcleaner/Logic/AppsUpdater/UpdateManager.swift **Files Created:** - Pearcleaner/Logic/MenuBar/MenuBarManager.swift - Pearcleaner/Views/MenuBar/MenuBarIconView.swift - Pearcleaner/Views/MenuBar/MenuBarContent.swift - .agent-os/specs/2025-11-26-menubar-foundation/IMPLEMENTATION_SUMMARY.md - .agent-os/specs/2025-11-26-menubar-foundation/TODO.md **Next Steps:** - Task 6: MenuBarExtra integration in PearcleanerApp.swift - Task 8: Settings UI toggle for menu bar visibility - Manual testing and build verification in Xcode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit completes the menu bar integration and user-facing controls: **Task 6: MenuBarExtra Integration** - Added MenuBarExtra scene to PearcleanerApp.swift - Conditional rendering based on AppState.showMenuBar - Uses MenuBarIconView for label, MenuBarContent for dropdown - Native macOS window style (.menuBarExtraStyle(.window)) - Updated AppDelegate to keep app running when menu bar enabled - Returns !AppState.shared.showMenuBar from applicationShouldTerminateAfterLastWindowClosed - Preserves original behavior (quit on window close) when disabled **Task 7: Main App Launcher Action** - Already implemented in Task 2 (MenuBarManager.openMainApp) - Integrated with MenuBarContent "View Updates" button - Smart navigation: goes to Updater only when updates available - Handles app activation, window unhiding, and navigation **Task 8: Settings UI Integration** - Added menu bar toggle in Settings → Interface - Positioned after "Badge notification overlays" setting - Dynamic icon: menubar.dock.rectangle (enabled) / menubar.dock.rectangle.badge.record (disabled) - Primary label reflects current state - Secondary label: "Show update count and quick actions in menu bar" - Two-way binding: Toggle ↔ AppState.showMenuBar ↔ UserDefaults - Follows existing settings pattern exactly **Key Features:** - No app restart required for menu bar visibility changes - Menu bar appears/disappears immediately when toggled - App lifecycle adapts dynamically (stay running vs. quit on close) - Persistent user preference across app launches - Native macOS appearance and behavior **User Experience:** 1. Enable menu bar in Settings → Interface 2. Menu bar icon appears immediately with update count badge 3. Click icon to see update summary and actions 4. Click "Open Pearcleaner" to activate main window 5. Close window → app stays running (accessible via menu bar) 6. Disable menu bar → icon disappears, app quits when window closes **Implementation Details:** - SwiftUI conditional Scene rendering (native approach) - Reactive state management via @Published/@ObservedObject - No manual view lifecycle management - Proper Scene hierarchy (WindowGroup + MenuBarExtra as siblings) - MenuBarManager singleton shared across all views **Files Modified:** - Pearcleaner/PearcleanerApp.swift - Pearcleaner/Views/Settings/Interface.swift **Documentation Created:** - .agent-os/specs/2025-11-26-menubar-foundation/IMPLEMENTATION_SUMMARY_TASKS_6_8.md - .agent-os/specs/2025-11-26-menubar-foundation/TODO.md (updated) **Phase 1 Progress: 70% complete (7 of 10 tasks)** **Next Steps:** - Task 9: Should-Have Features (optional "Check for Updates Now" button) - Task 10: Integration Testing and Polish (requires Xcode build verification) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Implements Phase 1 Task 1 of the Menu Bar Foundation spec: App Group and Shared State Infrastructure.
This establishes the foundation for state sharing between the main Pearcleaner app and the menu bar component that will be built in subsequent tasks.
Changes Made
1. App Group Configuration
group.com.alienator88.Pearcleanerentitlement in Pearcleaner.entitlements2. MenuBarUpdatePublisher Class
Location:
Pearcleaner/Logic/MenuBar/MenuBarUpdatePublisher.swiftpublishUpdateCount(_:)- Writes update count to shared storagepublishLastCheckDate(_:)- Writes last check timestamp to shared storagepublishUpdate(count:lastCheck:)- Atomic write of both valuesreadUpdateCount()/readLastCheckDate()- Read methods for testingprintOS()for troubleshootingBoolto indicate success/failure3. AppState Extension
Location:
Pearcleaner/Logic/AppState.swift@Published var showMenuBar: Boolproperty"settings.interface.showMenuBar"didSetobserverfalse(menu bar hidden initially)4. Shared Storage Keys
menubar.updateCount- Total pending updatesmenubar.lastCheck- Last update check Dategroup.com.alienator88.PearcleanerTesting
Manual test verification document created at:
.agent-os/specs/2025-11-26-menubar-foundation/test-verification/task-1-verification.mdTest Checklist:
Product Documentation
This PR also includes initial Agent OS product documentation:
Next Steps
Task 2 will build MenuBarManager to read from this shared state and manage menu bar UI state.
🤖 Generated with Claude Code