mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
* Localize provider usage UI * fix: restore compact provider usage meter * fix: localize compact context percentage * fix(app): deduplicate context meter refresh * fix(app): localize provider usage time counts * fix(app): stabilize compact provider usage row * fix(app): localize provider usage plurals * test(app): allow locale-specific plural keys * fix(app): show compact quota without context usage * fix(app): preserve context overflow percentage * fix(app): truncate provider balance reset values * fix(app): add Korean provider usage translations * fix(app): refresh provider usage timing * fix(app): localize provider usage labels * fix(app): clarify compact usage policies * fix(app): localize monthly provider usage * test(app): require Arabic provider usage plurals * docs(i18n): keep migration guidance focused * fix(app): hide empty compact usage slot * fix(app): keep usage in context tooltip * fix(app): align usage with sidebar reports * test(app): pass usage timing clock explicitly * fix(app): restore context usage details * fix(app): make context usage tooltip interactive * fix(app): support keyboard tooltip controls * fix(app): keep focused tooltip controls open * fix(app): address usage localization review * fix(app): localize bulk usage refresh * fix(app): localize Claude session window * fix(app): complete provider usage localization * fix(app): keep mounted usage times and translations current Pass time and translation values into usage rendering so React Compiler invalidates cached labels when they change. Use the same localized window names and percentage meanings in sidebar summaries. * test(desktop): wait for restored tabs before shortcut input Document load completes before the workspace reconnects. Wait for the restored selection so Ctrl+1 reaches the mounted workspace rather than the startup screen. * test(app): allow the shared usage clock minute phase The browser clock advances independently of daemon timestamps, and the shared ticker samples on wall-minute boundaries. Assert the bounded one-to-two-minute age and matching countdown progress instead of a single phase-dependent value. --------- Co-authored-by: Michael Wu <michaelmwu@gmail.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
100 lines
4.1 KiB
TypeScript
100 lines
4.1 KiB
TypeScript
import type { TFunction } from "i18next";
|
|
import { i18n } from "@/i18n/i18next";
|
|
import { formatPct, formatDisplayPct, formatProviderUsageLabel } from "./format";
|
|
import { displayPercent } from "./model";
|
|
import { effectiveUsagePins, type UsagePreferences } from "./preferences";
|
|
import { windowTone } from "./tone";
|
|
import type { UsageReportEntry, UsageTone } from "./types";
|
|
|
|
/** One summary window of one account, as the sidebar Usage item shows it. */
|
|
export interface PinnedUsageWindow {
|
|
key: string;
|
|
/** Accessible description, including the source, account, window and percent meaning. */
|
|
label: string;
|
|
/** The window's short name ("5h"), empty for none, or its label when the source sends none. */
|
|
shortLabel: string;
|
|
/** The share shown, under the user's used/remaining preference. */
|
|
percent: number;
|
|
percentText: string;
|
|
tone: UsageTone;
|
|
}
|
|
|
|
/** One account's summary windows, under its source's icon. */
|
|
export interface PinnedUsageSource {
|
|
key: string;
|
|
icon: string | null;
|
|
windows: PinnedUsageWindow[];
|
|
}
|
|
|
|
/** How much of each window the one-line summary has room for. */
|
|
export type PinnedUsageLayout = "meters" | "labels" | "percents";
|
|
|
|
// A meter narrower than this is a sliver that reads as noise rather than a share.
|
|
export const MIN_METER_WIDTH = 16;
|
|
/** The space between a meter and its text. */
|
|
export const METER_GAP = 4;
|
|
|
|
/**
|
|
* The richest layout that fits on one line: every window's meter, else its short label, else its
|
|
* percent alone. Meters are all or none; a line never mixes them. Until the widths are known it
|
|
* shows labels, which never flashes meters that turn out not to fit.
|
|
*/
|
|
export function choosePinnedUsageLayout(input: {
|
|
available: number | null;
|
|
labelsWidth: number | null;
|
|
percentsWidth: number | null;
|
|
windowCount: number;
|
|
}): PinnedUsageLayout {
|
|
const { available, labelsWidth, percentsWidth, windowCount } = input;
|
|
if (available === null || labelsWidth === null || percentsWidth === null) return "labels";
|
|
if (labelsWidth + windowCount * (MIN_METER_WIDTH + METER_GAP) <= available) return "meters";
|
|
if (labelsWidth <= available) return "labels";
|
|
return percentsWidth < labelsWidth ? "percents" : "labels";
|
|
}
|
|
|
|
function describe(entry: UsageReportEntry, windowLabel: string): string {
|
|
const account = entry.account.label ? ` (${entry.account.label})` : "";
|
|
return `${entry.sourceLabel}${account} ${windowLabel}`;
|
|
}
|
|
|
|
/** Reports with every account of a source together, sources in the order they first appear. */
|
|
function groupBySource(reports: readonly UsageReportEntry[]): UsageReportEntry[] {
|
|
const sourceIds = [...new Set(reports.map((entry) => entry.sourceId))];
|
|
return sourceIds.flatMap((sourceId) => reports.filter((entry) => entry.sourceId === sourceId));
|
|
}
|
|
|
|
/**
|
|
* The sidebar Usage item's summary: one group per account, grouped by source, each with its
|
|
* windows in its report's order. Accounts with nothing to show are left out.
|
|
*/
|
|
export function resolvePinnedUsage(
|
|
reports: readonly UsageReportEntry[],
|
|
preferences: UsagePreferences,
|
|
t: TFunction = i18n.t,
|
|
): PinnedUsageSource[] {
|
|
const pins = effectiveUsagePins(preferences, reports);
|
|
return groupBySource(reports).flatMap((entry) => {
|
|
if (entry.report.status !== "available") return [];
|
|
const windows = entry.report.windows
|
|
.filter(
|
|
(window) =>
|
|
displayPercent(window, preferences.displayAs) !== null &&
|
|
pins.some((pin) => pin.sourceId === entry.sourceId && pin.windowId === window.id),
|
|
)
|
|
.map((window) => {
|
|
const percent = displayPercent(window, preferences.displayAs) ?? 0;
|
|
const percentText = formatPct(percent);
|
|
return {
|
|
key: `${entry.id}/${window.id}`,
|
|
label: `${describe(entry, formatProviderUsageLabel(window.id, window.label, t))} ${formatDisplayPct(percent, preferences.displayAs, undefined, t)}`,
|
|
shortLabel: window.shortLabel ?? formatProviderUsageLabel(window.id, window.label, t),
|
|
percent,
|
|
percentText,
|
|
tone: windowTone(window),
|
|
};
|
|
});
|
|
if (windows.length === 0) return [];
|
|
return [{ key: entry.id, icon: entry.icon ?? null, windows }];
|
|
});
|
|
}
|