[eric] tool-ui: the ShowUI host header colours its own title instead of inheriting it into a dark card

This commit is contained in:
ciregenz
2026-08-31 18:09:21 -07:00
parent c752a77659
commit 753536128f
2 changed files with 40 additions and 2 deletions
@@ -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 <WeatherWidget props={payload.props} ambient={ambient} />;
if (payload.component === 'plan') return <PlanWidget props={payload.props} />;
if (payload.component === 'stats') return <StatsWidget props={payload.props} />;
@@ -50,8 +52,13 @@ function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambien
return (
<div>
<div style={{ marginBottom: 6, paddingLeft: 4, paddingRight: 4 }}>
{title && <div style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.35 }}>{title}</div>}
{desc && <div style={{ fontSize: '0.8125rem', opacity: 0.72, marginTop: 2, lineHeight: 1.4 }}>{desc}</div>}
{/* 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 && <div style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.35, color: c.text.primary }}>{title}</div>}
{desc && <div style={{ fontSize: '0.8125rem', color: c.text.secondary, marginTop: 2, lineHeight: 1.4 }}>{desc}</div>}
</div>
{widget}
</div>
@@ -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');
});