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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,30 @@ jobs:
- name: Generate release notes
shell: bash
run: |
# Try to find release notes from the merged PR
COMMIT_SHA=$(git rev-list -1 ${{ github.ref_name }})
PR_BODY=$(gh api "repos/TobiiNT/ClaudeTracker/commits/$COMMIT_SHA/pulls" --jq '.[0].body // empty' 2>/dev/null || echo "")
PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "")
PR_BODY=""

# Strategy 1: Check each commit between tags for an associated PR
if [ -n "$PREV_TAG" ]; then
for sha in $(git rev-list "$PREV_TAG"..${{ github.ref_name }}); do
PR_BODY=$(gh api "repos/TobiiNT/ClaudeTracker/commits/$sha/pulls" --jq '.[0].body // empty' 2>/dev/null || echo "")
if [ -n "$PR_BODY" ]; then
break
fi
done
fi

# Strategy 2: Find most recent merged PR targeting main
if [ -z "$PR_BODY" ]; then
PR_BODY=$(gh api "repos/TobiiNT/ClaudeTracker/pulls?state=closed&base=main&sort=updated&direction=desc&per_page=5" \
--jq '[.[] | select(.merged_at != null)] | .[0].body // empty' 2>/dev/null || echo "")
fi

if [ -n "$PR_BODY" ]; then
# Use PR body, strip test plan section
echo "$PR_BODY" | sed '/^## Test plan/,$d' | sed -e :a -e '/^\n*$/{$d;N;ba}' > release-notes.md
echo "$PR_BODY" | sed '/^## Test [Pp]lan/,$d' | sed -e :a -e '/^\n*$/{$d;N;ba}' > release-notes.md
else
# Fallback: list commits since previous tag
PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "")
echo "## What's Changed" > release-notes.md
if [ -n "$PREV_TAG" ]; then
git log --oneline "$PREV_TAG"..${{ github.ref_name }} | head -20 | while read line; do echo "- $line"; done >> release-notes.md
Expand Down
16 changes: 16 additions & 0 deletions src/ClaudeTracker/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,4 +522,20 @@ public static bool IsSystemDarkMode()
return false;
}
}

/// <summary>Checks if the taskbar/tray uses dark mode (separate from app theme on W11).</summary>
public static bool IsTaskbarDarkMode()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("SystemUsesLightTheme");
return value is int intValue && intValue == 0;
}
catch
{
return false;
}
}
}
6 changes: 3 additions & 3 deletions src/ClaudeTracker/ClaudeTracker.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<ApplicationIcon>Assets\app_icon.ico</ApplicationIcon>
<AssemblyName>ClaudeTracker</AssemblyName>
<RootNamespace>ClaudeTracker</RootNamespace>
<Version>2.1.1</Version>
<AssemblyVersion>2.1.1.0</AssemblyVersion>
<FileVersion>2.1.1.0</FileVersion>
<Version>2.1.2</Version>
<AssemblyVersion>2.1.2.0</AssemblyVersion>
<FileVersion>2.1.2.0</FileVersion>
<Description>Claude AI Usage Tracker for Windows</Description>
<Authors>TobiiNT</Authors>
<Company>Tobii Development</Company>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface IUsageRefreshCoordinator
void Stop();
/// <summary>Triggers an immediate out-of-cycle refresh.</summary>
void RefreshNow();
/// <summary>Invalidates API fetch cache so next RefreshNow() re-fetches immediately.</summary>
void InvalidateApiCache();
/// <summary>Updates the refresh interval in seconds.</summary>
void UpdateInterval(double seconds);
/// <summary>Whether the refresh timer is currently running.</summary>
Expand Down
16 changes: 11 additions & 5 deletions src/ClaudeTracker/Services/UsageRefreshCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class UsageRefreshCoordinator : IUsageRefreshCoordinator, IDisposable
private DateTime _lastStatusFetch = DateTime.MinValue;
private bool _isRefreshing;
private DateTime _rateLimitedUntil = DateTime.MinValue;
private DateTime _lastApiFetch = DateTime.MinValue;
private long _lastApiFetchTicks = DateTime.MinValue.Ticks;
private static readonly TimeSpan ApiRefreshInterval = TimeSpan.FromMinutes(5);

public bool IsRunning => _timer?.IsEnabled ?? false;
Expand Down Expand Up @@ -73,6 +73,11 @@ public void RefreshNow()
_ = RefreshAsync();
}

public void InvalidateApiCache()
{
Interlocked.Exchange(ref _lastApiFetchTicks, DateTime.MinValue.Ticks);
}

