From 648a48ac03ca1343506183f29016228597751c46 Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:20:48 +0700 Subject: [PATCH 1/4] Port v0.58 usage spend parity (cherry picked from commit f43cb6eda70f7c77b7000347800c38e84ff0c4be) --- .../src/components/MenuCard.test.tsx | 34 +++++++++ .../src/components/MenuCardDetails.tsx | 25 ++++++- .../components/UsageSpendDailyLedger.test.tsx | 53 ++++++++++++++ .../src/components/UsageSpendDailyLedger.tsx | 70 +++++++++++++++++++ apps/desktop-tauri/src/i18n/keys.ts | 6 ++ apps/desktop-tauri/src/styles.css | 13 ++++ .../surfaces/settings/tabs/UsageSpendTab.tsx | 3 + rust/src/locale.rs | 6 ++ rust/src/locale/en-US.ftl | 6 ++ 9 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx create mode 100644 apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx diff --git a/apps/desktop-tauri/src/components/MenuCard.test.tsx b/apps/desktop-tauri/src/components/MenuCard.test.tsx index c8ec795546..c944938a12 100644 --- a/apps/desktop-tauri/src/components/MenuCard.test.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.test.tsx @@ -146,6 +146,7 @@ describe("MenuCard", () => { WayfinderOffline: "Gateway offline", WayfinderDryRun: "Dry run", WayfinderMissingKeys: "Missing keys", + UsageSpendTokens: "tokens", DeepSeekPricingTitle: "DeepSeek pricing", DeepSeekPricingStandard: "Standard / pre-schedule", DeepSeekPricingPeak: "Peak hours", @@ -160,6 +161,7 @@ describe("MenuCard", () => { tauriMocks.getProviderChartData.mockResolvedValue({ providerId: "claude", costHistory: [{ date: "2026-05-24", value: 1.23 }], + tokensHistory: [{ date: "2026-05-24", tokens: 14_200 }], creditsHistory: [], usageBreakdown: [], localUsage: { @@ -601,6 +603,38 @@ describe("MenuCard", () => { expect(details.open).toBe(true); }); + it("includes the matching daily token count in the local cost tooltip", async () => { + const snapshot = provider(null); + snapshot.providerId = "codex"; + snapshot.displayName = "Codex"; + tauriMocks.getProviderChartData.mockResolvedValue({ + providerId: "codex", + costHistory: [{ date: "2026-05-24", value: 1.23 }], + creditsHistory: [], + usageBreakdown: [], + localUsage: { + todayCost: null, + thirtyDayCost: 1.23, + thirtyDayTokens: 14_200, + latestTokens: null, + topModel: "gpt-5", + estimateNote: "Estimated from local logs", + tokenCostUpdatedAtMs: 1234, + }, + tokensHistory: [{ date: "2026-05-24", tokens: 14_200 }], + tokensIncomplete: false, + }); + + const { container } = renderCard(snapshot); + const bar = await waitFor(() => { + const element = container.querySelector(".menu-card__local-chart span"); + if (!element) throw new Error("local chart bar not rendered yet"); + return element; + }); + + expect(bar).toHaveAttribute("title", "2026-05-24: $1.23 · 14K tokens"); + }); + it("places Claude accounts above metrics and the collapsed usage details", async () => { tauriMocks.claudeAccountsList.mockResolvedValue([ { id: "a", email: "a@example.com", organization: "Personal", isActive: true, isSaved: true }, diff --git a/apps/desktop-tauri/src/components/MenuCardDetails.tsx b/apps/desktop-tauri/src/components/MenuCardDetails.tsx index 31634bd910..18d495a72b 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -3,6 +3,7 @@ import type { CostSummaryDisplayStyle, DailyCostPoint, ProviderDisplayDetail, + DailyTokenPoint, PaceSnapshot, ProviderInventoryItem, ProviderChartData, @@ -90,12 +91,23 @@ function formatCurrency(amount: number, code: string): string { } function formatCompactCount(value: number | null): string { - if (value == null || value <= 0) return "—"; + if (value == null || !Number.isFinite(value) || value < 0) return "—"; return (value >= 1_000_000 ? compactCountFormat1 : compactCountFormat0).format( value, ); } +function formatLocalUsagePointTitle( + point: DailyCostPoint, + tokenCount: number | undefined, + tokenLabel: string, +): string { + if (point.value == null && tokenCount == null) return point.date; + const cost = point.value == null ? "—" : formatCurrency(point.value, "USD"); + const tokens = tokenCount == null ? "—" : `${formatCompactCount(tokenCount)} ${tokenLabel}`; + return `${point.date}: ${cost} · ${tokens}`; +} + function formatBudget(value: number): string { return value < 10 ? value.toFixed(1).replace(/\.0$/, "") @@ -106,10 +118,12 @@ function LocalUsageBlock({ providerId, summary, costHistory, + tokensHistory, }: { providerId: string; summary: ProviderLocalUsageSummary; costHistory: DailyCostPoint[]; + tokensHistory: DailyTokenPoint[]; }) { const { t } = useLocale(); const isCodex = providerId === "codex"; @@ -119,6 +133,7 @@ function LocalUsageBlock({ ...visibleHistory.flatMap((point) => (point.value == null ? [] : [point.value])), 0, ); + const tokensByDate = new Map(tokensHistory.map((point) => [point.date, point.tokens])); return (
@@ -166,7 +181,11 @@ function LocalUsageBlock({ height: `${point.value == null || maxCost <= 0 ? 1 : Math.max(4, Math.round((point.value / maxCost) * 64))}px`, opacity: point.value == null ? 0 : undefined, }} - title={point.value == null ? point.date : `${point.date}: ${formatCurrency(point.value, "USD")}`} + title={formatLocalUsagePointTitle( + point, + tokensByDate.get(point.date), + t("UsageSpendTokens"), + )} /> ))} @@ -540,6 +559,7 @@ export default function MenuCardDetails({ display.resetTimeRelative, ); const localCostHistory = chartData?.costHistory ?? []; + const localTokenHistory = chartData?.tokensHistory ?? []; const costStyle = display.costSummaryDisplayStyle ?? "detailed"; const { @@ -698,6 +718,7 @@ export default function MenuCardDetails({ providerId={provider.providerId} summary={localUsage} costHistory={localCostHistory} + tokensHistory={localTokenHistory} /> )} diff --git a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx new file mode 100644 index 0000000000..b1cbdacec4 --- /dev/null +++ b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx @@ -0,0 +1,53 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../hooks/useLocale", () => ({ + useLocale: () => ({ + t: (key: string) => + ({ + UsageSpendDailyLedger: "Daily ledger", + UsageSpendDailyLedgerHelper: "Recorded daily cost and token totals", + UsageSpendNoDailyData: "No daily history yet.", + UsageSpendColDate: "Date", + UsageSpendColCost: "Cost", + UsageSpendColTokens: "Tokens", + UsageSpendUnknown: "Unknown", + UsageSpendTokens: "tokens", + })[key] ?? key, + }), +})); + +import { UsageSpendDailyLedger } from "./UsageSpendDailyLedger"; + +describe("UsageSpendDailyLedger", () => { + it("renders newest daily rows and keeps zero distinct from unknown", () => { + render( + , + ); + + const table = screen.getByRole("table", { name: "Daily ledger" }); + const rows = within(table).getAllByRole("row"); + expect(rows[1]).toHaveTextContent("2026-09-13"); + expect(rows[1]).toHaveTextContent("$0.00"); + expect(rows[1]).toHaveTextContent("0 tokens"); + expect(rows[2]).toHaveTextContent("2026-09-12"); + expect(rows[2]).toHaveTextContent("$0.12"); + expect(rows[2]).toHaveTextContent("Unknown"); + expect(rows[3]).toHaveTextContent("2026-09-11"); + expect(rows[3]).toHaveTextContent("Unknown"); + expect(rows[3]).toHaveTextContent("8,100 tokens"); + }); + + it("shows an explicit empty state when no daily points are available", () => { + render(); + + expect(screen.getByText("No daily history yet.")).toBeInTheDocument(); + expect(screen.queryByRole("table")).not.toBeInTheDocument(); + }); +}); diff --git a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx new file mode 100644 index 0000000000..757c5ae142 --- /dev/null +++ b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx @@ -0,0 +1,70 @@ +import { useLocale } from "../hooks/useLocale"; +import type { SpendContract } from "../types/bridge"; + +const currencyFormatters = new Map(); + +function formatUsd(value: number | null, currency: string): string { + if (value == null || !Number.isFinite(value)) return "—"; + const code = currency || "USD"; + try { + let formatter = currencyFormatters.get(code); + if (!formatter) { + formatter = new Intl.NumberFormat(undefined, { + style: "currency", + currency: code, + maximumFractionDigits: 2, + }); + currencyFormatters.set(code, formatter); + } + return formatter.format(value); + } catch { + return `$${value.toFixed(2)}`; + } +} + +function formatTokens(value: number | null, tokenLabel: string, unknownLabel: string): string { + if (value == null || !Number.isFinite(value) || value < 0) { + return unknownLabel; + } + return `${value.toLocaleString()} ${tokenLabel}`; +} + +export function UsageSpendDailyLedger({ + daily, +}: { + daily: SpendContract["daily"]; +}) { + const { t } = useLocale(); + const rows = [...daily].sort((a, b) => b.day.localeCompare(a.day)); + + return ( +
+

{t("UsageSpendDailyLedger")}

+

{t("UsageSpendDailyLedgerHelper")}

+ {rows.length === 0 ? ( +

{t("UsageSpendNoDailyData")}

+ ) : ( +
+ + + + + + + + + + {rows.map((point) => ( + + + + + + ))} + +
{t("UsageSpendColDate")}{t("UsageSpendColCost")}{t("UsageSpendColTokens")}
{point.day}{point.costUsd == null ? t("UsageSpendUnknown") : formatUsd(point.costUsd, "USD")}{formatTokens(point.totalTokens, t("UsageSpendTokens"), t("UsageSpendUnknown"))}
+
+ )} +
+ ); +} diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 90c0984f49..7886ce0f91 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -417,6 +417,9 @@ export const ALL_LOCALE_KEYS = [ "UsageSpendTokens", "UsageSpendAllTimeHistory", "UsageSpendCustomPricing", + "UsageSpendDailyLedger", + "UsageSpendDailyLedgerHelper", + "UsageSpendNoDailyData", "OverviewSpendTitle", "OverviewSpendProviderCoverage", "OverviewSpendEstimate", @@ -434,6 +437,9 @@ export const ALL_LOCALE_KEYS = [ "UsageSpendCol30d", "UsageSpendColCurrency", "UsageSpendColSource", + "UsageSpendColDate", + "UsageSpendColCost", + "UsageSpendColTokens", "UsageSpendShareFailed", "UsageSpendShareEmpty", "UsageSpendShare", diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index c3465d0ec0..3a8e5405bf 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -5385,6 +5385,19 @@ html:has(.menu-surface--tray) { font-size: var(--font-caption); } +.usage-spend-daily { + margin-top: 20px; +} + +.usage-spend-daily h4 { + margin: 0; +} + +.usage-spend-daily__table { + overflow-x: auto; + margin-top: 10px; +} + /* About update controls block */ .about-update-controls { display: flex; diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/UsageSpendTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/UsageSpendTab.tsx index 0f7cc7ee89..01433c7270 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/UsageSpendTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/UsageSpendTab.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { save } from "@tauri-apps/plugin-dialog"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { useLocale } from "../../../hooks/useLocale"; +import { UsageSpendDailyLedger } from "../../../components/UsageSpendDailyLedger"; import { getSettingsSnapshot, getUsageSpendSummary, @@ -258,6 +259,8 @@ export default function UsageSpendTab(_props: TabProps) { {!error && summary && } + {!error && summary && } + {!error && summary && ( setShowAllModels((value) => !value)} t={t} /> )} diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 27f536ae93..1c5ca3f636 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -661,6 +661,9 @@ locale_keys! { UsageSpendTokens, UsageSpendAllTimeHistory, UsageSpendCustomPricing, + UsageSpendDailyLedger, + UsageSpendDailyLedgerHelper, + UsageSpendNoDailyData, OverviewSpendTitle, OverviewSpendProviderCoverage, OverviewSpendEstimate, @@ -678,6 +681,9 @@ locale_keys! { UsageSpendCol30d, UsageSpendColCurrency, UsageSpendColSource, + UsageSpendColDate, + UsageSpendColCost, + UsageSpendColTokens, UsageSpendShare, UsageSpendCopyJson, UsageSpendSaveJson, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index a0865280aa..9f1f7e89c8 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -355,6 +355,9 @@ UsageSpendRequests = requests UsageSpendTokens = tokens UsageSpendAllTimeHistory = All-time is backed by the latest UsageSpendCustomPricing = custom pricing +UsageSpendDailyLedger = Daily ledger +UsageSpendDailyLedgerHelper = Recorded daily cost and token totals. Unknown means the source did not establish that value. +UsageSpendNoDailyData = No daily history yet. OverviewSpendTitle = Usage & Spend · 30d OverviewSpendProviderCoverage = providers have spend OverviewSpendEstimate = local/provider estimates @@ -374,6 +377,9 @@ UsageSpendCol7d = 7 days UsageSpendCol30d = 30 days UsageSpendColCurrency = Currency UsageSpendColSource = Source +UsageSpendColDate = Date +UsageSpendColCost = Cost +UsageSpendColTokens = Tokens AgentSessionsTitle = Agent Sessions AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP From 4bea929431ff3fa351ccc11cde4521bea6df0aee Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:27:10 +0700 Subject: [PATCH 2/4] Share usage spend formatters Adapted to main: main already keeps the canonical spend formatters in lib/usageSpendSharing.ts (formatUsd, formatSpendMetric), so the daily ledger reuses them and the token-cell helper is added there as formatSpendTokens instead of a second lib/usageSpendFormatters.ts module. UsageSpendTab keeps main's layout unchanged. (cherry picked from commit 89edce66fe2988501f3e03737ae0af8613ad73b7) --- .../src/components/UsageSpendDailyLedger.tsx | 34 +++---------------- .../src/lib/usageSpendSharing.test.ts | 10 ++++++ .../src/lib/usageSpendSharing.ts | 10 ++++++ 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx index 757c5ae142..109cc56936 100644 --- a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx +++ b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx @@ -1,33 +1,9 @@ import { useLocale } from "../hooks/useLocale"; import type { SpendContract } from "../types/bridge"; - -const currencyFormatters = new Map(); - -function formatUsd(value: number | null, currency: string): string { - if (value == null || !Number.isFinite(value)) return "—"; - const code = currency || "USD"; - try { - let formatter = currencyFormatters.get(code); - if (!formatter) { - formatter = new Intl.NumberFormat(undefined, { - style: "currency", - currency: code, - maximumFractionDigits: 2, - }); - currencyFormatters.set(code, formatter); - } - return formatter.format(value); - } catch { - return `$${value.toFixed(2)}`; - } -} - -function formatTokens(value: number | null, tokenLabel: string, unknownLabel: string): string { - if (value == null || !Number.isFinite(value) || value < 0) { - return unknownLabel; - } - return `${value.toLocaleString()} ${tokenLabel}`; -} +import { + formatSpendTokens, + formatUsd, +} from "../lib/usageSpendSharing"; export function UsageSpendDailyLedger({ daily, @@ -58,7 +34,7 @@ export function UsageSpendDailyLedger({ {point.day} {point.costUsd == null ? t("UsageSpendUnknown") : formatUsd(point.costUsd, "USD")} - {formatTokens(point.totalTokens, t("UsageSpendTokens"), t("UsageSpendUnknown"))} + {formatSpendTokens(point.totalTokens, t("UsageSpendTokens"), t("UsageSpendUnknown"))} ))} diff --git a/apps/desktop-tauri/src/lib/usageSpendSharing.test.ts b/apps/desktop-tauri/src/lib/usageSpendSharing.test.ts index bf693ce7b8..078df29e11 100644 --- a/apps/desktop-tauri/src/lib/usageSpendSharing.test.ts +++ b/apps/desktop-tauri/src/lib/usageSpendSharing.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + formatSpendTokens, formatUsageSpendReportingDay, filterUsageSpendSummaryForOverview, renderUsageSpendSharePng, @@ -25,6 +26,15 @@ describe("usage spend sharing", () => { expect(formatUsageSpendReportingDay("2026-10-01", "Pacific/Norfolk")).toBe("Oct 1, 2026"); }); + it("keeps a zero token count distinct from an unknown one", () => { + expect(formatSpendTokens(0, "tokens", "Unknown")).toBe("0 tokens"); + expect(formatSpendTokens(8100, "tokens", "Unknown")).toBe("8,100 tokens"); + expect(formatSpendTokens(null, "tokens", "Unknown")).toBe("Unknown"); + expect(formatSpendTokens(undefined, "tokens", "Unknown")).toBe("Unknown"); + expect(formatSpendTokens(-1, "tokens", "Unknown")).toBe("Unknown"); + expect(formatSpendTokens(Number.NaN, "tokens", "Unknown")).toBe("Unknown"); + }); + it("falls back for an invalid dashboard timezone or date", () => { expect(formatUsageSpendReportingDay("2026-10-01", "Not/AZone")).toBe("2026-10-01"); expect(formatUsageSpendReportingDay("2026-02-31", "UTC")).toBe("2026-02-31"); diff --git a/apps/desktop-tauri/src/lib/usageSpendSharing.ts b/apps/desktop-tauri/src/lib/usageSpendSharing.ts index 2246e98505..c3f141fc25 100644 --- a/apps/desktop-tauri/src/lib/usageSpendSharing.ts +++ b/apps/desktop-tauri/src/lib/usageSpendSharing.ts @@ -139,6 +139,16 @@ export function formatSpendMetric( return parts.length > 0 ? parts.join(" · ") : "—"; } +/** Canonical token cell for spend ledgers; a missing or negative count is unknown, zero is known. */ +export function formatSpendTokens( + tokens: number | null | undefined, + tokenLabel: string, + unknownLabel: string, +): string { + if (tokens == null || !Number.isFinite(tokens) || tokens < 0) return unknownLabel; + return `${tokens.toLocaleString()} ${tokenLabel}`; +} + /** * Render the sanitized share-card PNG. * From 1d64dd77a70266bfe2d96c0fa8e0e7fb859b59a5 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:12:37 +0700 Subject: [PATCH 3/4] Translate daily ledger labels in every locale The re-landed daily ledger added six keys to en-US only. Add them to the other seven locale files so every .ftl stays aligned with locale.rs and keys.ts. --- rust/src/locale/es-MX.ftl | 6 ++++++ rust/src/locale/ja-JP.ftl | 6 ++++++ rust/src/locale/ko-KR.ftl | 6 ++++++ rust/src/locale/ru-RU.ftl | 6 ++++++ rust/src/locale/tr-TR.ftl | 6 ++++++ rust/src/locale/zh-CN.ftl | 6 ++++++ rust/src/locale/zh-TW.ftl | 6 ++++++ 7 files changed, 42 insertions(+) diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index 8195889ad1..2f3ff9bcd8 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -328,6 +328,12 @@ UsageSpendCol7d = 7 days UsageSpendCol30d = 30 days UsageSpendColCurrency = Currency UsageSpendColSource = Source +UsageSpendDailyLedger = Registro diario +UsageSpendDailyLedgerHelper = Totales diarios registrados de costo y tokens. Un valor desconocido indica que la fuente no lo estableció. +UsageSpendNoDailyData = Aún no hay historial diario. +UsageSpendColDate = Fecha +UsageSpendColCost = Costo +UsageSpendColTokens = Tokens AgentSessionsTitle = Agent Sessions AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index 70e02dec96..c8950be35e 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -315,6 +315,12 @@ UsageSpendCol7d = 7 days UsageSpendCol30d = 30 days UsageSpendColCurrency = Currency UsageSpendColSource = Source +UsageSpendDailyLedger = 日別の記録 +UsageSpendDailyLedgerHelper = 記録された日ごとのコストとトークンの合計です。不明な値は、ソースがその値を確定できなかったことを示します。 +UsageSpendNoDailyData = 日別の履歴はまだありません。 +UsageSpendColDate = 日付 +UsageSpendColCost = コスト +UsageSpendColTokens = トークン AgentSessionsTitle = エージェントセッション AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 394bf2d3bd..d3184005ad 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -315,6 +315,12 @@ UsageSpendCol7d = 7 days UsageSpendCol30d = 30 days UsageSpendColCurrency = Currency UsageSpendColSource = Source +UsageSpendDailyLedger = 일별 기록 +UsageSpendDailyLedgerHelper = 기록된 일별 비용 및 토큰 합계입니다. 알 수 없는 값은 소스가 해당 값을 확인하지 못했음을 의미합니다. +UsageSpendNoDailyData = 아직 일별 기록이 없습니다. +UsageSpendColDate = 날짜 +UsageSpendColCost = 비용 +UsageSpendColTokens = 토큰 AgentSessionsTitle = Agent Sessions AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index 31e8c9b0b3..6a8fd905c2 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -299,6 +299,12 @@ UsageSpendCol7d = 7 дней UsageSpendCol30d = 30 дней UsageSpendColCurrency = Валюта UsageSpendColSource = Источник +UsageSpendDailyLedger = Журнал по дням +UsageSpendDailyLedgerHelper = Записанные дневные итоги по стоимости и токенам. Неизвестное значение означает, что источник его не установил. +UsageSpendNoDailyData = Истории по дням пока нет. +UsageSpendColDate = Дата +UsageSpendColCost = Стоимость +UsageSpendColTokens = Токены AgentSessionsTitle = Сеансы агента AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index 94fa3750d5..6a49ef7986 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -336,6 +336,12 @@ UsageSpendCol7d = 7 gün UsageSpendCol30d = 30 gün UsageSpendColCurrency = Para birimi UsageSpendColSource = Kaynak +UsageSpendDailyLedger = Günlük kayıt +UsageSpendDailyLedgerHelper = Kaydedilen günlük maliyet ve token toplamları. Bilinmeyen bir değer, kaynağın bu değeri belirlemediği anlamına gelir. +UsageSpendNoDailyData = Henüz günlük geçmiş yok. +UsageSpendColDate = Tarih +UsageSpendColCost = Maliyet +UsageSpendColTokens = Token AgentSessionsTitle = Aracı Oturumları AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 71c6a6b2ea..ecf95169a1 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -314,6 +314,12 @@ UsageSpendCol7d = 7 days UsageSpendCol30d = 30 days UsageSpendColCurrency = Currency UsageSpendColSource = Source +UsageSpendDailyLedger = 每日明细 +UsageSpendDailyLedgerHelper = 已记录的每日费用和 token 总量。未知值表示来源未能确定该数值。 +UsageSpendNoDailyData = 暂无每日历史记录。 +UsageSpendColDate = 日期 +UsageSpendColCost = 费用 +UsageSpendColTokens = Token AgentSessionsTitle = Agent 会话 AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index 9c04c3a347..870f00de8a 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -314,6 +314,12 @@ UsageSpendCol7d = 7 days UsageSpendCol30d = 30 days UsageSpendColCurrency = Currency UsageSpendColSource = Source +UsageSpendDailyLedger = 每日明細 +UsageSpendDailyLedgerHelper = 已記錄的每日費用與 token 總量。未知值表示來源未能確定該數值。 +UsageSpendNoDailyData = 尚無每日歷史記錄。 +UsageSpendColDate = 日期 +UsageSpendColCost = 費用 +UsageSpendColTokens = Token AgentSessionsTitle = Agent 工作階段 AgentSessionsProviderPi = Pi AgentSessionsProviderOmp = OMP From 07f1bb7a4dcfaf5b31dbea2263520c343b8403d7 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:12:37 +0700 Subject: [PATCH 4/4] Start long daily ledgers with the newest 30 rows Port upstream 0.67.0 #3998 (ce1b80863) now that the daily ledger exists: the ledger opens with the newest 30 days and a Show all (N) / Show less control expands it to the complete history. Translates the upstream SpendDashboardDailyLedgerTests expansion test. --- .../components/UsageSpendDailyLedger.test.tsx | 65 ++++++++++++++++++- .../src/components/UsageSpendDailyLedger.tsx | 34 +++++++++- apps/desktop-tauri/src/styles.css | 4 ++ 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx index b1cbdacec4..e8c55b0e54 100644 --- a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx +++ b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, within } from "@testing-library/react"; +import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; vi.mock("../hooks/useLocale", () => ({ @@ -13,11 +13,47 @@ vi.mock("../hooks/useLocale", () => ({ UsageSpendColTokens: "Tokens", UsageSpendUnknown: "Unknown", UsageSpendTokens: "tokens", + UsageSpendShowAll: "Show all", + UsageSpendShowLess: "Show less", })[key] ?? key, }), })); -import { UsageSpendDailyLedger } from "./UsageSpendDailyLedger"; +import { + DAILY_LEDGER_COLLAPSED_ROWS, + UsageSpendDailyLedger, + dailyLedgerVisibleRows, +} from "./UsageSpendDailyLedger"; + +/** `days` consecutive UTC days in oldest-first order, like the contract's daily history. */ +function dailyHistory(days: number) { + return Array.from({ length: days }, (_, index) => ({ + day: new Date(Date.UTC(2026, 0, 1) + index * 86_400_000).toISOString().slice(0, 10), + costUsd: index, + totalTokens: index, + })); +} + +describe("dailyLedgerVisibleRows", () => { + // Translated from upstream v0.67.0 SpendDashboardDailyLedgerTests + // `ledger expansion preserves every day in newest first order` (#3998). + it.each([0, 7, 30, 31, 365])( + "ledger expansion preserves every day in newest first order (%i days)", + (days) => { + const daily = dailyHistory(days); + const collapsed = dailyLedgerVisibleRows(daily, false, 30); + expect(collapsed).toHaveLength(Math.min(days, 30)); + expect(collapsed).toEqual(daily.slice(-30).reverse()); + const expanded = dailyLedgerVisibleRows(daily, true, 30); + expect(expanded).toEqual([...daily].reverse()); + }, + ); + + it("collapses to the upstream default of 30 rows", () => { + expect(DAILY_LEDGER_COLLAPSED_ROWS).toBe(30); + expect(dailyLedgerVisibleRows(dailyHistory(45), false)).toHaveLength(30); + }); +}); describe("UsageSpendDailyLedger", () => { it("renders newest daily rows and keeps zero distinct from unknown", () => { @@ -42,6 +78,30 @@ describe("UsageSpendDailyLedger", () => { expect(rows[3]).toHaveTextContent("2026-09-11"); expect(rows[3]).toHaveTextContent("Unknown"); expect(rows[3]).toHaveTextContent("8,100 tokens"); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("starts long ledgers with the newest 30 rows and a Show all control", () => { + render(); + + const table = screen.getByRole("table", { name: "Daily ledger" }); + let bodyRows = within(table).getAllByRole("row").slice(1); + expect(bodyRows).toHaveLength(30); + expect(bodyRows[0]).toHaveTextContent("2026-01-31"); + expect(bodyRows[29]).toHaveTextContent("2026-01-02"); + expect(table).not.toHaveTextContent("2026-01-01"); + + const toggle = screen.getByRole("button", { name: "Show all (31)" }); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + fireEvent.click(toggle); + + bodyRows = within(table).getAllByRole("row").slice(1); + expect(bodyRows).toHaveLength(31); + expect(bodyRows[30]).toHaveTextContent("2026-01-01"); + const collapse = screen.getByRole("button", { name: "Show less" }); + expect(collapse).toHaveAttribute("aria-expanded", "true"); + fireEvent.click(collapse); + expect(within(table).getAllByRole("row").slice(1)).toHaveLength(30); }); it("shows an explicit empty state when no daily points are available", () => { @@ -49,5 +109,6 @@ describe("UsageSpendDailyLedger", () => { expect(screen.getByText("No daily history yet.")).toBeInTheDocument(); expect(screen.queryByRole("table")).not.toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); }); diff --git a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx index 109cc56936..48646742c4 100644 --- a/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx +++ b/apps/desktop-tauri/src/components/UsageSpendDailyLedger.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useLocale } from "../hooks/useLocale"; import type { SpendContract } from "../types/bridge"; import { @@ -5,13 +6,32 @@ import { formatUsd, } from "../lib/usageSpendSharing"; +type DailyPoints = SpendContract["daily"]; + +/** Upstream `SpendDailyLedger.collapsedRowCount`: long ranges start with the newest 30 days. */ +export const DAILY_LEDGER_COLLAPSED_ROWS = 30; + +/** + * Newest-first ledger rows. Collapsed shows the newest `collapsedRowCount` days; expanded shows the + * complete history (upstream `spendDailyLedgerVisibleSummaries`). + */ +export function dailyLedgerVisibleRows( + daily: DailyPoints, + showsAllRows: boolean, + collapsedRowCount: number = DAILY_LEDGER_COLLAPSED_ROWS, +): DailyPoints { + const newestFirst = [...daily].sort((a, b) => b.day.localeCompare(a.day)); + return showsAllRows ? newestFirst : newestFirst.slice(0, collapsedRowCount); +} + export function UsageSpendDailyLedger({ daily, }: { - daily: SpendContract["daily"]; + daily: DailyPoints; }) { const { t } = useLocale(); - const rows = [...daily].sort((a, b) => b.day.localeCompare(a.day)); + const [showsAllRows, setShowsAllRows] = useState(false); + const rows = dailyLedgerVisibleRows(daily, showsAllRows); return (
@@ -41,6 +61,16 @@ export function UsageSpendDailyLedger({ )} + {daily.length > DAILY_LEDGER_COLLAPSED_ROWS && ( + + )}
); } diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index 3a8e5405bf..7b7138aaeb 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -5398,6 +5398,10 @@ html:has(.menu-surface--tray) { margin-top: 10px; } +.usage-spend-daily__toggle { + margin-top: 8px; +} + /* About update controls block */ .about-update-controls { display: flex;