From 0cbff30deefa99579b13cfff89ebf71a442fcd4e Mon Sep 17 00:00:00 2001 From: leo frankel Date: Fri, 5 Dec 2025 00:23:54 -0600 Subject: [PATCH] Add macOS widget extension for Claude usage monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Medium-sized widget with circular gauge and progress bars - Shows 5-hour session, 7-day all, Opus, and Sonnet utilization - Displays reset times for each limit - Tap anywhere to refresh widget data - Auto-refreshes every 15 minutes - Reads OAuth token from Claude Code keychain - Local signing configuration (no provisioning profile required) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../AccentColor.colorset/Contents.json | 11 + .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + .../Claude_Monitor_Widget.entitlements | 10 + .../Claude_Monitor_Widget.swift | 22 ++ .../Claude_Monitor_WidgetBundle.swift | 14 ++ Claude Monitor Widget/Info.plist | 11 + Claude Monitor Widget/Models/UsageLimit.swift | 21 ++ .../Models/UsageResponse.swift | 46 +++++ Claude Monitor Widget/RefreshIntent.swift | 19 ++ .../Services/ClaudeAPIService.swift | 66 ++++++ .../Services/KeychainService.swift | 131 ++++++++++++ Claude Monitor Widget/UsageEntry.swift | 62 ++++++ .../UsageTimelineProvider.swift | 131 ++++++++++++ Claude Monitor Widget/Views/GaugeView.swift | 143 +++++++++++++ .../Views/ShadedProgressView.swift | 31 +++ .../Views/UsageWidgetView.swift | 106 ++++++++++ .../Views/WidgetLimitRow.swift | 90 ++++++++ Claude Monitor.xcodeproj/project.pbxproj | 192 ++++++++++++++++-- Claude Monitor/Claude_Monitor.entitlements | 10 + 20 files changed, 1121 insertions(+), 12 deletions(-) create mode 100644 Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Claude Monitor Widget/Assets.xcassets/Contents.json create mode 100644 Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 Claude Monitor Widget/Claude_Monitor_Widget.entitlements create mode 100644 Claude Monitor Widget/Claude_Monitor_Widget.swift create mode 100644 Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift create mode 100644 Claude Monitor Widget/Info.plist create mode 100644 Claude Monitor Widget/Models/UsageLimit.swift create mode 100644 Claude Monitor Widget/Models/UsageResponse.swift create mode 100644 Claude Monitor Widget/RefreshIntent.swift create mode 100644 Claude Monitor Widget/Services/ClaudeAPIService.swift create mode 100644 Claude Monitor Widget/Services/KeychainService.swift create mode 100644 Claude Monitor Widget/UsageEntry.swift create mode 100644 Claude Monitor Widget/UsageTimelineProvider.swift create mode 100644 Claude Monitor Widget/Views/GaugeView.swift create mode 100644 Claude Monitor Widget/Views/ShadedProgressView.swift create mode 100644 Claude Monitor Widget/Views/UsageWidgetView.swift create mode 100644 Claude Monitor Widget/Views/WidgetLimitRow.swift create mode 100644 Claude Monitor/Claude_Monitor.entitlements diff --git a/Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json b/Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Claude Monitor Widget/Assets.xcassets/Contents.json b/Claude Monitor Widget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Claude Monitor Widget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Claude Monitor Widget/Claude_Monitor_Widget.entitlements b/Claude Monitor Widget/Claude_Monitor_Widget.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/Claude Monitor Widget/Claude_Monitor_Widget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/Claude Monitor Widget/Claude_Monitor_Widget.swift b/Claude Monitor Widget/Claude_Monitor_Widget.swift new file mode 100644 index 0000000..9430c07 --- /dev/null +++ b/Claude Monitor Widget/Claude_Monitor_Widget.swift @@ -0,0 +1,22 @@ +// +// Claude_Monitor_Widget.swift +// Claude Monitor Widget +// + +import SwiftUI +import WidgetKit + +/// The Claude Monitor widget displaying API usage. +struct Claude_Monitor_Widget: Widget { + let kind: String = "Claude_Monitor_Widget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: UsageTimelineProvider()) { entry in + UsageWidgetView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Claude Usage") + .description("Monitor your Claude API usage limits.") + .supportedFamilies([.systemMedium]) + } +} diff --git a/Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift b/Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift new file mode 100644 index 0000000..0a520d6 --- /dev/null +++ b/Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift @@ -0,0 +1,14 @@ +// +// Claude_Monitor_WidgetBundle.swift +// Claude Monitor Widget +// + +import SwiftUI +import WidgetKit + +@main +struct Claude_Monitor_WidgetBundle: WidgetBundle { + var body: some Widget { + Claude_Monitor_Widget() + } +} diff --git a/Claude Monitor Widget/Info.plist b/Claude Monitor Widget/Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/Claude Monitor Widget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/Claude Monitor Widget/Models/UsageLimit.swift b/Claude Monitor Widget/Models/UsageLimit.swift new file mode 100644 index 0000000..b354753 --- /dev/null +++ b/Claude Monitor Widget/Models/UsageLimit.swift @@ -0,0 +1,21 @@ +// +// UsageLimit.swift +// Claude Monitor Widget +// + +import Foundation + +/// A usage limit representing a specific quota bucket from the Claude API. +struct UsageLimit: Identifiable { + /// A unique identifier for this limit, typically matching the API field name. + let id: String + + /// A localized display title for the limit (e.g., "Current session", "All models"). + let title: String + + /// The utilization percentage as a value between 0.0 and 1.0. + let utilization: Double + + /// The date when this limit will reset, or `nil` if unknown. + let resetsAt: Date? +} diff --git a/Claude Monitor Widget/Models/UsageResponse.swift b/Claude Monitor Widget/Models/UsageResponse.swift new file mode 100644 index 0000000..5cea5b5 --- /dev/null +++ b/Claude Monitor Widget/Models/UsageResponse.swift @@ -0,0 +1,46 @@ +// +// UsageResponse.swift +// Claude Monitor Widget +// + +import Foundation + +/// The raw JSON response from the Anthropic OAuth usage API endpoint. +struct UsageResponse: Codable { + /// The 5-hour rolling session limit. + let fiveHour: UsageBucket? + + /// The 7-day combined usage limit across all models. + let sevenDay: UsageBucket? + + /// The 7-day usage limit for OAuth applications. + let sevenDayOauthApps: UsageBucket? + + /// The 7-day usage limit specifically for Claude Opus. + let sevenDayOpus: UsageBucket? + + /// The 7-day usage limit specifically for Claude Sonnet. + let sevenDaySonnet: UsageBucket? + + enum CodingKeys: String, CodingKey { + case fiveHour = "five_hour" + case sevenDay = "seven_day" + case sevenDayOauthApps = "seven_day_oauth_apps" + case sevenDayOpus = "seven_day_opus" + case sevenDaySonnet = "seven_day_sonnet" + } +} + +/// A single usage bucket from the API response. +struct UsageBucket: Codable { + /// The current utilization as a percentage (0-100). + let utilization: Double + + /// The ISO 8601 timestamp when this limit will reset, or `nil` if not applicable. + let resetsAt: String? + + enum CodingKeys: String, CodingKey { + case utilization + case resetsAt = "resets_at" + } +} diff --git a/Claude Monitor Widget/RefreshIntent.swift b/Claude Monitor Widget/RefreshIntent.swift new file mode 100644 index 0000000..eb10d12 --- /dev/null +++ b/Claude Monitor Widget/RefreshIntent.swift @@ -0,0 +1,19 @@ +// +// RefreshIntent.swift +// Claude Monitor Widget +// + +import AppIntents +import WidgetKit + +/// An App Intent that refreshes the widget's data. +struct RefreshIntent: AppIntent { + static let title: LocalizedStringResource = "Refresh Usage" + static let description: IntentDescription = "Refreshes the Claude usage data" + + func perform() async throws -> some IntentResult { + // Trigger a timeline refresh + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} diff --git a/Claude Monitor Widget/Services/ClaudeAPIService.swift b/Claude Monitor Widget/Services/ClaudeAPIService.swift new file mode 100644 index 0000000..b8be390 --- /dev/null +++ b/Claude Monitor Widget/Services/ClaudeAPIService.swift @@ -0,0 +1,66 @@ +// +// ClaudeAPIService.swift +// Claude Monitor Widget +// + +import Foundation + +/// Errors that can occur when communicating with the Claude API. +enum APIError: Error, LocalizedError { + case noToken + case invalidURL + case httpError(statusCode: Int, message: String?) + case decodingError(Error) + case networkError(Error) + + var errorDescription: String? { + "Could not access the Claude API." + } +} + +/// A service for communicating with the Anthropic Claude API. +final class ClaudeAPIService: Sendable { + static let shared = ClaudeAPIService() + + private let baseURL = "https://api.anthropic.com" + private let usagePath = "/api/oauth/usage" + + private init() {} + + /// Fetches the current usage data from the Claude API. + func fetchUsage(token: String) async throws -> UsageResponse { + guard let url = URL(string: baseURL + usagePath) else { + throw APIError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("oauth-2025-04-20", forHTTPHeaderField: "anthropic-beta") + request.setValue("claude-code/2.0.32", forHTTPHeaderField: "User-Agent") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + do { + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.networkError(NSError(domain: "Invalid response", code: -1)) + } + + guard (200...299).contains(httpResponse.statusCode) else { + let message = String(data: data, encoding: .utf8) + throw APIError.httpError(statusCode: httpResponse.statusCode, message: message) + } + + do { + return try JSONDecoder().decode(UsageResponse.self, from: data) + } catch { + throw APIError.decodingError(error) + } + } catch let error as APIError { + throw error + } catch { + throw APIError.networkError(error) + } + } +} diff --git a/Claude Monitor Widget/Services/KeychainService.swift b/Claude Monitor Widget/Services/KeychainService.swift new file mode 100644 index 0000000..b4f9c42 --- /dev/null +++ b/Claude Monitor Widget/Services/KeychainService.swift @@ -0,0 +1,131 @@ +// +// KeychainService.swift +// Claude Monitor Widget +// + +import Foundation +import Security + +/// Errors that can occur when accessing the macOS Keychain. +enum KeychainError: Error { + case itemNotFound + case unexpectedStatus(OSStatus) + case invalidData +} + +/// The source of an OAuth token. +enum TokenSource: String { + case claudeCode + case manual +} + +/// A service for secure token retrieval using the macOS Keychain. +/// This widget version only reads tokens; it cannot write them. +final class KeychainService: Sendable { + static let shared = KeychainService() + + private let appServiceName = "codes.tim.Claude-Monitor" + private let appAccountName = "api-token" + private let claudeCodeServiceName = "Claude Code-credentials" + + private init() {} + + /// Reads the OAuth token from Claude Code's Keychain entry. + func readClaudeCodeToken() throws -> String { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: claudeCodeServiceName, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + if status == errSecItemNotFound { + throw KeychainError.itemNotFound + } + throw KeychainError.unexpectedStatus(status) + } + + guard let data = result as? Data, + let jsonString = String(data: data, encoding: .utf8) + else { + throw KeychainError.invalidData + } + + return try parseClaudeCodeCredentials(jsonString) + } + + private func parseClaudeCodeCredentials(_ json: String) throws -> String { + guard let data = json.data(using: .utf8) else { + throw KeychainError.invalidData + } + + struct Credentials: Codable { + let claudeAiOauth: OAuth + + struct OAuth: Codable { + let accessToken: String + } + } + + let credentials = try JSONDecoder().decode(Credentials.self, from: data) + return credentials.claudeAiOauth.accessToken + } + + /// Reads the manually-entered token from the app's Keychain entry. + /// Note: Without keychain access groups, this may not work in widget context. + func readManualToken() throws -> String { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: appServiceName, + kSecAttrAccount as String: appAccountName, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + if status == errSecItemNotFound { + throw KeychainError.itemNotFound + } + throw KeychainError.unexpectedStatus(status) + } + + guard let data = result as? Data, + let token = String(data: data, encoding: .utf8) + else { + throw KeychainError.invalidData + } + + return token + } + + /// Resolves the best available token. + func resolveToken(preferredSource: TokenSource) -> (token: String, source: TokenSource)? { + switch preferredSource { + case .claudeCode: + if let token = try? readClaudeCodeToken() { + return (token, .claudeCode) + } + case .manual: + if let token = try? readManualToken() { + return (token, .manual) + } + } + + // Fallback: try the other source + if let token = try? readClaudeCodeToken() { + return (token, .claudeCode) + } + if let token = try? readManualToken() { + return (token, .manual) + } + + return nil + } +} diff --git a/Claude Monitor Widget/UsageEntry.swift b/Claude Monitor Widget/UsageEntry.swift new file mode 100644 index 0000000..8c1e2bf --- /dev/null +++ b/Claude Monitor Widget/UsageEntry.swift @@ -0,0 +1,62 @@ +// +// UsageEntry.swift +// Claude Monitor Widget +// + +import WidgetKit + +/// A timeline entry containing usage data for the widget. +struct UsageEntry: TimelineEntry { + /// The date for this entry. + let date: Date + + /// The usage limits to display. + let limits: [UsageLimit] + + /// The session (5-hour) utilization for the gauge, or nil if unavailable. + let sessionUtilization: Double? + + /// An error message if data could not be fetched. + let error: String? + + /// Whether this is placeholder data. + let isPlaceholder: Bool + + init( + date: Date, + limits: [UsageLimit], + sessionUtilization: Double?, + error: String? = nil, + isPlaceholder: Bool = false + ) { + self.date = date + self.limits = limits + self.sessionUtilization = sessionUtilization + self.error = error + self.isPlaceholder = isPlaceholder + } + + /// A placeholder entry for the widget gallery. + static var placeholder: UsageEntry { + UsageEntry( + date: Date(), + limits: [ + UsageLimit(id: "session", title: "Session", utilization: 0.45, resetsAt: Date().addingTimeInterval(3600 * 3)), + UsageLimit(id: "all", title: "All", utilization: 0.32, resetsAt: Date().addingTimeInterval(3600 * 48)), + UsageLimit(id: "opus", title: "Opus", utilization: 0.12, resetsAt: Date().addingTimeInterval(3600 * 48)) + ], + sessionUtilization: 0.45, + isPlaceholder: true + ) + } + + /// An error entry when data cannot be fetched. + static func error(_ message: String) -> UsageEntry { + UsageEntry( + date: Date(), + limits: [], + sessionUtilization: nil, + error: message + ) + } +} diff --git a/Claude Monitor Widget/UsageTimelineProvider.swift b/Claude Monitor Widget/UsageTimelineProvider.swift new file mode 100644 index 0000000..633837a --- /dev/null +++ b/Claude Monitor Widget/UsageTimelineProvider.swift @@ -0,0 +1,131 @@ +// +// UsageTimelineProvider.swift +// Claude Monitor Widget +// + +import WidgetKit + +/// Provides timeline entries for the Claude Monitor widget. +struct UsageTimelineProvider: TimelineProvider { + private let apiService = ClaudeAPIService.shared + private let keychainService = KeychainService.shared + + /// The user's preferred token source. + /// Note: Without App Groups, we default to Claude Code token. + /// To enable preference sharing, configure App Groups in Developer Portal. + private var preferredTokenSource: TokenSource { + // App Groups not configured - default to Claude Code + return .claudeCode + } + + func placeholder(in context: Context) -> UsageEntry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping @Sendable (UsageEntry) -> Void) { + if context.isPreview { + completion(.placeholder) + return + } + + Task { @MainActor in + let entry = await fetchUsageEntry() + completion(entry) + } + } + + func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> Void) { + Task { @MainActor in + let entry = await fetchUsageEntry() + + // Refresh in 15 minutes + let nextUpdate = Date().addingTimeInterval(15 * 60) + let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) + + completion(timeline) + } + } + + /// Fetches usage data and creates a timeline entry. + @MainActor + private func fetchUsageEntry() async -> UsageEntry { + // Get token + guard let (token, _) = keychainService.resolveToken(preferredSource: preferredTokenSource) else { + return .error("No API token configured") + } + + // Fetch usage + do { + let response = try await apiService.fetchUsage(token: token) + let limits = parseUsageLimits(from: response) + let sessionUtilization = response.fiveHour.map { $0.utilization / 100.0 } + + return UsageEntry( + date: Date(), + limits: limits, + sessionUtilization: sessionUtilization + ) + } catch { + return .error("Failed to fetch usage") + } + } + + /// Parses the API response into UsageLimit objects. + private func parseUsageLimits(from response: UsageResponse) -> [UsageLimit] { + var limits: [UsageLimit] = [] + + if let bucket = response.fiveHour { + limits.append(UsageLimit( + id: "five_hour", + title: "Session", + utilization: bucket.utilization / 100.0, + resetsAt: parseDate(bucket.resetsAt) + )) + } + + if let bucket = response.sevenDay { + limits.append(UsageLimit( + id: "seven_day", + title: "All", + utilization: bucket.utilization / 100.0, + resetsAt: parseDate(bucket.resetsAt) + )) + } + + if let bucket = response.sevenDayOpus, bucket.utilization > 0 { + limits.append(UsageLimit( + id: "seven_day_opus", + title: "Opus", + utilization: bucket.utilization / 100.0, + resetsAt: parseDate(bucket.resetsAt) + )) + } + + if let bucket = response.sevenDaySonnet, bucket.utilization > 0 { + limits.append(UsageLimit( + id: "seven_day_sonnet", + title: "Sonnet", + utilization: bucket.utilization / 100.0, + resetsAt: parseDate(bucket.resetsAt) + )) + } + + return limits + } + + /// Parses an ISO 8601 date string. + private func parseDate(_ string: String?) -> Date? { + guard let string else { return nil } + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + if let date = formatter.date(from: string) { + return date + } + + // Try without fractional seconds + formatter.formatOptions = [.withInternetDateTime] + return formatter.date(from: string) + } +} diff --git a/Claude Monitor Widget/Views/GaugeView.swift b/Claude Monitor Widget/Views/GaugeView.swift new file mode 100644 index 0000000..58d0a55 --- /dev/null +++ b/Claude Monitor Widget/Views/GaugeView.swift @@ -0,0 +1,143 @@ +// +// GaugeView.swift +// Claude Monitor Widget +// + +import SwiftUI + +/// A circular gauge view showing utilization with the Claude logo in the center. +struct GaugeView: View { + let utilization: Double + let size: CGFloat + + private var clampedUtilization: Double { + max(0, min(1, utilization)) + } + + var body: some View { + ZStack { + // Background track (270° arc, open at bottom) + Circle() + .trim(from: 0, to: 0.75) + .stroke( + Color.secondary.opacity(0.2), + style: StrokeStyle(lineWidth: size * 0.08, lineCap: .round) + ) + .rotationEffect(.degrees(135)) + + // Filled arc based on utilization + Circle() + .trim(from: 0, to: 0.75 * clampedUtilization) + .stroke( + gaugeColor, + style: StrokeStyle(lineWidth: size * 0.08, lineCap: .round) + ) + .rotationEffect(.degrees(135)) + + // Claude logo in center + ClaudeLogoView() + .frame(width: size * 0.55, height: size * 0.55) + + // Percentage below + VStack { + Spacer() + Text("\(Int(clampedUtilization * 100))%") + .font(.system(size: size * 0.15, weight: .medium, design: .rounded)) + .foregroundStyle(.secondary) + } + .padding(.bottom, size * 0.05) + } + .frame(width: size, height: size) + } + + private var gaugeColor: Color { + switch clampedUtilization { + case 0..<0.5: .blue + case 0.5..<0.8: .yellow + case 0.8..<0.95: .orange + default: .red + } + } +} + +/// The Claude "C" logo as a SwiftUI shape. +struct ClaudeLogoView: View { + var body: some View { + GeometryReader { geometry in + let scale = min(geometry.size.width, geometry.size.height) / 24 + + Path { path in + // Simplified Claude logo path (star shape) + let points: [(Double, Double)] = [ + (7.55, 14.25), (10.11, 12.81), (10.15, 12.69), (10.11, 12.62), + (9.98, 12.62), (9.56, 12.59), (8.09, 12.56), (6.83, 12.50), + (5.60, 12.44), (5.29, 12.37), (5.0, 11.99), (5.03, 11.80), + (5.29, 11.62), (5.66, 11.66), (6.48, 11.71), (7.72, 11.80), + (8.61, 11.85), (9.94, 11.99), (10.15, 11.99), (10.18, 11.90), + (10.11, 11.85), (10.05, 11.80), (8.78, 10.93), (7.39, 10.02), + (6.67, 9.49), (6.28, 9.23), (6.08, 8.98), (5.99, 8.43), + (6.35, 8.04), (6.83, 8.07), (6.95, 8.10), (7.43, 8.48), + (8.47, 9.27), (9.82, 10.27), (10.01, 10.43), (10.09, 10.38), + (10.10, 10.34), (10.01, 10.19), (9.28, 8.86), (8.50, 7.51), + (8.15, 6.96), (8.06, 6.62), (8.0, 6.23), (8.40, 5.68), + (8.63, 5.60), (9.17, 5.68), (9.40, 5.87), (9.73, 6.64), + (10.27, 7.85), (11.12, 9.49), (11.36, 9.98), (11.50, 10.43), + (11.54, 10.56), (11.63, 10.56), (11.63, 10.49), (11.70, 9.56), + (11.83, 8.43), (11.95, 6.96), (11.99, 6.55), (12.20, 6.06), + (12.60, 5.79), (12.92, 5.94), (13.18, 6.32), (13.14, 6.56), + (12.99, 7.56), (12.69, 9.13), (12.49, 10.19), (12.60, 10.19), + (12.73, 10.05), (13.27, 9.35), (14.16, 8.23), (14.56, 7.78), + (15.02, 7.29), (15.31, 7.06), (15.87, 7.06), (16.29, 7.67), + (16.10, 8.30), (15.53, 9.03), (15.05, 9.65), (14.36, 10.57), + (13.94, 11.31), (13.98, 11.37), (14.08, 11.36), (15.62, 11.03), + (16.46, 10.88), (17.46, 10.71), (17.91, 10.92), (17.96, 11.13), + (17.78, 11.57), (16.71, 11.83), (15.46, 12.08), (13.60, 12.52), + (13.58, 12.54), (13.60, 12.57), (14.44, 12.65), (14.80, 12.67), + (15.68, 12.67), (17.32, 12.79), (17.74, 13.08), (18.0, 13.42), + (17.96, 13.69), (17.30, 14.02), (16.41, 13.81), (14.34, 13.32), + (13.63, 13.14), (13.53, 13.14), (13.53, 13.20), (14.12, 13.78), + (15.21, 14.76), (16.57, 16.02), (16.63, 16.33), (16.46, 16.58), + (16.28, 16.55), (15.08, 15.65), (14.62, 15.25), (13.58, 14.37), + (13.51, 14.37), (13.51, 14.46), (13.75, 14.82), (15.02, 16.72), + (15.08, 17.31), (14.99, 17.50), (14.66, 17.61), (14.30, 17.55), + (13.56, 16.51), (12.79, 15.33), (12.17, 14.28), (12.10, 14.32), + (11.73, 18.25), (11.56, 18.45), (11.16, 18.60), (10.84, 18.35), + (10.66, 17.95), (10.84, 17.15), (11.05, 16.11), (11.22, 15.28), + (11.37, 14.25), (11.46, 13.91), (11.46, 13.88), (11.38, 13.89), + (10.61, 14.96), (9.42, 16.55), (8.49, 17.55), (8.26, 17.64), + (7.88, 17.44), (7.91, 17.08), (8.13, 16.76), (9.42, 15.12), + (10.20, 14.10), (10.71, 13.51), (10.70, 13.43), (10.67, 13.43), + (7.24, 15.66), (6.63, 15.74), (6.36, 15.49), (6.39, 15.08), + (6.52, 14.95), (7.55, 14.24) + ] + + let centerX = geometry.size.width / 2 + let centerY = geometry.size.height / 2 + let offsetX = 11.5 * scale + let offsetY = 12.1 * scale + + for (index, point) in points.enumerated() { + let x = centerX + (CGFloat(point.0) * scale - offsetX) + let y = centerY - (CGFloat(point.1) * scale - offsetY) + + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + path.closeSubpath() + } + .fill(Color.primary) + } + } +} + +#Preview { + HStack(spacing: 20) { + GaugeView(utilization: 0.25, size: 80) + GaugeView(utilization: 0.67, size: 80) + GaugeView(utilization: 0.92, size: 80) + } + .padding() +} diff --git a/Claude Monitor Widget/Views/ShadedProgressView.swift b/Claude Monitor Widget/Views/ShadedProgressView.swift new file mode 100644 index 0000000..56809c0 --- /dev/null +++ b/Claude Monitor Widget/Views/ShadedProgressView.swift @@ -0,0 +1,31 @@ +// +// ShadedProgressView.swift +// Claude Monitor Widget +// + +import SwiftUI + +/// A custom progress bar with a solid fill and rounded corners. +struct ShadedProgressView: View { + let value: Double + let tint: Color + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.secondary.opacity(0.2)) + + RoundedRectangle(cornerRadius: 3) + .fill(tint) + .frame(width: geometry.size.width * min(max(value, 0), 1)) + } + } + .frame(height: 6) + } + + init(value: Double, tint: Color = .accentColor) { + self.value = value + self.tint = tint + } +} diff --git a/Claude Monitor Widget/Views/UsageWidgetView.swift b/Claude Monitor Widget/Views/UsageWidgetView.swift new file mode 100644 index 0000000..2b0d83d --- /dev/null +++ b/Claude Monitor Widget/Views/UsageWidgetView.swift @@ -0,0 +1,106 @@ +// +// UsageWidgetView.swift +// Claude Monitor Widget +// + +import SwiftUI +import WidgetKit +import AppIntents + +/// The main widget view for the medium size widget. +struct UsageWidgetView: View { + let entry: UsageEntry + + var body: some View { + if let error = entry.error { + errorView(message: error) + } else if entry.limits.isEmpty { + emptyView + } else { + contentView + } + } + + private var contentView: some View { + Button(intent: RefreshIntent()) { + HStack(spacing: 28) { + // Gauge on the left + GaugeView( + utilization: entry.sessionUtilization ?? 0, + size: 70 + ) + .padding(.leading, 4) + + // Limits on the right + VStack(alignment: .leading, spacing: 6) { + Spacer(minLength: 4) + + ForEach(entry.limits.prefix(3)) { limit in + WidgetLimitRow(limit: limit) + } + + Spacer(minLength: 8) + } + .padding(.trailing, 4) + } + .padding(.vertical, 8) + .padding(.horizontal, 4) + } + .buttonStyle(.plain) + } + + private func errorView(message: String) -> some View { + VStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle") + .font(.title2) + .foregroundStyle(.secondary) + + Text(message) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Button(intent: RefreshIntent()) { + Text("Retry") + .font(.caption) + } + .buttonStyle(.plain) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var emptyView: some View { + VStack(spacing: 8) { + Image(systemName: "chart.bar") + .font(.title2) + .foregroundStyle(.secondary) + + Text("No usage data") + .font(.caption) + .foregroundStyle(.secondary) + + Button(intent: RefreshIntent()) { + Text("Refresh") + .font(.caption) + } + .buttonStyle(.plain) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +#Preview(as: .systemMedium) { + Claude_Monitor_Widget() +} timeline: { + UsageEntry.placeholder + UsageEntry( + date: Date(), + limits: [ + UsageLimit(id: "session", title: "Session", utilization: 0.67, resetsAt: Date().addingTimeInterval(3600 * 3)), + UsageLimit(id: "all", title: "All", utilization: 0.34, resetsAt: Date().addingTimeInterval(3600 * 48)), + UsageLimit(id: "opus", title: "Opus", utilization: 0.15, resetsAt: Date().addingTimeInterval(3600 * 48)) + ], + sessionUtilization: 0.67 + ) + UsageEntry.error("No API token configured") +} diff --git a/Claude Monitor Widget/Views/WidgetLimitRow.swift b/Claude Monitor Widget/Views/WidgetLimitRow.swift new file mode 100644 index 0000000..7a3822a --- /dev/null +++ b/Claude Monitor Widget/Views/WidgetLimitRow.swift @@ -0,0 +1,90 @@ +// +// WidgetLimitRow.swift +// Claude Monitor Widget +// + +import SwiftUI + +/// A compact row displaying a single usage limit for the widget. +struct WidgetLimitRow: View { + let limit: UsageLimit + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(limit.title) + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(.primary) + .lineLimit(1) + + Spacer() + + Text(limit.utilization, format: .percent.precision(.fractionLength(0))) + .font(.system(size: 11, weight: .regular, design: .monospaced)) + .foregroundStyle(.secondary) + + if let resetsAt = limit.resetsAt { + Text(resetTimeString(for: resetsAt)) + .font(.system(size: 10)) + .foregroundStyle(.tertiary) + } + } + + ShadedProgressView(value: limit.utilization, tint: progressColor) + } + .frame(maxWidth: 150) + } + + private var progressColor: Color { + switch limit.utilization { + case 0..<0.5: .blue + case 0.5..<0.8: .yellow + case 0.8..<0.95: .orange + default: .red + } + } + + private func resetTimeString(for date: Date) -> String { + let now = Date() + let interval = date.timeIntervalSince(now) + + guard interval > 0 else { return "" } + + let hours = Int(interval) / 3600 + let days = hours / 24 + + if days > 0 { + return "in \(days)d" + } else if hours > 0 { + return "in \(hours)h" + } else { + let minutes = Int(interval) / 60 + return "in \(max(1, minutes))m" + } + } +} + +#Preview { + VStack(spacing: 8) { + WidgetLimitRow(limit: UsageLimit( + id: "session", + title: "Session", + utilization: 0.67, + resetsAt: Date().addingTimeInterval(3600 * 3) + )) + WidgetLimitRow(limit: UsageLimit( + id: "all", + title: "All", + utilization: 0.34, + resetsAt: Date().addingTimeInterval(3600 * 48) + )) + WidgetLimitRow(limit: UsageLimit( + id: "opus", + title: "Opus", + utilization: 0.15, + resetsAt: Date().addingTimeInterval(3600 * 48) + )) + } + .padding() + .frame(width: 200) +} diff --git a/Claude Monitor.xcodeproj/project.pbxproj b/Claude Monitor.xcodeproj/project.pbxproj index 09b89e7..1a4bf1c 100644 --- a/Claude Monitor.xcodeproj/project.pbxproj +++ b/Claude Monitor.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 01024E652EE1FDF0002CC293 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 01024E642EE1FDF0002CC293 /* Sentry */; }; 01F78B9E2EE1739B006D357C /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 01F78B9D2EE1739B006D357C /* Logging */; }; + 01WIDGET012EE30000000001 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01WIDGET002EE30000000001 /* WidgetKit.framework */; }; + 01WIDGET022EE30000000001 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01WIDGET002EE30000000002 /* SwiftUI.framework */; }; + 01WIDGET032EE30000000001 /* Claude Monitor Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -26,12 +29,34 @@ remoteGlobalIDString = 0160FC542EE0CEC1006E6A22; remoteInfo = "Claude Monitor"; }; + 01WIDGET042EE30000000001 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0160FC4D2EE0CEC1006E6A22 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 01WIDGET102EE30000000001; + remoteInfo = "Claude Monitor Widget"; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 01WIDGET062EE30000000001 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + dstPath = ""; + dstSubfolder = PlugIns; + files = ( + 01WIDGET032EE30000000001 /* Claude Monitor Widget.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 0160FC552EE0CEC1006E6A22 /* Claude Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Claude Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 0160FC622EE0CEC3006E6A22 /* Claude MonitorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Claude MonitorTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 0160FC6C2EE0CEC3006E6A22 /* Claude MonitorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Claude MonitorUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 01WIDGET002EE30000000001 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 01WIDGET002EE30000000002 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Claude Monitor Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -50,6 +75,11 @@ path = "Claude MonitorUITests"; sourceTree = ""; }; + 01WIDGET052EE30000000001 /* Claude Monitor Widget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "Claude Monitor Widget"; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,6 +100,13 @@ files = ( ); }; + 01WIDGET072EE30000000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( + 01WIDGET012EE30000000001 /* WidgetKit.framework in Frameworks */, + 01WIDGET022EE30000000001 /* SwiftUI.framework in Frameworks */, + ); + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -77,8 +114,10 @@ isa = PBXGroup; children = ( 0160FC572EE0CEC1006E6A22 /* Claude Monitor */, + 01WIDGET052EE30000000001 /* Claude Monitor Widget */, 0160FC652EE0CEC3006E6A22 /* Claude MonitorTests */, 0160FC6F2EE0CEC3006E6A22 /* Claude MonitorUITests */, + 01WIDGET082EE30000000001 /* Frameworks */, 0160FC562EE0CEC1006E6A22 /* Products */, ); sourceTree = ""; @@ -89,10 +128,20 @@ 0160FC552EE0CEC1006E6A22 /* Claude Monitor.app */, 0160FC622EE0CEC3006E6A22 /* Claude MonitorTests.xctest */, 0160FC6C2EE0CEC3006E6A22 /* Claude MonitorUITests.xctest */, + 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */, ); name = Products; sourceTree = ""; }; + 01WIDGET082EE30000000001 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 01WIDGET002EE30000000001 /* WidgetKit.framework */, + 01WIDGET002EE30000000002 /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -103,9 +152,13 @@ 0160FC512EE0CEC1006E6A22 /* Sources */, 0160FC522EE0CEC1006E6A22 /* Frameworks */, 0160FC532EE0CEC1006E6A22 /* Resources */, + 01WIDGET062EE30000000001 /* Embed Foundation Extensions */, ); buildRules = ( ); + dependencies = ( + 01WIDGET052EE30000000002 /* PBXTargetDependency */, + ); fileSystemSynchronizedGroups = ( 0160FC572EE0CEC1006E6A22 /* Claude Monitor */, ); @@ -160,6 +213,24 @@ productReference = 0160FC6C2EE0CEC3006E6A22 /* Claude MonitorUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 01WIDGET102EE30000000001 /* Claude Monitor Widget */ = { + isa = PBXNativeTarget; + buildConfigurationList = B3BB623B2EE2ABC1003188A1 /* Build configuration list for PBXNativeTarget "Claude Monitor Widget" */; + buildPhases = ( + 01WIDGET092EE30000000001 /* Sources */, + 01WIDGET072EE30000000001 /* Frameworks */, + 01WIDGET0A2EE30000000001 /* Resources */, + ); + buildRules = ( + ); + fileSystemSynchronizedGroups = ( + 01WIDGET052EE30000000001 /* Claude Monitor Widget */, + ); + name = "Claude Monitor Widget"; + productName = "Claude Monitor Widget"; + productReference = 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -181,6 +252,9 @@ CreatedOnToolsVersion = 26.1.1; TestTargetID = 0160FC542EE0CEC1006E6A22; }; + 01WIDGET102EE30000000001 = { + CreatedOnToolsVersion = 26.1.1; + }; }; }; buildConfigurationList = 0160FC502EE0CEC1006E6A22 /* Build configuration list for PBXProject "Claude Monitor" */; @@ -202,6 +276,7 @@ projectRoot = ""; targets = ( 0160FC542EE0CEC1006E6A22 /* Claude Monitor */, + 01WIDGET102EE30000000001 /* Claude Monitor Widget */, 0160FC612EE0CEC3006E6A22 /* Claude MonitorTests */, 0160FC6B2EE0CEC3006E6A22 /* Claude MonitorUITests */, ); @@ -224,6 +299,11 @@ files = ( ); }; + 01WIDGET0A2EE30000000001 /* Resources */ = { + isa = PBXResourcesBuildPhase; + files = ( + ); + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -242,6 +322,11 @@ files = ( ); }; + 01WIDGET092EE30000000001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + files = ( + ); + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -255,6 +340,11 @@ target = 0160FC542EE0CEC1006E6A22 /* Claude Monitor */; targetProxy = 0160FC6D2EE0CEC3006E6A22 /* PBXContainerItemProxy */; }; + 01WIDGET052EE30000000002 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 01WIDGET102EE30000000001 /* Claude Monitor Widget */; + targetProxy = 01WIDGET042EE30000000001 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -385,10 +475,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_ENTITLEMENTS = "Claude Monitor/Claude_Monitor.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2NFSK2WB24; + DEVELOPMENT_TEAM = ""; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; @@ -413,9 +505,9 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-Monitor"; + PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor"; PRODUCT_NAME = "$(TARGET_NAME)"; - REGISTER_APP_GROUPS = YES; + PROVISIONING_PROFILE_SPECIFIER = ""; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -428,10 +520,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_ENTITLEMENTS = "Claude Monitor/Claude_Monitor.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2NFSK2WB24; + DEVELOPMENT_TEAM = ""; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; @@ -456,9 +550,9 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-Monitor"; + PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor"; PRODUCT_NAME = "$(TARGET_NAME)"; - REGISTER_APP_GROUPS = YES; + PROVISIONING_PROFILE_SPECIFIER = ""; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -472,7 +566,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2NFSK2WB24; + DEVELOPMENT_TEAM = P7BV7FZWPF; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.1; MARKETING_VERSION = 1.0; @@ -492,7 +586,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2NFSK2WB24; + DEVELOPMENT_TEAM = P7BV7FZWPF; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.1; MARKETING_VERSION = 1.0; @@ -511,7 +605,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2NFSK2WB24; + DEVELOPMENT_TEAM = P7BV7FZWPF; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-MonitorUITests"; @@ -529,7 +623,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 2NFSK2WB24; + DEVELOPMENT_TEAM = P7BV7FZWPF; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-MonitorUITests"; @@ -542,6 +636,72 @@ }; name = Release; }; + B3BB62392EE2ABA9003188A1 /* Debug configuration for PBXNativeTarget "Claude Monitor Widget" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = "Claude Monitor Widget/Claude_Monitor_Widget.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Claude Monitor Widget/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Claude Monitor Widget"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + }; + name = Debug; + }; + B3BB623A2EE2ABA9003188A1 /* Release configuration for PBXNativeTarget "Claude Monitor Widget" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = "Claude Monitor Widget/Claude_Monitor_Widget.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Claude Monitor Widget/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Claude Monitor Widget"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -577,6 +737,14 @@ ); defaultConfigurationName = Release; }; + B3BB623B2EE2ABC1003188A1 /* Build configuration list for PBXNativeTarget "Claude Monitor Widget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3BB62392EE2ABA9003188A1 /* Debug configuration for PBXNativeTarget "Claude Monitor Widget" */, + B3BB623A2EE2ABA9003188A1 /* Release configuration for PBXNativeTarget "Claude Monitor Widget" */, + ); + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/Claude Monitor/Claude_Monitor.entitlements b/Claude Monitor/Claude_Monitor.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/Claude Monitor/Claude_Monitor.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + +