fix(usage): derive window names and unify pin selection (#5903)

* fix(usage): derive window names and unify pin selection

Quota slots do not identify a period. Derive numeric windows and IDs from reported duration and quota scope; keep unknown lengths neutral. Resolve source defaults and explicit pins through one model, with a persisted empty selection distinct from defaults.

* fix(plugins): declare the duration helper host requirement

* fix(usage): verify persisted selections at the storage boundary
This commit is contained in:
Mohamed Boudra
2026-10-02 11:18:13 +01:00
committed by GitHub
parent 6c985769be
commit 73d25317a4
27 changed files with 815 additions and 172 deletions
+58 -13
View File
@@ -15,6 +15,8 @@ import {
} from "../support/helpers/usage-reports";
import {
installLoginUsage,
installCodexWindowUsage,
expectCodexReportedWindows,
openUsage,
refreshLoginUsage,
hoverUsageWindow,
@@ -312,29 +314,26 @@ test("expired login refreshes to windows with visible pin toggles", async ({ pag
).toHaveCount(0);
await expectPinnedUsage(page, ["31% 5h", "54% wk"]);
const row = page.getByRole("checkbox", { name: /^Pin Claude Weekly, / });
await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "0");
await expect(row).toBeChecked();
await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "0");
await hoverUsageWindow(page, "Weekly");
await expect(page.getByText("Pin", { exact: true })).toBeVisible();
await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1");
await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute(
"fill",
"none",
);
await qaScreenshot(page, "usage-pin-hover");
await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly");
await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1");
await expect(row.getByTestId("usage-pin-glyph-pinned").locator("svg")).not.toHaveAttribute(
"fill",
"none",
);
await expectPinnedUsage(page, ["54% wk"]);
await qaScreenshot(page, "usage-pin-selected");
await qaScreenshot(page, "usage-pin-hover");
await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly");
await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1");
await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute(
"fill",
"none",
);
await expectPinnedUsage(page, ["31% 5h"]);
await qaScreenshot(page, "usage-pin-unselected");
await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly");
await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1");
await expectPinnedUsage(page, ["31% 5h", "54% wk"]);
} finally {
await fixture.cleanup();
@@ -348,11 +347,57 @@ test("compact usage rows always show the pin glyph", async ({ page }) => {
await gotoAppShell(page);
await openUsage(page);
const card = page.getByTestId("usage-report-login-journey:account");
await expect(card.getByTestId("usage-pin-glyph-unpinned")).toHaveCount(2);
await expect(card.getByTestId("usage-pin-glyph-unpinned").nth(0)).toHaveCSS("opacity", "1");
await expect(card.getByTestId("usage-pin-glyph-unpinned").nth(1)).toHaveCSS("opacity", "1");
await expect(card.getByTestId("usage-pin-glyph-pinned")).toHaveCount(2);
await expect(card.getByTestId("usage-pin-glyph-pinned").nth(0)).toHaveCSS("opacity", "1");
await expect(card.getByTestId("usage-pin-glyph-pinned").nth(1)).toHaveCSS("opacity", "1");
await qaScreenshot(page, "usage-pin-compact");
} finally {
await fixture.cleanup();
}
});
for (const shape of ["seven-day-only", "Spark"] as const) {
test(`Codex ${shape} windows use provider durations on the card and sidebar`, async ({
page,
}) => {
const reset = Math.floor(Date.now() / 1000) + 5 * 86400;
const source = await installCodexWindowUsage({
rate_limit: {
primary_window: { used_percent: 11, limit_window_seconds: 604800, reset_at: reset },
secondary_window: null,
},
additional_rate_limits:
shape === "Spark"
? [
{
limit_name: "GPT-5.3-Codex-Spark",
metered_feature: "codex_bengalfox",
rate_limit: {
primary_window: {
used_percent: 0,
limit_window_seconds: 18000,
reset_at: Math.floor(Date.now() / 1000) + 3600,
},
secondary_window: {
used_percent: 0,
limit_window_seconds: 604800,
reset_at: reset,
},
},
},
]
: [],
});
try {
await page.setViewportSize({ width: 1440, height: 900 });
await gotoAppShell(page);
await expect(usageItem(page)).toBeVisible();
await usageItem(page).click();
await qaScreenshot(page, `codex-${shape}-card-and-summary`);
await expectCodexReportedWindows(page, shape);
await qaScreenshot(page, `codex-${shape}-pins`);
} finally {
await source.cleanup();
}
});
}
@@ -114,6 +114,11 @@ test.describe("Usage item", () => {
await gotoAppShell(page);
await usageItem(page).click();
await expectOnUsageScreen(page);
await qaScreenshot(page, "default-pins-card");
await expect(pinRow(screen, "Claude", "Session")).toBeChecked();
await expect(pinRow(screen, "Claude", "Weekly")).toBeChecked();
await expect(pinRow(screen, "Codex", "Session")).toBeChecked();
await expect(pinRow(screen, "Codex", "Weekly")).toBeChecked();
});
await test.step("pinning Claude 5-hour and Codex weekly shows both in the Usage item", async () => {
@@ -122,9 +127,13 @@ test.describe("Usage item", () => {
await expect(page.locator('[data-testid="usage-host-filter-trigger"]:visible')).toHaveCount(
0,
);
await togglePin(screen, "Claude", "Session");
await expectPinnedUsage(page, ["31% 5h"]);
await togglePin(screen, "Codex", "Weekly");
await togglePin(screen, "Claude", "Weekly");
await expectPinnedUsage(page, ["31% 5h", "7% 5h", "12% wk"]);
await expect(pinRow(screen, "Claude", "Session")).toBeChecked();
await expect(pinRow(screen, "Codex", "Session")).toBeChecked();
await expect(pinRow(screen, "Codex", "Weekly")).toBeChecked();
await qaScreenshot(page, "default-pins-after-one-toggle");
await togglePin(screen, "Codex", "Session");
await expectPinnedUsage(page, ["31% 5h", "12% wk"]);
await expect(usageItem(page)).not.toHaveText("Usage");
await qaScreenshot(page, "desktop-footer-pins", { kind: "footer" });
@@ -226,16 +235,18 @@ test.describe("Usage item", () => {
await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 10_000 });
});
await test.step("unpinning both brings back default windows, and that survives a reload", async () => {
await test.step("unpinning everything stays empty after a reload", async () => {
await togglePin(screen, "Claude", "Session");
await togglePin(screen, "Codex", "Weekly");
await expectPinnedUsage(page, ["69% 5h", "46% wk", "93% 5h", "88% wk"]);
await expectNoUsageItem(page);
await page.reload();
await expect(screen.getByText("88% left")).toBeVisible({ timeout: 10_000 });
await expectPinnedUsage(page, ["69% 5h", "46% wk", "93% 5h", "88% wk"]);
await gotoAppShell(page);
await usageItem(page).click();
await expectOnUsageScreen(page);
await expectNoUsageItem(page);
await expect(pinRow(screen, "Claude", "Session")).not.toBeChecked();
await expect(pinRow(screen, "Claude", "Weekly")).not.toBeChecked();
await expect(pinRow(screen, "Codex", "Session")).not.toBeChecked();
await expect(pinRow(screen, "Codex", "Weekly")).not.toBeChecked();
await qaScreenshot(page, "explicit-empty-pins-after-reload");
});
await test.step("Settings > Sidebar lists the Usage item", async () => {
@@ -1,8 +1,9 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { cp, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import type { UsageReport } from "@getpaseo/protocol/messages";
import type { Page } from "@playwright/test";
import { expect, type Page } from "@playwright/test";
import { expectPinnedUsage, pinRow, usageItem } from "./usage-sidebar-item";
import { connectNewWorkspaceDaemonClient } from "./new-workspace";
import { pluginRequirements } from "./plugin-fixture";
@@ -61,6 +62,73 @@ export async function refreshLoginUsage(page: Page) {
export async function hoverUsageWindow(page: Page, window: string) {
await page
.getByRole("checkbox", { name: new RegExp(`^Pin Claude ${window}, `) })
.getByTestId("usage-pin-glyph-unpinned")
.getByTestId(/usage-pin-glyph-/)
.hover();
}
/** Exercises the built-in Codex adapter through the real plugin loader and daemon transport. */
export async function installCodexWindowUsage(payload: unknown) {
const directory = await mkdtemp(path.join(tmpdir(), "usage-codex-windows-"));
const client = await connectNewWorkspaceDaemonClient({ ownProjects: false });
const previous = await client.getDaemonConfig();
const cleanup = async () => {
try {
await client.removePlugin("codex-window-qa");
await client.patchDaemonConfig({ pluginsEnabled: previous.config.pluginsEnabled ?? false });
} finally {
await client.close();
await rm(directory, { recursive: true, force: true });
}
};
try {
const source = path.resolve(__dirname, "../../../../../plugins/codex-usage-source");
await cp(path.join(source, "server"), path.join(directory, "server"), { recursive: true });
await cp(path.join(source, "shared"), path.join(directory, "shared"), { recursive: true });
await writeFile(
path.join(directory, "auth.json"),
JSON.stringify({ tokens: { access_token: "fixture-codex" } }),
);
await writeFile(path.join(directory, "payload.json"), JSON.stringify(payload));
await writeFile(
path.join(directory, "paseo-plugin.json"),
JSON.stringify({ id: "codex-window-qa", requirements: pluginRequirements }),
);
await writeFile(
path.join(directory, "index.server.ts"),
`
import {readFile} from "node:fs/promises";
import {fetchUsage, discover} from "./server/usage.js";
import {inputSchema} from "./shared/input.js";
export default function contribute(server) {
server.registerUsageSource({id: "codex-window-qa", label: "Codex", input: inputSchema,
discover: () => discover({home: ${JSON.stringify(directory)}, env: {CODEX_HOME: ${JSON.stringify(directory)}}}),
fetch: (input) => fetchUsage(input, async () => Response.json(JSON.parse(await readFile(${JSON.stringify(path.join(directory, "payload.json"))}, "utf8")))),
});
return () => {};
}`,
);
await client.patchDaemonConfig({ pluginsEnabled: true });
await client.installPluginSource({ source: directory });
return { cleanup };
} catch (error) {
await cleanup();
throw error;
}
}
export async function expectCodexReportedWindows(page: Page, shape: "seven-day-only" | "Spark") {
await expectPinnedUsage(page, ["11% wk"]);
await expect(pinRow(page.locator("body"), "Codex", "Weekly")).toBeChecked();
await expect(page.getByText("Session", { exact: true })).toHaveCount(0);
if (shape === "Spark") {
const fiveHour = pinRow(page.locator("body"), "Codex", "GPT-5.3-Codex-Spark · 5-hour");
const weekly = pinRow(page.locator("body"), "Codex", "GPT-5.3-Codex-Spark · Weekly");
await expect(fiveHour).not.toBeChecked();
await expect(weekly).not.toBeChecked();
await fiveHour.click();
await expect(fiveHour).toBeChecked();
await expect(usageItem(page)).toHaveAccessibleName(
/Codex Weekly 11% used, Codex GPT-5.3-Codex-Spark · 5-hour 0% used/,
);
}
}
@@ -263,10 +263,10 @@ describe("loadAppSettingsFromStorage", () => {
]);
});
it("loads usage preferences with pins in order and defaults a fresh device to none", async () => {
it("loads legacy usage pins in order and defaults a fresh device to source defaults", async () => {
expect((await loadAppSettingsFromStorage(makeDeps())).usage).toEqual({
displayAs: "used",
pinned: [],
pins: null,
serverId: null,
});
@@ -287,7 +287,7 @@ describe("loadAppSettingsFromStorage", () => {
expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({
displayAs: "remaining",
pinned: [
pins: [
{ sourceId: "codex", windowId: "weekly" },
{ sourceId: "claude", windowId: "five-hour" },
],
@@ -295,6 +295,21 @@ describe("loadAppSettingsFromStorage", () => {
});
});
it("loads an explicitly empty usage selection without restoring defaults", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
[APP_SETTINGS_KEY]: JSON.stringify({
usage: { displayAs: "remaining", pins: [], serverId: "server" },
}),
}),
});
expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({
displayAs: "remaining",
pins: [],
serverId: "server",
});
});
it("keeps valid usage preferences when one field is malformed", async () => {
const deps = makeDeps({
storage: createInMemoryKeyValueStorage({
@@ -310,7 +325,7 @@ describe("loadAppSettingsFromStorage", () => {
expect((await loadAppSettingsFromStorage(deps)).usage).toEqual({
displayAs: "used",
pinned: [{ sourceId: "claude", windowId: "weekly" }],
pins: [{ sourceId: "claude", windowId: "weekly" }],
serverId: null,
});
});
+14 -5
View File
@@ -1,5 +1,7 @@
import { useCallback, useMemo } from "react";
import { useAppSettings } from "@/hooks/use-settings";
import { useUsageHostId } from "./hosts";
import { useUsageHostReports } from "./queries";
import {
isUsagePinned,
setUsageDisplayAs,
@@ -18,9 +20,14 @@ export interface UsageDisplay {
}
/** The device's usage preferences, and the commands that change them. */
export function useUsagePreferences(): { preferences: UsagePreferences; display: UsageDisplay } {
export function useUsagePreferences(serverId?: string): {
preferences: UsagePreferences;
display: UsageDisplay;
} {
const { settings, updateSettings } = useAppSettings();
const preferences = settings.usage;
const defaultServerId = useUsageHostId();
const reports = useUsageHostReports(serverId ?? defaultServerId);
const setDisplayAs = useCallback(
(displayAs: UsageDisplayAs) => {
void updateSettings((current) => ({ usage: setUsageDisplayAs(current.usage, displayAs) }));
@@ -29,18 +36,20 @@ export function useUsagePreferences(): { preferences: UsagePreferences; display:
);
const togglePin = useCallback(
(pin: UsagePin) => {
void updateSettings((current) => ({ usage: toggleUsagePin(current.usage, pin) }));
void updateSettings((current) => ({
usage: toggleUsagePin({ preferences: current.usage, pin, reports }),
}));
},
[updateSettings],
[updateSettings, reports],
);
const display = useMemo<UsageDisplay>(
() => ({
displayAs: preferences.displayAs,
setDisplayAs,
isPinned: (pin) => isUsagePinned(preferences, pin),
isPinned: (pin) => isUsagePinned(preferences, pin, reports),
togglePin,
}),
[preferences, setDisplayAs, togglePin],
[preferences, reports, setDisplayAs, togglePin],
);
return { preferences, display };
}
@@ -6,7 +6,7 @@ import { UsageSection } from "./usage-section";
/** A host's usage reports, for its settings page. */
export function HostUsageSection({ serverId }: { serverId: string }) {
const { view, refresh } = useHostUsage(serverId);
const { display } = useUsagePreferences();
const { display } = useUsagePreferences(serverId);
return (
<UsageSection
serverId={serverId}
+9 -8
View File
@@ -46,10 +46,10 @@ function windows(reports: UsageReportEntry[], prefs: UsagePreferences): PinnedUs
}
function preferences(
pinned: UsagePreferences["pinned"],
pinned: UsagePreferences["pins"],
displayAs: UsagePreferences["displayAs"] = "used",
): UsagePreferences {
return { displayAs, pinned, serverId: null };
return { displayAs, pins: pinned, serverId: null };
}
describe("choosePinnedUsageLayout", () => {
@@ -73,7 +73,7 @@ describe("choosePinnedUsageLayout", () => {
});
describe("resolvePinnedUsage", () => {
it("defaults to the first window with a percent for each source account", () => {
it("defaults to source-wide pins collected from each account", () => {
const work = report({
sourceId: "claude",
sourceLabel: "Claude",
@@ -89,12 +89,13 @@ describe("resolvePinnedUsage", () => {
windows: [{ id: "empty", label: "Empty" }],
});
expect(
windows([claude, codex, work, empty], preferences([])).map((item) => [
windows([claude, codex, work, empty], preferences(null)).map((item) => [
item.key,
item.percentText,
]),
).toEqual([
["claude:default/five-hour", "31%"],
["claude:default/weekly", "80%"],
["claude:work/weekly", "90%"],
["codex:default/weekly", "12%"],
]);
@@ -107,7 +108,7 @@ describe("resolvePinnedUsage", () => {
(item) => item.key,
),
).toEqual(["claude:default/weekly"]);
expect(windows(reports, preferences([])).map((item) => item.key)).toEqual([
expect(windows(reports, preferences(null)).map((item) => item.key)).toEqual([
"claude:default/five-hour",
"codex:default/weekly",
]);
@@ -176,7 +177,7 @@ describe("resolvePinnedUsage", () => {
],
});
expect(windows([marked, codex], preferences([])).map((item) => item.key)).toEqual([
expect(windows([marked, codex], preferences(null)).map((item) => item.key)).toEqual([
"claude:default/five-hour",
"claude:default/weekly",
"codex:default/weekly",
@@ -189,7 +190,7 @@ describe("resolvePinnedUsage", () => {
sourceLabel: "OpenCode Go",
windows: [{ id: "rolling", label: "Rolling", shortLabel: "", usedPct: 21 }],
});
const items = windows([opencode], preferences([]));
const items = windows([opencode], preferences(null));
expect(items.map((item) => item.shortLabel)).toEqual([""]);
});
@@ -268,5 +269,5 @@ it("keeps unavailable and error reports out of the sidebar summary", () => {
...codex,
report: { status: "error", error: "Store deleted" },
};
expect(resolvePinnedUsage([expired, failed], preferences([]))).toEqual([]);
expect(resolvePinnedUsage([expired, failed], preferences(null))).toEqual([]);
});
+22 -32
View File
@@ -1,8 +1,8 @@
import { formatPct } from "./format";
import { displayPercent } from "./model";
import type { UsagePreferences } from "./preferences";
import { effectiveUsagePins, type UsagePreferences } from "./preferences";
import { windowTone } from "./tone";
import type { UsageReportEntry, UsageTone, UsageWindow } from "./types";
import type { UsageReportEntry, UsageTone } from "./types";
/** One summary window of one account, as the sidebar Usage item shows it. */
export interface PinnedUsageWindow {
@@ -61,24 +61,6 @@ function groupBySource(reports: readonly UsageReportEntry[]): UsageReportEntry[]
return sourceIds.flatMap((sourceId) => reports.filter((entry) => entry.sourceId === sourceId));
}
/**
* Pins replace defaults. Without pins, an account shows the windows its source marks as summary,
* or its first window with a percent when the source marks none.
*/
function summaryWindows(entry: UsageReportEntry, preferences: UsagePreferences): UsageWindow[] {
if (entry.report.status !== "available") return [];
const withPercent = entry.report.windows.filter(
(window) => displayPercent(window, preferences.displayAs) !== null,
);
if (preferences.pinned.length === 0) {
const marked = withPercent.filter((window) => window.summary);
return marked.length > 0 ? marked : withPercent.slice(0, 1);
}
return withPercent.filter((window) =>
preferences.pinned.some((pin) => pin.sourceId === entry.sourceId && pin.windowId === window.id),
);
}
/**
* 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.
@@ -87,20 +69,28 @@ export function resolvePinnedUsage(
reports: readonly UsageReportEntry[],
preferences: UsagePreferences,
): PinnedUsageSource[] {
const pins = effectiveUsagePins(preferences, reports);
const meaning = preferences.displayAs === "remaining" ? "left" : "used";
return groupBySource(reports).flatMap((entry) => {
const windows = summaryWindows(entry, preferences).map((window) => {
const percent = displayPercent(window, preferences.displayAs) ?? 0;
const percentText = formatPct(percent);
return {
key: `${entry.id}/${window.id}`,
label: `${describe(entry, window.label)} ${percentText} ${meaning}`,
shortLabel: window.shortLabel ?? window.label,
percent,
percentText,
tone: windowTone(window),
};
});
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, window.label)} ${percentText} ${meaning}`,
shortLabel: window.shortLabel ?? window.label,
percent,
percentText,
tone: windowTone(window),
};
});
if (windows.length === 0) return [];
return [{ key: entry.id, icon: entry.icon ?? null, windows }];
});
+91 -18
View File
@@ -13,45 +13,118 @@ const codexWeekly = { sourceId: "codex", windowId: "weekly" };
describe("usage preferences", () => {
it("pins windows in the order the user pins them", () => {
const pinned = toggleUsagePin(
toggleUsagePin(DEFAULT_USAGE_PREFERENCES, codexWeekly),
claudeFiveHour,
);
const pinned = toggleUsagePin({
preferences: toggleUsagePin({
preferences: DEFAULT_USAGE_PREFERENCES,
pin: codexWeekly,
reports: [],
}),
pin: claudeFiveHour,
reports: [],
});
expect(pinned.pinned).toEqual([codexWeekly, claudeFiveHour]);
expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "weekly" })).toBe(true);
expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "five-hour" })).toBe(false);
expect(pinned.pins).toEqual([codexWeekly, claudeFiveHour]);
expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "weekly" }, [])).toBe(true);
expect(isUsagePinned(pinned, { sourceId: "codex", windowId: "five-hour" }, [])).toBe(false);
});
it("unpins a pinned window and keeps the others in order", () => {
const three = [codexWeekly, claudeFiveHour, { sourceId: "claude", windowId: "weekly" }];
const unpinned = toggleUsagePin(
{ displayAs: "used", pinned: three, serverId: null },
claudeFiveHour,
);
const unpinned = toggleUsagePin({
preferences: { displayAs: "used", pins: three, serverId: null },
pin: claudeFiveHour,
reports: [],
});
expect(unpinned.pinned).toEqual([codexWeekly, { sourceId: "claude", windowId: "weekly" }]);
expect(unpinned.pins).toEqual([codexWeekly, { sourceId: "claude", windowId: "weekly" }]);
});
it("switches between used and remaining without touching pins", () => {
const preferences = { displayAs: "used" as const, pinned: [codexWeekly], serverId: null };
const preferences = { displayAs: "used" as const, pins: [codexWeekly], serverId: null };
expect(setUsageDisplayAs(preferences, "remaining")).toEqual({
displayAs: "remaining",
pinned: [codexWeekly],
pins: [codexWeekly],
serverId: null,
});
});
it("remembers the picked host without touching pins", () => {
expect(setUsageHost({ ...DEFAULT_USAGE_PREFERENCES, pinned: [codexWeekly] }, "server")).toEqual(
{ displayAs: "used", pinned: [codexWeekly], serverId: "server" },
);
expect(setUsageHost({ ...DEFAULT_USAGE_PREFERENCES, pins: [codexWeekly] }, "server")).toEqual({
displayAs: "used",
pins: [codexWeekly],
serverId: "server",
});
});
it("reads preferences saved before hosts could be picked as no pick", () => {
expect(UsagePreferencesSchema.parse({ displayAs: "remaining", pinned: [codexWeekly] })).toEqual(
{ displayAs: "remaining", pinned: [codexWeekly], serverId: null },
{ displayAs: "remaining", pins: [codexWeekly], serverId: null },
);
});
});
it("default sidebar windows are pinned on cards and the first toggle removes only that row", () => {
const reports = [
{
id: "claude:default",
sourceId: "claude",
sourceLabel: "Claude",
account: {},
fetchedAt: "2026-10-02T00:00:00Z",
report: {
status: "available" as const,
windows: [
{ id: "five_hour", label: "5-hour", summary: true, usedPct: 31 },
{ id: "weekly", label: "Weekly", summary: true, usedPct: 54 },
],
},
},
{
id: "codex:default",
sourceId: "codex",
sourceLabel: "Codex",
account: {},
fetchedAt: "2026-10-02T00:00:00Z",
report: {
status: "available" as const,
windows: [{ id: "five_hour", label: "5-hour", summary: true, usedPct: 7 }],
},
},
];
const pins = [
{ sourceId: "claude", windowId: "five_hour" },
{ sourceId: "claude", windowId: "weekly" },
{ sourceId: "codex", windowId: "five_hour" },
];
for (const pin of pins) expect(isUsagePinned(DEFAULT_USAGE_PREFERENCES, pin, reports)).toBe(true);
const changed = toggleUsagePin({
preferences: DEFAULT_USAGE_PREFERENCES,
pin: pins[1]!,
reports,
});
expect(isUsagePinned(changed, pins[1]!, reports)).toBe(false);
expect(isUsagePinned(changed, pins[0]!, reports)).toBe(true);
expect(isUsagePinned(changed, pins[2]!, reports)).toBe(true);
const empty = toggleUsagePin({
preferences: toggleUsagePin({ preferences: changed, pin: pins[0]!, reports }),
pin: pins[2]!,
reports,
});
const restored = UsagePreferencesSchema.parse(JSON.parse(JSON.stringify(empty)));
for (const pin of pins) expect(isUsagePinned(restored, pin, reports)).toBe(false);
});
it("reads old selections without losing display or host and preserves an explicit empty selection", () => {
for (const [input, pins] of [
[{ pinned: [] }, null],
[{ pinned: [codexWeekly] }, [codexWeekly]],
[{ pins: [], pinned: [codexWeekly] }, []],
[{ pins: null, pinned: [codexWeekly] }, null],
[{ pins: "bad", pinned: [codexWeekly] }, null],
] as const) {
expect(
UsagePreferencesSchema.parse({ displayAs: "remaining", serverId: "host", ...input }),
).toEqual({ displayAs: "remaining", serverId: "host", pins });
}
});
+58 -12
View File
@@ -1,4 +1,6 @@
import { z } from "zod";
import { displayPercent } from "./model";
import type { UsageReportEntry } from "./types";
/** Whether percentages read as the share used or the share left. */
export type UsageDisplayAs = "used" | "remaining";
@@ -9,42 +11,86 @@ export interface UsagePin {
windowId: string;
}
/** Device-local. `pinned` is in display order. */
/** Device-local. `pins` is in selection order. */
export interface UsagePreferences {
displayAs: UsageDisplayAs;
pinned: UsagePin[];
/** null follows source defaults; an array is the user's selection, including none. */
pins: UsagePin[] | null;
/** The host the user picked to show usage for; null until they pick one. */
serverId: string | null;
}
export const DEFAULT_USAGE_PREFERENCES: UsagePreferences = {
displayAs: "used",
pinned: [],
pins: null,
serverId: null,
};
const pinsSchema = z.array(z.object({ sourceId: z.string(), windowId: z.string() }));
export const UsagePreferencesSchema = z
.object({
displayAs: z.enum(["used", "remaining"]).catch("used"),
pinned: z.array(z.object({ sourceId: z.string(), windowId: z.string() })).catch([]),
pins: pinsSchema.nullable().optional().catch(null),
// COMPAT(usagePinSelection): added in v0.11, remove after 2027-04-02.
// Old [] meant defaults. Read it only when the new selection is absent.
pinned: pinsSchema.optional().catch([]),
serverId: z.string().nullable().catch(null),
})
.transform(({ displayAs, pins, pinned, serverId }): UsagePreferences => {
const legacyPins = pinned && pinned.length > 0 ? pinned : null;
return { displayAs, pins: pins === undefined ? legacyPins : pins, serverId };
})
.catch(DEFAULT_USAGE_PREFERENCES);
function samePin(a: UsagePin, b: UsagePin): boolean {
return a.sourceId === b.sourceId && a.windowId === b.windowId;
}
export function isUsagePinned(preferences: UsagePreferences, pin: UsagePin): boolean {
return preferences.pinned.some((pinned) => samePin(pinned, pin));
/** The only owner of default and customized pin selection, shared by every usage surface. */
export function effectiveUsagePins(
preferences: UsagePreferences,
reports: readonly UsageReportEntry[],
): UsagePin[] {
if (preferences.pins !== null) return preferences.pins;
const pins: UsagePin[] = [];
for (const entry of reports) {
if (entry.report.status !== "available") continue;
const withPercent = entry.report.windows.filter(
(window) => displayPercent(window, preferences.displayAs) !== null,
);
const marked = withPercent.filter((window) => window.summary);
for (const window of marked.length > 0 ? marked : withPercent.slice(0, 1)) {
const pin = { sourceId: entry.sourceId, windowId: window.id };
if (!pins.some((existing) => samePin(existing, pin))) pins.push(pin);
}
}
return pins;
}
/** Unpins a pinned window; pins any other at the end, so the Usage item grows to the right. */
export function toggleUsagePin(preferences: UsagePreferences, pin: UsagePin): UsagePreferences {
const pinned = isUsagePinned(preferences, pin)
? preferences.pinned.filter((existing) => !samePin(existing, pin))
: [...preferences.pinned, { sourceId: pin.sourceId, windowId: pin.windowId }];
return { ...preferences, pinned };
export function isUsagePinned(
preferences: UsagePreferences,
pin: UsagePin,
reports: readonly UsageReportEntry[],
): boolean {
return effectiveUsagePins(preferences, reports).some((pinned) => samePin(pinned, pin));
}
/** The first edit snapshots the displayed defaults before toggling one window. */
export function toggleUsagePin({
preferences,
pin,
reports,
}: {
preferences: UsagePreferences;
pin: UsagePin;
reports: readonly UsageReportEntry[];
}): UsagePreferences {
const selected = effectiveUsagePins(preferences, reports);
const pins = selected.some((existing) => samePin(existing, pin))
? selected.filter((existing) => !samePin(existing, pin))
: [...selected, { sourceId: pin.sourceId, windowId: pin.windowId }];
return { ...preferences, pins };
}
export function setUsageDisplayAs(
+21
View File
@@ -4,6 +4,7 @@ import {
toneFromUsedPct,
usedPctOf,
windowFromUsedPct,
windowFromReportedDuration,
} from "./usage.js";
test("shared usage tones preserve the app threshold contract", () => {
@@ -84,3 +85,23 @@ describe("balanceToneFromRemaining", () => {
expect(balanceToneFromRemaining(null)).toBe("default");
});
});
test.each([
[18000, "five_hour", "5-hour", "5h"],
[604800, "weekly", "Weekly", "wk"],
[7200, "7200s", "2-hour", "2h"],
[86400, "86400s", "1-day", "1d"],
[90, "90s", "90-second", "90s"],
[null, "primary", "Primary limit", ""],
])(
"duration %s owns the window's identity and both names",
(durationSeconds, id, label, shortLabel) => {
expect(
windowFromReportedDuration({
durationSeconds: durationSeconds as number | null,
unknown: { id: "primary", label: "Primary limit", shortLabel: "" },
utilizationPct: 11,
}),
).toMatchObject({ id, label, shortLabel, usedPct: 11 });
},
);
+49
View File
@@ -96,6 +96,55 @@ export function windowFromUsedPct(input: {
return window;
}
/**
* Numeric provider windows have one identity and vocabulary, independent of response slots.
* Pass null when the provider omits the duration; reset countdowns are not window lengths.
* Named API fields (weekly, monthly, etc.) use windowFromUsedPct instead.
*/
export function windowFromReportedDuration(input: {
durationSeconds: number | null;
/** Stable quota identity and provider name for a model- or feature-scoped limit. */
scope?: { id: string; label: string };
/** Neutral identity and names when the provider does not report a positive duration. */
unknown: { id: string; label: string; shortLabel: string };
utilizationPct: number | null | undefined;
resetsAt?: string | null;
summary?: boolean;
tone?: UsageWindow["tone"];
}): UsageWindow {
const duration = input.durationSeconds;
const name =
duration !== null && Number.isFinite(duration) && duration > 0
? durationWindowName(duration)
: input.unknown;
const scope = input.scope;
return windowFromUsedPct({
id: scope ? `${scope.id}:${name.id}` : name.id,
label: scope ? `${scope.label} · ${name.label}` : name.label,
shortLabel: scope
? `${scope.label}${name.shortLabel ? ` ${name.shortLabel}` : ""}`
: name.shortLabel,
utilizationPct: input.utilizationPct,
resetsAt: input.resetsAt,
summary: input.summary,
tone: input.tone,
});
}
function durationWindowName(seconds: number): { id: string; label: string; shortLabel: string } {
if (seconds === 604800) return { id: "weekly", label: "Weekly", shortLabel: "wk" };
const id = seconds === 18000 ? "five_hour" : `${seconds}s`;
const units = [
[86400, "day", "d"],
[3600, "hour", "h"],
[60, "minute", "m"],
[1, "second", "s"],
] as const;
const unit = units.find(([size]) => seconds % size === 0) ?? units[units.length - 1]!;
const amount = seconds / unit[0];
return { id, label: `${amount}-${unit[1]}`, shortLabel: `${amount}${unit[2]}` };
}
/**
* The tone scale for anything measured against a known limit, windows and balances alike.
*
@@ -1,5 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { z } from "zod";
import { windowFromReportedDuration } from "@getpaseo/plugin/server/usage";
let fetches = 0;
export default function contribute(server: PluginServerContext) {
@@ -32,7 +33,13 @@ export default function contribute(server: PluginServerContext) {
fetches++;
return {
status: "available",
windows: [{ id: "count", label: "Count", usedPct: fetches }],
windows: [
windowFromReportedDuration({
durationSeconds: 604800,
unknown: { id: "unknown", label: "Limit", shortLabel: "" },
utilizationPct: fetches,
}),
],
};
},
});
@@ -1,5 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { z } from "zod";
import { windowFromReportedDuration } from "@getpaseo/plugin/server/usage";
let fetches = 0;
export default function contribute(server: PluginServerContext) {
@@ -32,7 +33,13 @@ export default function contribute(server: PluginServerContext) {
fetches++;
return {
status: "available",
windows: [{ id: "count", label: "Count", usedPct: fetches }],
windows: [
windowFromReportedDuration({
durationSeconds: 604800,
unknown: { id: "unknown", label: "Limit", shortLabel: "" },
utilizationPct: fetches,
}),
],
};
},
});
@@ -57,6 +57,13 @@ test("lists built-in and subprocess usage; validates input and isolates fetch er
await client.installDirectoryPlugin(subprocessDirectory, "fixture-directory");
const both = await client.listUsageReports({ forceRefresh: true });
expect(both.reports).toHaveLength(8);
for (const id of ["fixture:one", "fixture-directory:one"]) {
expect(both.reports.find((entry) => entry.id === id)?.report.windows[0]).toMatchObject({
id: "weekly",
label: "Weekly",
shortLabel: "wk",
});
}
await client.patchDaemonConfig({ pluginsEnabled: false });
await expect.poll(async () => (await client.listUsageReports()).reports.length).toBe(4);
} finally {