Compare commits

..
Author SHA1 Message Date
SuperDooper 0300e4a72e chore: release notes for beta.26 2026-05-08 16:12:06 +02:00
SuperDooper 7febb0384f fix: update WebView2 refresh script to use postMessage 2026-05-08 16:11:55 +02:00
SuperDooper 55e1f754c5 fix: switch WebViewFetchWindow to WebMessageReceived for script results 2026-05-08 16:11:54 +02:00
SuperDooper 03676a980f fix: use WebMessageReceived instead of ExecuteScriptAsync return value for async script results 2026-05-08 16:11:53 +02:00
SuperDooper a74d0cece5 chore: release notes for beta.25 2026-05-08 16:06:16 +02:00
SuperDooper b5cd7b1bc9 debug: show DebugInfo in settings panel after login 2026-05-08 16:06:06 +02:00
SuperDooper 3249dbd4aa debug: add DebugText block in settings panel 2026-05-08 16:06:05 +02:00
SuperDooper 189c513677 debug: capture bootstrap keys and save to DebugInfo 2026-05-08 16:06:04 +02:00
SuperDooper 05e66b22d1 feat: add DebugInfo field to AppSettings 2026-05-08 16:06:03 +02:00
SuperDooper 48e6f64c9a chore: release notes for beta.24 2026-05-08 16:00:28 +02:00
SuperDooper d55a92623c fix: add LoadFromCacheAsync, same JS fallbacks in WebView2 refresh path 2026-05-08 16:00:04 +02:00
SuperDooper c70a090983 fix: load from cache immediately after login, background refresh after 3s 2026-05-08 16:00:01 +02:00
SuperDooper cc0270326e fix: add /api/organizations + personal /api/usage fallbacks in login JS 2026-05-08 16:00:00 +02:00
SuperDooper 7601452f93 chore: release notes for beta.23 2026-05-08 15:50:04 +02:00
SuperDooper 5a78a021cb fix: trust cookies/cached auth for IsSignedIn, wider cookie domain filter 2026-05-08 15:49:47 +02:00
SuperDooper 7d465c13d8 fix: show update label in About, delay before refresh after login 2026-05-08 15:49:47 +02:00
SuperDooper ca67039e3e feat: add update-available label in About section 2026-05-08 15:49:46 +02:00
SuperDooper 1e4cdc09b3 fix: broaden JS orgId/email paths, URL-only auto-close 2026-05-08 15:49:45 +02:00
7 changed files with 205 additions and 66 deletions
+1
View File
@@ -14,6 +14,7 @@ public class AppSettings
public int RefreshInterval { get; set; } = 120;
public bool ShowInTaskbar { get; set; } = true;
public bool BetaChannel { get; set; } = false;
public string DebugInfo { get; set; } = "";
private static readonly string FilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+58 -14
View File
@@ -47,8 +47,7 @@ public partial class LoginWindow : Window
: "Complete sign-in, then click Done.";
});
// Auto-close as soon as we detect sign-in on any claude.ai page
if (signedIn && !uri.Contains("/login") && !uri.Contains("/signin"))
if (!uri.Contains("/login") && !uri.Contains("/signin") && signedIn)
await SaveAndClose(cookies);
};
}
@@ -67,20 +66,49 @@ public partial class LoginWindow : Window
UsageViewModel.SaveCookies(cookies);
// Fetch bootstrap + usage from within WebView2 (already authenticated, no header issues)
try
{
const string script = @"(async()=>{try{
const b=await(await fetch('/api/bootstrap',{headers:{accept:'application/json'}})).json();
const id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid||null;
const e=b?.account?.email_address||null;
if(!id)return{email:e,orgId:null,usage:null};
const u=await(await fetch('/api/organizations/'+id+'/usage',{headers:{accept:'application/json'}})).json();
return{email:e,orgId:id,usage:u};
}catch(ex){return{error:String(ex)};}})()";
// Use WebMessageReceived so the async script can post back without relying
// on Promise-awaiting support in the WebView2 runtime version.
var tcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
EventHandler<CoreWebView2WebMessageReceivedEventArgs>? msgHandler = null;
msgHandler = (_, args) =>
{
Browser.CoreWebView2.WebMessageReceived -= msgHandler;
tcs.TrySetResult(args.WebMessageAsString);
};
Browser.CoreWebView2.WebMessageReceived += msgHandler;
var json = await Browser.CoreWebView2.ExecuteScriptAsync(script);
if (json != "null" && !string.IsNullOrEmpty(json))
const string script = @"(async()=>{try{
const h={headers:{accept:'application/json'}};
const b=await(await fetch('/api/bootstrap',h)).json();
const bkeys=Object.keys(b||{}).join(',');
const akeys=Object.keys(b?.account||{}).join(',');
let id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid
||b?.default_organization?.uuid||null;
const e=b?.account?.email_address||b?.account?.email||b?.email||null;
let orgSrc='bootstrap';
if(!id){
try{const ol=await(await fetch('/api/organizations',h)).json();
if(Array.isArray(ol)&&ol.length>0){id=ol[0]?.uuid||null;orgSrc='orgs-list';}}catch(e2){}
}
if(!id){
let pu=null;
try{pu=await(await fetch('/api/usage',h)).json();}catch(e3){}
window.chrome.webview.postMessage(JSON.stringify({email:e,orgId:null,usage:(pu&&!pu.error?pu:null),debug:'no-org|bkeys:'+bkeys+'|akeys:'+akeys}));
return;
}
const u=await(await fetch('/api/organizations/'+id+'/usage',h)).json();
window.chrome.webview.postMessage(JSON.stringify({email:e,orgId:id,usage:u,debug:'orgSrc:'+orgSrc+'|bkeys:'+bkeys+'|akeys:'+akeys}));
}catch(ex){window.chrome.webview.postMessage(JSON.stringify({error:String(ex)}));}})()";
await Browser.CoreWebView2.ExecuteScriptAsync(script);
// Wait up to 15 s for the script to post its message
var completed = await Task.WhenAny(tcs.Task, Task.Delay(15000));
var json = completed == tcs.Task ? tcs.Task.Result : null;
if (!string.IsNullOrEmpty(json))
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
@@ -94,10 +122,26 @@ public partial class LoginWindow : Window
if (root.TryGetProperty("usage", out var us) && us.ValueKind == JsonValueKind.Object)
AppSettings.Default.UsageJson = us.GetRawText();
if (root.TryGetProperty("debug", out var dbg) && dbg.ValueKind == JsonValueKind.String)
AppSettings.Default.DebugInfo = dbg.GetString() ?? "";
else if (root.TryGetProperty("error", out var err))
AppSettings.Default.DebugInfo = "JS error: " + err.GetRawText();
else
AppSettings.Default.DebugInfo = "timeout or empty message";
AppSettings.Default.Save();
}
else
{
AppSettings.Default.DebugInfo = json == null ? "script timeout (15s)" : "empty message";
AppSettings.Default.Save();
}
}
catch { }
catch (Exception ex)
{
AppSettings.Default.DebugInfo = "exception: " + ex.Message;
AppSettings.Default.Save();
}
await Dispatcher.InvokeAsync(() => DialogResult = true);
}
+13 -3
View File
@@ -173,12 +173,22 @@
<TextBlock Text="Built by superdooper86 &amp; claude"
FontSize="11" Foreground="{DynamicResource SecondaryBrush}"/>
</StackPanel>
<Button Grid.Column="2" x:Name="CheckUpdateButton"
Style="{StaticResource SmallButton}"
Click="CheckUpdate_Click" VerticalAlignment="Center"/>
<StackPanel Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
<Button x:Name="CheckUpdateButton"
Style="{StaticResource SmallButton}"
Click="CheckUpdate_Click"/>
<TextBlock x:Name="UpdateAvailableText" FontSize="10"
Foreground="{DynamicResource BlueBrush}"
HorizontalAlignment="Center"
Visibility="Collapsed" Margin="0,4,0,0"/>
</StackPanel>
</Grid>
</Border>
<TextBlock x:Name="DebugText" FontSize="10" Foreground="{DynamicResource SecondaryBrush}"
TextWrapping="Wrap" Margin="0,12,0,0" FontFamily="Consolas"
Visibility="Collapsed"/>
</StackPanel>
</ScrollViewer>
</DockPanel>
+41 -2
View File
@@ -199,7 +199,10 @@ public partial class PopupWindow : Window
private void OnUpdaterChanged(string? prop)
{
if (prop is nameof(UpdateManager.UpdateAvailable) or nameof(UpdateManager.LatestVersion))
{
UpdateBannerState();
UpdateAboutSection();
}
if (prop is nameof(UpdateManager.DownloadProgress))
UpdateProgress.Value = Updater.DownloadProgress * 100;
if (prop is nameof(UpdateManager.StatusMessage))
@@ -208,6 +211,21 @@ public partial class PopupWindow : Window
InstallButton.Visibility = Visibility.Collapsed;
}
private void UpdateAboutSection()
{
if (Updater.UpdateAvailable)
{
UpdateAvailableText.Text = $"v{Updater.LatestVersion} available";
UpdateAvailableText.Visibility = Visibility.Visible;
CheckUpdateButton.Content = "Install";
}
else
{
UpdateAvailableText.Visibility = Visibility.Collapsed;
CheckUpdateButton.Content = "Check";
}
}
private void UpdateBannerState()
{
var visible = Updater.UpdateAvailable;
@@ -236,7 +254,7 @@ public partial class PopupWindow : Window
{
VersionLabel.Text = $"v{Updater.CurrentVersion}";
BetaToggle.IsChecked = Updater.BetaChannel;
CheckUpdateButton.Content = Updater.UpdateAvailable ? "Install" : "Check";
UpdateAboutSection();
var intervals = new[] { ("1 min", 60), ("2 min", 120), ("3 min", 180),
("4 min", 240), ("5 min", 300), ("10 min", 600) };
@@ -265,6 +283,17 @@ public partial class PopupWindow : Window
SignOutButton.Visibility = VM.IsSignedIn ? Visibility.Visible : Visibility.Collapsed;
SignInButton.Content = VM.IsSignedIn ? "Re-authenticate" : "Sign In";
var dbg = AppSettings.Default.DebugInfo;
if (!string.IsNullOrEmpty(dbg))
{
DebugText.Text = dbg;
DebugText.Visibility = Visibility.Visible;
}
else
{
DebugText.Visibility = Visibility.Collapsed;
}
}
// ── Event handlers ───────────────────────────────────────────────
@@ -315,8 +344,18 @@ public partial class PopupWindow : Window
var login = new LoginWindow { Owner = this };
if (login.ShowDialog() == true)
{
await VM.RefreshAsync();
// Show cached data from SaveAndClose immediately (no WebView2 needed)
await VM.LoadFromCacheAsync();
InitSettings();
ShowMain();
// Full refresh in background after WebView2 folder is definitely released
_ = Task.Run(async () =>
{
await Task.Delay(3000);
await VM.RefreshAsync();
await Application.Current.Dispatcher.InvokeAsync(InitSettings);
});
}
Show();
Activate();
+3 -3
View File
@@ -1,4 +1,4 @@
## What's new in beta.22
## What's new in beta.26
- Fixed build error: Task unwrap for Dispatcher.InvokeAsync with async lambda
- Removed unused field warning
- Fixed: script results now use window.chrome.webview.postMessage instead of async IIFE return value — older WebView2 runtimes don't await Promises from ExecuteScriptAsync, so data was never received
- Debug text in Settings will now always show what happened during sign-in
+68 -32
View File
@@ -78,7 +78,13 @@ public class UsageViewModel : INotifyPropertyChanged
try
{
var cookies = await GetCookiesAsync();
if (cookies.Count == 0)
// Check if we have any persistent proof that the user authenticated
bool hasCookies = cookies.Count > 0;
bool hasCachedAuth = !string.IsNullOrEmpty(AppSettings.Default.Email) ||
!string.IsNullOrEmpty(AppSettings.Default.OrgId);
if (!hasCookies && !hasCachedAuth)
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
@@ -89,20 +95,18 @@ public class UsageViewModel : INotifyPropertyChanged
return;
}
// Try HttpClient first (fast path)
var (limits, overage, prepaid, email, orgId, ok) = await TryHttpRefreshAsync(cookies);
// Try HttpClient first (fast path) — may be rejected by server CORS/header checks
var (limits, overage, prepaid, email, orgId, ok) = hasCookies
? await TryHttpRefreshAsync(cookies)
: ([], null, null, null, null, false);
// If HttpClient failed, fall back to WebView2 (uses existing browser session)
// Fall back to WebView2 (uses the shared browser session, not HttpClient)
if (!ok)
(limits, email, orgId) = await TryWebView2RefreshAsync();
// Still no org ID? Use cached values from last successful login
if (string.IsNullOrEmpty(orgId) && !string.IsNullOrEmpty(AppSettings.Default.OrgId))
orgId = AppSettings.Default.OrgId;
if (string.IsNullOrEmpty(email) && !string.IsNullOrEmpty(AppSettings.Default.Email))
email = AppSettings.Default.Email;
// If we have cached usage from login and still got nothing live, use that
// Fill in any blanks from cached values saved at login time
if (string.IsNullOrEmpty(email)) email = AppSettings.Default.Email;
if (string.IsNullOrEmpty(orgId)) orgId = AppSettings.Default.OrgId;
if (limits.Count == 0 && !string.IsNullOrEmpty(AppSettings.Default.UsageJson))
{
try
@@ -114,19 +118,6 @@ public class UsageViewModel : INotifyPropertyChanged
catch { }
}
bool isAuth = !string.IsNullOrEmpty(email) || !string.IsNullOrEmpty(orgId) || limits.Count > 0;
if (!isAuth && cookies.Count > 0)
{
// Cookies exist but nothing worked — session likely expired
await Application.Current.Dispatcher.InvokeAsync(() =>
{
IsSignedIn = false;
ErrorMessage = "Session expired — click Sign In to re-authenticate.";
IsLoading = false;
});
return;
}
foreach (var limit in limits)
{
var key = limit.Window.ToString();
@@ -144,6 +135,8 @@ public class UsageViewModel : INotifyPropertyChanged
Overage = overage;
Prepaid = prepaid;
if (!string.IsNullOrEmpty(email)) UserEmail = email;
// Trust that cookies / cached auth mean the user IS signed in,
// even if the live fetch failed this cycle.
IsSignedIn = true;
ErrorMessage = null;
LastUpdated = DateTime.Now;
@@ -198,13 +191,24 @@ public class UsageViewModel : INotifyPropertyChanged
try
{
const string script = @"(async()=>{try{
const b=await(await fetch('/api/bootstrap',{headers:{accept:'application/json'}})).json();
const id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid||null;
const em=b?.account?.email_address||null;
if(!id)return{email:em,orgId:null,usage:null};
const u=await(await fetch('/api/organizations/'+id+'/usage',{headers:{accept:'application/json'}})).json();
return{email:em,orgId:id,usage:u};
}catch(ex){return null;}})()";
const h={headers:{accept:'application/json'}};
const b=await(await fetch('/api/bootstrap',h)).json();
let id=b?.memberships?.[0]?.organization?.uuid||b?.organizations?.[0]?.uuid
||b?.default_organization?.uuid||null;
const em=b?.account?.email_address||b?.account?.email||b?.email||null;
if(!id){
try{const ol=await(await fetch('/api/organizations',h)).json();
if(Array.isArray(ol)&&ol.length>0)id=ol[0]?.uuid||null;}catch(e2){}
}
if(!id){
let pu=null;
try{pu=await(await fetch('/api/usage',h)).json();}catch(e3){}
window.chrome.webview.postMessage(JSON.stringify({email:em,orgId:null,usage:(pu&&!pu.error?pu:null)}));
return;
}
const u=await(await fetch('/api/organizations/'+id+'/usage',h)).json();
window.chrome.webview.postMessage(JSON.stringify({email:em,orgId:id,usage:u}));
}catch(ex){window.chrome.webview.postMessage(null);}})();";
var resultJson = await Application.Current.Dispatcher.InvokeAsync(async () =>
{
@@ -243,6 +247,38 @@ public class UsageViewModel : INotifyPropertyChanged
catch { return ([], null, null); }
}
public async Task LoadFromCacheAsync()
{
var email = AppSettings.Default.Email;
var orgId = AppSettings.Default.OrgId;
var cookie = AppSettings.Default.CookieStore;
bool isAuth = !string.IsNullOrEmpty(email) || !string.IsNullOrEmpty(orgId)
|| !string.IsNullOrEmpty(cookie);
if (!isAuth) return;
List<AgentLimit> limits = [];
if (!string.IsNullOrEmpty(AppSettings.Default.UsageJson))
{
try
{
var cached = JsonSerializer.Deserialize<UsageResponse>(
AppSettings.Default.UsageJson, JsonOpts);
limits = BuildLimits(cached);
}
catch { }
}
await Application.Current.Dispatcher.InvokeAsync(() =>
{
if (!string.IsNullOrEmpty(email)) UserEmail = email;
if (limits.Count > 0) Limits = limits;
IsSignedIn = true;
ErrorMessage = limits.Count == 0 ? "Signed in — refreshing data…" : null;
LastUpdated = limits.Count > 0 ? DateTime.Now : LastUpdated;
});
}
public async Task SignOutAsync()
{
AppSettings.Default.CookieStore = "";
@@ -276,7 +312,7 @@ public class UsageViewModel : INotifyPropertyChanged
public static void SaveCookies(IEnumerable<CoreWebView2Cookie> cookies)
{
var entries = cookies
.Where(c => c.Domain.Contains("claude.ai"))
.Where(c => c.Domain.Contains("claude.ai") || c.Domain.Contains("anthropic.com"))
.Select(c => new CookieEntry { Name = c.Name, Value = c.Value, Domain = c.Domain, Path = c.Path })
.ToList();
AppSettings.Default.CookieStore = JsonSerializer.Serialize(entries);
+21 -12
View File
@@ -34,25 +34,34 @@ internal sealed class WebViewFetchWindow : Window
await _wv.EnsureCoreWebView2Async(env);
var tcs = new TaskCompletionSource<string?>();
var tcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
EventHandler<CoreWebView2NavigationCompletedEventArgs>? handler = null;
handler = async (_, e) =>
// Use WebMessageReceived so the script can post back asynchronously without
// relying on Promise-awaiting support in the WebView2 runtime version.
EventHandler<CoreWebView2WebMessageReceivedEventArgs>? msgHandler = null;
msgHandler = (_, args) =>
{
_wv.CoreWebView2.NavigationCompleted -= handler;
_wv.CoreWebView2.WebMessageReceived -= msgHandler;
tcs.TrySetResult(args.WebMessageAsString);
};
_wv.CoreWebView2.WebMessageReceived += msgHandler;
EventHandler<CoreWebView2NavigationCompletedEventArgs>? navHandler = null;
navHandler = async (_, e) =>
{
_wv.CoreWebView2.NavigationCompleted -= navHandler;
if (!e.IsSuccess) { tcs.TrySetResult(null); return; }
try
{
var result = await _wv.CoreWebView2.ExecuteScriptAsync(script);
tcs.TrySetResult(result);
}
try { await _wv.CoreWebView2.ExecuteScriptAsync(script); }
catch { tcs.TrySetResult(null); }
};
_wv.CoreWebView2.NavigationCompleted += handler;
_wv.CoreWebView2.NavigationCompleted += navHandler;
_wv.CoreWebView2.Navigate(navigateUrl);
_ = Task.Delay(timeoutMs).ContinueWith(_ => tcs.TrySetResult(null));
_ = Task.Delay(timeoutMs).ContinueWith(_ =>
{
_wv.CoreWebView2.WebMessageReceived -= msgHandler;
tcs.TrySetResult(null);
});
return await tcs.Task;
}