Skip to content

Feat: Update Countdown Rate limit#1

Open
tuanle96 wants to merge 11 commits intonhonn:masterfrom
tuanle96:master
Open

Feat: Update Countdown Rate limit#1
tuanle96 wants to merge 11 commits intonhonn:masterfrom
tuanle96:master

Conversation

@tuanle96
Copy link
Copy Markdown

@tuanle96 tuanle96 commented Dec 10, 2025

Summary by CodeRabbit

  • New Features

    • Menu bar now shows a live countdown and richer account menu with per-account actions (set/reset time limits, check quota) and active-account indicator.
    • Quota retrieval and display for accounts with a readable snapshot dialog and cached results.
    • New utilities: robust time-limit parsing/formatting, progress panel, secure localhost HTTPS handling, and safer process termination.
  • Bug Fixes

    • Restore now clears absent backup keys to avoid stale state.
  • Chores

    • Updated app icon and added editor launch configurations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 10, 2025

Walkthrough

Adds a new menu-bar UI (MenuBarLabelView) with per-account controls and real‑time countdowns powered by MenuBarState and Account.time_limit; extends AccountManager with timer, updateTimeLimit, switchAccountApplyingLimitToCurrent, and checkQuota APIs. Introduces TimeLimitFormatter, QuotaService with QuotaSnapshot, and quota UI flows. Adds Shell utilities, LocalhostInsecureURLSessionDelegate, ProgressPanel, ProcessManager. Modifies DB restore to clear absent keys, expands AppConfiguration backup keys and bundle IDs, extends AppError, adds build pre-clean and AppIcon plist entry, VS Code launch configs, a verification script, and a new terminate/cleanup routine.

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Feat: Update Countdown Rate limit' is vague and does not clearly convey the extensive changes in this PR. The changeset introduces countdown timers, time limits, quota tracking, menu bar updates, account switching with limits, language server interaction, and supporting utilities—far broader than just 'updating countdown rate limit.' Consider a more descriptive title such as 'Feat: Add time limits, quota tracking, and menu bar countdown timer' or 'Feat: Implement account time limits and real-time quota monitoring' to better reflect the scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5813b84 and d91ea3a.

⛔ Files ignored due to path filters (1)
  • assets/icon.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • Sources/AntigravityMenuBar/AntigravityApp.swift (5 hunks)
  • Sources/AntigravityMenuBar/Logic/AccountManager.swift (2 hunks)
  • Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift (1 hunks)
  • build.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (2)
Sources/AntigravityMenuBar/Utils/DBManager.swift (1)
  • getCurrentAccountEmail (129-143)
Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift (1)
  • formatMenuBarCountdown (64-85)
Sources/AntigravityMenuBar/AntigravityApp.swift (2)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (2)
  • updateTimeLimit (255-269)
  • removeAccount (235-253)
Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift (4)
  • formatCountdown (22-43)
  • defaultTimeString (55-59)
  • defaultDateString (47-51)
  • parse (11-17)
🪛 SwiftLint (0.57.0)
Sources/AntigravityMenuBar/Logic/AccountManager.swift

[Warning] 24-24: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (5)
build.sh (1)

38-39: The CFBundleIconFile entry is not present in the current build.sh file.

Lines 38-39 contain <key>LSUIElement</key> and <true/>, not the CFBundleIconFile entry shown in the review comment snippet. The Info.plist in the current build.sh does not include an icon reference. If adding CFBundleIconFile is intended, ensure it is properly added to the plist generation in build.sh (around lines 23-43).

Likely an incorrect or invalid review comment.

Sources/AntigravityMenuBar/Logic/AccountManager.swift (2)

13-18: LGTM on MenuBarState separation.

Good design choice to separate the countdown state into its own ObservableObject. This prevents triggering full menu rebuilds on every countdown tick.


255-269: LGTM on updateTimeLimit implementation.

The method correctly handles both setting and clearing the time limit, with proper persistence.

Sources/AntigravityMenuBar/AntigravityApp.swift (2)

3-15: LGTM on MenuBarLabelView.

Clean implementation that observes the shared countdown state and displays it alongside the icon.


43-52: Good conditional UI for Reset vs Update Time Limit.

The logic correctly shows "Reset" only when there's an active countdown and "Update Time Limit" otherwise. This provides clear user feedback.

Comment on lines +66 to +70
// if account.email == "Unknown" {
// Image(systemName: "person.circle")
// } else {
// Image(systemName: "person.fill")
// }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove commented-out code.

Dead code should be removed to maintain code cleanliness. If this logic is needed later, it can be retrieved from version control.

                             Image(systemName: "checkmark")
                                 .foregroundColor(.green)
                         }
-                            // if account.email == "Unknown" {
-                            //     Image(systemName: "person.circle")
-                            // } else {
-                            //     Image(systemName: "person.fill")
-                            // }
                             // Build display name with countdown if applicable
                             Text(accountDisplayName(account))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// if account.email == "Unknown" {
// Image(systemName: "person.circle")
// } else {
// Image(systemName: "person.fill")
// }
Image(systemName: "checkmark")
.foregroundColor(.green)
}
// Build display name with countdown if applicable
Text(accountDisplayName(account))
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/AntigravityApp.swift around lines 66 to 70, remove
the commented-out conditional block that toggles Image(systemName:
"person.circle") vs "person.fill" as it is dead code; delete those commented
lines so the file contains only active code and rely on version control to
recover this logic if needed later.

Comment on lines +127 to +146
private func accountDisplayName(_ account: Account) -> String {
var displayName = account.name

// Append countdown if time_limit is set and not expired
if let timeLimitStr = account.time_limit,
let timeLimit = ISO8601DateFormatter().date(from: timeLimitStr),
let countdown = TimeLimitFormatter.formatCountdown(to: timeLimit) {
displayName += " \(countdown)"
}

return displayName
}

