diff --git a/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx b/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx index 31315853..bebdf390 100644 --- a/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx +++ b/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx @@ -6,10 +6,12 @@ import LinksWidget from './LinksWidget'; import VendoredToolUi from '@toolui/VendoredToolUi'; import type { ShowUiPayload } from './showUiPayload'; import { useOpenUrlInBrowserCard } from './useOpenUrlInBrowserCard'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; /** One switch for every surface that renders a ShowUI payload (chat bubble, pill artifact); ambient = low-cost render for resting surfaces. */ function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambient?: boolean }): React.ReactElement | null { const openUrl = useOpenUrlInBrowserCard(); + const c = useClaudeTokens(); if (payload.component === 'weather') return ; if (payload.component === 'plan') return ; if (payload.component === 'stats') return ; @@ -50,8 +52,13 @@ function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambien return (
- {title &&
{title}
} - {desc &&
{desc}
} + {/* Colour explicitly, never inherited. This header sits above a vendored card whose own + surface is dark, and with no colour of its own it took whatever the ancestor happened to + carry: on a light-mode app that is near-black text on a dark card, i.e. an invisible + title (screenshot 2026-08-31). Same class as ENG-419: a surface that fixes its own + background owes its text a token in the same place. */} + {title &&
{title}
} + {desc &&
{desc}
}
{widget}
diff --git a/frontend/src/app/pages/AgentChat/tool-ui/showUiHeaderContrast.test.ts b/frontend/src/app/pages/AgentChat/tool-ui/showUiHeaderContrast.test.ts new file mode 100644 index 00000000..f41d2bed --- /dev/null +++ b/frontend/src/app/pages/AgentChat/tool-ui/showUiHeaderContrast.test.ts @@ -0,0 +1,31 @@ +// The ShowUI host header (ENG-227) renders an agent-supplied title/description ABOVE a vendored +// card. It set size and weight but no COLOUR, so it inherited whatever the ancestor carried: on a +// light-mode app the inherited text is near-black, and the card underneath is dark, which is a +// title you can only find by selecting it (Eric's screenshot, 2026-08-31). +// +// Same class as ENG-419: a surface that fixes its own background owes its text an explicit token in +// the same place. Inheriting is the bug, not the styling. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const SRC = readFileSync('src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx', 'utf8'); +// The header block only; the rest of the file legitimately has uncoloured wrappers. +const HEADER = SRC.slice(SRC.indexOf('if (!title && !desc) return widget;'), SRC.indexOf('return widget;', SRC.indexOf('if (!title && !desc)') + 40)); + +test('the host header title sets its own colour from the theme', () => { + const title = HEADER.slice(HEADER.indexOf('{title &&'), HEADER.indexOf('{desc &&')); + assert.match(title, /color:\s*c\.text\.primary/, 'the title must not inherit its colour'); +}); + +test('the description does too, and does not fake contrast with opacity', () => { + const desc = HEADER.slice(HEADER.indexOf('{desc &&')); + assert.match(desc, /color:\s*c\.text\.secondary/, 'the description must not inherit its colour'); + assert.doesNotMatch(desc, /opacity:/, + 'opacity over an inherited colour keeps the bug and only dims it; use the secondary token'); +}); + +test('the tokens actually come from the theme hook, not a literal', () => { + assert.match(SRC, /useClaudeTokens/, 'the component must read live theme tokens'); + assert.doesNotMatch(HEADER, /color:\s*['"#]/, 'no hardcoded colour: it would break the other mode'); +});