diff --git a/webviews/test/agent-pane-turn-tokens-muted-browser.test.tsx b/webviews/test/agent-pane-turn-tokens-muted-browser.test.tsx new file mode 100644 index 000000000000..56db30b8bec6 --- /dev/null +++ b/webviews/test/agent-pane-turn-tokens-muted-browser.test.tsx @@ -0,0 +1,64 @@ +// The running turn's token count reads quieter than its words (agent-pane-turn-tokens-muted.test.tsx), +// resolved in Chromium from the real stylesheets under a host theme: "Working for 35s" and +// "Thinking" in the theme's secondary text, "· 679.9K tokens" in its tertiary, in tabular numerals. +import { describe, expect, setDefaultTimeout, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { chromium, type Browser } from "playwright"; +import { requireBrowserLane } from "./support/requireBrowserLane"; +import { tokenLines, tokenLineMarkup } from "./support/turnTokenLines"; + +await requireBrowserLane("agent-pane-turn-tokens-muted-browser.test.tsx", async () => { + setDefaultTimeout(60_000); + let browser: Browser; + try { + browser = await chromium.launch({ headless: true }); + } catch { + console.warn("agent-pane-turn-tokens-muted-browser: skipping (run `bunx playwright install chromium`)"); + describe.skip("turn tokens muted", () => test("chromium", () => {})); + return; + } + const src = path.resolve(import.meta.dir, "../src"); + const css = [ + "agent-session/shared/styles.css", + "agent-session/acpmux/styles.css", + "agent-session/acpmux/conversation/conversation.css", + ] + .map((file) => readFileSync(path.join(src, file), "utf8")) + .join("\n"); + // The host's theme (AgentPaneTheme.swift: mutedText is textSecondary, softText textTertiary). + const theme = + "--agent-text: rgb(230, 230, 230); --agent-muted: rgb(160, 160, 170); --agent-soft: rgb(110, 110, 120);"; + + describe("the count reads quieter than the line's words", () => { + for (const [name, line] of Object.entries(tokenLines)) + test(name, async () => { + const page = await browser.newPage(); + try { + // The static status look: the word Thinking holds still in its muted color, no band. + await page.setContent( + `
` + + `${tokenLineMarkup(line)}`, + ); + const looks = await page.evaluate(() => { + const style = (selector: string) => getComputedStyle(document.querySelector(selector)!); + return { + words: style(".cv-thinking, .cv-worked__label").color, + tokens: style(".cv-worked__tokens").color, + sep: style(".cv-worked__sep").color, + numerals: style(".cv-worked__tokens").fontVariantNumeric, + }; + }); + expect(looks.words).toBe("rgb(160, 160, 170)"); + expect(looks.tokens).toBe("rgb(110, 110, 120)"); + expect(looks.sep).toBe(looks.tokens); + expect(looks.numerals).toContain("tabular-nums"); + } finally { + await page.close(); + } + }); + test("closes the browser", async () => { + await browser.close(); + }); + }); +}); diff --git a/webviews/test/agent-pane-turn-tokens-muted.test.tsx b/webviews/test/agent-pane-turn-tokens-muted.test.tsx new file mode 100644 index 000000000000..1c1c25ce27bc --- /dev/null +++ b/webviews/test/agent-pane-turn-tokens-muted.test.tsx @@ -0,0 +1,44 @@ +// The running turn's token count is secondary to its words (Leo, 2026-10-21: "the token font +// shouldnt be this same color"). "Working for 35s" (or "Thinking 35s") keeps the line's muted color; +// "· 679.9K tokens" after it takes the tool rows' quieter one, the theme's tertiary text +// (`--cv-text-dim`, `--agent-soft`, the host's textTertiary), never a literal color, in tabular +// numerals so the count ticks in place. Both lines that say it, the Codex layout's live line at the +// bottom (Thinking.tsx) and the line over the turn with that layout off (WorkingFor.tsx), draw the +// count in the same span, so one rule covers both. agent-pane-turn-tokens-muted-browser.test.tsx +// resolves the real stylesheets in Chromium. +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { tokenLines, tokenLineMarkup } from "./support/turnTokenLines"; + +const conversationCss = readFileSync( + path.resolve(import.meta.dir, "../src/agent-session/acpmux/conversation/conversation.css"), + "utf8", +); +/// The declarations of the rule whose selector is exactly `selector`. +const rule = (selector: string) => { + const at = conversationCss.indexOf(`\n${selector} {`); + if (at < 0) throw new Error(`conversation.css has no rule for ${selector}`); + return conversationCss.slice(at, conversationCss.indexOf("}", at)); +}; + +for (const [name, line] of Object.entries(tokenLines)) + test(`${name}: the count is its own span, which the quieter rule keys on`, () => { + const html = tokenLineMarkup(line); + expect(html).toContain( + '679.9K tokens', + ); + // The line's words come before the count's span, outside it. + expect(html.indexOf("cv-worked__count")).toBeGreaterThan(html.search(/Working for|Thinking/)); + }); + +test("the count takes the theme's tertiary text token, in tabular numerals, never a literal color", () => { + const tokens = rule(".cv-worked__count:has(> .cv-worked__tokens)"); + expect(tokens).toContain("color: var(--cv-text-dim);"); + expect(tokens).toContain("font-variant-numeric: tabular-nums;"); + expect(tokens).not.toMatch(/#[0-9a-f]{3,8}\b|rgba?\(|hsla?\(|oklch\(/i); + // The line's words keep the muted (secondary) token; the count's is the tertiary one. + expect(rule(".cv-worked")).toContain("color: var(--cv-text-muted);"); + expect(conversationCss).toContain("--cv-text-muted: var(--agent-muted);"); + expect(conversationCss).toContain("--cv-text-dim: var(--agent-soft);"); +}); diff --git a/webviews/test/support/turnTokenLines.tsx b/webviews/test/support/turnTokenLines.tsx new file mode 100644 index 000000000000..602b5795e1ce --- /dev/null +++ b/webviews/test/support/turnTokenLines.tsx @@ -0,0 +1,33 @@ +// l10n-allow-file: test fixtures, not shipped UI. +// The two lines that say a running turn's tokens, at 35s and 679,900 tokens: the Codex layout's live +// line at the bottom (Thinking.tsx) and the line over the turn with that layout off (WorkingFor.tsx). +import type { ReactElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { Thinking } from "../../src/agent-session/acpmux/conversation/Thinking"; +import { + WorkingCountsContext, + WorkingFor, + type WorkingCounts, +} from "../../src/agent-session/acpmux/conversation/WorkingFor"; +import type { AcpmuxRow } from "../../src/agent-session/acpmux/model"; + +const START = Date.UTC(2026, 9, 21, 9, 0, 0); +const now = () => START + 35_000; +const counts: WorkingCounts = { + agents: 0, + commands: 0, + sessions: 0, + openAgents: () => {}, + openCommands: () => {}, + openSessions: () => {}, + tokens: 679_900, +}; +const working: AcpmuxRow = { id: "working", kind: "working", version: 1, at: START }; + +export const tokenLines: Record