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],
+ },
+}));