From 047f743e0b20bfbf573146ffc6acf2f9db565aad Mon Sep 17 00:00:00 2001 From: Gaoyang Date: Tue, 15 Sep 2026 14:23:46 +0800 Subject: [PATCH] feat: add time/token progress bars to watch --view limits Adds Time Progress and Token Progress columns to the limits view, showing how far each window has elapsed and how close it is to its token budget. Since Claude only ever shows a usage percentage, the token budget is derived once from a manually entered percentage and the tokens counted so far, then persisted and tracked live. New --session-token-progress / --week-token-progress options (and the r-key recalibration prompt) accept that percentage. Documents both in the README with a screenshot, and flags the WPF project as not yet ready for use. --- README.md | 20 +- docs/images/watch-limits-screenshot.png | Bin 0 -> 58862 bytes .../Commands/LimitsAnchors.cs | 177 +++++++++++++++--- .../Commands/WatchCommand.cs | 100 +++++++--- src/AIUsageMonitor.Cli/LimitsSettings.cs | 17 +- .../Rendering/SpectreRenderer.cs | 73 +++++++- 6 files changed, 328 insertions(+), 59 deletions(-) create mode 100644 docs/images/watch-limits-screenshot.png diff --git a/README.md b/README.md index d5fd825..b5ed90d 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,18 @@ Commands: - `watch` — live-updating view (`limits|today|week|models|sessions|hours`, default `limits`), refreshed on an interval - `export` — export raw analytics; supports `--format json|csv` and `--output ` (defaults to stdout, JSON) -`watch`'s default view, `limits`, approximates the "Current Session" (rolling 5-hour window) and "This Week" usage panels shown in Claude's own account UI. Since the real reset times live on the Anthropic account and can't be read locally, they're estimated from local transcript timestamps unless pinned with `--session-reset "HH:mm"` (e.g. `--session-reset "18:30"` — just copy the reset time Claude itself shows) / `--week-reset "Ddd HH:mm"` (e.g. `--week-reset "Mon 09:00"`). When the session reset time has elapsed and this machine has no activity in the last 5 hours (the account may be idle, or in use on another device), the session row shows as unknown rather than a made-up countdown. The weekly reset is a fixed time assigned to your account, unrelated to activity, so it can't be derived locally at all — without `--week-reset` that row also shows as unknown, with the trailing 7 days of usage shown as an upper bound on the current cycle instead; that fixed time only needs to be entered once, unlike the session reset. Press `r` while `watch` is running to re-enter either reset time (Enter skips one and keeps the local estimate). If neither reset time is configured and you're in an interactive terminal, `watch` prompts for them once and remembers the answer in `%LOCALAPPDATA%/aimon/limits-settings.json` (or the OS equivalent) for future runs. +`watch`'s default view, `limits`, approximates the "Current Session" (rolling 5-hour window) and "This Week" usage panels shown in Claude's own account UI, including a **Time Progress** bar (how far each window has elapsed) and a **Token Progress** bar (how close the window is to its token budget). Since the real reset times live on the Anthropic account and can't be read locally, they're estimated from local transcript timestamps unless pinned with `--session-reset "HH:mm"` (e.g. `--session-reset "18:30"` — just copy the reset time Claude itself shows) / `--week-reset "Ddd HH:mm"` (e.g. `--week-reset "Mon 09:00"`). When the session reset time has elapsed and this machine has no activity in the last 5 hours (the account may be idle, or in use on another device), the session row shows as unknown rather than a made-up countdown. The weekly reset is a fixed time assigned to your account, unrelated to activity, so it can't be derived locally at all — without `--week-reset` that row also shows as unknown, with the trailing 7 days of usage shown as an upper bound on the current cycle instead; that fixed time only needs to be entered once, unlike the session reset. + +Claude itself only ever shows a usage *percentage*, never the underlying token limit, so the Token Progress bar works the same way: pass `--session-token-progress ` / `--week-token-progress ` (e.g. `--session-token-progress 32` for "32%", copied from Settings > Usage or `/usage` in Claude Code) and `watch` derives a token budget from that percentage and the tokens it has counted for the window so far, then tracks the bar live against that budget without asking again. + +Press `r` while `watch` is running to re-enter any of the four values — reset times and usage percentages (Enter on a prompt keeps its current value or local estimate). If any of the four aren't configured and you're in an interactive terminal, `watch` prompts for them once up front and remembers the answers in `%LOCALAPPDATA%/aimon/limits-settings.json` (or the OS equivalent) for future runs. + +![watch --view limits](docs/images/watch-limits-screenshot.png) ### WPF dashboard (Windows only) +> **⚠️ Work in progress — not ready for use yet.** The WPF project is still under active development; expect missing features and rough edges. Use the CLI (`aimon`) for now. + ``` dotnet run --project src/AIUsageMonitor.WPF ``` @@ -137,10 +145,18 @@ aimon <命令> - `watch` — 实时刷新视图(`limits|today|week|models|sessions|hours`,默认为 `limits`),按指定间隔自动刷新 - `export` — 导出原始分析数据;支持 `--format json|csv` 与 `--output `(默认输出到标准输出,格式为 JSON) -`watch` 的默认视图 `limits` 近似展示 Claude 官方账户界面中的 "Current Session"(滚动 5 小时窗口)和 "This Week" 用量面板。由于真实的重置时间存储在 Anthropic 账号侧,本地无法读取,默认会根据本地会话记录的时间戳估算;也可以用 `--session-reset "HH:mm"`(如 `--session-reset "18:30"`,照抄 Claude 显示的重置时间即可)/ `--week-reset "Ddd HH:mm"`(如 `--week-reset "Mon 09:00"`)锚定从 Claude 官方界面查到的真实值。会话重置时间过期、且本机近 5 小时无活动记录时(可能账户空闲,也可能正在其他设备使用),会话行会显示为"未知"而不是编造的倒计时。周重置是账号固定的每周时刻,与活动无关,本地无法推算,所以未配置 `--week-reset` 时周行同样显示为"未知",只按近 7 天用量给出当前周期用量的上界;这个固定时刻只需录入一次,不会像会话那样过期。在 `watch` 运行中按 `r` 可随时依次重新录入这两个时刻(回车跳过任意一个即可继续用本地估算)。如果两者都未配置且在交互式终端中运行,`watch` 会提示输入一次,并把结果保存到 `%LOCALAPPDATA%/aimon/limits-settings.json`(或对应系统的等效路径)供后续运行复用。 +`watch` 的默认视图 `limits` 近似展示 Claude 官方账户界面中的 "Current Session"(滚动 5 小时窗口)和 "This Week" 用量面板,并附带 **Time Progress**(当前窗口已经过去的时间比例)与 **Token Progress**(当前窗口用量占预算的比例)两条进度条。由于真实的重置时间存储在 Anthropic 账号侧,本地无法读取,默认会根据本地会话记录的时间戳估算;也可以用 `--session-reset "HH:mm"`(如 `--session-reset "18:30"`,照抄 Claude 显示的重置时间即可)/ `--week-reset "Ddd HH:mm"`(如 `--week-reset "Mon 09:00"`)锚定从 Claude 官方界面查到的真实值。会话重置时间过期、且本机近 5 小时无活动记录时(可能账户空闲,也可能正在其他设备使用),会话行会显示为"未知"而不是编造的倒计时。周重置是账号固定的每周时刻,与活动无关,本地无法推算,所以未配置 `--week-reset` 时周行同样显示为"未知",只按近 7 天用量给出当前周期用量的上界;这个固定时刻只需录入一次,不会像会话那样过期。 + +Claude 官方界面本身也只显示用量**百分比**,从不显示背后的 token 上限,因此 Token Progress 走同样的逻辑:传入 `--session-token-progress <百分比>` / `--week-token-progress <百分比>`(如 `--session-token-progress 32`,照抄 Settings > Usage 或 Claude Code 里 `/usage` 显示的 "32%"),`watch` 会结合这个百分比与当前已统计到的 token 数反推出一个预算,之后就能持续实时对照这个预算刷新进度条,不用每次都重新问。 + +在 `watch` 运行中按 `r` 可随时重新录入这四个值——两个重置时刻和两个用量百分比(提示时直接回车会保留当前值或本地估算)。如果这四项中有任意一项未配置且在交互式终端中运行,`watch` 会一次性提示输入,并把结果保存到 `%LOCALAPPDATA%/aimon/limits-settings.json`(或对应系统的等效路径)供后续运行复用。 + +![watch --view limits](docs/images/watch-limits-screenshot.png) ### WPF 仪表盘(仅 Windows) +> **⚠️ 尚在开发中,暂不建议使用。** WPF 项目目前仍在积极开发,功能不完整,可能存在明显问题。请先使用 CLI(`aimon`)。 + ``` dotnet run --project src/AIUsageMonitor.WPF ``` diff --git a/docs/images/watch-limits-screenshot.png b/docs/images/watch-limits-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..fd4f35dc108a4c07de38d0439397c734dd41d275 GIT binary patch literal 58862 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 EA6usEqyPW_ literal 0 HcmV?d00001 diff --git a/src/AIUsageMonitor.Cli/Commands/LimitsAnchors.cs b/src/AIUsageMonitor.Cli/Commands/LimitsAnchors.cs index 353942f..a122fca 100644 --- a/src/AIUsageMonitor.Cli/Commands/LimitsAnchors.cs +++ b/src/AIUsageMonitor.Cli/Commands/LimitsAnchors.cs @@ -32,6 +32,103 @@ internal static (Option SessionReset, Option WeekReset) Create return (sessionResetOption, weekResetOption); } + /// + /// Creates the shared --session-token-progress/--week-token-progress options. These + /// take a *percentage* (what Claude itself displays), not a token count - see + /// for how that percentage is turned into a token limit. + /// + internal static (Option SessionTokenProgress, Option WeekTokenProgress) CreateTokenProgressOptions() + { + var sessionTokenProgressOption = new Option("--session-token-progress") + { + Description = "Current session usage percentage as shown on Claude's own usage page (Settings > Usage, or /usage in Claude Code), e.g. 32 for '32%'. Combined with the tokens counted so far this session to derive a token budget, which is then used to track the token-progress bar live. Only used when --view limits." + }; + var weekTokenProgressOption = new Option("--week-token-progress") + { + Description = "Current weekly usage percentage as shown on Claude's own usage page, e.g. 32 for '32%'. Combined with the tokens counted so far this week to derive a token budget. Only used when --view limits." + }; + return (sessionTokenProgressOption, weekTokenProgressOption); + } + + /// + /// Interactively prompts for both the session and weekly reset times and token limits, and + /// persists the result. Used by watch's recalibration hotkey. Each prompt shows the + /// currently configured value (if any) as its default, so pressing Enter keeps it unchanged - the + /// same value is then returned as-is, without being re-validated against the 5-hour session + /// window, so a stale (already elapsed) session reset can still be kept without an error round-trip. + /// + /// The tokens counted for the current session window so far, used to re-derive its token limit from a freshly entered percentage. + /// The tokens counted for the current week window so far. + /// The resolved session/weekly reset times and session/weekly token limits. + internal static ( + DateTimeOffset? SessionResetAt, (DayOfWeek Day, TimeSpan TimeOfDay)? WeekResetAt, + long? SessionTokenLimit, long? WeekTokenLimit) PromptAndSaveBoth(long sessionTokensSoFar, long weekTokensSoFar) + { + var saved = LimitsSettingsStore.Load(); + + var sessionResetAt = PromptForSessionReset(saved.SessionResetAt); + var (weekResetAt, weekResetRaw) = PromptForWeekReset(saved.WeekResetAt); + var sessionTokenLimit = PromptForTokenLimit( + saved.SessionTokenLimit, sessionTokensSoFar, + "[yellow]Session usage percentage.[/] Enter the percentage Claude's usage display shows " + + "(Settings > Usage, or /usage in Claude Code), e.g. 32, or press Enter to keep the current value:"); + var weekTokenLimit = PromptForTokenLimit( + saved.WeekTokenLimit, weekTokensSoFar, + "[yellow]Weekly usage percentage.[/] Enter the percentage Claude's usage display shows " + + "(Settings > Usage, or /usage in Claude Code), e.g. 32, or press Enter to keep the current value:"); + + LimitsSettingsStore.Save(new(sessionResetAt, weekResetRaw, sessionTokenLimit, weekTokenLimit)); + return (sessionResetAt, weekResetAt, sessionTokenLimit, weekTokenLimit); + } + + /// + /// Resolves the token limit for one usage window (session or week) from a usage *percentage* - + /// either an explicit CLI argument or (in an interactive terminal) a prompt - combined with the + /// tokens this tool has counted for that window so far: limit = tokensSoFar / (percent / 100). + /// Claude itself never shows the underlying limit, only a percentage, so this is the only way to + /// derive one locally. The derived limit (not the percentage) is what gets persisted and returned, + /// so future refreshes can keep tracking the token-progress bar live against it without asking + /// again - until the user recalibrates (e.g. because the account's limit changed). + /// + /// The raw --session-token-progress/--week-token-progress argument, if provided. + /// The previously persisted token limit for this window, if any. + /// The tokens this tool has counted for the window so far. + /// A short label (e.g. "Session", "Weekly") used in the interactive prompt text. + /// The resolved token limit, or if not configured. + /// An error message describing why resolution failed, if it did. + /// if resolution succeeded; if an explicit argument was invalid. + internal static bool ResolveTokenLimit( + double? progressPercentArg, long? savedLimit, long tokensSoFar, string promptLabel, + out long? tokenLimit, out string? error) + { + error = null; + + if (progressPercentArg is not null) + { + if (progressPercentArg <= 0) + { + tokenLimit = null; + error = $"Invalid usage percentage '{progressPercentArg}'. Expected a percentage greater than 0."; + return false; + } + + tokenLimit = DeriveTokenLimit(tokensSoFar, progressPercentArg.Value); + return true; + } + + if (savedLimit is not null || Console.IsInputRedirected) + { + tokenLimit = savedLimit; + return true; + } + + tokenLimit = PromptForTokenLimit( + currentLimit: null, tokensSoFar, + $"[yellow]{promptLabel} usage percentage not set.[/] Enter the percentage Claude's usage display shows " + + "(Settings > Usage, or /usage in Claude Code), e.g. 32, or press Enter to skip the token progress bar:"); + return true; + } + /// /// Parses a weekly reset time string of the form "Ddd HH:mm" (e.g. "Mon 09:00") /// into a day-of-week and local time-of-day pair. @@ -139,7 +236,14 @@ internal static bool TryResolve( if (explicitlyProvided || promptedForAnything) { - LimitsSettingsStore.Save(new(effectiveSessionReset, effectiveWeekResetRaw)); + // Token limits are resolved separately (see ResolveTokenLimit), since deriving them needs + // the tokens counted for the window - which isn't known until after the reset times below + // are settled. Carry the currently saved values through unchanged. + LimitsSettingsStore.Save(saved with + { + SessionResetAt = effectiveSessionReset, + WeekResetAt = effectiveWeekResetRaw + }); } sessionResetAt = effectiveSessionReset; @@ -148,23 +252,14 @@ internal static bool TryResolve( } /// - /// Interactively prompts for both the session and weekly reset times and persists the result. - /// Used by watch's recalibration hotkey. Each prompt shows the currently configured - /// value (if any) as its default, so pressing Enter keeps it unchanged — the same value is - /// then returned as-is, without being re-validated against the 5-hour session window, so a - /// stale (already elapsed) session reset can still be kept without an error round-trip. + /// Derives a token limit from the tokens counted so far and the percentage of that limit they + /// represent, as read off Claude's own usage display. Never returns less than 1, since a 0 limit + /// would later be divided into and would also get persisted as savedLimit, silently + /// breaking the token-progress bar until the user recalibrates - most commonly when tokensSoFar + /// is still 0 right after the window has reset. /// - /// The resolved session and weekly reset times. - internal static (DateTimeOffset? SessionResetAt, (DayOfWeek Day, TimeSpan TimeOfDay)? WeekResetAt) PromptAndSaveBoth() - { - var saved = LimitsSettingsStore.Load(); - - var sessionResetAt = PromptForSessionReset(saved.SessionResetAt); - var (weekResetAt, weekResetRaw) = PromptForWeekReset(saved.WeekResetAt); - - LimitsSettingsStore.Save(new(sessionResetAt, weekResetRaw)); - return (sessionResetAt, weekResetAt); - } + private static long DeriveTokenLimit(long tokensSoFar, double progressPercent) + => Math.Max(1, (long)Math.Round(tokensSoFar / (progressPercent / 100))); /// /// The currently configured reset time, if any. Shown as the prompt's default so pressing @@ -184,7 +279,7 @@ internal static (DateTimeOffset? SessionResetAt, (DayOfWeek Day, TimeSpan TimeOf while (true) { - var input = AnsiConsole.Ask(promptText, defaultText); + var input = AnsiConsole.Ask(promptText, defaultText); if (string.IsNullOrWhiteSpace(input)) { @@ -205,6 +300,44 @@ internal static (DateTimeOffset? SessionResetAt, (DayOfWeek Day, TimeSpan TimeOf } } + /// + /// Prompts for a usage percentage and derives a token limit from it and . + /// The prompt's default is the percentage currently implies (given + /// ), so pressing Enter keeps the existing limit unchanged rather + /// than re-deriving it from a rounded default percentage. + /// + /// The currently configured token limit, if any. + /// The tokens counted for the window so far. + /// The prompt text to display. + private static long? PromptForTokenLimit(long? currentLimit, long tokensSoFar, string promptText) + { + var defaultText = currentLimit is { } limit and > 0 + ? (tokensSoFar * 100.0 / limit).ToString("0.##", CultureInfo.InvariantCulture) + : ""; + + while (true) + { + var input = AnsiConsole.Ask(promptText, defaultText); + + if (string.IsNullOrWhiteSpace(input)) + { + return null; + } + + if (input == defaultText && currentLimit is not null) + { + return currentLimit; + } + + if (double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) && parsed > 0) + { + return DeriveTokenLimit(tokensSoFar, parsed); + } + + AnsiConsole.MarkupLine("[red]Invalid value. Expected a percentage greater than 0, e.g. 32.[/]"); + } + } + /// /// The currently configured reset time, in its originally typed 'Ddd HH:mm' form, if any. Shown /// as the prompt's default so pressing Enter keeps it. @@ -222,7 +355,7 @@ private static ((DayOfWeek Day, TimeSpan TimeOfDay)? Reset, string? Raw) PromptF while (true) { - var input = AnsiConsole.Ask(promptText, defaultText); + var input = AnsiConsole.Ask(promptText, defaultText); if (string.IsNullOrWhiteSpace(input)) { @@ -238,6 +371,9 @@ private static ((DayOfWeek Day, TimeSpan TimeOfDay)? Reset, string? Raw) PromptF } } + private static DateTimeOffset ToLocalOffset(DateTime localDateTime) + => new(localDateTime, TimeZoneInfo.Local.GetUtcOffset(localDateTime)); + private static bool TryParseDayOfWeek(string value, out DayOfWeek day) { foreach (var candidate in Enum.GetValues()) @@ -287,7 +423,4 @@ private static bool TryParseSessionReset(string value, out DateTimeOffset result error = null; return true; } - - private static DateTimeOffset ToLocalOffset(DateTime localDateTime) - => new(localDateTime, TimeZoneInfo.Local.GetUtcOffset(localDateTime)); } \ No newline at end of file diff --git a/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs b/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs index f5cbe32..97a841e 100644 --- a/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs +++ b/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs @@ -31,10 +31,13 @@ public static Command Create(DataService dataService) DefaultValueFactory = _ => 2 }; var (sessionResetOption, weekResetOption) = LimitsAnchors.CreateOptions(); + var (sessionTokenProgressOption, weekTokenProgressOption) = LimitsAnchors.CreateTokenProgressOptions(); command.Options.Add(viewOption); command.Options.Add(intervalOption); command.Options.Add(sessionResetOption); command.Options.Add(weekResetOption); + command.Options.Add(sessionTokenProgressOption); + command.Options.Add(weekTokenProgressOption); command.SetAction(async (parseResult, ct) => { @@ -42,16 +45,56 @@ public static Command Create(DataService dataService) var interval = TimeSpan.FromSeconds(Math.Max(1, parseResult.GetValue(intervalOption))); var sessionResetArg = parseResult.GetValue(sessionResetOption); var weekResetArg = parseResult.GetValue(weekResetOption); + var sessionTokenProgressArg = parseResult.GetValue(sessionTokenProgressOption); + var weekTokenProgressArg = parseResult.GetValue(weekTokenProgressOption); DateTimeOffset? effectiveSessionResetAt = null; (DayOfWeek Day, TimeSpan TimeOfDay)? effectiveWeekResetAt = null; + long? effectiveSessionTokenLimit = null; + long? effectiveWeekTokenLimit = null; + UsageWindowSummary? lastSessionWindow = null; + UsageWindowSummary? lastWeekWindow = null; + if (view == "limits") { - if (!LimitsAnchors.TryResolve(sessionResetArg, weekResetArg, out effectiveSessionResetAt, out effectiveWeekResetAt, out var error)) + var saved = LimitsSettingsStore.Load(); + + // Usage data is loaded once, up front, before any prompt - both so the "loading" step + // doesn't land in between the reset-time and token-percentage prompts below, and so + // that single fetch's result is reused for the token-limit derivation and the first + // frame rather than being re-fetched. These windows aren't pinned to the real reset + // times yet (those are still being asked about below), so the very next refresh tick + // re-fetches them properly pinned. + (lastSessionWindow, lastWeekWindow) = ProgressReporter.Run("Loading usage data...", p => + (dataService.GetCurrentSessionWindow(null, p), dataService.GetWeekWindow(null, p))); + + if (!LimitsAnchors.TryResolve( + sessionResetArg, weekResetArg, out effectiveSessionResetAt, out effectiveWeekResetAt, out var error)) + { + AnsiConsole.MarkupLine($"[red]{error}[/]"); + return 1; + } + + // Reload, since TryResolve may have just persisted the reset times entered above - + // reusing the pre-prompt snapshot here would overwrite them with their stale values. + saved = LimitsSettingsStore.Load(); + + if (!LimitsAnchors.ResolveTokenLimit( + sessionTokenProgressArg, saved.SessionTokenLimit, lastSessionWindow.TotalTokens, "Session", + out effectiveSessionTokenLimit, out error) + || !LimitsAnchors.ResolveTokenLimit( + weekTokenProgressArg, saved.WeekTokenLimit, lastWeekWindow.TotalTokens, "Weekly", + out effectiveWeekTokenLimit, out error)) { AnsiConsole.MarkupLine($"[red]{error}[/]"); return 1; } + + LimitsSettingsStore.Save(saved with + { + SessionTokenLimit = effectiveSessionTokenLimit, + WeekTokenLimit = effectiveWeekTokenLimit + }); } var recalibrationEnabled = view == "limits" && !Console.IsInputRedirected; @@ -75,15 +118,21 @@ public static Command Create(DataService dataService) IRenderable BuildLimits(IProgress? progress) { + lastSessionWindow = dataService.GetCurrentSessionWindow(effectiveSessionResetAt, progress); + lastWeekWindow = dataService.GetWeekWindow(effectiveWeekResetAt, progress); return SpectreRenderer.BuildUsageLimits( - dataService.GetCurrentSessionWindow(effectiveSessionResetAt, progress), - dataService.GetWeekWindow(effectiveWeekResetAt, progress), + lastSessionWindow, + lastWeekWindow, + effectiveSessionTokenLimit, + effectiveWeekTokenLimit, recalibrationEnabled); } ClearScreen(); - var current = ProgressReporter.Run("Loading usage data...", p => BuildCurrent(p)); + var current = view == "limits" + ? SpectreRenderer.BuildUsageLimits(lastSessionWindow!, lastWeekWindow!, effectiveSessionTokenLimit, effectiveWeekTokenLimit, recalibrationEnabled) + : ProgressReporter.Run("Loading usage data...", BuildCurrent); while (!ct.IsCancellationRequested) { @@ -113,10 +162,11 @@ await AnsiConsole.Live(current) } ClearScreen(); - AnsiConsole.MarkupLine("[grey]Press Enter on either prompt to keep using a local estimate.[/]"); - (effectiveSessionResetAt, effectiveWeekResetAt) = LimitsAnchors.PromptAndSaveBoth(); + AnsiConsole.MarkupLine("[grey]Press Enter on any prompt to keep using a local estimate or the current value.[/]"); + (effectiveSessionResetAt, effectiveWeekResetAt, effectiveSessionTokenLimit, effectiveWeekTokenLimit) = LimitsAnchors.PromptAndSaveBoth( + lastSessionWindow?.TotalTokens ?? 0, lastWeekWindow?.TotalTokens ?? 0); ClearScreen(); - current = ProgressReporter.Run("Loading usage data...", p => BuildCurrent(p)); + current = ProgressReporter.Run("Loading usage data...", BuildCurrent); } ClearScreen(); @@ -139,6 +189,24 @@ private static void ClearScreen() } } + private static bool RecalibrationKeyPressed() + { + try + { + var pressed = false; + while (Console.KeyAvailable) + { + pressed |= Console.ReadKey(intercept: true).Key == ConsoleKey.R; + } + return pressed; + } + catch (InvalidOperationException) + { + // No console input stream available (e.g. running without a real terminal). + return false; + } + } + /// /// Waits out the refresh interval, watching for the recalibration hotkey while it does. /// @@ -178,22 +246,4 @@ private static async Task WaitForRefreshAsync(TimeSpan interval, bool reca return false; } - - private static bool RecalibrationKeyPressed() - { - try - { - var pressed = false; - while (Console.KeyAvailable) - { - pressed |= Console.ReadKey(intercept: true).Key == ConsoleKey.R; - } - return pressed; - } - catch (InvalidOperationException) - { - // No console input stream available (e.g. running without a real terminal). - return false; - } - } } \ No newline at end of file diff --git a/src/AIUsageMonitor.Cli/LimitsSettings.cs b/src/AIUsageMonitor.Cli/LimitsSettings.cs index 366d981..2938356 100644 --- a/src/AIUsageMonitor.Cli/LimitsSettings.cs +++ b/src/AIUsageMonitor.Cli/LimitsSettings.cs @@ -8,12 +8,23 @@ namespace AIUsageMonitor.Cli; /// /// The real reset time of the current session window, if configured. /// The real weekly reset time, as originally typed (e.g. "Mon 09:00"), if configured. +/// +/// The account's session (5h) token limit, in tokens. Claude itself only ever shows a usage +/// *percentage*, never the underlying limit, so this value is not typed in directly - it is derived +/// once (currentTokens / (enteredPercent / 100)) from the percentage the user read off +/// Claude's usage display and the token count this tool had counted for the window at that moment. +/// Once derived it is persisted and reused so the token-progress bar can keep tracking live as +/// tokens accrue, without asking the user again every refresh. +/// +/// The account's weekly token limit, derived the same way as . public sealed record LimitsSettings( DateTimeOffset? SessionResetAt, - string? WeekResetAt) + string? WeekResetAt, + long? SessionTokenLimit = null, + long? WeekTokenLimit = null) { - /// An empty settings instance with no configured reset times. - public static readonly LimitsSettings Empty = new(null, null); + /// An empty settings instance with no configured reset times or token limits. + public static readonly LimitsSettings Empty = new(null, null, null, null); } /// diff --git a/src/AIUsageMonitor.Cli/Rendering/SpectreRenderer.cs b/src/AIUsageMonitor.Cli/Rendering/SpectreRenderer.cs index 89ea9d7..1eb8759 100644 --- a/src/AIUsageMonitor.Cli/Rendering/SpectreRenderer.cs +++ b/src/AIUsageMonitor.Cli/Rendering/SpectreRenderer.cs @@ -199,24 +199,35 @@ public static IRenderable BuildSessionStats(SessionStats stats) /// /// The current 5-hour session window data to render. /// The current weekly window data to render. + /// + /// The account's session token limit, or if not configured (in which case + /// the token-progress column is left blank for that row). Claude itself only ever shows a usage + /// *percentage*, never this limit, so it is derived once from a percentage the user read off + /// Claude's usage display - see - and then used here + /// to track 's live token count against it. + /// + /// The account's weekly token limit, or if not configured. /// /// Whether the "press r" hotkey hint applies, i.e. whether the caller is actually watching for /// it (it does nothing outside an interactive watch session). /// /// An representing both usage windows. public static IRenderable BuildUsageLimits( - UsageWindowSummary sessionWindow, UsageWindowSummary weekWindow, bool recalibrationAvailable = true) + UsageWindowSummary sessionWindow, UsageWindowSummary weekWindow, + long? sessionTokenLimit = null, long? weekTokenLimit = null, bool recalibrationAvailable = true) { - var table = new Table().Border(TableBorder.Rounded).Title("[bold yellow]Usage Limits[/]"); + var table = new Table().Border(TableBorder.Rounded).Title("[bold yellow]Usage Limits[/]").ShowRowSeparators(); table.AddColumn(new TableColumn("Window").NoWrap()); table.AddColumn(new TableColumn("Tokens").RightAligned().NoWrap()); table.AddColumn(new TableColumn("Msgs").RightAligned().NoWrap()); table.AddColumn(new TableColumn("Cost").RightAligned().NoWrap()); table.AddColumn(new TableColumn("Resets At").RightAligned().NoWrap()); table.AddColumn(new TableColumn("Time Left").RightAligned().NoWrap()); + table.AddColumn(new TableColumn("Time Progress").NoWrap()); + table.AddColumn(new TableColumn("Token Progress").NoWrap()); - AddRow(table, "Session (5h)", sessionWindow); - AddRow(table, "Week", weekWindow); + AddRow(table, "Session (5h)", sessionWindow, sessionTokenLimit); + AddRow(table, "Week", weekWindow, weekTokenLimit); var notes = new List { table }; @@ -249,15 +260,19 @@ public static IRenderable BuildUsageLimits( return notes.Count == 1 ? table : new Rows(notes); - static void AddRow(Table table, string label, UsageWindowSummary window) + static void AddRow(Table table, string label, UsageWindowSummary window, long? tokenLimit) { var (tokens, messages, cost) = window.WindowStart is null ? ("—", "—", "—") : (FormatTokens(window.TotalTokens), $"{window.Messages:N0}", $"{window.EstimatedCost:C2}"); + var tokenProgress = window.WindowStart is not null && tokenLimit is { } limit and > 0 + ? FormatProgressBar(window.TotalTokens / (double)limit) + : "—"; + if (window.ResetsAt is not { } resetsAt) { - table.AddRow(label, tokens, messages, cost, "?", "unknown"); + table.AddRow(label, tokens, messages, cost, "?", "unknown", "—", tokenProgress); return; } @@ -265,16 +280,60 @@ static void AddRow(Table table, string label, UsageWindowSummary window) var remaining = resetsAt > now ? resetsAt - now : TimeSpan.Zero; var marker = window.Confidence is WindowConfidence.Estimated ? "~" : ""; + var timeProgress = window.WindowStart is { } windowStart && resetsAt > windowStart + ? FormatNeutralProgressBar((now - windowStart).Ticks / (double)(resetsAt - windowStart).Ticks) + : "—"; + table.AddRow( label, tokens, messages, cost, $"{marker}{resetsAt:MM-dd HH:mm}", - FormatDuration(remaining)); + FormatDuration(remaining), + timeProgress, + tokenProgress); } } + /// + /// Renders a fixed-width text progress bar (e.g. [███████████████░░░░░] 72%) for a 0-1 + /// fraction, colored green/yellow/orange/red by how full it is so low/medium/high usage is + /// visible at a glance. Used for the token-progress column, where the fraction is a genuine + /// "how close to the limit" status. A fraction at or beyond 1 renders as a full, red bar with the + /// actual percentage (which can exceed 100%) rather than being silently capped. + /// + private static string FormatProgressBar(double fraction) + { + var color = fraction switch + { + >= 1.0 => "red", + >= 0.85 => "orange3", + >= 0.5 => "yellow", + _ => "green" + }; + + return FormatProgressBar(fraction, color); + } + + /// + /// Renders the same fixed-width text progress bar as , but + /// in a single neutral color regardless of how full it is. Used for the time-progress column, + /// where the fraction just reflects the clock ticking forward rather than a risk level - so + /// unlike token usage, it has no "bad" value worth flagging in color. + /// + private static string FormatNeutralProgressBar(double fraction) => FormatProgressBar(fraction, "grey"); + + private static string FormatProgressBar(double fraction, string color) + { + const int width = 20; + var filledCount = Math.Clamp((int)Math.Round(fraction * width), 0, width); + var bar = new string('█', filledCount) + new string('░', width - filledCount); + var percentText = $"{fraction * 100:0}%"; + + return $"[{color}][[{bar}]] {percentText}[/]"; + } + /// /// Renders the daily summary directly to the console. ///