From 0ee0551c4cc63e51b22042c51bdb1ebe2612ef58 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Sat, 3 Oct 2026 05:10:40 +0900 Subject: [PATCH] fix(app): restore context usage details --- .../e2e/browser/context-window-meter.spec.ts | 44 +++++++++++++++++++ .../app/e2e/browser/usage-settings.spec.ts | 3 ++ .../src/components/context-window-meter.tsx | 5 +++ packages/app/src/composer/index.tsx | 4 ++ packages/app/src/usage/balance-bar.tsx | 24 ++++++---- .../app/src/usage/context-window-section.tsx | 36 +++++++++++++++ 6 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 packages/app/src/usage/context-window-section.tsx diff --git a/packages/app/e2e/browser/context-window-meter.spec.ts b/packages/app/e2e/browser/context-window-meter.spec.ts index c7868ac94e..833d599374 100644 --- a/packages/app/e2e/browser/context-window-meter.spec.ts +++ b/packages/app/e2e/browser/context-window-meter.spec.ts @@ -1,7 +1,27 @@ import type { Locator } from "@playwright/test"; +import type { UsageReportEntry } from "@getpaseo/protocol/messages"; import { expect, test } from "../support/fixtures"; import { expectComposerVisible } from "../support/helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { installUsageReportsFixture } from "../support/helpers/usage-reports"; + +const USAGE_ICON = + ''; + +function contextUsageReport(): UsageReportEntry { + return { + id: "mock:account", + account: { label: "dev@example.com" }, + fetchedAt: "2026-01-01T00:00:00.000Z", + sourceId: "mock", + sourceLabel: "Mock plan", + icon: USAGE_ICON, + report: { + status: "available", + windows: [{ id: "session", label: "Session", usedPct: 42 }], + }, + }; +} // Where the progress arc is painted, as its centroid relative to the ring's centre in pixels. // Reads the rendered pixels, so any rotation that does not reach the screen counts as none. @@ -68,4 +88,28 @@ test.describe("context window meter", () => { await session.cleanup(); } }); + + test("shows the host's plan limits in the context tooltip", async ({ page }) => { + test.setTimeout(180_000); + const usage = await installUsageReportsFixture(page, { lists: [[contextUsageReport()]] }); + const session = await seedMockAgentWorkspace({ + repoPrefix: "context-window-usage-", + title: "Context window usage tooltip", + initialPrompt: "emit 32000 byte file agent stream payload", + }); + try { + await openAgentRoute(page, session); + await expectComposerVisible(page); + const meter = page.getByTestId("context-window-meter"); + await expect(meter).toHaveAccessibleName(/25%/, { timeout: 30_000 }); + await meter.hover(); + await usage.waitForListRequests(1); + + const tooltip = page.getByTestId("context-window-meter-tooltip"); + await expect(tooltip.getByText("Mock plan", { exact: true })).toBeVisible(); + await expect(tooltip.getByText("42% used", { exact: true })).toBeVisible(); + } finally { + await session.cleanup(); + } + }); }); diff --git a/packages/app/e2e/browser/usage-settings.spec.ts b/packages/app/e2e/browser/usage-settings.spec.ts index bd9d7ffc29..bda4751073 100644 --- a/packages/app/e2e/browser/usage-settings.spec.ts +++ b/packages/app/e2e/browser/usage-settings.spec.ts @@ -165,7 +165,10 @@ test.describe("usage settings", () => { const card = page.getByTestId("usage-card"); const value = page.getByTestId("provider-usage-balance-credits-value"); + const reset = page.getByTestId("provider-usage-balance-credits-reset"); await expect(value).toBeVisible({ timeout: 10_000 }); + await expect(reset).toHaveText(/resets \d+d/); + await expect(reset).toBeVisible(); const [cardBox, valueBox] = await Promise.all([card.boundingBox(), value.boundingBox()]); expect(cardBox).not.toBeNull(); expect(valueBox).not.toBeNull(); diff --git a/packages/app/src/components/context-window-meter.tsx b/packages/app/src/components/context-window-meter.tsx index f4c4c1edf4..615b37dc66 100644 --- a/packages/app/src/components/context-window-meter.tsx +++ b/packages/app/src/components/context-window-meter.tsx @@ -3,6 +3,7 @@ import Svg, { Circle } from "react-native-svg"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { ContextWindowUsageTooltip } from "@/usage/context-window-section"; import { formatContextPercentage, formatTokenCount } from "./context-window-meter.utils"; interface ContextWindowMeterProps { @@ -10,6 +11,7 @@ interface ContextWindowMeterProps { usedTokens: number | null; totalCostUsd?: number | null; showPercentage?: boolean; + serverId?: string; /** Reserve the meter footprint and show a loading ring while usage is pending. */ pending?: boolean; /** Optional glyph envelope for icon-toolbar alignment. */ @@ -95,6 +97,7 @@ export function ContextWindowMeter({ usedTokens, totalCostUsd, showPercentage = false, + serverId, pending = false, glyphSize, }: ContextWindowMeterProps) { @@ -202,6 +205,7 @@ export function ContextWindowMeter({ {t("contextWindow.sessionCost", { cost: formattedSessionCost })} ) : null} + {serverId ? : null} @@ -238,6 +242,7 @@ const styles = StyleSheet.create((theme) => ({ tooltipContent: { gap: theme.spacing[1.5], minWidth: 200, + maxWidth: 280, }, tooltipTitle: { color: theme.colors.foreground, diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index 1286f561d3..63098a50ec 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -283,6 +283,7 @@ function renderContextWindowMeter( contextWindowUsedTokens: number | null, totalCostUsd: number | null, showPercentage: boolean, + serverId: string, pending: boolean, glyphSize: number, ): ReactElement | null { @@ -296,6 +297,7 @@ function renderContextWindowMeter( usedTokens={contextWindowUsedTokens} totalCostUsd={totalCostUsd} showPercentage={showPercentage} + serverId={serverId} pending={pending} glyphSize={glyphSize} /> @@ -2090,6 +2092,7 @@ function ComposerContentImpl({ contextWindowUsedTokens, agentState.totalCostUsd, isCompactLayout, + serverId, contextWindowPending, contextWindowMeterGlyphSize, ), @@ -2098,6 +2101,7 @@ function ComposerContentImpl({ contextWindowUsedTokens, agentState.totalCostUsd, isCompactLayout, + serverId, contextWindowPending, contextWindowMeterGlyphSize, ], diff --git a/packages/app/src/usage/balance-bar.tsx b/packages/app/src/usage/balance-bar.tsx index cf6f5929fa..c0f1acb160 100644 --- a/packages/app/src/usage/balance-bar.tsx +++ b/packages/app/src/usage/balance-bar.tsx @@ -64,15 +64,16 @@ export function UsageBalanceBar({ balance }: { balance: UsageBalance }) { {formatProviderUsageLabel(balance.id, balance.label)} - - {amountText} - {resetLabel ? {` ยท ${resetLabel}`} : null} - + + + {amountText} + + {resetLabel ? ( + + {resetLabel} + + ) : null} + {usedPct != null ? ( @@ -100,13 +101,18 @@ const styles = StyleSheet.create((theme) => ({ }, value: { flexShrink: 1, + alignItems: "flex-end", + }, + amount: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, }, reset: { color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, + textAlign: "right", }, track: { height: 4, diff --git a/packages/app/src/usage/context-window-section.tsx b/packages/app/src/usage/context-window-section.tsx new file mode 100644 index 0000000000..2770f38bf7 --- /dev/null +++ b/packages/app/src/usage/context-window-section.tsx @@ -0,0 +1,36 @@ +import { View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { UsageCard } from "./card"; +import { useUsagePreferences } from "./display"; +import { useHostUsage } from "./queries"; + +/** The current host's available plan limits inside the context-window tooltip. */ +export function ContextWindowUsageTooltip({ serverId }: { serverId: string }) { + const { view } = useHostUsage(serverId); + const { display } = useUsagePreferences(serverId); + if (view.kind !== "ready" || view.reports.length === 0) return null; + + return ( + <> + + + {view.reports.map((entry) => ( + + ))} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + divider: { + height: 1, + backgroundColor: theme.colors.borderAccent, + marginVertical: theme.spacing[2], + marginHorizontal: -theme.spacing[2], + }, + reports: { + alignSelf: "stretch", + gap: theme.spacing[3], + }, +}));