Skip to content

Commit ff38d84

Browse files
kenehongCopilotkennierthanuranjeshj
authored
Show chat usage in assistant timestamp footer (#551)
* Add chat usage display options Add usage placement controls for the native chat explorations UI and render context usage in the selected chat/composer locations. Share usage formatting between chat and tray session cards, keep assistant usage monotonic, and round the tray usage bar fill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Trim chat usage PR scope Keep the usage display focused on the assistant timestamp footer and remove the exploration placement options from the PR surface. Hide the chat explorations launcher from Diagnostics and update the page contract test accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix assistant usage footer precedence Prefer latest assistant entry metadata for timestamp footer usage and cover lower thread total regressions. Keep UseMemo dependencies nullable-safe after rebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove chat exploration dead code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: kenehong <22486640+kenehong@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Kenny Hong <kehong@microsoft.com> Co-authored-by: Ranjesh Jaganathan <ranjeshj@microsoft.com>
1 parent d4284d4 commit ff38d84

31 files changed

Lines changed: 1055 additions & 2588 deletions

src/OpenClaw.Chat/ChatModels.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public record ChatThread
8383
public string? ProfileName { get; init; }
8484
public string? Model { get; init; }
8585
public string? ThinkingLevel { get; init; }
86+
public long InputTokens { get; init; }
87+
public long OutputTokens { get; init; }
88+
public long TotalTokens { get; init; }
89+
public long ContextTokens { get; init; }
8690
public int? HistoryCursor { get; init; }
8791
public DateTimeOffset? CreatedAt { get; init; }
8892
public DateTimeOffset? UpdatedAt { get; init; }

src/OpenClaw.Shared/OpenClawGatewayClient.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ private static ChatHistoryInfo ParseChatHistory(JsonElement payload, string sess
548548
if (string.IsNullOrEmpty(text)) continue;
549549
if (string.IsNullOrEmpty(role)) continue;
550550

551+
var (inputTokens, outputTokens, responseTokens, contextPercent) = ExtractChatUsage(m);
551552
list.Add(new ChatMessageInfo
552553
{
553554
SessionKey = sessionKey,
@@ -557,7 +558,11 @@ private static ChatHistoryInfo ParseChatHistory(JsonElement payload, string sess
557558
Ts = ts,
558559
OpenClawId = openClawId,
559560
OpenClawSeq = openClawSeq,
560-
StopReason = stopReason
561+
StopReason = stopReason,
562+
InputTokens = inputTokens,
563+
OutputTokens = outputTokens,
564+
ResponseTokens = responseTokens,
565+
ContextPercent = contextPercent
561566
});
562567
}
563568
info.Messages = list;

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,9 +513,6 @@ _dispatcherQueue is null
513513
// async boundary so onboarding failures are logged instead of escaping
514514
// before the tray ever initializes.
515515
InitializeTrayIcon();
516-
// Apply the user's saved default chat preset (if any) before any chat
517-
// surface mounts so initial render uses their preferred styling.
518-
OpenClawTray.Chat.Explorations.ChatExplorationPresetStore.ApplyDefaultIfPresent();
519516
ShowSurfaceImprovementsTipIfNeeded();
520517

521518
// Initialize connection manager before setup flow.

src/OpenClaw.Tray.WinUI/Chat/ChatEntryMetadata.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,20 @@ namespace OpenClawTray.Chat;
3636
/// Percentage of the model's context window consumed by the conversation
3737
/// when this entry was generated (0–100). Shown as <c>23% ctx</c>.
3838
/// </param>
39+
/// <param name="ContextTokens">
40+
/// Total context window size captured with a session usage snapshot. Used by
41+
/// timestamp usage placement to render <c>usage/total (%)</c> exactly.
42+
/// </param>
43+
/// <param name="UsageContributionTokens">
44+
/// Raw token contribution reported for this assistant response before it was
45+
/// converted into the displayed cumulative session snapshot.
46+
/// </param>
3947
public sealed record ChatEntryMetadata(
4048
DateTimeOffset? Timestamp,
4149
string? Model,
4250
int? InputTokens = null,
4351
int? OutputTokens = null,
4452
int? ResponseTokens = null,
45-
int? ContextPercent = null);
53+
int? ContextPercent = null,
54+
long? ContextTokens = null,
55+
int? UsageContributionTokens = null);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using OpenClaw.Chat;
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace OpenClawTray.Chat;
6+
7+
public static class ChatUsageFormatter
8+
{
9+
public static string? Format(ChatThread? thread)
10+
{
11+
if (thread is null) return null;
12+
13+
var usedTokens = thread.TotalTokens;
14+
if (usedTokens <= 0)
15+
usedTokens = thread.InputTokens + thread.OutputTokens;
16+
17+
return Format(usedTokens, thread.ContextTokens);
18+
}
19+
20+
public static string? Format(
21+
IReadOnlyList<ChatTimelineItem>? entries,
22+
IReadOnlyDictionary<string, ChatEntryMetadata>? metadata)
23+
{
24+
if (entries is null || metadata is null) return null;
25+
26+
for (var i = entries.Count - 1; i >= 0; i--)
27+
{
28+
var entry = entries[i];
29+
if (entry.Kind != ChatTimelineItemKind.Assistant)
30+
continue;
31+
32+
if (!metadata.TryGetValue(entry.Id, out var meta))
33+
continue;
34+
35+
var summary = Format(meta);
36+
if (!string.IsNullOrWhiteSpace(summary))
37+
return summary;
38+
}
39+
40+
return null;
41+
}
42+
43+
public static string? Format(ChatEntryMetadata? metadata)
44+
=> metadata is null
45+
? null
46+
: Format(metadata.InputTokens, metadata.OutputTokens, metadata.ResponseTokens, metadata.ContextPercent, metadata.ContextTokens ?? 0);
47+
48+
public static string? Format(int? inputTokens, int? outputTokens, int? responseTokens, int? contextPercent)
49+
=> Format(inputTokens, outputTokens, responseTokens, contextPercent, 0);
50+
51+
public static string? Format(int? inputTokens, int? outputTokens, int? responseTokens, int? contextPercent, long fallbackContextTokens)
52+
{
53+
long? usedTokens = null;
54+
if (fallbackContextTokens > 0 && contextPercent is int pct)
55+
usedTokens = (long)Math.Round(fallbackContextTokens * Math.Clamp(pct, 0, 100) / 100d, MidpointRounding.AwayFromZero);
56+
usedTokens ??= responseTokens
57+
?? (inputTokens is int input && outputTokens is int output ? input + output : null);
58+
59+
return usedTokens is long used
60+
? Format(used, fallbackContextTokens, contextPercent)
61+
: null;
62+
}
63+
64+
public static string? Format(long usedTokens, long contextTokens, int? contextPercent = null)
65+
{
66+
if (usedTokens < 0) usedTokens = 0;
67+
if (contextTokens < 0) contextTokens = 0;
68+
69+
var percent = contextPercent;
70+
if (percent is null && contextTokens > 0)
71+
percent = (int)Math.Min(100d, (double)usedTokens / contextTokens * 100d);
72+
73+
if (contextTokens <= 0 && usedTokens > 0 && percent is > 0)
74+
contextTokens = (long)Math.Round(usedTokens * 100d / percent.Value, MidpointRounding.AwayFromZero);
75+
76+
if (percent is not null)
77+
percent = Math.Clamp(percent.Value, 0, 100);
78+
79+
if (usedTokens == 0 && contextTokens == 0)
80+
return null;
81+
82+
if (contextTokens > 0)
83+
return $"{FormatCount(usedTokens)}/{FormatCount(contextTokens)} ({percent ?? 0}%)";
84+
85+
return percent is int pct
86+
? $"{FormatCount(usedTokens)} ({pct}%)"
87+
: null;
88+
}
89+
90+
private static string FormatCount(long value)
91+
{
92+
if (value >= 1_000_000)
93+
{
94+
var millions = value / 1_000_000d;
95+
return millions % 1d == 0d
96+
? $"{millions:0}M"
97+
: $"{millions:0.#}M";
98+
}
99+
100+
if (value >= 1_000)
101+
return $"{value / 1_000d:0.0}K";
102+
103+
return value.ToString();
104+
}
105+
}

0 commit comments

Comments
 (0)