mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
fix(app): restore context usage details
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user