public void UpdateInterval(double seconds)
{
seconds = Math.Clamp(seconds, Constants.RefreshIntervals.MinSeconds, Constants.RefreshIntervals.MaxSeconds);
Expand Down Expand Up @@ -107,8 +112,8 @@ private async Task RefreshAsync()

try
{
// Fetch Claude.ai usage
if (profile.HasClaudeAI || !string.IsNullOrEmpty(profile.CliCredentialsJSON))
// Fetch Claude.ai subscription usage (session key or CLI auto-detect)
if (profile.HasClaudeAI || profile.HasCliAccount)
{
var usage = await _apiService.FetchUsageData();
_profileService.UpdateUsageData(profile.Id, claudeUsage: usage);
Expand All @@ -121,8 +126,9 @@ private async Task RefreshAsync()
_notificationService.CheckKeyExpiry(profile);

// Fetch API Console + personal metrics (non-fatal, throttled to every 5 min)
var lastApiFetch = new DateTime(Interlocked.Read(ref _lastApiFetchTicks));
var shouldFetchApi = profile.HasAPIConsole
&& (DateTime.UtcNow - _lastApiFetch) >= ApiRefreshInterval;
&& (DateTime.UtcNow - lastApiFetch) >= ApiRefreshInterval;
if (shouldFetchApi)
{
try
Expand Down Expand Up @@ -160,7 +166,7 @@ private async Task RefreshAsync()
}
}

_lastApiFetch = DateTime.UtcNow;
Interlocked.Exchange(ref _lastApiFetchTicks, DateTime.UtcNow.Ticks);
}

// Fetch Claude system status (every 5 minutes)
Expand Down
35 changes: 28 additions & 7 deletions src/ClaudeTracker/TrayIcon/TrayIconManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,43 @@ public void UpdateIcon()
try
{
var profile = _profileService.ActiveProfile;
var usage = profile?.ClaudeUsage;
var claudeUsage = profile?.HasClaudeAI == true ? profile.ClaudeUsage : null;
var apiUsage = profile?.HasAPIConsole == true ? profile.ApiUsage : null;
var iconConfig = profile?.IconConfig ?? MenuBarIconConfiguration.Default;
var sessionConfig = iconConfig.GetConfig(MenuBarMetricType.Session);

var percentage = usage?.EffectiveSessionPercentage ?? 0;
double percentage;
string tooltipText;

if (claudeUsage != null)
{
// Subscription user: show session usage %
percentage = claudeUsage.EffectiveSessionPercentage;
tooltipText = $"Claude Usage: {FormatterHelper.FormatPercentage(percentage)} used\nResets: {FormatterHelper.FormatTimeRemaining(claudeUsage.SessionResetTime)}";
}
else if (apiUsage != null)
{
// API-only user: show budget usage %
percentage = apiUsage.UsagePercentage;
tooltipText = $"API Budget: {apiUsage.FormattedUsed} / {apiUsage.FormattedTotal} ({FormatterHelper.FormatPercentage(percentage)} used)";
}
else
{
percentage = 0;
tooltipText = "Claude Tracker - No usage data";
}

var displayPercentage = UsageStatusCalculator.GetDisplayPercentage(
percentage, iconConfig.ShowRemainingPercentage);
var status = UsageStatusCalculator.CalculateStatus(
percentage, iconConfig.ShowRemainingPercentage);
var style = sessionConfig?.IconStyle ?? MenuBarIconStyle.Battery;
var isDark = App.IsSystemDarkMode();
var isDark = App.IsTaskbarDarkMode();

var customColor = iconConfig.UseCustomColor ? iconConfig.CustomColorHex : null;
var metricPrefix = iconConfig.ShowIconNames ? "S:" : null;
var metricPrefix = iconConfig.ShowIconNames
? (claudeUsage != null ? "S:" : "$:")
: null;
var icon = _renderer.RenderIcon(
displayPercentage, status, style,
iconConfig.MonochromeMode, isDark, customColor,
Expand All @@ -121,9 +144,7 @@ public void UpdateIcon()
_trayIcon.Icon = icon;
oldIcon?.Dispose();

_tooltipText = usage != null
? $"Claude Usage: {FormatterHelper.FormatPercentage(percentage)} used\nResets: {FormatterHelper.FormatTimeRemaining(usage.SessionResetTime)}"
: "Claude Tracker - No usage data";
_tooltipText = tooltipText;
});
}
catch (Exception ex)
Expand Down
16 changes: 14 additions & 2 deletions src/ClaudeTracker/Utilities/PopupStackManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,24 @@ private static double GetDpiScale()
{
try
{
var source = PresentationSource.FromVisual(Application.Current.MainWindow);
var window = Application.Current?.MainWindow;
if (window == null) return GetDpiScaleFromSystem();

var source = PresentationSource.FromVisual(window);
if (source?.CompositionTarget != null)
return source.CompositionTarget.TransformToDevice.M11;
}
catch { /* fall through */ }
return 1.0;
return GetDpiScaleFromSystem();
}

[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern uint GetDpiForSystem();

private static double GetDpiScaleFromSystem()
{
try { return GetDpiForSystem() / 96.0; }
catch { return 1.0; }
}

public static void PositionWindow(Window popup)
Expand Down
Loading
Loading