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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user