Files
paseo/packages/plugin/src/server/usage.ts
T
Mohamed Boudra e939dd4c7a Streamline sidebar usage and stacked sheet interactions (#5805)
* feat(app): streamline sidebar usage and stacked sheets

* fix(app): restore compact sidebar navigation clicks

The close button container emitted box-none through Unistyles as invalid CSS and intercepted New workspace clicks. Let React Native apply click-through behavior at the view boundary. Keep the import journey on its existing New workspace screen and assert only the visible transcript.

* fix(app): cap menu sheets at eighty percent height

Keep long sidebar usage reports recognizably sheet-sized while allowing the content to scroll. Short menu sheets retain dynamic sizing.
2026-10-01 04:55:17 +01:00

133 lines
3.9 KiB
TypeScript

import { createHash } from "node:crypto";
import type { ZodType } from "zod";
import type { JsonValue } from "@getpaseo/protocol/agent-types";
export interface UsageWindow {
id: string;
label: string;
/**
* A few characters naming the window where space is tight, e.g. "5h" or "wk". An empty string
* shows the percent alone; leaving it out shows `label`.
*/
shortLabel?: string;
/** Shown in the usage summary until the user pins windows of their own. */
summary?: boolean;
usedPct?: number | null;
remainingPct?: number | null;
resetsAt?: string | null;
runsOutAt?: string | null;
shortfallPct?: number | null;
tone?: "default" | "ok" | "warning" | "danger";
}
export interface UsageBalance {
id: string;
label: string;
used?: number | null;
remaining?: number | null;
limit?: number | null;
unit: "usd" | "credits" | "requests" | "tokens";
resetsAt?: string | null;
tone?: UsageWindow["tone"];
}
export interface UsageDetail {
id: string;
label: string;
value: string;
tone?: UsageWindow["tone"];
}
export interface UsageReport {
status: "available" | "unavailable" | "error";
planLabel?: string;
windows: UsageWindow[];
balances?: UsageBalance[];
details?: UsageDetail[];
error?: string;
}
export interface UsageSourceRegistration {
id: string;
label: string;
icon?: string;
input: ZodType;
/** Stable account identity, resolved without fetching usage. */
identify(input: unknown): Promise<{ key: string; label?: string } | null>;
fetch(input: unknown): Promise<UsageReport>;
discover(): Promise<JsonValue[]>;
}
export function windowFromUsedPct(input: {
id: string;
label: string;
shortLabel?: string;
summary?: boolean;
utilizationPct: number | null | undefined;
resetsAt?: string | null;
tone?: UsageWindow["tone"];
}): UsageWindow {
const usedPct = typeof input.utilizationPct === "number" ? input.utilizationPct : null;
const window: UsageWindow = {
id: input.id,
label: input.label,
usedPct,
remainingPct: usedPct === null ? null : Math.max(0, 100 - usedPct),
resetsAt: input.resetsAt ?? null,
};
if (input.shortLabel !== undefined) window.shortLabel = input.shortLabel;
if (input.summary) window.summary = true;
if (input.tone) window.tone = input.tone;
return window;
}
/**
* The tone scale for anything measured against a known limit, windows and balances alike.
*
* Thresholds match `deriveTone` in the app's provider-usage/tone.ts, which is what the
* client falls back to when a window arrives without a tone. Healthy is "ok" rather than
* "default" because that is what every provider setting a tone has always sent, and it is
* what the bars render today below their thresholds.
*/
export function toneFromUsedPct(usedPct: number | null | undefined): UsageWindow["tone"] {
if (typeof usedPct !== "number") return "default";
if (usedPct > 90) return "danger";
if (usedPct >= 70) return "warning";
return "ok";
}
/**
* Tone for a balance with no known limit, where a percentage cannot be computed and the
* only signal is whether anything is left. Prefer `toneFromUsedPct` when a limit exists:
* this one stays "ok" until the balance is completely spent.
*/
export function balanceToneFromRemaining(
remaining: number | null | undefined,
): UsageBalance["tone"] {
if (typeof remaining !== "number") return "default";
if (remaining <= 0) return "danger";
return "ok";
}
/** Percentage of a limit consumed, or null when either side is unknown. */
export function usedPctOf(
used: number | null | undefined,
limit: number | null | undefined,
): number | null {
if (typeof used !== "number" || typeof limit !== "number" || limit <= 0) return null;
return (used / limit) * 100;
}
export function hashAccountKey(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
export function unavailableUsage(): UsageReport {
return {
status: "unavailable",
windows: [],
balances: [],
details: [],
};
}