Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5c3cc6379 | ||
|
|
c8adaf1baf | ||
|
|
3e99e1417e | ||
|
|
e41d3d0c0f | ||
|
|
1078951c43 | ||
|
|
ed6a5e1b51 | ||
|
|
8701fb1326 | ||
|
|
247b5fc202 | ||
|
|
255de797f8 | ||
|
|
0046c53283 | ||
|
|
8d222d63f0 | ||
|
|
5720370649 | ||
|
|
0843730ae1 | ||
|
|
9c030fcdd5 | ||
|
|
dd68ff39d6 | ||
|
|
b76bdf3bc1 | ||
|
|
c31950fd76 | ||
|
|
4e6ae21825 | ||
|
|
c0bf2e4873 | ||
|
|
081d4804ab | ||
|
|
be3899e188 | ||
|
|
286761e51a | ||
|
|
8e0d66468c | ||
|
|
ed64bcc9e5 | ||
|
|
b2275a74d3 | ||
|
|
3b05188a88 | ||
|
|
ea77454c10 |
@@ -0,0 +1,95 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
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
|
||||
run: |
|
||||
sed -i '' \
|
||||
's/version-[0-9][^-]*-orange/version-${{ steps.version.outputs.version }}-orange/' \
|
||||
README.md
|
||||
|
||||
- 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 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
|
||||
@@ -199,6 +199,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@objc func showPopover() {
|
||||
guard let button = statusItem?.button, let popover else { return }
|
||||
if !popover.isShown {
|
||||
NotificationCenter.default.post(name: .popoverWillOpen, object: nil)
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
@@ -209,6 +210,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
if popover.isShown {
|
||||
popover.performClose(nil)
|
||||
} else {
|
||||
NotificationCenter.default.post(name: .popoverWillOpen, object: nil)
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,13 @@ struct ContentView: View {
|
||||
}
|
||||
.frame(width: 480)
|
||||
.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) {
|
||||
LoginSheetView(isPresented: $showLogin) {
|
||||
Task { await vm.refresh() }
|
||||
@@ -774,14 +781,16 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(4)
|
||||
.fixedSize()
|
||||
} else if updater.betaAvailable {
|
||||
Text("v\(updater.latestBetaVersion) beta")
|
||||
Text("v\(updater.latestBetaVersion) available")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
Text("Built by superdooper86 & claude")
|
||||
@@ -971,6 +980,36 @@ 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
|
||||
|
||||
struct UpdateBanner: View {
|
||||
@@ -986,16 +1025,19 @@ struct UpdateBanner: View {
|
||||
Text("Update available — v\(updater.latestVersion)")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
if !updater.releaseNotes.isEmpty {
|
||||
Text(updater.releaseNotes)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
ReleaseNotesText(text: updater.releaseNotes, font: .system(size: 11))
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button("Update") { showSheet = true }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
Button { updater.updateAvailable = false } label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
@@ -1049,9 +1091,7 @@ struct UpdateSheet: View {
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
if !updater.releaseNotes.isEmpty {
|
||||
Text(updater.releaseNotes)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
ReleaseNotesText(text: updater.releaseNotes)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.0</string>
|
||||
<string>1.0.10</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>18</string>
|
||||
<string>30</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>LSUIElement</key>
|
||||
|
||||
@@ -46,6 +46,8 @@ class UpdateManager: ObservableObject {
|
||||
@Published var updateComplete = false
|
||||
@Published var justUpdated = false
|
||||
|
||||
private var notifiedVersion: String = ""
|
||||
|
||||
@Published var betaChannel: Bool = false {
|
||||
didSet {
|
||||
UserDefaults.standard.set(betaChannel, forKey: "beta_channel")
|
||||
@@ -70,14 +72,13 @@ class UpdateManager: ObservableObject {
|
||||
if let stable = await stableResult {
|
||||
print("[UpdateManager] Stable remote=\(stable.version), isNewer=\(isNewer(stable.version, than: currentVersion))")
|
||||
if isNewer(stable.version, than: currentVersion) {
|
||||
let wasAvailable = updateAvailable
|
||||
latestVersion = stable.version
|
||||
releaseNotes = stable.notes ?? ""
|
||||
downloadURL = stable.url
|
||||
updateAvailable = true
|
||||
print("[UpdateManager] updateAvailable set to true")
|
||||
// Only notify once — when first detected
|
||||
if !wasAvailable {
|
||||
if notifiedVersion != stable.version {
|
||||
notifiedVersion = stable.version
|
||||
NotificationCenter.default.post(name: .updateDetected, object: stable.version)
|
||||
}
|
||||
} else {
|
||||
@@ -86,6 +87,8 @@ class UpdateManager: ObservableObject {
|
||||
}
|
||||
} else {
|
||||
print("[UpdateManager] Stable fetch returned nil")
|
||||
updateAvailable = false
|
||||
latestVersion = ""
|
||||
}
|
||||
|
||||
if let beta = await betaResult {
|
||||
@@ -98,10 +101,19 @@ class UpdateManager: ObservableObject {
|
||||
releaseNotes = beta.notes ?? ""
|
||||
downloadURL = beta.url
|
||||
updateAvailable = true
|
||||
if notifiedVersion != beta.version {
|
||||
notifiedVersion = beta.version
|
||||
NotificationCenter.default.post(name: .updateDetected, object: beta.version)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
betaAvailable = false
|
||||
latestBetaVersion = ""
|
||||
}
|
||||
} else {
|
||||
print("[UpdateManager] Beta fetch returned nil")
|
||||
betaAvailable = false
|
||||
latestBetaVersion = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class UpdateNotificationWindowController: NSWindowController {
|
||||
|
||||
// Called when a new update is detected while running
|
||||
static func showUpdate(version: String, notes: String, near statusItem: NSStatusItem?) {
|
||||
showWindow(version: version, subtitle: notes.isEmpty ? "Tap to update now." : notes, near: statusItem, isUpdate: true)
|
||||
showWindow(version: version, subtitle: "Tap to view release notes & update now!", near: statusItem, isUpdate: true)
|
||||
}
|
||||
|
||||
static func showLimitWarning(windowName: String, percent: Int, near statusItem: NSStatusItem?) {
|
||||
@@ -39,7 +39,12 @@ class UpdateNotificationWindowController: NSWindowController {
|
||||
sharedLimit?.close()
|
||||
let content = UpdateNotificationView(
|
||||
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 = nil
|
||||
@@ -53,7 +58,16 @@ class UpdateNotificationWindowController: NSWindowController {
|
||||
|
||||
private static func showWindow(version: String, subtitle: String, near statusItem: NSStatusItem?, isUpdate: Bool) {
|
||||
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 = nil
|
||||
}
|
||||
@@ -129,6 +143,7 @@ struct UpdateNotificationView: View {
|
||||
var titleText: String? = nil
|
||||
var badgeIcon: String? = nil
|
||||
var badgeColor: Color? = nil
|
||||
var onTap: (() -> Void)? = nil
|
||||
let onDismiss: () -> Void
|
||||
|
||||
private var effectiveTitle: String {
|
||||
@@ -142,30 +157,40 @@ struct UpdateNotificationView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Image(nsImage: NSImage(named: "AppIcon") ?? NSImage())
|
||||
.resizable()
|
||||
.frame(width: 44, height: 44)
|
||||
.cornerRadius(10)
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Button(action: { onTap?() }) {
|
||||
HStack(spacing: 12) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Image(nsImage: NSImage(named: "AppIcon") ?? NSImage())
|
||||
.resizable()
|
||||
.frame(width: 44, height: 44)
|
||||
.cornerRadius(10)
|
||||
|
||||
Image(systemName: effectiveBadgeIcon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(effectiveBadgeColor)
|
||||
.background(Color(nsColor: .windowBackgroundColor).clipShape(Circle()))
|
||||
.offset(x: 4, y: 4)
|
||||
Image(systemName: effectiveBadgeIcon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(effectiveBadgeColor)
|
||||
.background(Color(nsColor: .windowBackgroundColor).clipShape(Circle()))
|
||||
.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())
|
||||
}
|
||||
|
||||
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)
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button(action: onDismiss) {
|
||||
Image(systemName: "xmark")
|
||||
@@ -173,10 +198,8 @@ struct UpdateNotificationView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(14)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 14)
|
||||
.frame(width: 340, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 13)
|
||||
.fill(Color(nsColor: .windowBackgroundColor))
|
||||
|
||||
@@ -307,7 +307,9 @@ extension Notification.Name {
|
||||
static let refreshIntervalChanged = Notification.Name("refreshIntervalChanged")
|
||||
static let closePopover = Notification.Name("closePopover")
|
||||
static let showPopover = Notification.Name("showPopover")
|
||||
static let popoverWillOpen = Notification.Name("popoverWillOpen")
|
||||
static let updateDetected = Notification.Name("updateDetected")
|
||||
static let limitWarning = Notification.Name("limitWarning")
|
||||
static let limitReset = Notification.Name("limitReset")
|
||||
static let openUpdateSheet = Notification.Name("openUpdateSheet")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
[](https://www.apple.com/macos/)
|
||||
[](https://swift.org)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](LICENSE)
|
||||
|
||||
</div>
|
||||
@@ -113,25 +113,29 @@ No API key required. It reads directly from your `claude.ai` session using a bui
|
||||
## Requirements
|
||||
|
||||
- macOS 13 Ventura or later
|
||||
- Xcode 15+ (to build from source)
|
||||
- A claude.ai account
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Download (recommended)
|
||||
### Homebrew (easiest)
|
||||
```bash
|
||||
brew tap superdooper86/tap
|
||||
brew install --cask claudechecker
|
||||
```
|
||||
|
||||
### Download
|
||||
1. Go to [Releases](https://github.com/superdooper86/claudechecker/releases)
|
||||
2. Download `ClaudeChecker.zip` from the latest release
|
||||
3. Unzip and drag `ClaudeChecker.app` to `/Applications`
|
||||
4. Open from Applications — it will appear in your menubar
|
||||
|
||||
### Build from source
|
||||
```bash
|
||||
git clone https://github.com/superdooper86/claudechecker.git
|
||||
open ClaudeChecker/ClaudeChecker.xcodeproj
|
||||
```
|
||||
Then in Xcode: set your Team under Signing & Capabilities, then **⌘R** to run.
|
||||
> **macOS security prompt?** If macOS blocks the app on first launch, run this in Terminal:
|
||||
> ```bash
|
||||
> xattr -cr /Applications/ClaudeChecker.app
|
||||
> ```
|
||||
> This is not needed when installing via Homebrew.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## What's new in v1.0.10
|
||||
|
||||
### Main window always opens on click
|
||||
Closing the app while Settings was open would cause the next click to reopen straight to Settings. The app now always returns to the main window when reopened.
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": "3.1.0-beta",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v3.0.0/ClaudeChecker.zip",
|
||||
"notes": "Beta test build."
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "3.0.0",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v3.0.0/ClaudeChecker.zip",
|
||||
"notes": "Limit notifications for 80% and 95% usage, and on quota reset."
|
||||
"version": "1.1.0",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.0/ClaudeChecker.zip",
|
||||
"notes": "## What's new in v1.1.0\n\n### Main window always opens on click\nClosing the app while Settings was open would cause the next click to reopen straight to Settings. The app now always returns to the main window when reopened."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user