From 3b41183606a98fb9b0d7e20d2eeecf0ab03b30aa Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:57:36 +0200 Subject: [PATCH] fix(app): start the context meter ring at twelve o'clock on web (#5598) The ring's quarter-turn lived in a Unistyles style on the react-native-svg root. On web, Unistyles only turns styles into classes for the React Native components its Babel plugin rewrites, so the Svg received an empty style and the arc started at three o'clock. The rotation now lives on the progress circle as an SVG transform, which renders the same on web and native. Closes #5596 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../e2e/browser/context-window-meter.spec.ts | 71 +++++++++++++++++++ .../src/components/context-window-meter.tsx | 7 +- 2 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 packages/app/e2e/browser/context-window-meter.spec.ts 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,