Compare commits

..
50 Commits
Author SHA1 Message Date
superdooper86 02440c5340 Add fake beta version 3.1.0-beta for testing 2026-05-07 16:06:23 +02:00
superdooper86 6fe3e9722b v3.0.0 — built-by credit fix, version bump 2026-05-07 16:03:23 +02:00
superdooper86 f5114a31f8 README: add notification details, split update section, remove internal beta notes 2026-05-07 15:59:28 +02:00
superdooper86 4f260a3d4b v2.0.10 — limit notifications, fix positioning, add notification screenshots 2026-05-07 15:57:06 +02:00
superdooper86 3d1bae3d0a v2.0.9 — limit notifications using custom floating window (same style as update notifications) 2026-05-07 15:44:38 +02:00
superdooper86 b95102cb1b v2.0.9 — limit notifications at 80%/95% and on reset 2026-05-07 15:40:13 +02:00
superdooper86 2b48114996 Rename screenshots to bust CDN cache 2026-05-07 15:29:47 +02:00
superdooper86 6a4bb73c4f v2.0.8 — Fix data source endpoints in settings, update screenshots 2026-05-07 15:28:08 +02:00
superdooper86 d2815d65ce v2.0.7 — Show account email in settings auth section 2026-05-07 15:21:35 +02:00
superdooper86 a1c3c0b314 v2.0.6 — Auth section shows sign-in status, re-authenticate and sign out buttons 2026-05-07 15:17:11 +02:00
superdooper86 d3ec3f5dd7 revert: restore v2.0.5 after testing 2026-05-07 14:47:04 +02:00
superdooper86 5fee0af18b temp: bump fake version to 9.9.10 2026-05-07 14:37:39 +02:00
superdooper86 4edca2962c temp: fake update for testing 2026-05-07 14:33:51 +02:00
superdooper86 ef2cdd8bf3 v2.0.5 — Fixed window size on open, update banner now clears when up to date 2026-05-07 14:28:33 +02:00
superdooper86 2da3eeaf72 Improve screenshots layout with grid table 2026-05-07 14:24:36 +02:00
superdooper86 d229581de1 Add screenshots to README 2026-05-07 14:21:26 +02:00
superdooper86 169cfd8950 revert: restore v2.0.4 after screenshot 2026-05-07 14:17:37 +02:00
superdooper86 a5218fa44f temp: bump version for screenshot 2026-05-07 14:16:08 +02:00
superdooper86 1f7afab61c Update Data Sources section with new billing endpoints 2026-05-07 14:06:46 +02:00
superdooper86 1507c6490f v2.0.4 — Show spent, limit and balance from dedicated billing endpoints 2026-05-07 14:04:19 +02:00
superdooper86 ccf3468621 v2.0.3 — Clean up extra usage display, remove non-existent balance field 2026-05-07 13:56:31 +02:00
superdooper86 3199b228a8 v2.0.2 — Show spent, limit, and balance in extra usage section 2026-05-07 13:46:36 +02:00
superdooper86 255f3f6cd5 v2.0.2 — Fix unlimited monthly limit display in extra usage section 2026-05-07 13:44:05 +02:00
superdooper86 773fcb1a46 Bump to v2.0.1 2026-05-07 13:39:18 +02:00
superdooper86 464884e88f Fix crash when extra_usage fields are null 2026-05-07 13:36:23 +02:00
superdooper86 edd8d42c8d Bump CFBundleShortVersionString to 2.0 2026-05-07 13:33:16 +02:00
superdooper86 e123b48812 Bump to v2.0 2026-05-07 13:22:06 +02:00
superdooper86 603d405c87 Fix icon URL in README to raw GitHub path 2026-05-07 13:19:25 +02:00
superdooper86 f976a90875 Add Xcode project source for v1.6 2026-05-07 13:18:00 +02:00
SuperDooper c79bd41504 Bump version to 1.6 and update download URL 2026-05-07 12:53:37 +02:00
SuperDooper dfbd276616 Remove instructions from README
Removed instructions
2026-05-07 12:38:27 +02:00
SuperDooper dbf14bdd70 Update contributors in README.md 2026-05-07 12:37:08 +02:00
SuperDooper 6c894c32a4 Create README.md for ClaudeChecker app
Add README for ClaudeChecker macOS app with features and installation instructions.
2026-05-07 12:36:35 +02:00
SuperDooper 345150e894 Update version to 1.5 with notification fixes 2026-05-07 12:32:57 +02:00
SuperDooper d724622533 Downgrade version from 1.5 to 1.4 2026-05-07 12:21:37 +02:00
SuperDooper 8ecb5f3670 Bump version to 1.5 and update release notes 2026-05-07 12:17:08 +02:00
SuperDooper cf6f9184e3 Remove beta version information from JSON 2026-05-07 12:16:49 +02:00
SuperDooper d734fec836 Update version to 1.4 and change download URL 2026-05-07 12:08:18 +02:00
SuperDooper b6542e099a Update version to 1.3 and change download URL 2026-05-07 11:56:24 +02:00
SuperDooper c4e798b484 Update download URL in version.json 2026-05-07 11:47:15 +02:00
SuperDooper 0dd1ac3a8f Update version.json for release 1.2
Updated the version URL and release notes for version 1.2.
2026-05-07 11:45:28 +02:00
SuperDooper 2d8a3629da Rename version-beta to version-beta.json 2026-05-07 11:35:10 +02:00
SuperDooper e07af519a6 Rename version to version.json 2026-05-07 11:35:00 +02:00
SuperDooper 6b1f85dbaf Update version to 1.2 2026-05-07 11:31:12 +02:00
SuperDooper 74fe785898 Add version information for beta release 2026-05-07 11:26:21 +02:00
SuperDooper 3de4e09876 Create version-beta 2026-05-07 11:24:20 +02:00
SuperDooper 43dbb155f5 Delete version-beta.json 2026-05-07 11:24:08 +02:00
SuperDooper 79209eee6f Create version-beta.json 2026-05-07 11:23:50 +02:00
SuperDooper d9598a70ea Add version file with release information 2026-05-07 10:59:12 +02:00
SuperDooper c8cae1827f Initial commit 2026-05-07 10:58:23 +02:00
13 changed files with 95 additions and 446 deletions
-79
View File
@@ -1,79 +0,0 @@
name: Release Beta
on:
push:
tags:
- 'v*-beta*'
jobs:
release-beta:
runs-on: macos-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Build
run: |
xcodebuild \
-project ClaudeChecker.xcodeproj \
-scheme ClaudeChecker \
-configuration Release \
-derivedDataPath build \
clean build
- name: Zip app
run: |
ditto -c -k --keepParent \
build/Build/Products/Release/ClaudeChecker.app \
ClaudeChecker.zip
- name: Create GitHub pre-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \
--title "$GITHUB_REF_NAME" \
--prerelease \
--notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Beta release ${{ steps.version.outputs.version }}')"
- name: Update version-beta.json
run: |
notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "Beta release ${{ steps.version.outputs.version }}")
jq -n \
--arg version "${{ steps.version.outputs.version }}" \
--arg url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/ClaudeChecker.zip" \
--arg notes "$notes" \
'{version: $version, url: $url, notes: $notes}' > version-beta.json
- name: Update beta badge in README
run: |
shields_version=$(echo "${{ steps.version.outputs.version }}" | sed 's/-/--/g')
tag="${{ github.ref_name }}"
export BADGE="[![Beta](https://img.shields.io/badge/beta-${shields_version}-orange?style=flat)](https://github.com/${{ github.repository }}/releases/tag/${tag}) <!-- BETA_BADGE -->"
python3 -c "
import os
badge = os.environ['BADGE']
lines = open('README.md').readlines()
out = []
for line in lines:
out.append(badge + chr(10) if '<!-- BETA_BADGE -->' in line else line)
open('README.md', 'w').writelines(out)
"
- name: Commit version-beta.json and README
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add version-beta.json README.md
git commit -m "Beta release ${{ github.ref_name }}"
git pull --rebase origin main
git push origin HEAD:main
-111
View File
@@ -1,111 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
if: ${{ !contains(github.ref_name, '-') }}
runs-on: macos-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Build
run: |
xcodebuild \
-project ClaudeChecker.xcodeproj \
-scheme ClaudeChecker \
-configuration Release \
-derivedDataPath build \
clean build
- name: Zip app
run: |
ditto -c -k --keepParent \
build/Build/Products/Release/ClaudeChecker.app \
ClaudeChecker.zip
- name: Compute SHA256
id: sha
run: echo "sha256=$(shasum -a 256 ClaudeChecker.zip | awk '{print $1}')" >> $GITHUB_OUTPUT
- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \
--title "$GITHUB_REF_NAME" \
--notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Release ${{ steps.version.outputs.version }}')"
- name: Update version.json
run: |
notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "Release ${{ steps.version.outputs.version }}")
jq -n \
--arg version "${{ steps.version.outputs.version }}" \
--arg url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/ClaudeChecker.zip" \
--arg notes "$notes" \
'{version: $version, url: $url, notes: $notes}' > version.json
- name: Update Homebrew cask
run: |
sed -i '' \
-e 's/version "[^"]*"/version "${{ steps.version.outputs.version }}"/' \
-e 's/sha256 "[^"]*"/sha256 "${{ steps.sha.outputs.sha256 }}"/' \
../homebrew-tap/Casks/claudechecker.rb 2>/dev/null || true
- name: Update README badge
env:
NEW_VERSION: ${{ steps.version.outputs.version }}
run: |
python3 -c "
import os, re
v = os.environ['NEW_VERSION']
content = open('README.md').read()
content = re.sub(r'version-[0-9][^-]*-orange', 'version-' + v + '-orange', content)
lines = content.splitlines(keepends=True)
out = []
for line in lines:
if '<!-- BETA_BADGE -->' in line:
continue
out.append(line)
if 'license-MIT-blue' in line:
out.append('<!-- BETA_BADGE -->' + chr(10))
open('README.md', 'w').writelines(out)
"
- name: Commit version.json and README
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add version.json README.md
git commit -m "Release ${{ github.ref_name }}"
git pull --rebase origin main
git push origin HEAD:main
- name: Update Homebrew tap
env:
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
run: |
git clone https://x-access-token:${{ secrets.TAP_TOKEN }}@github.com/${{ github.repository_owner }}/homebrew-tap /tmp/homebrew-tap
sed -i '' \
-e 's/version "[^"]*"/version "${{ steps.version.outputs.version }}"/' \
-e 's/sha256 "[^"]*"/sha256 "${{ steps.sha.outputs.sha256 }}"/' \
/tmp/homebrew-tap/Casks/claudechecker.rb
cd /tmp/homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/claudechecker.rb
git commit -m "Update claudechecker to ${{ github.ref_name }}"
git push
+3 -18
View File
@@ -40,15 +40,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Popover // Popover
popover = NSPopover() popover = NSPopover()
popover?.contentSize = NSSize(width: 480, height: 640)
popover?.behavior = .transient popover?.behavior = .transient
popover?.animates = true popover?.animates = true
let hc = NSHostingController( popover?.contentViewController = NSHostingController(
rootView: ContentView() rootView: ContentView()
.environmentObject(usageViewModel) .environmentObject(usageViewModel)
.environmentObject(updateManager) .environmentObject(updateManager)
) )
hc.sizingOptions = .preferredContentSize
popover?.contentViewController = hc
// React to limits or menubar toggle changes // React to limits or menubar toggle changes
usageViewModel.$limits usageViewModel.$limits
@@ -61,11 +60,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
.sink { [weak self] _ in self?.updateStatusItem() } .sink { [weak self] _ in self?.updateStatusItem() }
.store(in: &cancellables) .store(in: &cancellables)
updateManager.$updateAvailable
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.updateStatusItem() }
.store(in: &cancellables)
// Refresh timer // Refresh timer
scheduleTimer(interval: usageViewModel.refreshInterval) scheduleTimer(interval: usageViewModel.refreshInterval)
@@ -172,13 +166,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let sdStr = sd.map { "\(Int($0.usedPercent.rounded()))%" } ?? "" let sdStr = sd.map { "\(Int($0.usedPercent.rounded()))%" } ?? ""
button.image = nil button.image = nil
button.attributedTitle = NSAttributedString(string: "") button.attributedTitle = NSAttributedString(string: "")
let updatePrefix = updateManager.updateAvailable ? "↑ Update " : "" button.title = "\(fhStr) \(sdStr)"
button.title = "\(updatePrefix)\(fhStr) \(sdStr)"
button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium)
} else if updateManager.updateAvailable {
button.image = nil
button.attributedTitle = NSAttributedString(string: "")
button.title = "↑ Update"
button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium) button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium)
} else { } else {
button.title = "" button.title = ""
@@ -211,7 +199,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc func showPopover() { @objc func showPopover() {
guard let button = statusItem?.button, let popover else { return } guard let button = statusItem?.button, let popover else { return }
if !popover.isShown { if !popover.isShown {
NotificationCenter.default.post(name: .popoverWillOpen, object: nil)
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@@ -222,7 +209,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if popover.isShown { if popover.isShown {
popover.performClose(nil) popover.performClose(nil)
} else { } else {
NotificationCenter.default.post(name: .popoverWillOpen, object: nil)
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@@ -237,4 +223,3 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
+32 -85
View File
@@ -47,29 +47,13 @@ struct ContentView: View {
EmptyStateView() EmptyStateView()
.padding(.vertical, 40) .padding(.vertical, 40)
} else { } else {
VStack(spacing: 8) { VStack(spacing: 1) {
ForEach(vm.limits) { limit in ForEach(vm.limits) { limit in
VStack(spacing: 0) { LimitCard(limit: limit)
HStack {
Text(limit.window == .fiveHour ? "5 Hour Limit" : "7 Day Limit")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.textCase(.uppercase)
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 10)
.padding(.bottom, 4)
LimitCard(limit: limit)
.background(Color(nsColor: .controlBackgroundColor))
.cornerRadius(8)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.primary.opacity(0.08), lineWidth: 0.5))
.padding(.horizontal, 8)
}
} }
} }
.padding(.bottom, 4) .background(Color.primary.opacity(0.05))
.padding(.vertical, 1)
// Extra credits row // Extra credits row
if let extra = vm.extraUsage, extra.isEnabled { if let extra = vm.extraUsage, extra.isEnabled {
@@ -110,13 +94,6 @@ struct ContentView: View {
} }
.frame(width: 480) .frame(width: 480)
.animation(.easeInOut(duration: 0.2), value: showSettings) .animation(.easeInOut(duration: 0.2), value: showSettings)
.onReceive(NotificationCenter.default.publisher(for: .popoverWillOpen)) { _ in
showSettings = false
}
.onReceive(NotificationCenter.default.publisher(for: .openUpdateSheet)) { _ in
showSettings = false
showUpdateSheet = true
}
.sheet(isPresented: $showLogin) { .sheet(isPresented: $showLogin) {
LoginSheetView(isPresented: $showLogin) { LoginSheetView(isPresented: $showLogin) {
Task { await vm.refresh() } Task { await vm.refresh() }
@@ -212,7 +189,7 @@ struct LimitCard: View {
AgentIconView(agent: .claude, size: 14) AgentIconView(agent: .claude, size: 14)
Text("Claude") Text("Claude")
.font(.system(size: 13, weight: .semibold)) .font(.system(size: 13, weight: .semibold))
Text("· \(vm.planLabel)") Text("· \(vm.planLabel) · \(limit.window.label)")
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(.secondary) .foregroundColor(.secondary)
Spacer() Spacer()
@@ -429,9 +406,7 @@ struct SparklineView: View {
var body: some View { var body: some View {
GeometryReader { geo in GeometryReader { geo in
let plotData = data.count == 1 ? [data[0], data[0]] : data if data.count > 1 {
if plotData.count > 1 {
let data = plotData
let w = geo.size.width let w = geo.size.width
let h = geo.size.height let h = geo.size.height
let minV = data.min() ?? 0 let minV = data.min() ?? 0
@@ -791,23 +766,23 @@ struct SettingsView: View {
.padding(.vertical, 1) .padding(.vertical, 1)
.background(Color.primary.opacity(0.06)) .background(Color.primary.opacity(0.06))
.cornerRadius(4) .cornerRadius(4)
} if updater.updateAvailable {
if updater.updateAvailable { Text("v\(updater.latestVersion) available")
Text("v\(updater.latestVersion) available") .font(.system(size: 10, weight: .medium))
.font(.system(size: 10, weight: .medium)) .foregroundColor(.white)
.foregroundColor(.white) .padding(.horizontal, 6)
.padding(.horizontal, 6) .padding(.vertical, 2)
.padding(.vertical, 2) .background(Color.blue)
.background(Color.blue) .cornerRadius(4)
.cornerRadius(4) } else if updater.betaAvailable {
} else if updater.betaAvailable { Text("v\(updater.latestBetaVersion) beta")
Text("v\(updater.latestBetaVersion) available") .font(.system(size: 10, weight: .medium))
.font(.system(size: 10, weight: .medium)) .foregroundColor(.white)
.foregroundColor(.white) .padding(.horizontal, 6)
.padding(.horizontal, 6) .padding(.vertical, 2)
.padding(.vertical, 2) .background(Color.orange)
.background(Color.orange) .cornerRadius(4)
.cornerRadius(4) }
} }
Text("Built by superdooper86 & claude") Text("Built by superdooper86 & claude")
.font(.system(size: 11)) .font(.system(size: 11))
@@ -996,36 +971,6 @@ struct UpdateSuccessBanner: View {
} }
} }
// MARK: - Markdown notes renderer
struct ReleaseNotesText: View {
let text: String
var font: Font = .system(size: 12)
var body: some View {
let processed = text
.split(separator: "\n", omittingEmptySubsequences: false)
.map { line -> String in
let s = String(line)
if s.hasPrefix("### ") { return "**\(s.dropFirst(4))**" }
if s.hasPrefix("## ") { return "**\(s.dropFirst(3))**" }
if s.hasPrefix("# ") { return "**\(s.dropFirst(2))**" }
if s.hasPrefix("- ") { return "\(s.dropFirst(2))" }
return s
}
.joined(separator: "\n")
if let attr = try? AttributedString(markdown: processed,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
Text(attr).font(font).foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
Text(text).font(font).foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
// MARK: - Update Banner // MARK: - Update Banner
struct UpdateBanner: View { struct UpdateBanner: View {
@@ -1040,9 +985,12 @@ struct UpdateBanner: View {
VStack(alignment: .leading, spacing: 1) { VStack(alignment: .leading, spacing: 1) {
Text("Update available — v\(updater.latestVersion)") Text("Update available — v\(updater.latestVersion)")
.font(.system(size: 12, weight: .semibold)) .font(.system(size: 12, weight: .semibold))
Text("Tap Update to view what's new & install.") if !updater.releaseNotes.isEmpty {
.font(.system(size: 11)) Text(updater.releaseNotes)
.foregroundColor(.secondary) .font(.system(size: 11))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
} }
Spacer() Spacer()
Button("Update") { showSheet = true } Button("Update") { showSheet = true }
@@ -1053,7 +1001,6 @@ struct UpdateBanner: View {
.padding(.vertical, 10) .padding(.vertical, 10)
.background(Color.blue.opacity(0.08)) .background(Color.blue.opacity(0.08))
.overlay(Divider(), alignment: .bottom) .overlay(Divider(), alignment: .bottom)
.onTapGesture { showSheet = true }
} }
} }
@@ -1102,7 +1049,9 @@ struct UpdateSheet: View {
.foregroundColor(.blue) .foregroundColor(.blue)
} }
if !updater.releaseNotes.isEmpty { if !updater.releaseNotes.isEmpty {
ReleaseNotesText(text: updater.releaseNotes) Text(updater.releaseNotes)
.font(.system(size: 12))
.foregroundColor(.secondary)
} }
} }
Spacer() Spacer()
@@ -1171,5 +1120,3 @@ struct UpdateSheet: View {
.frame(width: 380) .frame(width: 380)
} }
} }
+2 -4
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.1.3-beta.4</string> <string>3.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>38</string> <string>18</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
@@ -45,5 +45,3 @@
</dict> </dict>
</dict> </dict>
</plist> </plist>
+6 -21
View File
@@ -24,27 +24,21 @@ struct LoginWebView: NSViewRepresentable {
class Coordinator: NSObject, WKNavigationDelegate { class Coordinator: NSObject, WKNavigationDelegate {
let onAuthenticated: () -> Void let onAuthenticated: () -> Void
var didAuthenticate = false
init(onAuthenticated: @escaping () -> Void) { init(onAuthenticated: @escaping () -> Void) {
self.onAuthenticated = onAuthenticated self.onAuthenticated = onAuthenticated
} }
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard !didAuthenticate else { return }
// Don't fire on the login/auth pages themselves
if let url = webView.url?.absoluteString,
url.contains("/login") || url.contains("/auth") { return }
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
let hasSession = cookies.contains { let hasSession = cookies.contains {
$0.domain.contains("claude.ai") && $0.domain.contains("claude.ai") &&
($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token") ($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token")
} }
guard hasSession, !self.didAuthenticate else { return } if hasSession {
self.didAuthenticate = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.onAuthenticated()
self.onAuthenticated() }
} }
} }
} }
@@ -64,18 +58,9 @@ struct LoginSheetView: View {
Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude") Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude")
.font(.system(size: 13, weight: .semibold)) .font(.system(size: 13, weight: .semibold))
Spacer() Spacer()
if authenticated { Button("Cancel") { isPresented = false }
Button("Done") { .buttonStyle(.bordered)
isPresented = false
onDone()
}
.buttonStyle(.borderedProminent)
.controlSize(.small) .controlSize(.small)
} else {
Button("Cancel") { isPresented = false }
.buttonStyle(.bordered)
.controlSize(.small)
}
} }
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.vertical, 10) .padding(.vertical, 10)
+8 -38
View File
@@ -46,8 +46,6 @@ class UpdateManager: ObservableObject {
@Published var updateComplete = false @Published var updateComplete = false
@Published var justUpdated = false @Published var justUpdated = false
private var notifiedVersion: String = ""
@Published var betaChannel: Bool = false { @Published var betaChannel: Bool = false {
didSet { didSet {
UserDefaults.standard.set(betaChannel, forKey: "beta_channel") UserDefaults.standard.set(betaChannel, forKey: "beta_channel")
@@ -72,13 +70,14 @@ class UpdateManager: ObservableObject {
if let stable = await stableResult { if let stable = await stableResult {
print("[UpdateManager] Stable remote=\(stable.version), isNewer=\(isNewer(stable.version, than: currentVersion))") print("[UpdateManager] Stable remote=\(stable.version), isNewer=\(isNewer(stable.version, than: currentVersion))")
if isNewer(stable.version, than: currentVersion) { if isNewer(stable.version, than: currentVersion) {
let wasAvailable = updateAvailable
latestVersion = stable.version latestVersion = stable.version
releaseNotes = stable.notes ?? "" releaseNotes = stable.notes ?? ""
downloadURL = stable.url downloadURL = stable.url
updateAvailable = true updateAvailable = true
print("[UpdateManager] updateAvailable set to true") print("[UpdateManager] updateAvailable set to true")
if notifiedVersion != stable.version { // Only notify once when first detected
notifiedVersion = stable.version if !wasAvailable {
NotificationCenter.default.post(name: .updateDetected, object: stable.version) NotificationCenter.default.post(name: .updateDetected, object: stable.version)
} }
} else { } else {
@@ -87,8 +86,6 @@ class UpdateManager: ObservableObject {
} }
} else { } else {
print("[UpdateManager] Stable fetch returned nil") print("[UpdateManager] Stable fetch returned nil")
updateAvailable = false
latestVersion = ""
} }
if let beta = await betaResult { if let beta = await betaResult {
@@ -101,19 +98,10 @@ class UpdateManager: ObservableObject {
releaseNotes = beta.notes ?? "" releaseNotes = beta.notes ?? ""
downloadURL = beta.url downloadURL = beta.url
updateAvailable = true updateAvailable = true
if notifiedVersion != beta.version {
notifiedVersion = beta.version
NotificationCenter.default.post(name: .updateDetected, object: beta.version)
}
} }
} else {
betaAvailable = false
latestBetaVersion = ""
} }
} else { } else {
print("[UpdateManager] Beta fetch returned nil") print("[UpdateManager] Beta fetch returned nil")
betaAvailable = false
latestBetaVersion = ""
} }
} }
@@ -318,31 +306,13 @@ class UpdateManager: ObservableObject {
} }
private func isNewer(_ version: String, than current: String) -> Bool { private func isNewer(_ version: String, than current: String) -> Bool {
func parse(_ v: String) -> (base: [Int], pre: [Int]?) { let toInts = { (v: String) in v.split(separator: ".").compactMap { Int($0) } }
guard !v.isEmpty else { return ([], nil) } let a = toInts(version), b = toInts(current)
let halves = v.split(separator: "-", maxSplits: 1) for i in 0..<max(a.count, b.count) {
let base = halves[0].split(separator: ".").compactMap { Int($0) } let av = i < a.count ? a[i] : 0
let pre = halves.count > 1 ? String(halves[1]).split(separator: ".").compactMap { Int($0) } : nil let bv = i < b.count ? b[i] : 0
return (base, pre)
}
let a = parse(version), b = parse(current)
// Compare base version numbers
for i in 0..<max(a.base.count, b.base.count) {
let av = i < a.base.count ? a.base[i] : 0
let bv = i < b.base.count ? b.base[i] : 0
if av != bv { return av > bv } if av != bv { return av > bv }
} }
// Base equal: stable beats pre-release
if a.pre == nil && b.pre != nil { return true }
if a.pre != nil && b.pre == nil { return false }
// Both pre-release: compare pre-release numbers
if let ap = a.pre, let bp = b.pre {
for i in 0..<max(ap.count, bp.count) {
let av = i < ap.count ? ap[i] : 0
let bv = i < bp.count ? bp[i] : 0
if av != bv { return av > bv }
}
}
return false return false
} }
} }
+28 -51
View File
@@ -14,7 +14,7 @@ class UpdateNotificationWindowController: NSWindowController {
// Called when a new update is detected while running // Called when a new update is detected while running
static func showUpdate(version: String, notes: String, near statusItem: NSStatusItem?) { static func showUpdate(version: String, notes: String, near statusItem: NSStatusItem?) {
showWindow(version: version, subtitle: "Tap to view release notes & update now!", near: statusItem, isUpdate: true) showWindow(version: version, subtitle: notes.isEmpty ? "Tap to update now." : notes, near: statusItem, isUpdate: true)
} }
static func showLimitWarning(windowName: String, percent: Int, near statusItem: NSStatusItem?) { static func showLimitWarning(windowName: String, percent: Int, near statusItem: NSStatusItem?) {
@@ -39,12 +39,7 @@ class UpdateNotificationWindowController: NSWindowController {
sharedLimit?.close() sharedLimit?.close()
let content = UpdateNotificationView( let content = UpdateNotificationView(
version: "", subtitle: subtitle, isUpdate: false, version: "", subtitle: subtitle, isUpdate: false,
titleText: title, badgeIcon: badgeIcon, badgeColor: badgeColor, titleText: title, badgeIcon: badgeIcon, badgeColor: badgeColor
onTap: {
sharedLimit?.close()
sharedLimit = nil
NotificationCenter.default.post(name: .showPopover, object: nil)
}
) { ) {
sharedLimit?.close() sharedLimit?.close()
sharedLimit = nil sharedLimit = nil
@@ -58,16 +53,7 @@ class UpdateNotificationWindowController: NSWindowController {
private static func showWindow(version: String, subtitle: String, near statusItem: NSStatusItem?, isUpdate: Bool) { private static func showWindow(version: String, subtitle: String, near statusItem: NSStatusItem?, isUpdate: Bool) {
shared?.close() shared?.close()
let content = UpdateNotificationView(version: version, subtitle: subtitle, isUpdate: isUpdate, let content = UpdateNotificationView(version: version, subtitle: subtitle, isUpdate: isUpdate) {
onTap: {
shared?.close()
shared = nil
NotificationCenter.default.post(name: .showPopover, object: nil)
if isUpdate {
NotificationCenter.default.post(name: .openUpdateSheet, object: nil)
}
}
) {
shared?.close() shared?.close()
shared = nil shared = nil
} }
@@ -143,7 +129,6 @@ struct UpdateNotificationView: View {
var titleText: String? = nil var titleText: String? = nil
var badgeIcon: String? = nil var badgeIcon: String? = nil
var badgeColor: Color? = nil var badgeColor: Color? = nil
var onTap: (() -> Void)? = nil
let onDismiss: () -> Void let onDismiss: () -> Void
private var effectiveTitle: String { private var effectiveTitle: String {
@@ -157,40 +142,30 @@ struct UpdateNotificationView: View {
} }
var body: some View { var body: some View {
ZStack(alignment: .topTrailing) { HStack(spacing: 12) {
Button(action: { onTap?() }) { ZStack(alignment: .bottomTrailing) {
HStack(spacing: 12) { Image(nsImage: NSImage(named: "AppIcon") ?? NSImage())
ZStack(alignment: .bottomTrailing) { .resizable()
Image(nsImage: NSImage(named: "AppIcon") ?? NSImage()) .frame(width: 44, height: 44)
.resizable() .cornerRadius(10)
.frame(width: 44, height: 44)
.cornerRadius(10)
Image(systemName: effectiveBadgeIcon) Image(systemName: effectiveBadgeIcon)
.font(.system(size: 16, weight: .semibold)) .font(.system(size: 16, weight: .semibold))
.foregroundColor(effectiveBadgeColor) .foregroundColor(effectiveBadgeColor)
.background(Color(nsColor: .windowBackgroundColor).clipShape(Circle())) .background(Color(nsColor: .windowBackgroundColor).clipShape(Circle()))
.offset(x: 4, y: 4) .offset(x: 4, y: 4)
}
VStack(alignment: .leading, spacing: 3) {
Text(effectiveTitle)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.primary)
Text(subtitle)
.font(.system(size: 11))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 24)
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
.frame(width: 340, alignment: .leading)
.contentShape(Rectangle())
} }
.buttonStyle(.plain)
VStack(alignment: .leading, spacing: 3) {
Text(effectiveTitle)
.font(.system(size: 13, weight: .semibold))
Text(subtitle)
.font(.system(size: 11))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 8)
Button(action: onDismiss) { Button(action: onDismiss) {
Image(systemName: "xmark") Image(systemName: "xmark")
@@ -198,8 +173,10 @@ struct UpdateNotificationView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(14)
} }
.padding(.horizontal, 14)
.padding(.vertical, 14)
.frame(width: 340, alignment: .leading)
.background( .background(
RoundedRectangle(cornerRadius: 13) RoundedRectangle(cornerRadius: 13)
.fill(Color(nsColor: .windowBackgroundColor)) .fill(Color(nsColor: .windowBackgroundColor))
+1 -8
View File
@@ -37,9 +37,6 @@ class UsageViewModel: ObservableObject {
let saved = UserDefaults.standard.double(forKey: "refresh_interval") let saved = UserDefaults.standard.double(forKey: "refresh_interval")
refreshInterval = saved > 0 ? saved : 60 refreshInterval = saved > 0 ? saved : 60
showInMenuBar = UserDefaults.standard.object(forKey: "show_in_menubar") as? Bool ?? true showInMenuBar = UserDefaults.standard.object(forKey: "show_in_menubar") as? Bool ?? true
if let saved = UserDefaults.standard.object(forKey: "burn_history") as? [String: [Double]] {
burnHistoryStore = saved
}
loadPlaceholderData() loadPlaceholderData()
} }
@@ -87,9 +84,8 @@ class UsageViewModel: ObservableObject {
history.append(limits[i].usedPercent) history.append(limits[i].usedPercent)
if history.count > maxHistorySamples { history.removeFirst() } if history.count > maxHistorySamples { history.removeFirst() }
burnHistoryStore[key] = history burnHistoryStore[key] = history
limits[i].burnHistory = history if history.count > 1 { limits[i].burnHistory = history }
} }
UserDefaults.standard.set(burnHistoryStore, forKey: "burn_history")
checkLimitNotifications(for: limits) checkLimitNotifications(for: limits)
} catch AppError.notAuthenticated { } catch AppError.notAuthenticated {
isNotAuthenticated = true isNotAuthenticated = true
@@ -311,10 +307,7 @@ extension Notification.Name {
static let refreshIntervalChanged = Notification.Name("refreshIntervalChanged") static let refreshIntervalChanged = Notification.Name("refreshIntervalChanged")
static let closePopover = Notification.Name("closePopover") static let closePopover = Notification.Name("closePopover")
static let showPopover = Notification.Name("showPopover") static let showPopover = Notification.Name("showPopover")
static let popoverWillOpen = Notification.Name("popoverWillOpen")
static let updateDetected = Notification.Name("updateDetected") static let updateDetected = Notification.Name("updateDetected")
static let limitWarning = Notification.Name("limitWarning") static let limitWarning = Notification.Name("limitWarning")
static let limitReset = Notification.Name("limitReset") static let limitReset = Notification.Name("limitReset")
static let openUpdateSheet = Notification.Name("openUpdateSheet")
} }
+9 -14
View File
@@ -8,9 +8,8 @@
[![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/) [![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/)
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org) [![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org)
[![Version](https://img.shields.io/badge/version-1.1.2-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases) [![Version](https://img.shields.io/badge/version-3.0.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
[![Beta](https://img.shields.io/badge/beta-1.1.3--beta.3-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.1.3-beta.3) <!-- BETA_BADGE -->
</div> </div>
@@ -114,29 +113,25 @@ No API key required. It reads directly from your `claude.ai` session using a bui
## Requirements ## Requirements
- macOS 13 Ventura or later - macOS 13 Ventura or later
- Xcode 15+ (to build from source)
- A claude.ai account - A claude.ai account
--- ---
## Installation ## Installation
### Homebrew (easiest) ### Download (recommended)
```bash
brew tap superdooper86/tap
brew install --cask claudechecker
```
### Download
1. Go to [Releases](https://github.com/superdooper86/claudechecker/releases) 1. Go to [Releases](https://github.com/superdooper86/claudechecker/releases)
2. Download `ClaudeChecker.zip` from the latest release 2. Download `ClaudeChecker.zip` from the latest release
3. Unzip and drag `ClaudeChecker.app` to `/Applications` 3. Unzip and drag `ClaudeChecker.app` to `/Applications`
4. Open from Applications — it will appear in your menubar 4. Open from Applications — it will appear in your menubar
> **macOS security prompt?** If macOS blocks the app on first launch, run this in Terminal: ### Build from source
> ```bash ```bash
> xattr -cr /Applications/ClaudeChecker.app git clone https://github.com/superdooper86/claudechecker.git
> ``` open ClaudeChecker/ClaudeChecker.xcodeproj
> This is not needed when installing via Homebrew. ```
Then in Xcode: set your Team under Signing & Capabilities, then **⌘R** to run.
--- ---
-11
View File
@@ -1,11 +0,0 @@
## What's new in v1.1.3
### UI polish
- Limit headers now read "5 Hour Limit" and "7 Day Limit" instead of "5hr"
- Tapping anywhere on the blue update banner now opens the update window
### Session Diary fix
- Burn history is now persisted across app launches — the sparkline populates immediately on first open
### Window sizing fix
- The popover now resizes correctly when switching between the main view and settings — no more empty space below the main view
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.1.3-beta.3", "version": "3.1.0-beta",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.3-beta.3/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v3.0.0/ClaudeChecker.zip",
"notes": "## What's new in v1.1.3\n\n### UI polish\n- Limit headers now read \"5 Hour Limit\" and \"7 Day Limit\" instead of \"5hr\"\n- Tapping anywhere on the blue update banner now opens the update window\n\n### Session Diary fix\n- Burn history is now persisted across app launches — the sparkline populates immediately on first open instead of requiring a manual refresh" "notes": "Beta test build."
} }
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.1.2", "version": "3.0.0",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.2/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v3.0.0/ClaudeChecker.zip",
"notes": "## What's new in v1.1.2\n\n### Cleaner usage cards\nRemoved the redundant \"5h\" / \"7d\" labels from each card row — the section headers already make it clear which limit is which.\n\n### Beta update support\nThe app now correctly detects and compares beta versions (e.g. v1.1.2-beta.2 > v1.1.2-beta.1) when the beta channel is enabled in Settings. Stable releases also correctly supersede beta builds of the same version — so users on a beta are prompted to install the final release.\n\n### Fixed crash when enabling beta channel\nEnabling the beta toggle could crash the app if no stable update was available. This is now fixed.\n\n### Fixed settings panel truncation\nThe app name and version number no longer get cut off when an update badge is displayed in Settings." "notes": "Limit notifications for 80% and 95% usage, and on quota reset."
} }