diff --git a/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx b/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx
index bebdf390..849446d8 100644
--- a/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx
+++ b/frontend/src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx
@@ -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 = ;
+ const shaped = ambient && !perfBaselineFor('ambient') ? ambientShape(payload.name, raw) : { props: payload.props, note: null };
+ let widget = ;
+ if (shaped.note) {
+ widget = (
+
+ {widget}
+
{shaped.note}
+
+ );
+ }
// 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);
diff --git a/frontend/src/app/pages/AgentChat/tool-ui/ToolUiBubble.tsx b/frontend/src/app/pages/AgentChat/tool-ui/ToolUiBubble.tsx
index 1ce0712c..1ae258a5 100644
--- a/frontend/src/app/pages/AgentChat/tool-ui/ToolUiBubble.tsx
+++ b/frontend/src/app/pages/AgentChat/tool-ui/ToolUiBubble.tsx
@@ -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(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);
diff --git a/frontend/src/app/pages/AgentChat/tool-ui/showUiAmbient.test.ts b/frontend/src/app/pages/AgentChat/tool-ui/showUiAmbient.test.ts
new file mode 100644
index 00000000..9c0ae779
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/tool-ui/showUiAmbient.test.ts
@@ -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)"));
+});
diff --git a/frontend/src/app/pages/AgentChat/tool-ui/showUiAmbient.ts b/frontend/src/app/pages/AgentChat/tool-ui/showUiAmbient.ts
new file mode 100644
index 00000000..0ff1bf8b
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/tool-ui/showUiAmbient.ts
@@ -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;
+ /** One line under the widget when something was left out, or null. */
+ note: string | null;
+}
+
+export function ambientShape(name: string, props: Record): 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 };
+}
diff --git a/frontend/src/app/pages/AgentChat/tool-ui/toolUiBubbleEqual.ts b/frontend/src/app/pages/AgentChat/tool-ui/toolUiBubbleEqual.ts
new file mode 100644
index 00000000..46d99ec0
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/tool-ui/toolUiBubbleEqual.ts
@@ -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);
+}
diff --git a/frontend/src/app/pages/AgentChat/tool-ui/toolUiBubbleMemo.test.ts b/frontend/src/app/pages/AgentChat/tool-ui/toolUiBubbleMemo.test.ts
new file mode 100644
index 00000000..7f1b88f4
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/tool-ui/toolUiBubbleMemo.test.ts
@@ -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])'));
+});
diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx
index 2598a3a3..5ed7d07a 100644
--- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx
+++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx
@@ -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(
const dispatch = useAppDispatch();
const elementSelection = useElementSelection();
const containerRef = useRef(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(null);
const historyInputRef = useRef(null);
const historyListRef = useRef(null);
@@ -465,7 +482,7 @@ const DashboardToolbar = React.forwardRef(
onHistoryScroll={handleHistoryScroll}
/>
- ) : canvasEmpty ? null : (
+ ) : canvasEmpty || pillCovered ? null : (
{
if (newAgentBounce) onNewAgentBounceEnd?.();
diff --git a/frontend/src/app/pages/Dashboard/canvas/spawnPillCover.test.ts b/frontend/src/app/pages/Dashboard/canvas/spawnPillCover.test.ts
new file mode 100644
index 00000000..5a8ea305
--- /dev/null
+++ b/frontend/src/app/pages/Dashboard/canvas/spawnPillCover.test.ts
@@ -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 = {
+ 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);
+});
diff --git a/frontend/src/app/pages/Dashboard/canvas/spawnPillCover.ts b/frontend/src/app/pages/Dashboard/canvas/spawnPillCover.ts
new file mode 100644
index 00000000..0feca023
--- /dev/null
+++ b/frontend/src/app/pages/Dashboard/canvas/spawnPillCover.ts
@@ -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;
+}
diff --git a/frontend/src/shared/perfBaseline.ts b/frontend/src/shared/perfBaseline.ts
index 51f37d6b..13b18ab2 100644
--- a/frontend/src/shared/perfBaseline.ts
+++ b/frontend/src/shared/perfBaseline.ts
@@ -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 | null = null;
+function p_flags(): Set {
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');
+}
diff --git a/frontend/src/toolui/components/stats-display/stats-display.tsx b/frontend/src/toolui/components/stats-display/stats-display.tsx
index 87eca275..65e82945 100644
--- a/frontend/src/toolui/components/stats-display/stats-display.tsx
+++ b/frontend/src/toolui/components/stats-display/stats-display.tsx
@@ -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 (
@@ -200,7 +203,7 @@ function StatCard({
{stats.map((stat, index) => (
@@ -270,6 +276,7 @@ export function StatsDisplay({
locale={locale}
isSingle={isSingle}
index={index}
+ compact={compact}
/>
))}
diff --git a/frontend/src/toolui/components/stats-display/statsCompact.test.ts b/frontend/src/toolui/components/stats-display/statsCompact.test.ts
new file mode 100644
index 00000000..ec05d29e
--- /dev/null
+++ b/frontend/src/toolui/components/stats-display/statsCompact.test.ts
@@ -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');
+});