private func hasActiveCountdown(_ account: Account) -> Bool {
guard let timeLimitStr = account.time_limit,
let timeLimit = ISO8601DateFormatter().date(from: timeLimitStr) else {
return false
}
return timeLimit > Date() // Active if not expired
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Cache ISO8601DateFormatter and consider extracting to a shared helper.

Both accountDisplayName and hasActiveCountdown create new ISO8601DateFormatter() instances on each call. This duplicates logic already present in AccountManager.updateMenuBarCountdown. Consider:

  1. Caching the formatter as a static property
  2. Adding a helper method on Account or in TimeLimitFormatter to parse the time_limit field

You could add a computed property to Account:

extension Account {
    var timeLimitDate: Date? {
        guard let timeLimitStr = time_limit else { return nil }
        return ISO8601DateFormatter().date(from: timeLimitStr)
    }
}

Or better, add a cached formatter in TimeLimitFormatter:

static func parseISO8601(_ string: String) -> Date? {
    // Use a cached ISO8601DateFormatter
}
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/AntigravityApp.swift around lines 127 to 146, both
accountDisplayName(_) and hasActiveCountdown(_) create new ISO8601DateFormatter
instances on each call; change this to use a single cached formatter and a
parsing helper: add a shared ISO8601DateFormatter (preferably as a static on
TimeLimitFormatter) and a helper function (e.g.,
TimeLimitFormatter.parseISO8601(_:)) or a computed property on Account
(timeLimitDate) that uses the cached formatter, then replace the inline
ISO8601DateFormatter().date(from:) calls with the new helper so parsing is
centralized and allocations are avoided.

Comment on lines +223 to +231
// Validate and parse
guard let parsedDate = TimeLimitFormatter.parse(time: timeValue, date: dateValue) else {
let errorAlert = NSAlert()
errorAlert.messageText = "Invalid Format"
errorAlert.informativeText = "Please enter time as HH:mm (e.g., 14:30) and date as dd.MM.yyyy (e.g., 31.12.2026)"
errorAlert.alertStyle = .warning
errorAlert.runModal()
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider validating that the time limit is in the future.

The code validates the format but doesn't check if the parsed date is in the future. Setting a time limit in the past would be immediately expired and have no effect.

             // Validate and parse
             guard let parsedDate = TimeLimitFormatter.parse(time: timeValue, date: dateValue) else {
                 let errorAlert = NSAlert()
                 errorAlert.messageText = "Invalid Format"
                 errorAlert.informativeText = "Please enter time as HH:mm (e.g., 14:30) and date as dd.MM.yyyy (e.g., 31.12.2026)"
                 errorAlert.alertStyle = .warning
                 errorAlert.runModal()
                 return
             }
             
+            guard parsedDate > Date() else {
+                let errorAlert = NSAlert()
+                errorAlert.messageText = "Invalid Date"
+                errorAlert.informativeText = "The time limit must be in the future."
+                errorAlert.alertStyle = .warning
+                errorAlert.runModal()
+                return
+            }
+            
             accountManager.updateTimeLimit(id: account.id, date: parsedDate)
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/AntigravityApp.swift around lines 223 to 231,
after parsing the date/time you must verify the resulting parsedDate is in the
future; if parsedDate <= Date() show an NSAlert with a clear message (e.g.,
"Time limit must be in the future") and return. Implement the comparison using
Date() (or Calendar.current.compare) to avoid timezone issues and keep the
existing UI flow (alertStyle .warning and runModal()) before returning.

static let shared = AccountManager()

@Published var accounts: [Account] = []
@Published var currentEmail: String? = nil // Currently active account email
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove redundant nil initialization.

Initializing an optional variable with nil is redundant in Swift.

-    @Published var currentEmail: String? = nil  // Currently active account email
+    @Published var currentEmail: String?  // Currently active account email
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Published var currentEmail: String? = nil // Currently active account email
@Published var currentEmail: String? // Currently active account email
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 24-24: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Logic/AccountManager.swift at line 24, the
optional property is redundantly initialized to nil; remove the explicit "= nil"
so the declaration reads simply "@Published var currentEmail: String?" and keep
the rest of the code unchanged (verify no other code relies on an explicit
initializer).

Comment on lines +38 to +48
private func startRefreshTimer() {
// Create timer in common run loop mode so it runs even when menu is open
refreshTimer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateMenuBarCountdown()
}
// Add to common run loop mode for background updates
RunLoop.main.add(refreshTimer!, forMode: .common)

// Initial update
updateMenuBarCountdown()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid force unwrap and consider timer cleanup.

While the force unwrap is technically safe here since the timer was just created, using optional chaining or guard let is more defensive. Also, the timer should be invalidated if AccountManager is ever deallocated (though as a singleton this may not occur in practice).

     private func startRefreshTimer() {
-        // Create timer in common run loop mode so it runs even when menu is open
-        refreshTimer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
+        refreshTimer?.invalidate()
+        let timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
             self?.updateMenuBarCountdown()
         }
-        // Add to common run loop mode for background updates
-        RunLoop.main.add(refreshTimer!, forMode: .common)
+        RunLoop.main.add(timer, forMode: .common)
+        refreshTimer = timer
         
         // Initial update
         updateMenuBarCountdown()
     }
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Logic/AccountManager.swift around lines 38 to 48,
avoid force-unwrapping refreshTimer and ensure cleanup: after creating the
timer, use a safe binding (e.g., guard let timer = refreshTimer or optional
chaining) when adding it to RunLoop instead of refreshTimer!, and add a
stopRefreshTimer() method that invalidates and nils the timer; also call that
cleanup from deinit (or provide a public stop method) so the timer is
invalidated if the AccountManager is ever deallocated.

Comment on lines +60 to +69
private func updateMenuBarCountdown() {
// Find all accounts with active countdown
let activeAccounts = accounts.compactMap { account -> (Account, Date)? in
guard let timeLimitStr = account.time_limit,
let timeLimit = ISO8601DateFormatter().date(from: timeLimitStr),
timeLimit > Date() else {
return nil
}
return (account, timeLimit)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Cache ISO8601DateFormatter to avoid repeated instantiation.

ISO8601DateFormatter() is created on every iteration inside compactMap, which runs every second. This is inefficient.

+    private static let iso8601Formatter = ISO8601DateFormatter()
+    
     private func updateMenuBarCountdown() {
+        let formatter = Self.iso8601Formatter
         // Find all accounts with active countdown
         let activeAccounts = accounts.compactMap { account -> (Account, Date)? in
             guard let timeLimitStr = account.time_limit,
-                  let timeLimit = ISO8601DateFormatter().date(from: timeLimitStr),
+                  let timeLimit = formatter.date(from: timeLimitStr),
                   timeLimit > Date() else {
                 return nil
             }
             return (account, timeLimit)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func updateMenuBarCountdown() {
// Find all accounts with active countdown
let activeAccounts = accounts.compactMap { account -> (Account, Date)? in
guard let timeLimitStr = account.time_limit,
let timeLimit = ISO8601DateFormatter().date(from: timeLimitStr),
timeLimit > Date() else {
return nil
}
return (account, timeLimit)
}
private static let iso8601Formatter = ISO8601DateFormatter()
private func updateMenuBarCountdown() {
let formatter = Self.iso8601Formatter
// Find all accounts with active countdown
let activeAccounts = accounts.compactMap { account -> (Account, Date)? in
guard let timeLimitStr = account.time_limit,
let timeLimit = formatter.date(from: timeLimitStr),
timeLimit > Date() else {
return nil
}
return (account, timeLimit)
}
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Logic/AccountManager.swift around lines 60 to 69,
the ISO8601DateFormatter() is being instantiated inside the compactMap for every
account (and every second), causing needless allocations; move the formatter
creation out of the loop and reuse a single instance — either create a local let
formatter = ISO8601DateFormatter() immediately before the compactMap or add a
stored/static property on AccountManager (e.g., private let isoFormatter =
ISO8601DateFormatter()) and use that formatter in the compactMap to avoid
repeated instantiation.

Comment on lines +11 to +17
static func parse(time: String, date: String) -> Date? {
let combined = "\(time) \(date)"
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm dd.MM.yyyy"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.date(from: combined)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Cache DateFormatter instances for performance.

DateFormatter creation is expensive and these methods may be called frequently (e.g., every second for countdown updates). Consider caching the formatters as static properties.

Additionally, defaultDateString() and defaultTimeString() don't specify a locale, which may produce unexpected results depending on user's locale settings.

Apply this diff to cache formatters:

 struct TimeLimitFormatter {
+    
+    private static let parseFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH:mm dd.MM.yyyy"
+        formatter.locale = Locale(identifier: "en_US_POSIX")
+        return formatter
+    }()
+    
+    private static let dateStringFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "dd.MM.yyyy"
+        formatter.locale = Locale(identifier: "en_US_POSIX")
+        return formatter
+    }()
+    
+    private static let timeStringFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH:mm"
+        formatter.locale = Locale(identifier: "en_US_POSIX")
+        return formatter
+    }()
     
     static func parse(time: String, date: String) -> Date? {
         let combined = "\(time) \(date)"
-        let formatter = DateFormatter()
-        formatter.dateFormat = "HH:mm dd.MM.yyyy"
-        formatter.locale = Locale(identifier: "en_US_POSIX")
-        return formatter.date(from: combined)
+        return parseFormatter.date(from: combined)
     }

Also applies to: 47-59

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift around lines 11-17
(and also apply same change to lines 47-59), the code creates new DateFormatter
instances on each call and omits an explicit locale; change this by adding
static (shared) DateFormatter properties configured once (thread-safe static
lets) with the correct dateFormat and locale = Locale(identifier:
"en_US_POSIX"), then use those cached formatters in parse(time:date:) and in
defaultDateString()/defaultTimeString() so formatting/parsing is fast and
locale-deterministic.

Comment on lines +22 to +43
static func formatCountdown(to date: Date) -> String? {
let now = Date()

if date <= now {
return nil // Don't show anything when expired
}

let calendar = Calendar.current
let components = calendar.dateComponents([.day, .hour, .minute, .second], from: now, to: date)

let days = components.day ?? 0
let hours = components.hour ?? 0
let minutes = components.minute ?? 0
let seconds = components.second ?? 0

// Format as HH:mm:ss with optional days prefix
if days > 0 {
return String(format: " ⏳ %dd %d:%02d:%02d", days, hours, minutes, seconds)
} else {
return String(format: " ⏳ %d:%02d:%02d", hours, minutes, seconds)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Reduce code duplication between formatCountdown and formatMenuBarCountdown.

These two methods share nearly identical logic for calculating date components. Consider extracting the common logic into a private helper.

+    private static func countdownComponents(to date: Date) -> (days: Int, hours: Int, minutes: Int, seconds: Int)? {
+        let now = Date()
+        guard date > now else { return nil }
+        
+        let calendar = Calendar.current
+        let components = calendar.dateComponents([.day, .hour, .minute, .second], from: now, to: date)
+        return (
+            components.day ?? 0,
+            components.hour ?? 0,
+            components.minute ?? 0,
+            components.second ?? 0
+        )
+    }
+    
     static func formatCountdown(to date: Date) -> String? {
-        let now = Date()
-        
-        if date <= now {
-            return nil
-        }
-        
-        let calendar = Calendar.current
-        let components = calendar.dateComponents([.day, .hour, .minute, .second], from: now, to: date)
-        
-        let days = components.day ?? 0
-        let hours = components.hour ?? 0
-        let minutes = components.minute ?? 0
-        let seconds = components.second ?? 0
-        
-        if days > 0 {
+        guard let (days, hours, minutes, seconds) = countdownComponents(to: date) else {
+            return nil
+        }
+        if days > 0 {
             return String(format: "  ⏳ %dd %d:%02d:%02d", days, hours, minutes, seconds)
         } else {
             return String(format: "  ⏳ %d:%02d:%02d", hours, minutes, seconds)
         }
     }

Also applies to: 64-85

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift around lines 22-43
(and similarly lines 64-85), both formatCountdown and formatMenuBarCountdown
duplicate the same date-component computation and formatting; extract that
shared logic into a private helper function (e.g., a helper that takes a target
Date and returns either an optional formatted String or an optional struct/tuple
containing days/hours/minutes/seconds) that performs: now check -> return nil if
expired -> compute calendar dateComponents -> produce the same "  ⏳ ..." string
(or components) so both public methods simply call the helper and return its
result, preserving existing formatting and nil behavior.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (1)

3-11: Consider storing time_limit as Date? (or centralize ISO8601 encode/decode) to avoid parse drift.
Right now updateTimeLimit() writes via date.ISO8601Format(), while reads use a default ISO8601DateFormatter(); these formats/options can diverge and silently break countdown parsing.

♻️ Duplicate comments (3)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (3)

23-26: Remove redundant optional nil initialization (SwiftLint).
Line 24 can be @Published var currentEmail: String? (no = nil).


38-48: Avoid refreshTimer! and add timer cleanup/invalidation.
Force unwrap at Line 44 is avoidable, and startRefreshTimer() should invalidate any prior timer + provide a stop/deinit cleanup hook.


60-99: Cache ISO8601DateFormatter and ensure main-actor consistency for accounts.

  • ISO8601DateFormatter() is created inside the per-account loop every second (allocation churn).
  • Also, AccountManager isn’t @MainActor; if any caller invokes updateTimeLimit/removeAccount/saveAccounts/loadAccounts off-main while this timer reads accounts, you can race/crash.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d91ea3a and e2923b5.

📒 Files selected for processing (3)
  • .vscode/launch.json (1 hunks)
  • Sources/AntigravityMenuBar/Logic/AccountManager.swift (3 hunks)
  • antigravity-switcher (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (2)
Sources/AntigravityMenuBar/Utils/DBManager.swift (1)
  • getCurrentAccountEmail (129-143)
Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift (1)
  • formatMenuBarCountdown (64-85)
🪛 SwiftLint (0.57.0)
Sources/AntigravityMenuBar/Logic/AccountManager.swift

[Warning] 24-24: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (2)
.vscode/launch.json (1)

1-24: Configs look fine; verify referenced preLaunchTask names exist.
If .vscode/tasks.json doesn’t define swift: Build Debug AntigravityMenuBar / swift: Build Release AntigravityMenuBar, these launch configs will fail to start.

Sources/AntigravityMenuBar/Logic/AccountManager.swift (1)

222-227: Good: updates currentEmail right after restore to keep the checkmark accurate.

@@ -0,0 +1 @@
Subproject commit cfec68659d343e7ea2c94bb01e5d15ebeda45258
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if antigravity-switcher submodule exists and is initialized
git config --file .gitmodules --name-only --get-regexp path | grep antigravity-switcher

Repository: nhonn/antigravity-switcher

Length of output: 52


🏁 Script executed:

# Check the current submodule reference in the file
cat antigravity-switcher

Repository: nhonn/antigravity-switcher

Length of output: 111


🏁 Script executed:

# List all submodules and their configuration
git config --file .gitmodules --get-regexp '^submodule' | head -20

Repository: nhonn/antigravity-switcher

Length of output: 52


🏁 Script executed:

# Check git status for submodules
git status --porcelain | grep antigravity-switcher

Repository: nhonn/antigravity-switcher

Length of output: 52


🏁 Script executed:

# Check if .gitmodules exists and look for antigravity-switcher
test -f .gitmodules && cat .gitmodules || echo ".gitmodules not found"

Repository: nhonn/antigravity-switcher

Length of output: 91


🏁 Script executed:

# Check git submodule status
git submodule status 2>&1 || echo "No submodules or error"

Repository: nhonn/antigravity-switcher

Length of output: 173


🏁 Script executed:

# List files in root directory to understand structure
ls -la | head -30

Repository: nhonn/antigravity-switcher

Length of output: 962


🏁 Script executed:

# Check if antigravity-switcher is a submodule and its commit
git ls-files --stage | grep antigravity-switcher

Repository: nhonn/antigravity-switcher

Length of output: 140


🏁 Script executed:

# Check if antigravity-switcher directory has its own .git folder
ls -la antigravity-switcher/ | head -15

Repository: nhonn/antigravity-switcher

Length of output: 178


🏁 Script executed:

# Check git log to see when this entry was added
git log --oneline --all -- antigravity-switcher | head -10

Repository: nhonn/antigravity-switcher

Length of output: 109


🏁 Script executed:

# Check git diff to see the actual change in this file
git diff HEAD~1 antigravity-switcher 2>&1 || echo "Cannot diff submodule entry"

Repository: nhonn/antigravity-switcher

Length of output: 284


🏁 Script executed:

# Verify the commit hash format and check if it's a valid git object
git cat-file -t cfec68659d343e7ea2c94bb01e5d15ebeda45258 2>&1 || echo "Commit object not found in this repository"

Repository: nhonn/antigravity-switcher

Length of output: 284


Remove or properly configure the broken submodule entry for antigravity-switcher.

The submodule reference at antigravity-switcher is in an invalid state:

  • The commit hash cfec68659d343e7ea2c94bb01e5d15ebeda45258 does not exist in the repository and cannot be fetched
  • The antigravity-switcher directory is empty with no .git folder or configuration
  • No .gitmodules file exists to define this as a proper submodule

Either remove this entry from the git index (git rm --cached antigravity-switcher) or properly initialize it as a submodule with valid configuration and a reachable commit reference.

🤖 Prompt for AI Agents
In antigravity-switcher around lines 1 to 1, the repository contains a broken
submodule reference (missing .gitmodules, empty directory, and an unreachable
commit cfec68659d343e7ea2c94bb01e5d15ebeda45258); either remove the entry from
the git index or properly configure it as a submodule: to remove, run git rm
--cached antigravity-switcher, delete the empty directory if present, commit the
removal and push; to fix as a submodule, add a .gitmodules entry with the
correct repository URL and path, run git submodule sync && git submodule init &&
git submodule update --remote (or add with git submodule add <url>
antigravity-switcher) ensuring the submodule points to a reachable commit, then
commit .gitmodules and the submodule update.

Comment on lines +13 to 18
/// Separate class for menu bar countdown to avoid triggering menu content refresh
class MenuBarState: ObservableObject {
static let shared = MenuBarState()
@Published var countdown: String = ""
private init() {}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Make MenuBarState @MainActor (published UI state).
It’s updated from multiple call sites; marking it @MainActor prevents accidental off-main publishes as this evolves.

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Logic/AccountManager.swift around lines 13–18, the
MenuBarState ObservableObject is not annotated with @MainActor which can allow
off-main-thread publishes; fix by annotating the class declaration with
@MainActor (i.e. add @MainActor before `class MenuBarState`) so all property
updates and @Published publishes run on the main actor; keep the existing
`shared` singleton and private init as-is.

Comment on lines +265 to +279
func updateTimeLimit(id: String, date: Date?) {
guard let index = accounts.firstIndex(where: { $0.id == id }) else {
return
}

if let date = date {
accounts[index].time_limit = date.ISO8601Format()
print("⏱ Set time limit for \(accounts[index].name): \(date)")
} else {
accounts[index].time_limit = nil
print("⏱ Cleared time limit for \(accounts[index].name)")
}

saveAccounts()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Ensure updateTimeLimit runs on main (and consider removing account-name logs).
This mutates @Published accounts; if it’s ever called off-main, SwiftUI will warn and you can race the timer. Also print includes account name (minor privacy/PII concern in logs).

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Logic/AccountManager.swift around lines 265 to
279, updateTimeLimit mutates the @Published accounts array and currently logs
account names; ensure the mutation and saveAccounts call execute on the main
thread to avoid SwiftUI threading warnings (wrap mutation/save in
DispatchQueue.main.async if not on main, or assert/guard Thread.isMainThread at
entry), and remove or redact account.name from the print statements (use a
generic message or omit prints entirely) to avoid PII in logs.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

♻️ Duplicate comments (8)
Sources/AntigravityMenuBar/AntigravityApp.swift (3)

74-78: Remove commented-out code.

This dead code was flagged in a previous review. It should be removed to maintain code cleanliness.


147-166: Cache ISO8601DateFormatter to avoid repeated instantiation.

Both accountDisplayName and hasActiveCountdown create new ISO8601DateFormatter() instances on each call. This was flagged in a previous review.


374-382: Consider validating that the parsed date is in the future.

Setting a time limit in the past would be immediately expired and have no effect. This was flagged in a previous review.

Sources/AntigravityMenuBar/Logic/AccountManager.swift (5)

24-24: Remove redundant nil initialization.

This was flagged in a previous review.


45-55: Avoid force unwrap of timer.

This was flagged in a previous review.


82-91: Cache ISO8601DateFormatter to avoid repeated instantiation.

This was flagged in a previous review.


13-18: Make MenuBarState @MainActor.

This was flagged in a previous review.


310-324: Ensure updateTimeLimit runs on main thread.

This was flagged in a previous review regarding thread safety for @Published property mutations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2923b5 and 653802c.

📒 Files selected for processing (12)
  • Sources/AntigravityMenuBar/AntigravityApp.swift (6 hunks)
  • Sources/AntigravityMenuBar/Logic/AccountManager.swift (5 hunks)
  • Sources/AntigravityMenuBar/Logic/QuotaService.swift (1 hunks)
  • Sources/AntigravityMenuBar/Utils/AppConfiguration.swift (1 hunks)
  • Sources/AntigravityMenuBar/Utils/AppError.swift (2 hunks)
  • Sources/AntigravityMenuBar/Utils/DBManager.swift (1 hunks)
  • Sources/AntigravityMenuBar/Utils/LocalhostInsecureURLSessionDelegate.swift (1 hunks)
  • Sources/AntigravityMenuBar/Utils/ProcessManager.swift (1 hunks)
  • Sources/AntigravityMenuBar/Utils/ProgressPanel.swift (1 hunks)
  • Sources/AntigravityMenuBar/Utils/Shell.swift (1 hunks)
  • build.sh (2 hunks)
  • scripts/verify_switch_quota.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
Sources/AntigravityMenuBar/Utils/ProcessManager.swift (1)
Sources/AntigravityMenuBar/Utils/Shell.swift (1)
  • run (21-88)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (4)
Sources/AntigravityMenuBar/Utils/DBManager.swift (3)
  • getCurrentAccountEmail (138-152)
  • backupData (21-64)
  • restoreData (67-95)
Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift (1)
  • formatMenuBarCountdown (64-85)
Sources/AntigravityMenuBar/Utils/ProcessManager.swift (1)
  • terminateLanguageServers (73-121)
Sources/AntigravityMenuBar/Logic/QuotaService.swift (1)
  • fetchQuota (51-68)
Sources/AntigravityMenuBar/Logic/QuotaService.swift (1)
Sources/AntigravityMenuBar/Utils/Shell.swift (1)
  • run (21-88)
Sources/AntigravityMenuBar/AntigravityApp.swift (3)
Sources/AntigravityMenuBar/Logic/AccountManager.swift (2)
  • updateTimeLimit (310-324)
  • checkQuota (333-435)
Sources/AntigravityMenuBar/Utils/TimeLimitFormatter.swift (4)
  • formatCountdown (22-43)
  • defaultTimeString (55-59)
  • defaultDateString (47-51)
  • parse (11-17)
Sources/AntigravityMenuBar/Utils/ProgressPanel.swift (2)
  • show (53-56)
  • close (58-60)
🪛 Ruff (0.14.8)
scripts/verify_switch_quota.py

9-9: typing.Dict is deprecated, use dict instead

(UP035)


9-9: typing.List is deprecated, use list instead

(UP035)


9-9: typing.Tuple is deprecated, use tuple instead

(UP035)


59-59: Do not catch blind exception: Exception

(BLE001)


107-107: Avoid specifying long messages outside the exception class

(TRY003)


114-114: Avoid specifying long messages outside the exception class

(TRY003)


118-118: subprocess call: check for execution of untrusted input

(S603)


147-147: subprocess call: check for execution of untrusted input

(S603)


178-178: subprocess call: check for execution of untrusted input

(S603)


195-195: Unnecessary mode argument

Remove mode argument

(UP015)


215-215: Unnecessary mode argument

Remove mode argument

(UP015)

🔇 Additional comments (16)
build.sh (3)

12-13: ✓ Pre-build cleanup is good practice.

The rm -rf .build/ ensures a clean build state before compilation, which helps prevent stale artifacts and ensures reproducible builds. The subsequent swift build (line 13) will recreate the directory correctly.


39-40: ✓ Icon reference follows macOS conventions.

The CFBundleIconFile key correctly references "AppIcon" (base name without extension), and line 55 properly copies the icon asset as AppIcon.icns in the Resources directory. The conditional check (line 54) ensures graceful degradation if the icon asset is missing.


54-56: Ensure assets/icon.icns exists in the repository.

The icon asset is referenced in the plist (lines 39–40) and copied here. If the source file is missing, the app will still build and run, but without an icon. Verify that assets/icon.icns is committed to the repository to ensure the app displays the icon as intended.

Sources/AntigravityMenuBar/Utils/AppConfiguration.swift (1)

12-29: LGTM!

The expanded backup keys cover additional Antigravity state (user settings, onboarding, analytics, UI) that aligns with the account-switching feature. The new bundle ID addition is consistent with the existing pattern.

Sources/AntigravityMenuBar/Utils/ProcessManager.swift (1)

73-96: Implementation looks solid for the use case.

The language server termination logic correctly identifies processes by command pattern and handles graceful/forced termination. The best-effort approach with error swallowing is appropriate here.

Sources/AntigravityMenuBar/Utils/AppError.swift (1)

10-36: LGTM!

The new error cases are well-structured with clear, actionable messages. The associated values for quotaFetchFailed and commandFailed provide useful context for debugging.

Sources/AntigravityMenuBar/Utils/ProgressPanel.swift (1)

3-61: LGTM! Clean implementation of a progress panel utility.

The @MainActor annotation is appropriate for UI code, and the panel configuration with floating behavior and proper constraint setup is well done.

Minor consideration: The spinner's animation is never explicitly stopped in close(). While orderOut hides the panel (and the spinner is effectively invisible), calling spinner.stopAnimation(nil) in close() would be slightly more correct for resource cleanup if the panel instance is reused.

Sources/AntigravityMenuBar/Utils/Shell.swift (1)

3-18: LGTM!

The ShellError enum provides clear, descriptive error cases with proper LocalizedError conformance.

scripts/verify_switch_quota.py (1)

183-265: LGTM! Well-structured verification script.

The main function has good error handling with distinct exit codes, proper state restoration, and reasonable timing for process coordination. The rollback logic ensures the original DB state is restored regardless of outcome.

Sources/AntigravityMenuBar/AntigravityApp.swift (1)

3-15: LGTM! Clean MenuBarLabelView implementation.

The view correctly observes MenuBarState.shared and conditionally displays the countdown text alongside the icon.

Sources/AntigravityMenuBar/Logic/AccountManager.swift (3)

33-70: LGTM! Quota cache implementation is thread-safe.

The quotaCacheLock properly protects concurrent access to the cache dictionary, and the cache expiry logic is correct.


446-455: waitForAntigravityRunning checks app running state, not language server readiness.

The function checks ProcessManager.shared.isRunning() which likely verifies the Antigravity app is running. However, the caller (checkQuota) needs the language server to be ready before fetching quota. Consider renaming for clarity or waiting for the language server specifically:

-    private func waitForAntigravityRunning(timeoutSeconds: TimeInterval) async -> Bool {
+    private func waitForAntigravityAppRunning(timeoutSeconds: TimeInterval) async -> Bool {

Or, if QuotaService.fetchQuota() already retries internally (which it does via detectLanguageServer), this may be acceptable—but the 12-second timeout here plus the 15-second internal timeout in QuotaService could lead to long waits.


275-288: LGTM! Clean implementation of applying limit before switch.

The logic correctly identifies the active account by email, applies the time limit, and then performs the switch.

Sources/AntigravityMenuBar/Logic/QuotaService.swift (3)

3-37: LGTM! Well-designed quota snapshot model.

The nested structs with computed properties (remainingPercentage, isExhausted) provide clean API for consumers.


102-175: LGTM! Robust language server detection with scoring.

The scoring system prioritizes known process signatures while still being flexible enough to handle variations. The fallback from etimes to etime handles macOS BSD vs GNU ps differences well.


417-429: LGTM! Simple and effective regex helper.

The helper correctly extracts the first capture group from a pattern match.

import sqlite3
import subprocess
import time
from typing import Dict, List, Optional, Tuple
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use modern type hint syntax (Python 3.9+).

Per static analysis, typing.Dict, typing.List, typing.Tuple, and typing.Optional are deprecated. Use built-in generics instead:

-from typing import Dict, List, Optional, Tuple
+from typing import Optional

Then update signatures:

  • Dict[str, str]dict[str, str]
  • List[str]list[str]
  • List[int]list[int]
  • Tuple[int, str, List[int]]tuple[int, str, list[int]]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from typing import Dict, List, Optional, Tuple
from typing import Optional
🧰 Tools
🪛 Ruff (0.14.8)

9-9: typing.Dict is deprecated, use dict instead

(UP035)


9-9: typing.List is deprecated, use list instead

(UP035)


9-9: typing.Tuple is deprecated, use tuple instead

(UP035)

🤖 Prompt for AI Agents
In scripts/verify_switch_quota.py around line 9, the file imports typing aliases
(Dict, List, Optional, Tuple) that are deprecated; replace uses with built-in
generics (dict, list, tuple, Optional -> optional built-in annotation using |
None or typing.Optional removal). Remove the typing import of
Dict/List/Tuple/Optional and update all type hints accordingly: Dict[str, str]
-> dict[str, str], List[str] -> list[str], List[int] -> list[int], Tuple[int,
str, List[int]] -> tuple[int, str, list[int]], and change Optional[X] to X |
None (or just X | None where appropriate) so all annotations use Python 3.9+
built-ins.

Comment on lines +30 to +40
def db_get_values(keys: List[str]) -> Dict[str, str]:
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
out: Dict[str, str] = {}
for k in keys:
cur.execute("select value from ItemTable where key=?", (k,))
row = cur.fetchone()
if row and row[0] is not None:
out[k] = row[0]
con.close()
return out
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

DB connection not closed on exception.

If cur.execute or cur.fetchone raises an exception, con.close() is never called, leaking the connection.

 def db_get_values(keys: List[str]) -> Dict[str, str]:
-    con = sqlite3.connect(DB_PATH)
-    cur = con.cursor()
     out: Dict[str, str] = {}
-    for k in keys:
-        cur.execute("select value from ItemTable where key=?", (k,))
-        row = cur.fetchone()
-        if row and row[0] is not None:
-            out[k] = row[0]
-    con.close()
+    with sqlite3.connect(DB_PATH) as con:
+        cur = con.cursor()
+        for k in keys:
+            cur.execute("select value from ItemTable where key=?", (k,))
+            row = cur.fetchone()
+            if row and row[0] is not None:
+                out[k] = row[0]
     return out
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def db_get_values(keys: List[str]) -> Dict[str, str]:
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
out: Dict[str, str] = {}
for k in keys:
cur.execute("select value from ItemTable where key=?", (k,))
row = cur.fetchone()
if row and row[0] is not None:
out[k] = row[0]
con.close()
return out
def db_get_values(keys: List[str]) -> Dict[str, str]:
out: Dict[str, str] = {}
with sqlite3.connect(DB_PATH) as con:
cur = con.cursor()
for k in keys:
cur.execute("select value from ItemTable where key=?", (k,))
row = cur.fetchone()
if row and row[0] is not None:
out[k] = row[0]
return out
🤖 Prompt for AI Agents
In scripts/verify_switch_quota.py around lines 30-40 the DB connection is opened
but not guaranteed to be closed if cur.execute or cur.fetchone raises; wrap the
sqlite3.connect call in a context that always closes the connection (use a
try/finally or use "with sqlite3.connect(DB_PATH) as con:" and acquire a cursor
inside that block) so con.close() is executed even on exceptions, and return or
re-raise the exception as appropriate; ensure the function still returns the
expected dict when no error occurs.

Comment on lines +43 to +52
def db_set_values(d: Dict[str, str]) -> None:
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
for k, v in d.items():
cur.execute(
"insert or replace into ItemTable(key,value) values (?,?)",
(k, v),
)
con.commit()
con.close()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same connection leak issue in db_set_values.

Use a context manager here as well:

 def db_set_values(d: Dict[str, str]) -> None:
-    con = sqlite3.connect(DB_PATH)
-    cur = con.cursor()
-    for k, v in d.items():
-        cur.execute(
-            "insert or replace into ItemTable(key,value) values (?,?)",
-            (k, v),
-        )
-    con.commit()
-    con.close()
+    with sqlite3.connect(DB_PATH) as con:
+        cur = con.cursor()
+        for k, v in d.items():
+            cur.execute(
+                "insert or replace into ItemTable(key,value) values (?,?)",
+                (k, v),
+            )
+        con.commit()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def db_set_values(d: Dict[str, str]) -> None:
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
for k, v in d.items():
cur.execute(
"insert or replace into ItemTable(key,value) values (?,?)",
(k, v),
)
con.commit()
con.close()
def db_set_values(d: Dict[str, str]) -> None:
with sqlite3.connect(DB_PATH) as con:
cur = con.cursor()
for k, v in d.items():
cur.execute(
"insert or replace into ItemTable(key,value) values (?,?)",
(k, v),
)
con.commit()
🤖 Prompt for AI Agents
In scripts/verify_switch_quota.py around lines 43 to 52, db_set_values opens a
sqlite3 connection and cursor without a context manager which can leak
connections on exceptions; change it to use a with sqlite3.connect(DB_PATH) as
con: block (and obtain the cursor from that connection) so the connection is
automatically closed and committed even on errors, then perform the same insert
or replace loop inside that block.

Comment on lines +195 to +196
with open(ACCOUNTS_PATH, "r") as f:
accounts = list(json.load(f).values())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unnecessary "r" mode argument.

Per static analysis, the mode argument is redundant for open() when reading:

-    with open(ACCOUNTS_PATH, "r") as f:
+    with open(ACCOUNTS_PATH) as f:
-    with open(backup_file, "r") as f:
+    with open(backup_file) as f:

Also applies to: 215-216

🧰 Tools
🪛 Ruff (0.14.8)

195-195: Unnecessary mode argument

Remove mode argument

(UP015)

🤖 Prompt for AI Agents
In scripts/verify_switch_quota.py around lines 195-196 (and likewise at lines
215-216), the open() calls include an unnecessary "r" mode argument when only
reading; remove the "r" argument so the calls use open(ACCOUNTS_PATH) (or
open(ACCOUNTS_PATH, encoding='utf-8') if you want explicit encoding) and keep
the rest of the logic unchanged to read the JSON.

Comment on lines +196 to +200
defer {
Task { @MainActor in
panel.close()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

defer may not execute as expected in async context.

The defer block captures panel and schedules a Task to close it. However, if an exception is thrown, the defer runs synchronously on the current actor context, but the Task { @MainActor in ... } schedules work asynchronously. This should work, but consider using a simpler pattern:

-                defer {
-                    Task { @MainActor in
-                        panel.close()
-                    }
-                }
-
-                let snapshot = try await accountManager.checkQuota(id: account.id)
+                let snapshot: QuotaSnapshot
+                do {
+                    snapshot = try await accountManager.checkQuota(id: account.id)
+                } catch {
+                    await MainActor.run { panel.close() }
+                    throw error
+                }
+                await MainActor.run { panel.close() }

Or wrap the entire operation in a do-catch that ensures panel.close() is called synchronously on the main actor.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/AntigravityApp.swift around lines 196–200, the
defer currently schedules panel.close() inside Task { @MainActor in ... } which
runs asynchronously and can skip guaranteed synchronous cleanup; replace this
pattern by wrapping the async work in a do-catch (or try/await) and after the
operation (in the finally-equivalent path) call MainActor.run to invoke
panel.close() on the main actor (i.e., perform await MainActor.run {
panel.close() }) so the panel is closed deterministically on the main actor even
if errors occur.

Comment on lines +107 to 132
// Restore allowlisted keys; for keys missing from the backup, clear them to avoid
// carrying over stale account state from the previously active account.
for key in keysToBackup {
if let value = data[key] {
let query = "INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)"
var statement: OpaquePointer?

if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
sqlite3_bind_text(statement, 1, (key as NSString).utf8String, -1, nil)
sqlite3_bind_text(statement, 2, (value as NSString).utf8String, -1, nil)

if sqlite3_step(statement) != SQLITE_DONE {
print("❌ Error writing key: \(key)")
}
}
sqlite3_finalize(statement)
} else {
let query = "DELETE FROM ItemTable WHERE key = ?"
var statement: OpaquePointer?
if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
sqlite3_bind_text(statement, 1, (key as NSString).utf8String, -1, nil)
_ = sqlite3_step(statement)
}
sqlite3_finalize(statement)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider wrapping restore in a transaction for atomicity.

If a failure occurs mid-restore, the database could be left in a partially-updated state with some keys restored and some deleted. Wrapping the loop in a BEGIN/COMMIT transaction ensures all-or-nothing semantics.

+        // Begin transaction for atomicity
+        sqlite3_exec(db, "BEGIN TRANSACTION", nil, nil, nil)
+        
         // Restore allowlisted keys; for keys missing from the backup, clear them to avoid
         // carrying over stale account state from the previously active account.
         for key in keysToBackup {
             if let value = data[key] {
                 // ... existing INSERT OR REPLACE logic ...
             } else {
                 // ... existing DELETE logic ...
             }
         }
+        
+        sqlite3_exec(db, "COMMIT", nil, nil, nil)
         
         return .success(())

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/DBManager.swift around lines 107 to 132, the
restore loop should be wrapped in a SQLite transaction to ensure atomicity:
start a transaction (BEGIN) before the for-loop, run all INSERT/DELETE
operations, and if any sqlite3_step or prepare returns an error perform a
rollback (ROLLBACK) and surface the error; otherwise commit (COMMIT) after the
loop. Ensure you check return codes from
sqlite3_exec/sqlite3_step/sqlite3_prepare_v2, finalize statements in all paths,
and handle errors so the transaction is always either committed on success or
rolled back on failure.

Comment on lines +123 to 131
} else {
let query = "DELETE FROM ItemTable WHERE key = ?"
var statement: OpaquePointer?
if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
sqlite3_bind_text(statement, 1, (key as NSString).utf8String, -1, nil)
_ = sqlite3_step(statement)
}
sqlite3_finalize(statement)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider logging DELETE failures for consistency.

The INSERT branch logs errors (line 119), but DELETE failures are silently ignored. Adding similar logging would help debug restore issues.

             } else {
                 let query = "DELETE FROM ItemTable WHERE key = ?"
                 var statement: OpaquePointer?
                 if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
                     sqlite3_bind_text(statement, 1, (key as NSString).utf8String, -1, nil)
-                    _ = sqlite3_step(statement)
+                    if sqlite3_step(statement) != SQLITE_DONE {
+                        print("❌ Error deleting key: \(key)")
+                    }
                 }
                 sqlite3_finalize(statement)
             }
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/DBManager.swift around lines 123 to 131, the
DELETE branch silently ignores failures while the INSERT branch logs errors;
update the DELETE code to mirror the INSERT branch error handling by checking
sqlite3_prepare_v2 and sqlite3_step return values and logging failures using the
same logging mechanism used for INSERT, including the sqlite error message from
sqlite3_errmsg(db) (and ensure sqlite3_finalize(statement) still runs in all
cases).

Comment on lines +11 to +16
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust,
let host = challenge.protectionSpace.host.lowercased() as String? else {
completionHandler(.performDefaultHandling, nil)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant cast and missing IPv6 localhost.

Line 13 has a redundant as String? cast—host.lowercased() already returns String. Additionally, ::1 (IPv6 localhost) isn't handled.

-        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
-              let trust = challenge.protectionSpace.serverTrust,
-              let host = challenge.protectionSpace.host.lowercased() as String? else {
+        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
+              let trust = challenge.protectionSpace.serverTrust else {
             completionHandler(.performDefaultHandling, nil)
             return
         }
+        let host = challenge.protectionSpace.host.lowercased()

And update the check:

-        if host == "127.0.0.1" || host == "localhost" {
+        if host == "127.0.0.1" || host == "localhost" || host == "::1" {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/LocalhostInsecureURLSessionDelegate.swift
around lines 11–16, remove the redundant `as String?` cast from
`host.lowercased()` (lowercased() already returns String) and extend the
localhost check to include IPv6 by treating `"::1"` as trusted (e.g., check host
== "localhost" || host == "127.0.0.1" || host == "::1" — or include any other
project-accepted localhost variants); keep the existing guard semantics and call
completionHandler(.performDefaultHandling, nil) if the host is not one of these
trusted values.

Comment on lines +103 to +116
for (pid, _) in candidates {
var alive = true
for _ in 0..<25 {
if (try? Shell.run("/bin/kill", ["-0", String(pid)], timeoutSeconds: 1)) == nil {
alive = false
break
}
Thread.sleep(forTimeInterval: 0.2)
}
if alive {
print("⚠️ Language server \(pid) still running, forcing...")
_ = try? Shell.run("/bin/kill", ["-KILL", String(pid)], timeoutSeconds: 2)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider clarifying the process-alive check logic.

The logic relies on Shell.run throwing for non-zero exit codes (when the process is dead), making the try? ... == nil check work correctly but counter-intuitively. A brief comment would help future maintainers.

             for _ in 0..<25 {
+                // kill -0 returns 0 if process exists (Shell.run succeeds → non-nil)
+                // and non-zero if dead (Shell.run throws → try? yields nil)
                 if (try? Shell.run("/bin/kill", ["-0", String(pid)], timeoutSeconds: 1)) == nil {
                     alive = false
                     break
                 }
                 Thread.sleep(forTimeInterval: 0.2)
             }
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/ProcessManager.swift around lines 103–116,
the process-alive check is counter-intuitive because it relies on try? returning
nil when Shell.run throws for non-zero exit codes (kill -0 returns non‑zero if
the PID is gone); add a short inline comment above the inner loop explaining
that Shell.run("/bin/kill", ["-0", pid]) will throw/return nil via try? when the
process is not running and therefore alive is set to false, and optionally note
a clearer alternative (e.g., explicitly handling Shell.run errors or using a
dedicated process-liveness API) for future refactors.

Comment on lines +67 to +74
// Stop handlers and drain any remaining data.
stdoutPipe.fileHandleForReading.readabilityHandler = nil
stderrPipe.fileHandleForReading.readabilityHandler = nil

lock.lock()
stdoutData.append(stdoutPipe.fileHandleForReading.readDataToEndOfFile())
stderrData.append(stderrPipe.fileHandleForReading.readDataToEndOfFile())
lock.unlock()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Potential race condition when draining pipe data after handler removal.

Setting readabilityHandler = nil stops the handler, but there's a brief window where the handler might be executing concurrently with the subsequent readDataToEndOfFile() call. While the lock protects the data append, the readDataToEndOfFile() call itself could block or miss data if the handler was mid-read.

Consider waiting briefly or synchronizing more explicitly:

         // Stop handlers and drain any remaining data.
         stdoutPipe.fileHandleForReading.readabilityHandler = nil
         stderrPipe.fileHandleForReading.readabilityHandler = nil
 
+        // Brief pause to let any in-flight handler invocations complete.
+        Thread.sleep(forTimeInterval: 0.01)
+
         lock.lock()
         stdoutData.append(stdoutPipe.fileHandleForReading.readDataToEndOfFile())
         stderrData.append(stderrPipe.fileHandleForReading.readDataToEndOfFile())
         lock.unlock()

Alternatively, use a serial queue for handlers and drain synchronously on that queue.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Stop handlers and drain any remaining data.
stdoutPipe.fileHandleForReading.readabilityHandler = nil
stderrPipe.fileHandleForReading.readabilityHandler = nil
lock.lock()
stdoutData.append(stdoutPipe.fileHandleForReading.readDataToEndOfFile())
stderrData.append(stderrPipe.fileHandleForReading.readDataToEndOfFile())
lock.unlock()
// Stop handlers and drain any remaining data.
stdoutPipe.fileHandleForReading.readabilityHandler = nil
stderrPipe.fileHandleForReading.readabilityHandler = nil
// Brief pause to let any in-flight handler invocations complete.
Thread.sleep(forTimeInterval: 0.01)
lock.lock()
stdoutData.append(stdoutPipe.fileHandleForReading.readDataToEndOfFile())
stderrData.append(stderrPipe.fileHandleForReading.readDataToEndOfFile())
lock.unlock()
🤖 Prompt for AI Agents
In Sources/AntigravityMenuBar/Utils/Shell.swift around lines 67–74, removing the
readabilityHandler and then immediately calling readDataToEndOfFile risks a race
where the handler is concurrently executing; synchronize handler work by
introducing a dedicated serial DispatchQueue used by the readabilityHandler and
perform the draining readDataToEndOfFile synchronously on that same queue (i.e.,
set the handler to nil only after dispatching a synchronous barrier or
dispatchSync to the handler queue so you know the handler has finished), or
alternatively keep the handler appending into stdoutData/stderrData on that
serial queue and call readDataToEndOfFile from that queue to ensure no
concurrent access and no missed data.

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.

2 participants