on a4745cd help sign in
9 files changed+88−213
since splitsnapshots
unifiedsplit
CLAUDE.md+2−2 README.md+6−6 UsageMenubar/Info.plist+1−1 UsageMenubar/KeychainHelper.swift+2−2 UsageMenubar/MenuView.swift+7−58 UsageMenubar/UsageMenubarApp.swift+1−2 UsageMenubar/ViewModel.swift+19−32 UsageMenubarTests/StatusBarTextTests.swift+46−106 project.yml+4−4
CLAUDE.md+2−2
@@ -20,8 +20,8 @@ click-to-copy on the headline, and threshold-crossing notifications.
2020- `KeychainHelper.swift` — Keychain wrapper (KeychainStore + KeychainHelper + ZaiKeychainHelper)
2121- `VersionFormatter.swift` — Version string formatting
2222
23−The status bar title shows `⚡{balance}` (Hyper), `⁕{percent}%` (Claude 5-hour),
24−`⁕{percent}%` (Claude 7-day), `Z{percent}%` (z.ai 5-hour), and `Z{percent}%`
23+The status bar title shows `⚡{balance}` (Hyper), `🕐{percent}%` (Claude 5-hour),
24+`📅{percent}%` (Claude 7-day), `🤖{percent}%` (z.ai 5-hour), and `📆{percent}%`
2525(z.ai weekly) — each omitted when absent or at 0%, and entirely absent when the
2626service is not configured (no API key or no credentials). An unconfigured service
2727produces no placeholder in the menu bar. The three fetches are independent —
README.md+6−6
@@ -9,8 +9,8 @@ A macOS menu bar application that displays usage limits and credit balances for
99## Features
1010
1111- ⚡ **Hyper credits** — balance from `hyper.charm.land`, with sparkline trend, click-to-copy, and low-balance notifications
12−- ⁕ **Claude Code limits** — Pro/Max 5-hour and 7-day usage windows, with countdown to next reset. No setup: if you've signed in with the Claude Code CLI, it just appears
13−- Z **z.ai Coding Plan** — 5-hour and weekly token quota percentages, with reset countdowns. Enter your API key in settings
12+- 🤖 **Claude Code limits** — Pro/Max 5-hour and 7-day usage windows, with countdown to next reset. No setup: if you've signed in with the Claude Code CLI, it just appears
13+- 🤖 **z.ai Coding Plan** — 5-hour and weekly token quota percentages, with reset countdowns. Enter your API key in settings
1414- 🔄 **Auto-refresh** — configurable interval (1m / 5m / 15m / 30m), plus refresh on wake from sleep
1515- 🖱️ **Right-click menu** — quick access to Refresh and Quit
1616- 🔒 **Keychain storage** — API keys stored securely in macOS Keychain
@@ -24,10 +24,10 @@ Each service shows an emoji-prefixed segment when configured and above 0%:
2424| Service | Emoji | Example |
2525|---------|-------|---------|
2626| Hyper balance | ⚡ | `⚡42` |
27−| Claude 5-hour | ⁕ | `⁕62%` |
28−| Claude 7-day | ⁕ | `⁕8%` |
29−| z.ai 5-hour | Z | `Z12%` |
30−| z.ai weekly | Z | `Z3%` |
27+| Claude 5-hour | 🕐 | `🕐62%` |
28+| Claude 7-day | 📅 | `📅8%` |
29+| z.ai 5-hour | 🤖 | `🤖12%` |
30+| z.ai weekly | 📆 | `📆3%` |
3131
3232Unconfigured services don't appear at all — no placeholder, no icon.
3333
UsageMenubar/Info.plist+1−1
@@ -7,7 +7,7 @@
77 <key>CFBundleDisplayName</key>
88 <string>Usage Menubar</string>
99 <key>CFBundleIdentifier</key>
10− <string>soy.iko.usage-menubar</string>
10+ <string>com.ilyakooo0.usage-menubar</string>
1111 <key>CFBundleVersion</key>
1212 <string>1</string>
1313 <key>CFBundleShortVersionString</key>
UsageMenubar/KeychainHelper.swift+2−2
@@ -82,7 +82,7 @@ struct KeychainStore {
8282
8383/// Simple wrapper around the macOS Keychain for storing the Hyper API key.
8484enum KeychainHelper {
85− private static let service = "soy.iko.usage-menubar"
85+ private static let service = "com.ilyakooo0.usage-menubar"
8686 private static let account = "hyper-api-key"
8787
8888 private static let store = KeychainStore(service: service, account: account)
@@ -112,7 +112,7 @@ enum KeychainHelper {
112112/// Uses the same keychain service as the Hyper key but a different account, so the
113113/// two keys coexist without clobbering each other.
114114enum ZaiKeychainHelper {
115− private static let service = "soy.iko.usage-menubar"
115+ private static let service = "com.ilyakooo0.usage-menubar"
116116 private static let account = "zai-api-key"
117117
118118 private static let store = KeychainStore(service: service, account: account)
UsageMenubar/MenuView.swift+7−58
@@ -259,7 +259,7 @@ struct MenuView: View {
259259 VStack(spacing: 8) {
260260 HStack(alignment: .firstTextBaseline, spacing: 8) {
261261 headlineNumber(
262− text: "⁕ \\(percent)%",
262+ text: "🕐 \(percent)%",
263263 color: Self.usageColor(percent),
264264 trend: viewModel.claudeTrend,
265265 copiedConfirmation: viewModel.copiedClaudeConfirmation,
@@ -379,7 +379,7 @@ struct MenuView: View {
379379 VStack(spacing: 8) {
380380 HStack(alignment: .firstTextBaseline, spacing: 8) {
381381 headlineNumber(
382− text: "Z \\(fiveHour)%",
382+ text: "🤖 \(fiveHour)%",
383383 color: Self.usageColor(fiveHour),
384384 trend: viewModel.zaiTrend,
385385 copiedConfirmation: viewModel.copiedZaiConfirmation,
@@ -407,26 +407,11 @@ struct MenuView: View {
407407 }
408408 }
409409
410− // Peak hours notice.
411− zaiPeakHours
412−
413− // Sparkline for the z.ai 5-hour % history, with an orange tint
414− // when currently in peak hours.
415− if viewModel.zaiSparklineValues.count >= 2 {
416− Sparkline(
417− values: viewModel.zaiSparklineValues,
418− color: Self.usageColor(zaiFiveHourPercent ?? 0)
419− )
420− .frame(height: 26)
421− .padding(.top, 4)
422− .transition(.opacity)
423− .background(
424− ViewModel.zaiInPeakHours
425− ? RoundedRectangle(cornerRadius: 4, style: .continuous)
426− .fill(Color.orange.opacity(0.08))
427− : nil
428− )
429− }
410+ // Sparkline for the z.ai 5-hour % history.
411+ sparkline(
412+ values: viewModel.zaiSparklineValues,
413+ color: Self.usageColor(zaiFiveHourPercent ?? 0)
414+ )
430415 }
431416 }
432417
@@ -454,42 +439,6 @@ struct MenuView: View {
454439 }
455440 }
456441
457− /// z.ai peak hours: 14:00–18:00 CST (UTC+8), i.e. 06:00–10:00 UTC.
458− /// Limits are most likely to bind during this window. Displayed in the
459− /// user's local timezone so they know when to expect slowdowns.
460− private var zaiPeakHours: some View {
461− let inPeak = ViewModel.zaiInPeakHours
462−
463− return HStack(spacing: 6) {
464− Circle()
465− .fill(inPeak ? Color.orange : Color.secondary.opacity(0.4))
466− .frame(width: 6, height: 6)
467−
468− Text(inPeak ? "Peak hours now" : "Peak \(Self.zaiPeakHoursLocal)")
469− .font(Self.footnoteFont)
470− .foregroundColor(inPeak ? .orange : .secondary)
471− }
472− }
473−
474− /// z.ai peak hours converted to the user's local timezone, e.g. "09:00–13:00".
475− private static var zaiPeakHoursLocal: String {
476− let formatter = DateFormatter()
477− formatter.timeZone = TimeZone.current
478− formatter.dateFormat = "HH:mm"
479−
480− var utc = DateComponents()
481− utc.timeZone = TimeZone(identifier: "UTC")
482− utc.hour = 6
483− var calendar = Calendar(identifier: .gregorian)
484− calendar.timeZone = TimeZone(identifier: "UTC")!
485− let start = calendar.date(from: utc)!
486−
487− utc.hour = 10
488− let end = calendar.date(from: utc)!
489−
490− return "\(formatter.string(from: start))–\(formatter.string(from: end))"
491− }
492−
493442 /// Formats the time until a z.ai window resets, e.g. "3h 20m".
494443 private static func zaiResetsIn(from date: Date) -> String {
495444 let interval = date.timeIntervalSince(Date())
UsageMenubar/UsageMenubarApp.swift+1−2
@@ -89,8 +89,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
8989 claudeFiveHourPercent: fiveHour,
9090 claudeSevenDayPercent: sevenDay,
9191 zaiFiveHourPercent: zaiFiveHour,
92− zaiWeeklyPercent: zaiWeekly,
93− zaiInPeakHours: ViewModel.zaiInPeakHours
92+ zaiWeeklyPercent: zaiWeekly
9493 )
9594 }
9695 .removeDuplicates()
UsageMenubar/ViewModel.swift+19−32
@@ -7,7 +7,7 @@ import UserNotifications
77/// Posted when the user changes the refresh interval, so the app delegate can
88/// restart its timer with the new period.
99extension Notification.Name {
10− static let refreshIntervalDidChange = Notification.Name("soy.iko.usage-menubar.refreshIntervalDidChange")
10+ static let refreshIntervalDidChange = Notification.Name("com.ilyakooo0.usage-menubar.refreshIntervalDidChange")
1111}
1212
1313/// The direction a metric has moved over the most recent history points.
@@ -238,12 +238,11 @@ final class ViewModel: ObservableObject {
238238 }
239239
240240 /// The text to show in the menu bar. Each configured provider contributes a
241− /// segment prefixed with a provider emoji; segments are separated by ` · `
242− /// (middle dot). Within a segment, windows are listed as bare percentages
243− /// separated by spaces. An unconfigured service produces no segment.
244− /// When z.ai is in peak hours, a 🔶 marker is appended to the z.ai segment.
241+ /// segment; segments are separated by ` · ` (middle dot). Within a segment,
242+ /// windows are separated by a space. An unconfigured service produces no
243+ /// segment — no placeholder, no icon.
245244 ///
246− /// Example: `⚡42 · ⁕62% 8% · Z12% 3%🔶`
245+ /// Example: `⚡42 · 🕐62% 📅8% · 🤖12% 📆3%`
247246 ///
248247 /// Takes its values as parameters rather than reading the properties: `@Published`
249248 /// publishes in `willSet`, so a Combine subscriber that called back into the view
@@ -255,8 +254,7 @@ final class ViewModel: ObservableObject {
255254 claudeFiveHourPercent: Int?,
256255 claudeSevenDayPercent: Int?,
257256 zaiFiveHourPercent: Int?,
258− zaiWeeklyPercent: Int?,
259− zaiInPeakHours: Bool
257+ zaiWeeklyPercent: Int?
260258 ) -> String {
261259 var segments: [String] = []
262260
@@ -265,32 +263,28 @@ final class ViewModel: ObservableObject {
265263 segments.append("⚡\(formatBalance(balance))")
266264 }
267265
268− // Claude — provider emoji + bare percentages
266+ // Claude — windows joined by space within the segment
269267 var claudeWindows: [String] = []
270− if let fiveHour = claudeFiveHourPercent {
271− claudeWindows.append("\(fiveHour)%")
268+ if let fiveHour = claudeFiveHourPercent, fiveHour > 0 {
269+ claudeWindows.append("🕐\(fiveHour)%")
272270 }
273− if let sevenDay = claudeSevenDayPercent {
274− claudeWindows.append("\(sevenDay)%")
271+ if let sevenDay = claudeSevenDayPercent, sevenDay > 0 {
272+ claudeWindows.append("📅\(sevenDay)%")
275273 }
276274 if !claudeWindows.isEmpty {
277− segments.append("⁕" + claudeWindows.joined(separator: " "))
275+ segments.append(claudeWindows.joined(separator: " "))
278276 }
279277
280− // z.ai — provider emoji + bare percentages
278+ // z.ai — windows joined by space within the segment
281279 var zaiWindows: [String] = []
282− if let zaiFiveHour = zaiFiveHourPercent {
283− zaiWindows.append("\(zaiFiveHour)%")
280+ if let zaiFiveHour = zaiFiveHourPercent, zaiFiveHour > 0 {
281+ zaiWindows.append("🤖\(zaiFiveHour)%")
284282 }
285− if let zaiWeekly = zaiWeeklyPercent {
286− zaiWindows.append("\(zaiWeekly)%")
283+ if let zaiWeekly = zaiWeeklyPercent, zaiWeekly > 0 {
284+ zaiWindows.append("📆\(zaiWeekly)%")
287285 }
288286 if !zaiWindows.isEmpty {
289− var segment = "Z" + zaiWindows.joined(separator: " ")
290− if zaiInPeakHours {
291− segment += "🔶"
292− }
293− segments.append(segment)
287+ segments.append(zaiWindows.joined(separator: " "))
294288 }
295289
296290 if segments.isEmpty {
@@ -310,17 +304,10 @@ final class ViewModel: ObservableObject {
310304 claudeFiveHourPercent: claudeFiveHourPercent,
311305 claudeSevenDayPercent: claudeSevenDayPercent,
312306 zaiFiveHourPercent: zaiFiveHourPercent,
313− zaiWeeklyPercent: zaiWeeklyPercent,
314− zaiInPeakHours: Self.zaiInPeakHours
307+ zaiWeeklyPercent: zaiWeeklyPercent
315308 )
316309 }
317310
318− /// Whether z.ai is currently in peak hours (06:00–10:00 UTC).
319− static var zaiInPeakHours: Bool {
320− let hour = Calendar(identifier: .gregorian).component(.hour, from: Date())
321− return hour >= 6 && hour < 10
322− }
323−
324311 /// The formatted balance string for display in the popover.
325312 var formattedBalance: String {
326313 guard let balance = balance else { return "?" }
UsageMenubarTests/StatusBarTextTests.swift+46−106
@@ -15,10 +15,9 @@ final class StatusBarTextTests: XCTestCase {
1515 balance: 42, isLoading: false,
1616 hyperConfigured: true,
1717 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
18− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
19− zaiInPeakHours: false
18+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
2019 )
21− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8%")
20+ XCTAssertEqual(title, "⚡42 · 🕐62% 📅8%")
2221 }
2322
2423 func testHyperOnly() {
@@ -26,8 +25,7 @@ final class StatusBarTextTests: XCTestCase {
2625 balance: 42, isLoading: false,
2726 hyperConfigured: true,
2827 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
29− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
30− zaiInPeakHours: false
28+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
3129 )
3230 XCTAssertEqual(title, "⚡42")
3331 }
@@ -37,10 +35,9 @@ final class StatusBarTextTests: XCTestCase {
3735 balance: nil, isLoading: false,
3836 hyperConfigured: false,
3937 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
40− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
41− zaiInPeakHours: false
38+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
4239 )
43− XCTAssertEqual(title, "\u{2055}62% 8%")
40+ XCTAssertEqual(title, "🕐62% 📅8%")
4441 }
4542
4643 func testLoadingWithNothingToShow() {
@@ -48,8 +45,7 @@ final class StatusBarTextTests: XCTestCase {
4845 balance: nil, isLoading: true,
4946 hyperConfigured: false,
5047 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
51− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
52− zaiInPeakHours: false
48+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
5349 )
5450 XCTAssertEqual(title, "…")
5551 }
@@ -59,8 +55,7 @@ final class StatusBarTextTests: XCTestCase {
5955 balance: nil, isLoading: false,
6056 hyperConfigured: false,
6157 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
62− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
63− zaiInPeakHours: false
58+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
6459 )
6560 XCTAssertEqual(title, "")
6661 }
@@ -73,10 +68,9 @@ final class StatusBarTextTests: XCTestCase {
7368 balance: nil, isLoading: true,
7469 hyperConfigured: false,
7570 claudeFiveHourPercent: 62, claudeSevenDayPercent: nil,
76− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
77− zaiInPeakHours: false
71+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
7872 )
79− XCTAssertEqual(title, "\u{2055}62%")
73+ XCTAssertEqual(title, "🕐62%")
8074 }
8175
8276 func testLoadingKeepsAStaleBalanceOnScreen() {
@@ -84,21 +78,19 @@ final class StatusBarTextTests: XCTestCase {
8478 balance: 42, isLoading: true,
8579 hyperConfigured: true,
8680 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
87− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
88− zaiInPeakHours: false
81+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
8982 )
90− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8%")
83+ XCTAssertEqual(title, "⚡42 · 🕐62% 📅8%")
9184 }
9285
93− func testZeroPercentIsShown() {
86+ func testZeroPercentIsShownRatherThanOmitted() {
9487 let title = ViewModel.statusBarText(
9588 balance: 42, isLoading: false,
9689 hyperConfigured: true,
9790 claudeFiveHourPercent: 0, claudeSevenDayPercent: 8,
98− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
99− zaiInPeakHours: false
91+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
10092 )
101− XCTAssertEqual(title, "⚡42 · \u{2055}0% 8%")
93+ XCTAssertEqual(title, "⚡42 · 📅8%")
10294 }
10395
10496 func testBothClaudeWindowsZero() {
@@ -106,10 +98,9 @@ final class StatusBarTextTests: XCTestCase {
10698 balance: 42, isLoading: false,
10799 hyperConfigured: true,
108100 claudeFiveHourPercent: 0, claudeSevenDayPercent: 0,
109− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
110− zaiInPeakHours: false
101+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
111102 )
112− XCTAssertEqual(title, "⚡42 · \u{2055}0% 0%")
103+ XCTAssertEqual(title, "⚡42")
113104 }
114105
115106 func testFiveHourOnly() {
@@ -117,10 +108,9 @@ final class StatusBarTextTests: XCTestCase {
117108 balance: nil, isLoading: false,
118109 hyperConfigured: false,
119110 claudeFiveHourPercent: 62, claudeSevenDayPercent: nil,
120− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
121− zaiInPeakHours: false
111+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
122112 )
123− XCTAssertEqual(title, "\u{2055}62%")
113+ XCTAssertEqual(title, "🕐62%")
124114 }
125115
126116 func testSevenDayOnly() {
@@ -128,10 +118,9 @@ final class StatusBarTextTests: XCTestCase {
128118 balance: nil, isLoading: false,
129119 hyperConfigured: false,
130120 claudeFiveHourPercent: nil, claudeSevenDayPercent: 8,
131− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
132− zaiInPeakHours: false
121+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
133122 )
134− XCTAssertEqual(title, "\u{2055}8%")
123+ XCTAssertEqual(title, "📅8%")
135124 }
136125
137126 // MARK: - Hyper not configured
@@ -143,8 +132,7 @@ final class StatusBarTextTests: XCTestCase {
143132 balance: 42, isLoading: false,
144133 hyperConfigured: false,
145134 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
146− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
147− zaiInPeakHours: false
135+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
148136 )
149137 XCTAssertEqual(title, "")
150138 }
@@ -154,8 +142,7 @@ final class StatusBarTextTests: XCTestCase {
154142 balance: nil, isLoading: true,
155143 hyperConfigured: false,
156144 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
157− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
158− zaiInPeakHours: false
145+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
159146 )
160147 XCTAssertEqual(title, "…")
161148 }
@@ -165,10 +152,9 @@ final class StatusBarTextTests: XCTestCase {
165152 balance: nil, isLoading: false,
166153 hyperConfigured: false,
167154 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
168− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
169− zaiInPeakHours: false
155+ zaiFiveHourPercent: nil, zaiWeeklyPercent: nil
170156 )
171− XCTAssertEqual(title, "\u{2055}62% 8%")
157+ XCTAssertEqual(title, "🕐62% 📅8%")
172158 }
173159
174160 // MARK: - z.ai in Title
@@ -178,10 +164,9 @@ final class StatusBarTextTests: XCTestCase {
178164 balance: nil, isLoading: false,
179165 hyperConfigured: false,
180166 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
181− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
182− zaiInPeakHours: false
167+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
183168 )
184− XCTAssertEqual(title, "\u{005A}12%")
169+ XCTAssertEqual(title, "🤖12%")
185170 }
186171
187172 func testZaiWithHyper() {
@@ -189,10 +174,9 @@ final class StatusBarTextTests: XCTestCase {
189174 balance: 42, isLoading: false,
190175 hyperConfigured: true,
191176 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
192− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
193− zaiInPeakHours: false
177+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
194178 )
195− XCTAssertEqual(title, "⚡42 · \u{005A}12%")
179+ XCTAssertEqual(title, "⚡42 · 🤖12%")
196180 }
197181
198182 func testZaiWithClaude() {
@@ -200,10 +184,9 @@ final class StatusBarTextTests: XCTestCase {
200184 balance: nil, isLoading: false,
201185 hyperConfigured: false,
202186 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
203− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
204− zaiInPeakHours: false
187+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
205188 )
206− XCTAssertEqual(title, "\u{2055}62% 8% · \u{005A}12%")
189+ XCTAssertEqual(title, "🕐62% 📅8% · 🤖12%")
207190 }
208191
209192 func testAllThreeServices() {
@@ -211,21 +194,19 @@ final class StatusBarTextTests: XCTestCase {
211194 balance: 42, isLoading: false,
212195 hyperConfigured: true,
213196 claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
214− zaiFiveHourPercent: 12, zaiWeeklyPercent: 3,
215− zaiInPeakHours: false
197+ zaiFiveHourPercent: 12, zaiWeeklyPercent: 3
216198 )
217− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8% · \u{005A}12% 3%")
199+ XCTAssertEqual(title, "⚡42 · 🕐62% 📅8% · 🤖12% 📆3%")
218200 }
219201
220− func testZaiZeroIsShown() {
202+ func testZaiZeroIsOmitted() {
221203 let title = ViewModel.statusBarText(
222204 balance: 42, isLoading: false,
223205 hyperConfigured: true,
224206 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
225− zaiFiveHourPercent: 0, zaiWeeklyPercent: 0,
226− zaiInPeakHours: false
207+ zaiFiveHourPercent: 0, zaiWeeklyPercent: 0
227208 )
228− XCTAssertEqual(title, "⚡42 · \u{005A}0% 0%")
209+ XCTAssertEqual(title, "⚡42")
229210 }
230211
231212 func testLoadingKeepsZaiPercentOnScreen() {
@@ -233,10 +214,9 @@ final class StatusBarTextTests: XCTestCase {
233214 balance: nil, isLoading: true,
234215 hyperConfigured: false,
235216 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
236− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
237− zaiInPeakHours: false
217+ zaiFiveHourPercent: 12, zaiWeeklyPercent: nil
238218 )
239− XCTAssertEqual(title, "\u{005A}12%")
219+ XCTAssertEqual(title, "🤖12%")
240220 }
241221
242222 // MARK: - z.ai Weekly
@@ -246,10 +226,9 @@ final class StatusBarTextTests: XCTestCase {
246226 balance: nil, isLoading: false,
247227 hyperConfigured: false,
248228 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
249− zaiFiveHourPercent: nil, zaiWeeklyPercent: 3,
250− zaiInPeakHours: false
229+ zaiFiveHourPercent: nil, zaiWeeklyPercent: 3
251230 )
252− XCTAssertEqual(title, "\u{005A}3%")
231+ XCTAssertEqual(title, "📆3%")
253232 }
254233
255234 func testZaiWeeklyWithFiveHour() {
@@ -257,21 +236,19 @@ final class StatusBarTextTests: XCTestCase {
257236 balance: nil, isLoading: false,
258237 hyperConfigured: false,
259238 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
260− zaiFiveHourPercent: 12, zaiWeeklyPercent: 3,
261− zaiInPeakHours: false
239+ zaiFiveHourPercent: 12, zaiWeeklyPercent: 3
262240 )
263− XCTAssertEqual(title, "\u{005A}12% 3%")
241+ XCTAssertEqual(title, "🤖12% 📆3%")
264242 }
265243
266− func testZaiWeeklyZeroIsShown() {
244+ func testZaiWeeklyZeroIsOmitted() {
267245 let title = ViewModel.statusBarText(
268246 balance: nil, isLoading: false,
269247 hyperConfigured: false,
270248 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
271− zaiFiveHourPercent: 12, zaiWeeklyPercent: 0,
272− zaiInPeakHours: false
249+ zaiFiveHourPercent: 12, zaiWeeklyPercent: 0
273250 )
274− XCTAssertEqual(title, "\u{005A}12% 0%")
251+ XCTAssertEqual(title, "🤖12%")
275252 }
276253
277254 func testLoadingKeepsZaiWeeklyOnScreen() {
@@ -279,46 +256,9 @@ final class StatusBarTextTests: XCTestCase {
279256 balance: nil, isLoading: true,
280257 hyperConfigured: false,
281258 claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
282− zaiFiveHourPercent: nil, zaiWeeklyPercent: 3,
283− zaiInPeakHours: false
284− )
285− XCTAssertEqual(title, "\u{005A}3%")
286− }
287−
288− // MARK: - z.ai Peak Hours
289−
290− func testPeakHoursAddsDiamondToZaiSegment() {
291− let title = ViewModel.statusBarText(
292− balance: 42, isLoading: false,
293− hyperConfigured: true,
294− claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
295− zaiFiveHourPercent: 12, zaiWeeklyPercent: 3,
296− zaiInPeakHours: true
297− )
298− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8% · \u{005A}12% 3%\u{1F536}")
299− }
300−
301− func testPeakHoursWithZaiOnly() {
302− let title = ViewModel.statusBarText(
303− balance: nil, isLoading: false,
304− hyperConfigured: false,
305− claudeFiveHourPercent: nil, claudeSevenDayPercent: nil,
306− zaiFiveHourPercent: 12, zaiWeeklyPercent: nil,
307− zaiInPeakHours: true
308− )
309− XCTAssertEqual(title, "\u{005A}12%\u{1F536}")
310− }
311−
312− func testPeakHoursIgnoredWhenZaiNotConfigured() {
313− let title = ViewModel.statusBarText(
314− balance: 42, isLoading: false,
315− hyperConfigured: true,
316− claudeFiveHourPercent: 62, claudeSevenDayPercent: 8,
317− zaiFiveHourPercent: nil, zaiWeeklyPercent: nil,
318− zaiInPeakHours: true
259+ zaiFiveHourPercent: nil, zaiWeeklyPercent: 3
319260 )
320− // No z.ai segment, so peak marker has nothing to attach to.
321− XCTAssertEqual(title, "⚡42 · \u{2055}62% 8%")
261+ XCTAssertEqual(title, "📆3%")
322262 }
323263
324264 // MARK: - Claude Window Percentages
project.yml+4−4
@@ -6,7 +6,7 @@ name: UsageMenubar
66options:
77 # Reverse-DNS prefix xcodegen uses when it needs to synthesize a bundle id.
88 # The app target pins its own id explicitly below, so this is just a default.
9− bundleIdPrefix: soy.iko
9+ bundleIdPrefix: com.ilyakooo0
1010 # Oldest macOS we support. SMAppService (launch at login) requires 13.0+;
1111 # we target 14.0 (Sonoma). Keep in sync with MACOSX_DEPLOYMENT_TARGET below.
1212 deploymentTarget:
@@ -47,7 +47,7 @@ targets:
4747 properties:
4848 CFBundleName: UsageMenubar
4949 CFBundleDisplayName: Usage Menubar
50− CFBundleIdentifier: soy.iko.usage-menubar
50+ CFBundleIdentifier: com.ilyakooo0.usage-menubar
5151 # Overwritten by CI — see the note on MARKETING_VERSION above.
5252 CFBundleVersion: "1"
5353 CFBundleShortVersionString: "1.0.0"
@@ -57,7 +57,7 @@ targets:
5757 NSHighResolutionCapable: true
5858 settings:
5959 base:
60− PRODUCT_BUNDLE_IDENTIFIER: soy.iko.usage-menubar
60+ PRODUCT_BUNDLE_IDENTIFIER: com.ilyakooo0.usage-menubar
6161 PRODUCT_NAME: UsageMenubar
6262 INFOPLIST_FILE: UsageMenubar/Info.plist
6363 # Grants the outgoing-network-connection entitlement needed to reach
@@ -84,7 +84,7 @@ targets:
8484 - target: UsageMenubar
8585 settings:
8686 base:
87− PRODUCT_BUNDLE_IDENTIFIER: soy.iko.usage-menubar.tests
87+ PRODUCT_BUNDLE_IDENTIFIER: com.ilyakooo0.usage-menubar.tests
8888 PRODUCT_NAME: UsageMenubarTests
8989 BUNDLE_LOADER: "$(TEST_HOST)"
9090 TEST_HOST: "$(BUILT_PRODUCTS_DIR)/UsageMenubar.app/Contents/MacOS/UsageMenubar"