[eric] canvas: a pill caps its artifact (8 rows, compact stats), ShowUI rows memoized on message identity, spawn pill yields to a tiled composer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ciregenz
2026-09-05 09:29:00 -07:00
co-authored by Claude Fable 5.1
parent 05e8310691
commit 224fd6884a
12 changed files with 229 additions and 26 deletions
@@ -7,6 +7,8 @@ import VendoredToolUi from '@toolui/VendoredToolUi';
import type { ShowUiPayload } from './showUiPayload';
import { useOpenUrlInBrowserCard } from './useOpenUrlInBrowserCard';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { ambientShape } from './showUiAmbient';
import { perfBaselineFor } from '@/shared/perfBaseline';
/** 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 {
@@ -32,7 +34,16 @@ function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambien
const fallback = [raw.src, raw.url].find((v): v is string => typeof v === 'string');
if (fallback) nav.href = fallback;
}
let widget = <VendoredToolUi name={payload.name} props={payload.props} quietFail={ambient} extraProps={nav} />;
const shaped = ambient && !perfBaselineFor('ambient') ? ambientShape(payload.name, raw) : { props: payload.props, note: null };
let widget = <VendoredToolUi name={payload.name} props={shaped.props} quietFail={ambient} extraProps={nav} />;
if (shaped.note) {
widget = (
<div>
{widget}
<div style={{ fontSize: '0.75rem', color: c.text.secondary, padding: '6px 8px 2px' }}>{shaped.note}</div>
</div>
);
}
// The post components only wire clicks on their media, so a text-only post has no way to reach the actual post; the whole card opens it, like the platforms themselves (skipped on ambient pills, where a click means expand).
if (postUrl && !ambient) {
widget = (
@@ -67,4 +78,5 @@ function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambien
return null;
}
export default ShowUiWidgetView;
// Memoized: a chat re-renders on every streamed delta and this sits inside every ShowUI message; the payload is memoized upstream on the message objects.
export default perfBaselineFor('ambient') ? ShowUiWidgetView : React.memo(ShowUiWidgetView);
@@ -1,22 +1,17 @@
import React, { useMemo, useRef } from 'react';
import Box from '@mui/material/Box';
import ToolCallBubble from '../tool-bubbles/ToolCallBubble';
import type { ToolPair } from '../tool-bubbles/ToolCallBubble';
import { toolUiBubblePropsEqual, type ToolUiBubbleProps } from './toolUiBubbleEqual';
import { perfBaselineFor } from '@/shared/perfBaseline';
import { parseShowUiPayload, freezeIfDone } from './showUiPayload';
import ShowUiWidgetView from './ShowUiWidgetView';
import WidgetCopyChip from './WidgetCopyChip';
interface ToolUiBubbleProps {
pair: ToolPair;
sessionId: string;
isPending: boolean;
suppressReveal: boolean;
sessionRunning?: boolean;
}
/** Renders a ShowUI call as its inline component; any schema mismatch falls back to the plain tool bubble. */
function ToolUiBubble({ pair, sessionId, isPending, suppressReveal, sessionRunning = false }: ToolUiBubbleProps): React.ReactElement {
const rawPayload = parseShowUiPayload(pair);
// Keyed on the message objects, not the pair: the transcript rebuilds its pair list on every delta while the messages keep their identity.
const rawPayload = useMemo(() => parseShowUiPayload(pair), [pair.call, pair.result]);
const widgetRef = useRef<HTMLDivElement>(null);
const payload = useMemo(
() => (rawPayload ? freezeIfDone(rawPayload, sessionRunning) : null),
@@ -57,4 +52,4 @@ function ToolUiBubble({ pair, sessionId, isPending, suppressReveal, sessionRunni
);
}
export default ToolUiBubble;
export default perfBaselineFor('ambient') ? ToolUiBubble : React.memo(ToolUiBubble, toolUiBubblePropsEqual);
@@ -0,0 +1,34 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { ambientShape, AMBIENT_TABLE_ROWS } from './showUiAmbient';
// A collapsed pill on the drill board rendered a 5,000-row data-table in full: 370K of 424K fibers,
// paid on every layout pass by every chat (ENG-467/468). The resting artifact gets a screenful.
test('a pill data-table is cut to a screenful and says how much it left out', () => {
const data = Array.from({ length: 5000 }, (_, i) => ({ id: i, name: `row ${i}` }));
const shaped = ambientShape('data-table', { columns: [{ key: 'name' }], data });
assert.equal((shaped.props.data as unknown[]).length, AMBIENT_TABLE_ROWS);
assert.equal(shaped.note, 'Showing 8 of 5,000 rows. Open the chat for the whole table.');
assert.equal(data.length, 5000, 'the original props are not mutated');
});
test('a short table and other widgets pass through untouched', () => {
const small = { columns: [], data: [{ id: 1 }, { id: 2 }] };
assert.equal(ambientShape('data-table', small).props, small);
assert.equal(ambientShape('data-table', small).note, null);
const chart = { series: Array.from({ length: 5000 }, (_, i) => i) };
assert.equal(ambientShape('line-chart', chart).props, chart);
});
test('a pill stats card is asked for its compact density', () => {
const shaped = ambientShape('stats-display', { stats: [] });
assert.equal(shaped.props.compact, true);
});
test('the widget view applies the shaping only on the ambient surface, and is memoized', () => {
const src = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx'), 'utf8');
assert.ok(src.includes("ambient && !perfBaselineFor('ambient') ? ambientShape(payload.name, raw)"), 'the chat keeps the whole table; only the pill is shaped');
assert.ok(src.includes("export default perfBaselineFor('ambient') ? ShowUiWidgetView : React.memo(ShowUiWidgetView)"));
});
@@ -0,0 +1,25 @@
// A widget under a collapsed pill is a resting surface, not the place to read 5,000 rows. On the
// 150-card drill board one such pill carried a 5,000-row data-table: 370,000 of the page's 424,000
// React fibers lived under it, and every layout or style pass on the board paid for them
// (2026-09-05, ENG-467/468/469). The pill gets a screenful; the chat keeps the whole thing.
export const AMBIENT_TABLE_ROWS = 8;
export interface AmbientShape {
props: Record<string, unknown>;
/** One line under the widget when something was left out, or null. */
note: string | null;
}
export function ambientShape(name: string, props: Record<string, unknown>): AmbientShape {
if (name === 'data-table' && Array.isArray(props.data) && props.data.length > AMBIENT_TABLE_ROWS) {
const total = props.data.length;
return {
props: { ...props, data: props.data.slice(0, AMBIENT_TABLE_ROWS) },
note: `Showing ${AMBIENT_TABLE_ROWS} of ${total.toLocaleString()} rows. Open the chat for the whole table.`,
};
}
// The vendored stats card stacks its cells vertically under 440 px; the pill is narrower than that.
if (name === 'stats-display') return { props: { ...props, compact: true }, note: null };
return { props, note: null };
}
@@ -0,0 +1,19 @@
import type { ToolPair } from '../tool-bubbles/ToolCallBubble';
export interface ToolUiBubbleProps {
pair: ToolPair;
sessionId: string;
isPending: boolean;
suppressReveal: boolean;
sessionRunning?: boolean;
}
/** The transcript re-renders on every streamed delta and rebuilds every pair object; the messages inside keep their identity, so that is what decides whether a ShowUI bubble has anything new to draw. */
export function toolUiBubblePropsEqual(prev: ToolUiBubbleProps, next: ToolUiBubbleProps): boolean {
return prev.pair.call === next.pair.call
&& prev.pair.result === next.pair.result
&& prev.sessionId === next.sessionId
&& prev.isPending === next.isPending
&& prev.suppressReveal === next.suppressReveal
&& (prev.sessionRunning ?? false) === (next.sessionRunning ?? false);
}
@@ -0,0 +1,28 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { toolUiBubblePropsEqual } from './toolUiBubbleEqual';
// The transcript rebuilds every pair object on every streamed delta (the pair list is a useMemo on
// the messages array), so a memo on pair identity would never bail. The messages inside keep their
// identity across deltas, and that is what the comparator reads.
const call = { id: 'c1', role: 'tool_call', content: { tool: 'ShowUI', input: {} } } as never;
const result = { id: 'r1', role: 'tool_result', content: 'ok' } as never;
const base = { pair: { type: 'tool_pair' as const, id: 'p', call, result }, sessionId: 's', isPending: false, suppressReveal: false, sessionRunning: false };
test('a rebuilt pair around the same messages is equal', () => {
assert.equal(toolUiBubblePropsEqual(base, { ...base, pair: { ...base.pair } }), true);
});
test('a new result object, a pending flip, or a run-state flip is not equal', () => {
assert.equal(toolUiBubblePropsEqual(base, { ...base, pair: { ...base.pair, result: { ...(result as object) } as never } }), false);
assert.equal(toolUiBubblePropsEqual(base, { ...base, isPending: true }), false);
assert.equal(toolUiBubblePropsEqual(base, { ...base, sessionRunning: true }), false);
});
test('the bubble is exported through React.memo with that comparator and parses the payload off the messages', () => {
const src = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/tool-ui/ToolUiBubble.tsx'), 'utf8');
assert.ok(src.includes("export default perfBaselineFor('ambient') ? ToolUiBubble : React.memo(ToolUiBubble, toolUiBubblePropsEqual)"));
assert.ok(src.includes('useMemo(() => parseShowUiPayload(pair), [pair.call, pair.result])'));
});
@@ -7,6 +7,7 @@ import Snackbar from '@mui/material/Snackbar';
import Icon from '@mui/material/Icon';
import DesktopSpawnPill from './desktop/DesktopSpawnPill';
import { coveredByTiledZones } from './canvas/spawnPillCover';
import SearchIcon from '@mui/icons-material/Search';
import { motion } from 'framer-motion';
import ChatInput from '@/app/pages/AgentChat/ChatInput';
@@ -79,6 +80,22 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
const dispatch = useAppDispatch();
const elementSelection = useElementSelection();
const containerRef = useRef<HTMLDivElement>(null);
// The pill steps aside when a tiled card sits on it; measured from its own rect, re-checked when the tiles or the window change.
const tiledZonesKey = useAppSelector((s) => Object.values(s.dashboardLayout.tiledCards).sort().join(','));
const [pillCovered, setPillCovered] = useState(false);
useEffect(() => {
const check = (): void => {
const el = containerRef.current;
if (!el) { setPillCovered(false); return; }
const r = el.getBoundingClientRect();
const zones = tiledZonesKey ? tiledZonesKey.split(',') : [];
setPillCovered(zones.length > 0 && coveredByTiledZones(zones, { x: r.left, y: r.top, w: r.width, h: r.height }));
};
check();
window.addEventListener('resize', check);
const t = window.setTimeout(check, 400);
return () => { window.removeEventListener('resize', check); window.clearTimeout(t); };
}, [tiledZonesKey]);
const searchInputRef = useRef<HTMLInputElement>(null);
const historyInputRef = useRef<HTMLInputElement>(null);
const historyListRef = useRef<HTMLDivElement>(null);
@@ -465,7 +482,7 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
onHistoryScroll={handleHistoryScroll}
/>
</div>
) : canvasEmpty ? null : (
) : canvasEmpty || pillCovered ? null : (
<DesktopSpawnPill
onOpenComposer={() => {
if (newAgentBounce) onNewAgentBounceEnd?.();
@@ -0,0 +1,30 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { coveredByTiledZones, rectsIntersect } from './spawnPillCover';
// With a chat tiled to the bottom-left quarter, the floating "Ask me anything" pill sat on top of the
// card's own composer, two inputs on one spot (ENG-469). The pill yields to any tile that covers it.
const viewport = { w: 1400, h: 900 };
const fakeZone = (zone: string) => {
const z: Record<string, { x: number; y: number; w: number; h: number }> = {
bl: { x: 0, y: 0.5, w: 0.5, h: 0.5 }, tl: { x: 0, y: 0, w: 0.5, h: 0.5 }, right: { x: 0.5, y: 0, w: 0.5, h: 1 },
};
const r = z[zone]; return r ? { x: r.x * viewport.w, y: r.y * viewport.h, w: r.w * viewport.w, h: r.h * viewport.h } : null;
};
const pill = { x: 480, y: 830, w: 440, h: 56 };
test('a bottom-left tile covers the pill; a top-left tile does not', () => {
assert.equal(coveredByTiledZones(['bl'], pill, fakeZone), true);
assert.equal(coveredByTiledZones(['tl'], pill, fakeZone), false);
});
test('a right-half tile reaches the pill too, an unknown zone is ignored, no tiles means not covered', () => {
assert.equal(coveredByTiledZones(['right'], pill, fakeZone), true);
assert.equal(coveredByTiledZones(['nope'], pill, fakeZone), false);
assert.equal(coveredByTiledZones([], pill, fakeZone), false);
});
test('rectsIntersect is a strict overlap, not a touch', () => {
assert.equal(rectsIntersect({ x: 0, y: 0, w: 10, h: 10 }, { x: 10, y: 0, w: 10, h: 10 }), false);
assert.equal(rectsIntersect({ x: 0, y: 0, w: 10, h: 10 }, { x: 9, y: 9, w: 10, h: 10 }), true);
});
@@ -0,0 +1,14 @@
import { zoneRect, type ZoneRect } from './tiledGeometry';
export function rectsIntersect(a: ZoneRect, b: ZoneRect): boolean {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
/** True when any tiled card's zone sits over the floating spawn pill (a bottom-left tile put "Ask me anything" on top of the card's own composer, ENG-469). */
export function coveredByTiledZones(zones: string[], pill: ZoneRect, rectFor: (zone: string) => ZoneRect | null = zoneRect): boolean {
for (const zone of zones) {
const r = rectFor(zone);
if (r && rectsIntersect(r, pill)) return true;
}
return false;
}
+16 -8
View File
@@ -1,15 +1,23 @@
// Drill seam for interleaved A/B runs on one live board: `localStorage.setItem('osw.perf.baseline', '1')`
// restores the pre-2026-09-02 gesture behaviour (measure transcript heights on every commit, flush every
// held stream in the same tick). Read once per page load, so flipping an arm is a reload, never a rebuild.
let p_cached: boolean | null = null;
export function perfBaseline(): boolean {
// Drill seam for interleaved A/B runs on one live board. `localStorage.setItem('osw.perf.baseline', '1')`
// restores every pre-fix behaviour; a comma list names the fixes to switch off, so one arm can isolate one
// change ('gesture': the 2026-09-02 transcript measure + stream flush; 'ambient': the pill artifact cap +
// the ShowUI memo boundaries). Read once per page load, so flipping an arm is a reload, never a rebuild.
export type PerfFix = 'gesture' | 'ambient';
let p_cached: Set<string> | null = null;
function p_flags(): Set<string> {
if (p_cached === null) {
try {
p_cached = localStorage.getItem('osw.perf.baseline') === '1';
const raw = (localStorage.getItem('osw.perf.baseline') || '').trim();
p_cached = new Set(raw === '1' ? ['gesture', 'ambient'] : raw ? raw.split(',').map((s) => s.trim()) : []);
} catch {
p_cached = false;
p_cached = new Set();
}
}
return p_cached;
}
export function perfBaselineFor(fix: PerfFix): boolean {
return p_flags().has(fix);
}
export function perfBaseline(): boolean {
return perfBaselineFor('gesture');
}
@@ -158,6 +158,7 @@ interface StatCardProps {
locale?: string;
isSingle?: boolean;
index?: number;
compact?: boolean;
}
function StatCard({
@@ -165,6 +166,7 @@ function StatCard({
locale,
isSingle = false,
index = 0,
compact = false,
}: StatCardProps) {
const sparklineColor = stat.sparkline?.color ?? "var(--muted-foreground)";
const hasSparkline = Boolean(stat.sparkline);
@@ -173,7 +175,8 @@ function StatCard({
return (
<div
className={cn(
"relative flex min-h-28 flex-col gap-1 px-6",
"relative flex flex-col gap-1",
compact ? "min-h-16 px-3" : "min-h-28 px-6",
isSingle ? "justify-center" : "justify-end",
)}
>
@@ -200,7 +203,7 @@ function StatCard({
<span
className={cn(
"font-light tracking-normal",
isSingle ? "text-5xl" : "text-3xl",
isSingle ? "text-5xl" : compact ? "text-xl" : "text-3xl",
)}
>
<FormattedValue
@@ -222,7 +225,8 @@ export function StatsDisplay({
stats,
className,
locale: localeProp,
}: StatsDisplayProps) {
compact = false,
}: StatsDisplayProps & { compact?: boolean }) {
const locale =
localeProp ??
(typeof navigator !== "undefined" ? navigator.language : undefined);
@@ -234,7 +238,8 @@ export function StatsDisplay({
data-slot="stats-display"
data-tool-ui-id={id}
className={cn(
"w-full min-w-80 max-w-xl",
"w-full max-w-xl",
compact ? "min-w-0" : "min-w-80",
isSingle && "max-w-sm",
className,
)}
@@ -254,7 +259,8 @@ export function StatsDisplay({
<div
className="grid @[440px]:-ml-px @[440px]:-mt-px"
style={{
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
// Under a collapsed pill (compact) three cells must sit side by side at ~380 px; the page-sized minimum stacked them into a 336 px column.
gridTemplateColumns: compact ? "repeat(auto-fit, minmax(110px, 1fr))" : "repeat(auto-fit, minmax(220px, 1fr))",
}}
>
{stats.map((stat, index) => (
@@ -270,6 +276,7 @@ export function StatsDisplay({
locale={locale}
isSingle={isSingle}
index={index}
compact={compact}
/>
</div>
))}
@@ -0,0 +1,14 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
// Under a collapsed pill the vendored stats card stacked its three cells into a 336 px column (its grid
// minimum is 220 px per cell and the pill is 380 px wide). Compact density puts them side by side.
test('compact density narrows the grid minimum and the cell so three stats fit a pill', () => {
const src = fs.readFileSync(path.join(process.cwd(), 'src/toolui/components/stats-display/stats-display.tsx'), 'utf8');
assert.ok(src.includes('compact ? "repeat(auto-fit, minmax(110px, 1fr))" : "repeat(auto-fit, minmax(220px, 1fr))"'));
assert.ok(src.includes('compact ? "min-h-16 px-3" : "min-h-28 px-6"'));
assert.ok(src.includes('compact={compact}'), 'the density reaches every cell');
assert.ok(src.includes('compact ? "min-w-0" : "min-w-80"'), 'the 320 px floor would overflow the pill');
});