diff --git a/packages/app/e2e/browser/context-window-meter.spec.ts b/packages/app/e2e/browser/context-window-meter.spec.ts new file mode 100644 index 0000000000..c7868ac94e --- /dev/null +++ b/packages/app/e2e/browser/context-window-meter.spec.ts @@ -0,0 +1,71 @@ +import type { Locator } from "@playwright/test"; +import { expect, test } from "../support/fixtures"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; + +// 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. +async function progressArcCentroid(meter: Locator): Promise<{ x: number; y: number }> { + const ring = meter.locator("svg"); + const progressColor = await ring + .locator("circle") + .last() + .evaluate((circle) => getComputedStyle(circle).stroke); + const screenshot = await ring.screenshot(); + return ring.evaluate( + async (_svg, { png, color }) => { + const image = await createImageBitmap( + await (await fetch(`data:image/png;base64,${png}`)).blob(), + ); + const canvas = new OffscreenCanvas(image.width, image.height); + const context = canvas.getContext("2d")!; + context.drawImage(image, 0, 0); + const { data } = context.getImageData(0, 0, image.width, image.height); + const [r, g, b] = color.match(/\d+/g)!.map(Number); + let sumX = 0; + let sumY = 0; + let count = 0; + for (let y = 0; y < image.height; y += 1) { + for (let x = 0; x < image.width; x += 1) { + const i = (y * image.width + x) * 4; + const distance = + Math.abs(data[i] - r) + Math.abs(data[i + 1] - g) + Math.abs(data[i + 2] - b); + if (distance < 40) { + sumX += x; + sumY += y; + count += 1; + } + } + } + if (count === 0) { + throw new Error(`No pixels painted in the progress colour ${color}`); + } + return { x: sumX / count - image.width / 2, y: sumY / count - image.height / 2 }; + }, + { png: screenshot.toString("base64"), color: progressColor }, + ); +} + +test.describe("context window meter", () => { + test("draws usage clockwise from twelve o'clock", async ({ page }) => { + test.setTimeout(180_000); + // 32,000 of the mock's 128,000-token window: a quarter, from twelve to three o'clock. + const session = await seedMockAgentWorkspace({ + repoPrefix: "context-window-meter-", + title: "Context window meter e2e", + 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 }); + + const centroid = await progressArcCentroid(meter); + expect(centroid.x).toBeGreaterThan(1); + expect(centroid.y).toBeLessThan(-1); + } finally { + await session.cleanup(); + } + }); +}); diff --git a/packages/app/src/components/context-window-meter.tsx b/packages/app/src/components/context-window-meter.tsx index e587123f19..88c46a0150 100644 --- a/packages/app/src/components/context-window-meter.tsx +++ b/packages/app/src/components/context-window-meter.tsx @@ -117,7 +117,6 @@ export function ContextWindowMeter({ width={geometry.svgSize} height={geometry.svgSize} viewBox={`0 0 ${geometry.svgSize} ${geometry.svgSize}`} - style={styles.svg} accessibilityElementsHidden importantForAccessibility="no-hide-descendants" > @@ -158,7 +157,6 @@ export function ContextWindowMeter({ width={svgSize} height={svgSize} viewBox={`0 0 ${svgSize} ${svgSize}`} - style={styles.svg} accessibilityElementsHidden importantForAccessibility="no-hide-descendants" > @@ -180,6 +178,8 @@ export function ContextWindowMeter({ strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={dashOffset} + // SVG strokes start at three o'clock; the ring reads clockwise from twelve. + transform={`rotate(-90 ${center} ${center})`} /> {showPercentage ? ( @@ -226,9 +226,6 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[1], borderRadius: theme.borderRadius.full, }, - svg: { - transform: [{ rotate: "-90deg" }], - }, percentageLabel: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base,