feat(usage): open the TokenTracker dashboard from the usage monitor

The modal deliberately shows only a slice of the analytics; this hands off
to TokenTracker's own local dashboard for the rest. The worker starts
`tokentracker serve` on demand and returns the URL, the UI opens the browser.

Three things the spawn has to get right: port 7680 is not free on Windows
(Delivery Optimization holds [::]:7680) and serve does not fall back, so we
scan 7680-7689 with a dual-stack bind probe; --no-open because the CLI would
open the browser before the server answers; and the child is a cmd.exe shim,
so shutdown kills the process tree. --no-sync keeps our no-cloud-sync rule.
This commit is contained in:
mika kuns
2026-08-24 16:51:00 +02:00
parent 2dec590dfc
commit 3003cfa561
19 changed files with 413 additions and 4 deletions
@@ -210,4 +210,8 @@ public interface IWorkerClient : INotifyPropertyChanged
Task<TokenTrackerStatusDto?> GetTokenTrackerStatusAsync();
Task<TokenTrackerStatusDto?> RefreshTokenTrackerAsync();
Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync();
/// <summary>Starts TokenTracker's local dashboard on the worker and returns its URL; opening
/// the browser is the caller's job.</summary>
Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync();
}
+22
View File
@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
@@ -7,6 +8,27 @@ public static class ShellOpen
{
// "nothing to open" (blank/missing path) is not an error — callers stay silent, matching the
// existing Directory.Exists/File.Exists guards this helper replaces.
/// <summary>Opens an http(s) URL in the default browser. Anything else is refused rather than
/// handed to the shell — a URL comes from the worker, and `UseShellExecute` would happily run
/// a local executable path.</summary>
public static (bool Ok, string? Error) Url(string? url)
{
if (string.IsNullOrWhiteSpace(url)) return (false, null);
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
return (false, $"Not a web URL: {url}");
try
{
Process.Start(new ProcessStartInfo { FileName = uri.AbsoluteUri, UseShellExecute = true });
return (true, null);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
public static (bool Ok, string? Error) Path(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return (false, null);
+5
View File
@@ -698,6 +698,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync()
=> TryInvokeAsync<TokenTrackerStatusDto>("InstallTokenTracker");
public Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync()
=> TryInvokeAsync<TokenTrackerDashboardDto>("OpenTokenTrackerDashboard");
// IWorkerClient explicit implementations (drop typed return values)
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
=> await StartPlanningSessionAsync(taskId, ct);
@@ -872,6 +875,8 @@ public sealed record TaskUsageRowDto(
bool? Productive = null,
bool? OneShot = null);
public sealed record TokenTrackerDashboardDto(bool Ok, string? Url, string? Error);
public sealed record TokenTrackerStatusDto(
bool Installed,
string? Version,
@@ -68,6 +68,8 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(ShowInstallButton))]
private bool _isInstalling;
[ObservableProperty] private bool _isOpeningDashboard;
public bool ModelsEmpty => !IsBusy && ModelRows.Count == 0;
public bool TasksEmpty => !IsBusy && TaskRows.Count == 0;
@@ -229,6 +231,35 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
finally { IsInstalling = false; }
}
/// <summary>Hands off to TokenTracker's own dashboard for everything this modal deliberately
/// doesn't show. The worker starts the local server (first start takes a few seconds), we only
/// open the browser.</summary>
[RelayCommand]
private async Task OpenDashboard()
{
IsOpeningDashboard = true;
try
{
var dashboard = await _worker.OpenTokenTrackerDashboardAsync();
if (dashboard is not { Ok: true, Url: { Length: > 0 } url })
{
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.dashboardFailed",
dashboard?.Error ?? Loc.T("vm.usageMonitor.dashboardUnavailable")));
return;
}
var (ok, error) = ShellOpen.Url(url);
if (!ok)
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.dashboardFailed",
error ?? Loc.T("vm.usageMonitor.dashboardUnavailable")));
}
catch (Exception ex)
{
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.dashboardFailed", ex.Message));
}
finally { IsOpeningDashboard = false; }
}
private Task SetPreset(int days)
{
ApplyPresetRange(days);
@@ -180,6 +180,12 @@
<TextBlock Classes="meta" VerticalAlignment="Center" Text="{Binding AnalyticsStampText}"/>
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.analyticsRefresh}"
Command="{Binding RefreshAnalyticsCommand}"/>
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.openDashboard}"
ToolTip.Tip="{loc:Tr modals.usageMonitor.openDashboardTip}"
IsEnabled="{Binding !IsOpeningDashboard}"
Command="{Binding OpenDashboardCommand}"/>
<Ellipse Classes="spinner" Width="14" Height="14" VerticalAlignment="Center"
IsVisible="{Binding IsOpeningDashboard}"/>
</StackPanel>
<TabControl Padding="20,12" TabStripPlacement="Top">