diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f3f9834..adcec60 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/src/ClaudeTracker/App.xaml.cs b/src/ClaudeTracker/App.xaml.cs
index 21a19bc..afc7cf8 100644
--- a/src/ClaudeTracker/App.xaml.cs
+++ b/src/ClaudeTracker/App.xaml.cs
@@ -522,4 +522,20 @@ public static bool IsSystemDarkMode()
return false;
}
}
+
+ /// Checks if the taskbar/tray uses dark mode (separate from app theme on W11).
+ 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;
+ }
+ }
}
diff --git a/src/ClaudeTracker/ClaudeTracker.csproj b/src/ClaudeTracker/ClaudeTracker.csproj
index 55c4172..5a6c2ec 100644
--- a/src/ClaudeTracker/ClaudeTracker.csproj
+++ b/src/ClaudeTracker/ClaudeTracker.csproj
@@ -9,9 +9,9 @@
Assets\app_icon.ico
ClaudeTracker
ClaudeTracker
- 2.1.1
- 2.1.1.0
- 2.1.1.0
+ 2.1.2
+ 2.1.2.0
+ 2.1.2.0
Claude AI Usage Tracker for Windows
TobiiNT
Tobii Development
diff --git a/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs b/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs
index a44559e..17fa8d8 100644
--- a/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs
+++ b/src/ClaudeTracker/Services/Interfaces/IUsageRefreshCoordinator.cs
@@ -14,6 +14,8 @@ public interface IUsageRefreshCoordinator
void Stop();
/// Triggers an immediate out-of-cycle refresh.
void RefreshNow();
+ /// Invalidates API fetch cache so next RefreshNow() re-fetches immediately.
+ void InvalidateApiCache();
/// Updates the refresh interval in seconds.
void UpdateInterval(double seconds);
/// Whether the refresh timer is currently running.
diff --git a/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs b/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs
index 704cf16..f7ebef9 100644
--- a/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs
+++ b/src/ClaudeTracker/Services/UsageRefreshCoordinator.cs
@@ -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;
@@ -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);
@@ -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);
@@ -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
@@ -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)
diff --git a/src/ClaudeTracker/TrayIcon/TrayIconManager.cs b/src/ClaudeTracker/TrayIcon/TrayIconManager.cs
index e9666bf..ccc6467 100644
--- a/src/ClaudeTracker/TrayIcon/TrayIconManager.cs
+++ b/src/ClaudeTracker/TrayIcon/TrayIconManager.cs
@@ -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,
@@ -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)
diff --git a/src/ClaudeTracker/Utilities/PopupStackManager.cs b/src/ClaudeTracker/Utilities/PopupStackManager.cs
index 82bb267..d72b64b 100644
--- a/src/ClaudeTracker/Utilities/PopupStackManager.cs
+++ b/src/ClaudeTracker/Utilities/PopupStackManager.cs
@@ -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)
diff --git a/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs b/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs
index c5ab7cf..2935da4 100644
--- a/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs
+++ b/src/ClaudeTracker/ViewModels/ApiBillingViewModel.cs
@@ -11,6 +11,7 @@ public partial class ApiBillingViewModel : ObservableObject
private readonly IClaudeApiService _apiService;
private readonly IProfileService _profileService;
private readonly ISettingsService _settingsService;
+ private readonly IUsageRefreshCoordinator _refreshCoordinator;
[ObservableProperty] private string _apiKey = "";
[ObservableProperty] private bool _isTesting;
@@ -20,19 +21,24 @@ public partial class ApiBillingViewModel : ObservableObject
[ObservableProperty] private APIOrganization? _selectedOrg;
[ObservableProperty] private bool _isConfigured;
[ObservableProperty] private bool _showUserPicker;
+ [ObservableProperty] private bool _isLoadingUsers;
[ObservableProperty] private ClaudeCodeUserMetrics? _selectedUser;
+ [ObservableProperty] private string _trackedUserLabel = "";
public ObservableCollection Organizations { get; } = new();
public ObservableCollection ClaudeCodeUsers { get; } = new();
- public ApiBillingViewModel(IClaudeApiService apiService, IProfileService profileService, ISettingsService settingsService)
+ public ApiBillingViewModel(IClaudeApiService apiService, IProfileService profileService,
+ ISettingsService settingsService, IUsageRefreshCoordinator refreshCoordinator)
{
_apiService = apiService;
_profileService = profileService;
_settingsService = settingsService;
+ _refreshCoordinator = refreshCoordinator;
var profile = _profileService.ActiveProfile;
IsConfigured = profile?.HasAPIConsole ?? false;
+ TrackedUserLabel = profile?.ApiUserSearch ?? "";
}
[RelayCommand]
@@ -42,6 +48,8 @@ private async Task TestConnection()
IsTesting = true;
TestStatus = "Testing connection...";
+ ShowOrgPicker = false;
+ ShowUserPicker = false;
try
{
@@ -54,12 +62,20 @@ private async Task TestConnection()
if (orgs.Count == 1)
{
SelectedOrg = orgs[0];
- await SaveConfiguration();
+ TestStatus = $"Connected to {orgs[0].DisplayName}";
+ TestSuccess = true;
+ await FetchUsersForOrg(orgs[0]);
}
- else
+ else if (orgs.Count > 1)
{
ShowOrgPicker = true;
- TestStatus = $"Found {orgs.Count} organizations.";
+ TestStatus = "Select your organization:";
+ TestSuccess = true;
+ }
+ else
+ {
+ TestStatus = "No organizations found.";
+ TestSuccess = false;
}
}
catch (Exception ex)
@@ -73,33 +89,30 @@ private async Task TestConnection()
}
}
+ ///
+ /// Called when org is selected from the dropdown — immediately fetches users.
+ ///
[RelayCommand]
private async Task SelectOrganization()
{
if (SelectedOrg == null) return;
- await SaveConfiguration();
+ TestStatus = $"Connected to {SelectedOrg.DisplayName}";
+ TestSuccess = true;
+ await FetchUsersForOrg(SelectedOrg);
}
- private async Task SaveConfiguration()
+ private async Task FetchUsersForOrg(APIOrganization org)
{
var profile = _profileService.ActiveProfile;
- if (profile == null || SelectedOrg == null) return;
-
- var credentials = _profileService.LoadCredentials(profile.Id);
- credentials.ApiSessionKey = ApiKey.Trim();
- credentials.ApiOrganizationId = SelectedOrg.Id;
- _profileService.SaveCredentials(profile.Id, credentials);
+ if (profile == null) return;
- TestStatus = $"Connected to {SelectedOrg.DisplayName}";
- TestSuccess = true;
- IsConfigured = true;
- ShowOrgPicker = false;
+ IsLoadingUsers = true;
+ ClaudeCodeUsers.Clear();
+ ShowUserPicker = false;
- // Auto-fetch Claude Code users for identity picker
try
{
- var users = await _apiService.FetchClaudeCodeAllUsers(SelectedOrg.Id, ApiKey.Trim());
- ClaudeCodeUsers.Clear();
+ var users = await _apiService.FetchClaudeCodeAllUsers(org.Id, ApiKey.Trim());
foreach (var user in users)
ClaudeCodeUsers.Add(user);
@@ -109,22 +122,70 @@ private async Task SaveConfiguration()
if (!string.IsNullOrEmpty(profile.ApiUserSearch))
SelectedUser = users.FirstOrDefault(u => u.DisplayName == profile.ApiUserSearch);
}
+ else
+ {
+ TestStatus = $"Connected to {org.DisplayName}, but no users found.";
+ }
}
catch (Exception ex)
{
+ TestStatus = $"Connected, but failed to load users: {ex.Message}";
+ TestSuccess = false;
Services.LoggingService.Instance.LogWarning($"Failed to fetch Claude Code users: {ex.Message}");
}
+ finally
+ {
+ IsLoadingUsers = false;
+ }
}
+ ///
+ /// Final confirm — saves org + user selection together.
+ ///
[RelayCommand]
- private async Task LoadClaudeCodeUsers()
+ private void SaveUserSelection()
+ {
+ if (SelectedUser == null || SelectedOrg == null) return;
+ var profile = _profileService.ActiveProfile;
+ if (profile == null) return;
+
+ // Save credentials
+ var credentials = _profileService.LoadCredentials(profile.Id);
+ credentials.ApiSessionKey = ApiKey.Trim();
+ credentials.ApiOrganizationId = SelectedOrg.Id;
+ _profileService.SaveCredentials(profile.Id, credentials);
+
+ // Save user selection
+ profile.ApiUserSearch = SelectedUser.DisplayName;
+ _settingsService.Save();
+
+ ShowUserPicker = false;
+ ShowOrgPicker = false;
+ IsConfigured = true;
+ TrackedUserLabel = SelectedUser.DisplayName;
+ TestStatus = $"Tracking: {SelectedUser.DisplayName}";
+ TestSuccess = true;
+
+ _refreshCoordinator.InvalidateApiCache();
+ _refreshCoordinator.RefreshNow();
+ }
+
+ [RelayCommand]
+ private async Task ChangeUser()
{
var profile = _profileService.ActiveProfile;
if (profile == null) return;
var credentials = _profileService.LoadCredentials(profile.Id);
if (string.IsNullOrEmpty(credentials.ApiSessionKey) || string.IsNullOrEmpty(credentials.ApiOrganizationId))
+ {
+ TestStatus = "No API credentials configured.";
+ TestSuccess = false;
return;
+ }
+
+ ApiKey = credentials.ApiSessionKey;
+ IsLoadingUsers = true;
try
{
@@ -139,26 +200,22 @@ private async Task LoadClaudeCodeUsers()
if (!string.IsNullOrEmpty(profile.ApiUserSearch))
SelectedUser = users.FirstOrDefault(u => u.DisplayName == profile.ApiUserSearch);
}
+ else
+ {
+ TestStatus = "No users found in this organization.";
+ TestSuccess = false;
+ }
}
catch (Exception ex)
{
+ TestStatus = $"Failed to load users: {ex.Message}";
+ TestSuccess = false;
Services.LoggingService.Instance.LogWarning($"Failed to fetch Claude Code users: {ex.Message}");
}
- }
-
- [RelayCommand]
- private void SaveUserSelection()
- {
- if (SelectedUser == null) return;
- var profile = _profileService.ActiveProfile;
- if (profile == null) return;
-
- profile.ApiUserSearch = SelectedUser.DisplayName;
- _settingsService.Save();
-
- ShowUserPicker = false;
- TestStatus = $"Tracking: {SelectedUser.DisplayName}";
- TestSuccess = true;
+ finally
+ {
+ IsLoadingUsers = false;
+ }
}
[RelayCommand]
@@ -172,8 +229,20 @@ private void Disconnect()
credentials.ApiOrganizationId = null;
_profileService.SaveCredentials(profile.Id, credentials);
+ profile.ApiUserSearch = null;
+ profile.ApiUsage = null;
+ profile.PersonalMetrics = null;
+ profile.DailyMetrics = null;
+ _settingsService.Save();
+
ApiKey = "";
IsConfigured = false;
+ ShowUserPicker = false;
+ ShowOrgPicker = false;
+ TrackedUserLabel = "";
TestStatus = "";
+
+ _refreshCoordinator.InvalidateApiCache();
+ _refreshCoordinator.RefreshNow();
}
}
diff --git a/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs b/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs
index 74b6a3a..f8df100 100644
--- a/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs
+++ b/src/ClaudeTracker/ViewModels/PersonalUsageViewModel.cs
@@ -11,6 +11,7 @@ public partial class PersonalUsageViewModel : ObservableObject
{
private readonly IClaudeApiService _apiService;
private readonly IProfileService _profileService;
+ private readonly IUsageRefreshCoordinator _refreshCoordinator;
private readonly ClaudeCodeSyncService _cliSyncService;
[ObservableProperty] private string _sessionKey = "";
@@ -30,14 +31,16 @@ public partial class PersonalUsageViewModel : ObservableObject
public PersonalUsageViewModel(
IClaudeApiService apiService,
IProfileService profileService,
+ IUsageRefreshCoordinator refreshCoordinator,
ClaudeCodeSyncService cliSyncService)
{
_apiService = apiService;
_profileService = profileService;
+ _refreshCoordinator = refreshCoordinator;
_cliSyncService = cliSyncService;
var profile = _profileService.ActiveProfile;
- IsConfigured = profile?.HasAnyCredentials ?? false;
+ IsConfigured = profile?.HasClaudeAI == true || !string.IsNullOrEmpty(profile?.CliCredentialsJSON);
if (profile?.ClaudeSessionKey != null)
SessionKey = profile.ClaudeSessionKey;
@@ -178,6 +181,7 @@ private void Disconnect()
profile.CliCredentialsJSON = null;
profile.HasCliAccount = false;
profile.CliAccountSyncedAt = null;
+ profile.ClaudeUsage = null;
_profileService.UpdateProfile(profile);
SessionKey = "";
@@ -188,6 +192,9 @@ private void Disconnect()
AutoDetectSuccess = false;
ConnectedLabel = "";
ConnectedDetail = "";
+
+ _refreshCoordinator.InvalidateApiCache();
+ _refreshCoordinator.RefreshNow();
}
private void UpdateConnectedInfo()
diff --git a/src/ClaudeTracker/ViewModels/PopoverViewModel.cs b/src/ClaudeTracker/ViewModels/PopoverViewModel.cs
index 5a1c8c3..1ac091c 100644
--- a/src/ClaudeTracker/ViewModels/PopoverViewModel.cs
+++ b/src/ClaudeTracker/ViewModels/PopoverViewModel.cs
@@ -161,7 +161,7 @@ public void RefreshData()
ProfileName = profile.Name;
HasCredentials = profile.HasUsageCredentials;
- var usage = profile.ClaudeUsage;
+ var usage = profile.HasClaudeAI ? profile.ClaudeUsage : null;
HasClaudeUsage = usage != null;
if (usage != null)
{
diff --git a/src/ClaudeTracker/ViewModels/SettingsViewModel.cs b/src/ClaudeTracker/ViewModels/SettingsViewModel.cs
index 383112b..74e269e 100644
--- a/src/ClaudeTracker/ViewModels/SettingsViewModel.cs
+++ b/src/ClaudeTracker/ViewModels/SettingsViewModel.cs
@@ -11,7 +11,6 @@ public partial class SettingsViewModel : ObservableObject
private object? _currentView;
public PersonalUsageViewModel PersonalUsage { get; }
- public ApiBillingViewModel ApiBilling { get; }
public CliAccountViewModel CliAccount { get; }
public AppearanceViewModel Appearance { get; }
public GeneralSettingsViewModel GeneralSettings { get; }
@@ -21,7 +20,6 @@ public partial class SettingsViewModel : ObservableObject
public SettingsViewModel(
PersonalUsageViewModel personalUsage,
- ApiBillingViewModel apiBilling,
CliAccountViewModel cliAccount,
AppearanceViewModel appearance,
GeneralSettingsViewModel generalSettings,
@@ -30,7 +28,6 @@ public SettingsViewModel(
AboutViewModel about)
{
PersonalUsage = personalUsage;
- ApiBilling = apiBilling;
CliAccount = cliAccount;
Appearance = appearance;
GeneralSettings = generalSettings;
@@ -44,7 +41,6 @@ partial void OnSelectedTabChanged(string value)
CurrentView = value switch
{
"PersonalUsage" => PersonalUsage,
- "ApiBilling" => ApiBilling,
"CliAccount" => CliAccount,
"Appearance" => Appearance,
"General" => GeneralSettings,
diff --git a/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs b/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs
index e0104df..873c711 100644
--- a/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs
+++ b/src/ClaudeTracker/Views/FloatingUsageWindow.xaml.cs
@@ -57,7 +57,7 @@ public FloatingUsageWindow()
var profile = App.Services.GetRequiredService().ActiveProfile;
if (profile == null) return;
- var usage = profile.ClaudeUsage;
+ var usage = profile.HasClaudeAI ? profile.ClaudeUsage : null;
if (usage != null)
LastUpdatedText.Text = $"Updated {Utilities.FormatterHelper.FormatTimeAgo(usage.LastUpdated)}";
else if (profile.ApiUsage != null)
diff --git a/src/ClaudeTracker/Views/PopoverWindow.xaml.cs b/src/ClaudeTracker/Views/PopoverWindow.xaml.cs
index 5a896e5..21ac11c 100644
--- a/src/ClaudeTracker/Views/PopoverWindow.xaml.cs
+++ b/src/ClaudeTracker/Views/PopoverWindow.xaml.cs
@@ -215,7 +215,10 @@ private void UpdateUI()
else if (!_viewModel.HasClaudeUsage && !_viewModel.HasApiUsage && !_viewModel.HasPersonalMetrics)
{
StatusDot.Fill = new SolidColorBrush(Color.FromRgb(0xFF, 0x98, 0x00)); // orange
- StatusText.Text = "Waiting for usage data...";
+ // Show error/rate-limit message if available, otherwise generic waiting text
+ StatusText.Text = !string.IsNullOrEmpty(_viewModel.LastUpdatedText)
+ ? _viewModel.LastUpdatedText
+ : "Waiting for usage data...";
}
else
{
diff --git a/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml b/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml
deleted file mode 100644
index 4ecac32..0000000
--- a/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml.cs b/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml.cs
deleted file mode 100644
index 965b3cd..0000000
--- a/src/ClaudeTracker/Views/Settings/ApiBillingView.xaml.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-using Microsoft.Extensions.DependencyInjection;
-using ClaudeTracker.ViewModels;
-
-namespace ClaudeTracker.Views.Settings;
-
-public partial class ApiBillingView : UserControl
-{
- private readonly ApiBillingViewModel _vm;
-
- public ApiBillingView()
- {
- InitializeComponent();
- _vm = App.Services.GetRequiredService();
- DataContext = _vm;
-
- TestButton.Click += async (_, _) =>
- {
- _vm.ApiKey = ApiKeyInput.Text;
- LoadingBar.Visibility = Visibility.Visible;
- await _vm.TestConnectionCommand.ExecuteAsync(null);
- LoadingBar.Visibility = Visibility.Collapsed;
- };
-
- DisconnectButton.Click += (_, _) => _vm.DisconnectCommand.Execute(null);
- SaveButton.Click += async (_, _) =>
- {
- _vm.SelectedOrg = OrgCombo.SelectedItem as Models.APIOrganization;
- await _vm.SelectOrganizationCommand.ExecuteAsync(null);
- };
-
- OrgCombo.ItemsSource = _vm.Organizations;
-
- _vm.PropertyChanged += (_, _) => Dispatcher.Invoke(() =>
- {
- ConnectedPanel.Visibility = _vm.IsConfigured ? Visibility.Visible : Visibility.Collapsed;
- SetupPanel.Visibility = _vm.IsConfigured ? Visibility.Collapsed : Visibility.Visible;
- StatusText.Text = _vm.TestStatus;
- OrgPickerPanel.Visibility = _vm.ShowOrgPicker ? Visibility.Visible : Visibility.Collapsed;
- });
- }
-}
diff --git a/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml b/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml
index 4d4d5e4..ad77a32 100644
--- a/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml
+++ b/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml
@@ -4,8 +4,8 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
-
-
+
@@ -29,42 +29,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Open claude.ai, press F12, go to Application > Cookies, and copy the sessionKey value.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -135,28 +51,38 @@
Style="{StaticResource MaterialDesignLinearProgressBar}"
Margin="0,8" />
-
+
-
-
+
-
+
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+ Margin="0,0,0,12" />
diff --git a/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml.cs b/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml.cs
index 62a4e18..ddf9e9e 100644
--- a/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml.cs
+++ b/src/ClaudeTracker/Views/Settings/PersonalUsageView.xaml.cs
@@ -1,5 +1,6 @@
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Extensions.DependencyInjection;
using ClaudeTracker.Services.Interfaces;
@@ -20,33 +21,21 @@ public PersonalUsageView()
_apiVm = App.Services.GetRequiredService();
DataContext = _vm;
- // --- Browser sign-in (WebView2) ---
- if (BrowserSignInWindow.IsWebView2Available())
+ // --- Claude.ai Subscription (CLI auto-detect only) ---
+ AutoDetectButton.Click += async (_, _) =>
{
- BrowserSignInCard.Visibility = Visibility.Visible;
- BrowserApiSignInCard.Visibility = Visibility.Visible;
- }
+ LoadingBar.Visibility = Visibility.Visible;
+ await _vm.AutoDetectCommand.ExecuteAsync(null);
+ LoadingBar.Visibility = Visibility.Collapsed;
+ };
- BrowserSignInButton.Click += async (_, _) =>
- {
- var window = new BrowserSignInWindow("https://claude.ai/login", "claude.ai");
- window.Owner = Window.GetWindow(this);
- window.Show();
- var result = await window.ResultTask;
- if (result.HasValue)
- {
- SessionKeyInput.Text = result.Value.sessionKey;
- _vm.SessionKey = result.Value.sessionKey;
+ DisconnectButton.Click += (_, _) => _vm.DisconnectCommand.Execute(null);
- var profile = App.Services.GetRequiredService().ActiveProfile;
- if (profile != null && result.Value.expiry.HasValue)
- profile.ClaudeSessionKeyExpiry = result.Value.expiry;
+ _vm.PropertyChanged += (_, _) => UpdateUI();
- LoadingBar.Visibility = Visibility.Visible;
- await _vm.TestConnectionCommand.ExecuteAsync(null);
- LoadingBar.Visibility = Visibility.Collapsed;
- }
- };
+ // --- Claude Code API Usage ---
+ if (BrowserSignInWindow.IsWebView2Available())
+ BrowserApiSignInCard.Visibility = Visibility.Visible;
BrowserApiSignInButton.Click += async (_, _) =>
{
@@ -69,36 +58,6 @@ public PersonalUsageView()
}
};
- // --- Claude.ai connection ---
- AutoDetectButton.Click += async (_, _) =>
- {
- LoadingBar.Visibility = Visibility.Visible;
- await _vm.AutoDetectCommand.ExecuteAsync(null);
- LoadingBar.Visibility = Visibility.Collapsed;
- };
-
- TestButton.Click += async (_, _) =>
- {
- LoadingBar.Visibility = Visibility.Visible;
- await _vm.TestConnectionCommand.ExecuteAsync(null);
- LoadingBar.Visibility = Visibility.Collapsed;
- };
-
- DisconnectButton.Click += (_, _) => _vm.DisconnectCommand.Execute(null);
- SaveOrgButton.Click += async (_, _) =>
- {
- _vm.SelectedOrg = OrgCombo.SelectedItem as Models.AccountInfo;
- await _vm.SelectOrganizationCommand.ExecuteAsync(null);
- };
-
- _vm.PropertyChanged += (_, _) => UpdateUI();
-
- SessionKeyInput.TextChanged += (_, _) => _vm.SessionKey = SessionKeyInput.Text;
- SessionKeyInput.Text = _vm.SessionKey;
-
- OrgCombo.ItemsSource = _vm.Organizations;
-
- // --- API Billing ---
ApiTestButton.Click += async (_, _) =>
{
_apiVm.ApiKey = ApiKeyInput.Text;
@@ -108,12 +67,23 @@ public PersonalUsageView()
};
ApiDisconnectButton.Click += (_, _) => _apiVm.DisconnectCommand.Execute(null);
- ApiSaveButton.Click += async (_, _) =>
+ ApiChangeUserButton.Click += async (_, _) =>
+ {
+ await _apiVm.ChangeUserCommand.ExecuteAsync(null);
+ };
+
+ // Org dropdown: only fetch on deliberate click selection, block scroll wheel
+ ApiOrgCombo.PreviewMouseWheel += ComboBox_BlockScrollWheel;
+ ApiOrgCombo.DropDownClosed += async (_, _) =>
{
- _apiVm.SelectedOrg = ApiOrgCombo.SelectedItem as Models.APIOrganization;
+ var selected = ApiOrgCombo.SelectedItem as Models.APIOrganization;
+ if (selected == null || selected == _apiVm.SelectedOrg) return;
+ _apiVm.SelectedOrg = selected;
await _apiVm.SelectOrganizationCommand.ExecuteAsync(null);
};
+ ApiUserCombo.PreviewMouseWheel += ComboBox_BlockScrollWheel;
+
ApiOrgCombo.ItemsSource = _apiVm.Organizations;
ApiUserCombo.ItemsSource = _apiVm.ClaudeCodeUsers;
@@ -127,10 +97,13 @@ public PersonalUsageView()
UpdateUI();
UpdateApiUI();
+ }
- // Auto-load user picker if API is already configured
- if (_apiVm.IsConfigured)
- _ = _apiVm.LoadClaudeCodeUsersCommand.ExecuteAsync(null);
+ /// Prevents mouse wheel from changing ComboBox selection when dropdown is closed.
+ private static void ComboBox_BlockScrollWheel(object sender, MouseWheelEventArgs e)
+ {
+ if (sender is ComboBox combo && !combo.IsDropDownOpen)
+ e.Handled = true;
}
private void UpdateUI()
@@ -141,11 +114,6 @@ private void UpdateUI()
SetupPanel.Visibility = _vm.IsConfigured ? Visibility.Collapsed : Visibility.Visible;
ConnectedText.Text = _vm.ConnectedLabel;
ConnectedDetailText.Text = _vm.ConnectedDetail;
- TestStatusText.Text = _vm.TestStatus;
- TestStatusText.Foreground = _vm.TestSuccess
- ? new SolidColorBrush(Color.FromRgb(0x4C, 0xAF, 0x50))
- : new SolidColorBrush(Color.FromRgb(0xF4, 0x43, 0x36));
- OrgPickerPanel.Visibility = _vm.ShowOrgPicker ? Visibility.Visible : Visibility.Collapsed;
AutoDetectStatus.Text = _vm.AutoDetectStatusText;
AutoDetectStatus.Foreground = _vm.AutoDetectSuccess
@@ -161,8 +129,24 @@ private void UpdateApiUI()
ApiConnectedPanel.Visibility = _apiVm.IsConfigured ? Visibility.Visible : Visibility.Collapsed;
ApiSetupPanel.Visibility = _apiVm.IsConfigured ? Visibility.Collapsed : Visibility.Visible;
ApiStatusText.Text = _apiVm.TestStatus;
+ ApiStatusText.Foreground = _apiVm.TestSuccess
+ ? new SolidColorBrush(Color.FromRgb(0x4C, 0xAF, 0x50))
+ : new SolidColorBrush(Color.FromRgb(0xF4, 0x43, 0x36));
+
+ var wasOrgHidden = ApiOrgPickerPanel.Visibility != Visibility.Visible;
+ var wasUserHidden = ApiUserPickerPanel.Visibility != Visibility.Visible;
+
ApiOrgPickerPanel.Visibility = _apiVm.ShowOrgPicker ? Visibility.Visible : Visibility.Collapsed;
ApiUserPickerPanel.Visibility = _apiVm.ShowUserPicker ? Visibility.Visible : Visibility.Collapsed;
+ ApiUserLoadingBar.Visibility = _apiVm.IsLoadingUsers ? Visibility.Visible : Visibility.Collapsed;
+ ApiTrackedUserText.Text = string.IsNullOrEmpty(_apiVm.TrackedUserLabel) ? "" : $"Tracking: {_apiVm.TrackedUserLabel}";
+ ApiErrorText.Text = _apiVm.IsConfigured && !_apiVm.TestSuccess ? _apiVm.TestStatus : "";
+
+ // Auto-scroll into view when panels first appear
+ if (wasUserHidden && _apiVm.ShowUserPicker)
+ ApiUserPickerPanel.BringIntoView();
+ else if (wasOrgHidden && _apiVm.ShowOrgPicker)
+ ApiOrgPickerPanel.BringIntoView();
});
}
}