mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(ui): read popover cost from usage.cost and key usage empty state off content (#123269)
The chat message meta popover read message.cost, a field no producer ever writes — AssistantMessage nests cost under usage.cost — so the $ line was permanently dead. Read usage.cost with the bare-field fallback removed to a single expression. The usage page empty state keyed off !data.totals, but the gateway always returns a totals object (all-zero when idle), so the "no usage data yet" card never rendered; it also ignored data.error, so a failed load could show the empty card under the error callout. Key it off actual content and never render it in the error state.
This commit is contained in:
committed by
GitHub
parent
575467aa58
commit
f2862fa265
@@ -161,7 +161,12 @@ export function extractGroupMeta(
|
||||
cacheWrite += callCacheWrite;
|
||||
maxPromptTokens = Math.max(maxPromptTokens, callInput + callCacheRead + callCacheWrite);
|
||||
}
|
||||
const c = m.cost as Record<string, number> | undefined;
|
||||
// Producers write cost nested under usage.cost (the AssistantMessage
|
||||
// shape); a bare message.cost never exists, so reading only it left the
|
||||
// popover's $ line permanently dead.
|
||||
const c =
|
||||
(usage as { cost?: { total?: number } } | undefined)?.cost ??
|
||||
(m.cost as Record<string, number> | undefined);
|
||||
if (c?.total) {
|
||||
cost += c.total;
|
||||
}
|
||||
|
||||
@@ -1332,6 +1332,18 @@ describe("grouped chat rendering", () => {
|
||||
10_000,
|
||||
);
|
||||
expect(outputHeavy.querySelector(".msg-meta__ctx")?.textContent).toBe("10% ctx");
|
||||
|
||||
// Cost is nested under usage.cost in the canonical AssistantMessage
|
||||
// shape; the popover must surface it (it was dead reading message.cost).
|
||||
const withCost = renderUsage(
|
||||
{
|
||||
input: 1_000,
|
||||
output: 500,
|
||||
cost: { total: 0.1234 } as unknown as number,
|
||||
} as Record<string, number>,
|
||||
10_000,
|
||||
);
|
||||
expect(withCost.querySelector(".msg-meta__cost")?.textContent).toContain("$0.12");
|
||||
});
|
||||
|
||||
it("previews message context from the timestamp and pins it on click", () => {
|
||||
|
||||
@@ -650,4 +650,47 @@ describe("renderUsage", () => {
|
||||
expect(container.querySelector(".cost-window-analysis")).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows the empty state for an all-zero successful response", () => {
|
||||
const zeroTotals = {
|
||||
totalTokens: 0,
|
||||
totalCost: 0,
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
missingCostEntries: 0,
|
||||
};
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderUsage(
|
||||
createUsageProps({
|
||||
data: {
|
||||
...createUsageProps().data,
|
||||
// The gateway always returns a totals object, even with no usage.
|
||||
totals: zeroTotals as UsageProps["data"]["totals"],
|
||||
},
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.querySelector(".usage-empty-state")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not render the empty state under an error callout", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderUsage(
|
||||
createUsageProps({
|
||||
data: {
|
||||
...createUsageProps().data,
|
||||
error: "usage failed",
|
||||
},
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
expect(container.querySelector(".usage-callout")).not.toBeNull();
|
||||
expect(container.querySelector(".usage-empty-state")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -369,7 +369,14 @@ export function renderUsage(props: UsageProps) {
|
||||
: data.costDaily;
|
||||
|
||||
const insightStats = buildUsageInsightStats(aggregateSessions, insightTotals, insightAggregates);
|
||||
const isEmpty = !data.loading && !data.totals && data.sessions.length === 0;
|
||||
// The gateway always returns a totals object (all-zero when idle), so key
|
||||
// the empty state off content — and never render it under an error callout,
|
||||
// where "no usage data yet" would misexplain the failure.
|
||||
const isEmpty =
|
||||
!data.loading &&
|
||||
!data.error &&
|
||||
data.sessions.length === 0 &&
|
||||
(data.totals?.totalTokens ?? 0) === 0;
|
||||
const cacheStatusTitle = getUsageCacheRefreshTitle(data.cacheStatus);
|
||||
const hasMissingCost =
|
||||
(insightTotals?.missingCostEntries ?? 0) > 0 ||
|
||||
|
||||
Reference in New Issue
Block a user