fix(app): restore context usage details

This commit is contained in:
Michael Wu
2026-10-03 05:10:40 +09:00
parent c2335a6b00
commit 0ee0551c4c
6 changed files with 107 additions and 9 deletions
@@ -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 =
'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor"/></svg>';
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();
}
});
});
@@ -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();
@@ -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 })}
</Text>
) : null}
{serverId ? <ContextWindowUsageTooltip serverId={serverId} /> : null}
</View>
</TooltipContent>
</Tooltip>
@@ -238,6 +242,7 @@ const styles = StyleSheet.create((theme) => ({
tooltipContent: {
gap: theme.spacing[1.5],
minWidth: 200,
maxWidth: 280,
},
tooltipTitle: {
color: theme.colors.foreground,
+4
View File
@@ -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,
],
+15 -9
View File
@@ -64,15 +64,16 @@ export function UsageBalanceBar({ balance }: { balance: UsageBalance }) {
<Text style={styles.label} numberOfLines={1}>
{formatProviderUsageLabel(balance.id, balance.label)}
</Text>
<Text
style={styles.value}
numberOfLines={1}
ellipsizeMode="tail"
testID={`provider-usage-balance-${balance.id}-value`}
>
{amountText}
{resetLabel ? <Text style={styles.reset}>{` · ${resetLabel}`}</Text> : null}
</Text>
<View style={styles.value} testID={`provider-usage-balance-${balance.id}-value`}>
<Text style={styles.amount} numberOfLines={1} ellipsizeMode="tail">
{amountText}
</Text>
{resetLabel ? (
<Text style={styles.reset} testID={`provider-usage-balance-${balance.id}-reset`}>
{resetLabel}
</Text>
) : null}
</View>
</View>
{usedPct != null ? (
<View style={styles.track}>
@@ -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,
@@ -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 style={styles.divider} />
<View style={styles.reports}>
{view.reports.map((entry) => (
<UsageCard key={entry.id} serverId={serverId} entry={entry} display={display} compact />
))}
</View>
</>
);
}
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],
},
}));