Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Packages/Sources/RxCodeChatKit/StatusLineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ struct StatusLineView: View {
body: "Tracks usage against Codex's rolling 5-hour limit. Resets gradually as older requests age out."
)
rateLimitSegment(
label: String(localized: "24h", bundle: .module),
label: String(localized: "7d", bundle: .module),
icon: "calendar",
percent: rateLimit?.twentyFourHourPercent,
resetsAt: rateLimit?.twentyFourHourResetsAt,
percent: rateLimit?.sevenDayPercent,
resetsAt: rateLimit?.sevenDayResetsAt,
isPresented: $showSevenDayPopover,
title: "24-hour rate limit",
body: "Tracks usage against Codex's rolling 24-hour limit. Resets gradually as older requests age out."
title: "7-day rate limit",
body: "Tracks usage against Codex's rolling 7-day limit. Resets gradually as older requests age out."
)
case .acp:
EmptyView()
Expand Down
15 changes: 14 additions & 1 deletion Packages/Sources/RxCodeSync/APNs/EncryptedAlert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,28 @@ public struct WidgetSnapshotPayload: Codable, Sendable {
public let jobs: Int?
/// Claude Code 5-hour utilization (0...100), or `nil` to leave untouched.
public let cc: Double?
/// Claude Code 7-day utilization (0...100), or `nil` to leave untouched.
public let ccWeekly: Double?
/// Codex 5-hour utilization (0...100), or `nil` to leave untouched.
public let codex: Double?
/// Codex 7-day utilization (0...100), or `nil` to leave untouched.
public let codexWeekly: Double?
/// When the desktop produced this snapshot, unix seconds.
public let updatedAt: Double

public init(jobs: Int? = nil, cc: Double? = nil, codex: Double? = nil, updatedAt: Double) {
public init(
jobs: Int? = nil,
cc: Double? = nil,
ccWeekly: Double? = nil,
codex: Double? = nil,
codexWeekly: Double? = nil,
updatedAt: Double
) {
self.jobs = jobs
self.cc = cc
self.ccWeekly = ccWeekly
self.codex = codex
self.codexWeekly = codexWeekly
self.updatedAt = updatedAt
}
}
Expand Down
2 changes: 1 addition & 1 deletion Packages/Sources/RxCodeSync/Protocol/Payload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public struct RequestSnapshotPayload: Codable, Sendable {
public struct MobileUsageSnapshot: Codable, Sendable, Equatable {
/// Claude Code usage: 5-hour and 7-day limits.
public let claudeCode: RateLimitUsage?
/// Codex usage: 5-hour and 24-hour limits.
/// Codex usage: 5-hour and 7-day limits.
public let codex: RateLimitUsage?

public init(claudeCode: RateLimitUsage? = nil, codex: RateLimitUsage? = nil) {
Expand Down
9 changes: 4 additions & 5 deletions Packages/Tests/RxCodeSyncTests/PayloadTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,9 @@ struct PayloadTests {
),
codex: RateLimitUsage(
fiveHourPercent: 7,
sevenDayPercent: 0,
twentyFourHourPercent: 55,
sevenDayPercent: 55,
fiveHourResetsAt: nil,
sevenDayResetsAt: nil,
twentyFourHourResetsAt: Date(timeIntervalSince1970: 300)
sevenDayResetsAt: Date(timeIntervalSince1970: 300)
)
)
)
Expand All @@ -135,7 +133,8 @@ struct PayloadTests {
#expect(snapshot.usage?.hasAnyUsage == true)
#expect(snapshot.usage?.claudeCode?.fiveHourPercent == 42)
#expect(snapshot.usage?.claudeCode?.sevenDayResetsAt == Date(timeIntervalSince1970: 200))
#expect(snapshot.usage?.codex?.twentyFourHourPercent == 55)
#expect(snapshot.usage?.codex?.sevenDayPercent == 55)
#expect(snapshot.usage?.codex?.sevenDayResetsAt == Date(timeIntervalSince1970: 300))
}

@Test("snapshot without usage decodes to nil")
Expand Down
8 changes: 6 additions & 2 deletions RxCode/App/AppState+Lifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,12 @@ extension AppState {
self?.projects.first { $0.id == id }?.name
}
MobileSyncService.shared.usageSnapshotProvider = { [weak self] in
(self?.latestRateLimitUsage?.fiveHourPercent,
self?.latestCodexRateLimitUsage?.fiveHourPercent)
(
cc: self?.latestRateLimitUsage?.fiveHourPercent,
ccWeekly: self?.latestRateLimitUsage?.sevenDayPercent,
codex: self?.latestCodexRateLimitUsage?.fiveHourPercent,
codexWeekly: self?.latestCodexRateLimitUsage?.sevenDayPercent
)
}

await refreshAgentInstallations()
Expand Down
6 changes: 3 additions & 3 deletions RxCode/App/RxCodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,23 +219,23 @@ private struct MenuBarContentView: View {
private var secondaryLimitLabel: String {
switch appState.selectedAgentProvider {
case .claudeCode: return "7-day limit"
case .codex: return "24-hour limit"
case .codex: return "7-day limit"
case .acp: return "Usage"
}
}

private var secondaryLimitPercent: Double? {
switch appState.selectedAgentProvider {
case .claudeCode: return selectedUsage?.sevenDayPercent
case .codex: return selectedUsage?.twentyFourHourPercent
case .codex: return selectedUsage?.sevenDayPercent
case .acp: return nil
}
}

private var secondaryLimitResetsAt: Date? {
switch appState.selectedAgentProvider {
case .claudeCode: return selectedUsage?.sevenDayResetsAt
case .codex: return selectedUsage?.twentyFourHourResetsAt
case .codex: return selectedUsage?.sevenDayResetsAt
case .acp: return nil
}
}
Expand Down
7 changes: 4 additions & 3 deletions RxCode/Services/CodexAppServer+Parsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,17 @@ extension CodexAppServer {

guard !windows.isEmpty else { return nil }
let fiveHour = windows.first { $0.durationMinutes == 300 } ?? windows.first
let twentyFourHour = windows.first { $0.durationMinutes == 1_440 }
let sevenDay = windows.first { $0.durationMinutes == 10_080 }
?? windows.first { $0.durationMinutes != fiveHour?.durationMinutes }
Comment on lines +72 to 73
?? windows.dropFirst().first
let twentyFourHour = windows.first { $0.durationMinutes == 1_440 }

return RateLimitUsage(
fiveHourPercent: fiveHour?.percent ?? 0,
sevenDayPercent: 0,
sevenDayPercent: sevenDay?.percent ?? 0,
twentyFourHourPercent: twentyFourHour?.percent,
fiveHourResetsAt: fiveHour?.resetsAt,
sevenDayResetsAt: nil,
sevenDayResetsAt: sevenDay?.resetsAt,
twentyFourHourResetsAt: twentyFourHour?.resetsAt
)
}
Expand Down
2 changes: 1 addition & 1 deletion RxCode/Services/CodexAppServer+Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension CodexAppServer {
if Self.idString(object["id"]) == "2", let result = object["result"] {
let usage = Self.parseCodexRateLimits(from: result)
if let usage {
logger.info("Codex rate limits 5h=\(usage.fiveHourPercent)% 24h=\(usage.twentyFourHourPercent ?? 0)%")
logger.info("Codex rate limits 5h=\(usage.fiveHourPercent)% 7d=\(usage.sevenDayPercent)%")
} else {
logger.warning("Codex account/rateLimits/read returned no parseable limits")
}
Expand Down
56 changes: 53 additions & 3 deletions RxCode/Services/MobileSyncService+EventDispatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,60 @@ extension MobileSyncService {
}
}

/// Handle events from a specific relay server (multi-relay support).
func handle(event: RelayClient.Event, forServer server: SavedRelayServer) {
switch event {
case .stateChanged(let s):
relayConnectionStates[server.id] = s
// Update global connectionState to reflect aggregate status
updateAggregateConnectionState()
logger.info("[MobileSync] relay state=\(String(describing: s), privacy: .public) server=\(server.name, privacy: .public)")
case .deliveryFailed(let toHex):
logger.warning("[MobileSync] relay delivery failed to mobileKey=\(String(toHex.prefix(12)), privacy: .public) server=\(server.name, privacy: .public)")
case .inbound(let inbound):
handleInbound(inbound, fromServer: server)
}
}

/// Update aggregate connection state based on all relay states.
private func updateAggregateConnectionState() {
let states = Array(relayConnectionStates.values)
if states.isEmpty {
connectionState = .disconnected
} else if states.contains(.connected) {
connectionState = .connected
} else if states.contains(.connecting) {
connectionState = .connecting
} else if let reconnecting = states.first(where: {
if case .reconnecting = $0 { return true }
return false
}) {
connectionState = reconnecting
} else {
connectionState = .disconnected
}
}

/// Handle inbound messages from a specific relay server.
private func handleInbound(_ inbound: RelayClient.Inbound, fromServer server: SavedRelayServer) {
// Route to standard handler but track which server the message came from
// This allows pairing to know which relay was used
if case .pairRequest = inbound.payload {
pairingRelayServerID = server.id
}
handleInbound(inbound, viaClient: additionalClients[server.id])
}

func handleInbound(_ inbound: RelayClient.Inbound) {
handleInbound(inbound, viaClient: nil)
}

private func handleInbound(_ inbound: RelayClient.Inbound, viaClient: SyncClient?) {
let activeClient = viaClient ?? client
switch inbound.payload {
case .pairRequest(let req):
pendingPairing = req
Task { try? await client.addPeer(req.mobilePubkeyHex) }
Task { try? await activeClient.addPeer(req.mobilePubkeyHex) }
case .unpair:
guard isPairedPeer(inbound.fromHex) else { return }
handleRemoteUnpair(pubkeyHex: inbound.fromHex)
Expand Down Expand Up @@ -327,7 +376,7 @@ extension MobileSyncService {
)
case .ping:
guard acceptPairedOnlyPayload(from: inbound.fromHex, type: "ping") else { return }
Task { try? await client.send(.pong(PongPayload()), toHex: inbound.fromHex) }
Task { try? await activeClient.send(.pong(PongPayload()), toHex: inbound.fromHex) }
default:
break
}
Expand Down Expand Up @@ -366,8 +415,9 @@ extension MobileSyncService {
/// Send a payload to a single peer (used by AppState when replying to
/// `request_snapshot` etc).
func send(_ payload: Payload, toHex hex: String) async {
let targetClient = clientForPeer(hex)
do {
try await client.send(payload, toHex: hex)
try await targetClient.send(payload, toHex: hex)
logger.debug("[MobileSync] sent type=\(payload.logName, privacy: .public) to mobileKey=\(String(hex.prefix(12)), privacy: .public)")
} catch {
logger.error("[MobileSync] send failed type=\(payload.logName, privacy: .public) to mobileKey=\(String(hex.prefix(12)), privacy: .public): \(error.localizedDescription, privacy: .public)")
Expand Down
2 changes: 2 additions & 0 deletions RxCode/Services/MobileSyncService+LiveActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ extension MobileSyncService {
let snapshot = WidgetSnapshotPayload(
jobs: jobCount,
cc: usage?.cc,
ccWeekly: usage?.ccWeekly,
codex: usage?.codex,
codexWeekly: usage?.codexWeekly,
updatedAt: Date().timeIntervalSince1970
)
for device in devices {
Expand Down
Loading