mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
[eric] chat: stats density follows the container, the capped note sits on the widget, the reconnect pill yields to a tile, the docked browser grows in
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ae297de242
commit
c1fcfa6797
@@ -10,6 +10,12 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { hideProviderHealthToast } from '@/shared/state/subscriptionsSlice';
|
||||
import { openSettingsCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { healthToastAnchor, type HealthToastAnchor } from '@/app/pages/Dashboard/canvas/spawnPillCover';
|
||||
|
||||
// Where the toast would sit at its bottom-left home (MUI's 24 px inset), so a toast already moved up is judged on the spot it came from.
|
||||
function HOME_RECT(r: DOMRect): { x: number; y: number; w: number; h: number } {
|
||||
return { x: 24, y: window.innerHeight - 24 - r.height, w: r.width, h: r.height };
|
||||
}
|
||||
|
||||
export default function ProviderHealthToast() {
|
||||
const c = useClaudeTokens();
|
||||
@@ -17,6 +23,18 @@ export default function ProviderHealthToast() {
|
||||
const open = useAppSelector((s) => s.subscriptions.healthToastOpen);
|
||||
const dead = useAppSelector((s) => s.subscriptions.healthDead);
|
||||
const cliMissing = useAppSelector((s) => s.subscriptions.healthCliMissing);
|
||||
const tiledZonesKey = useAppSelector((s) => Object.values(s.dashboardLayout.tiledCards).sort().join(','));
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
const [anchor, setAnchor] = React.useState<HealthToastAnchor>({ vertical: 'bottom', horizontal: 'left' });
|
||||
React.useLayoutEffect(() => {
|
||||
// Measured at its home spot, never at the moved one, or a toast that moved up would never come back down.
|
||||
const el = rootRef.current;
|
||||
if (!el || !open) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const zones = tiledZonesKey ? tiledZonesKey.split(',') : [];
|
||||
setAnchor(healthToastAnchor(zones, anchor.vertical === 'bottom' ? { x: r.x, y: r.y, w: r.width, h: r.height } : HOME_RECT(r)));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tiledZonesKey, open]);
|
||||
|
||||
const onReconnect = React.useCallback(() => {
|
||||
dispatch(openSettingsCard({ tab: 'models' }));
|
||||
@@ -32,7 +50,8 @@ export default function ProviderHealthToast() {
|
||||
autoHideDuration={null}
|
||||
// Clickaway would kill the pill on the user's first canvas click, before they read it; only the X or Reconnect dismisses.
|
||||
onClose={(event, reason) => { if (reason !== 'clickaway') dispatch(hideProviderHealthToast()); }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
anchorOrigin={anchor}
|
||||
ref={rootRef}
|
||||
>
|
||||
<Alert
|
||||
icon={false}
|
||||
|
||||
@@ -89,6 +89,9 @@ import { parseMcpToolName, getMcpInputSummary } from '@/shared/mcpToolMeta';
|
||||
import { isNarration } from './parsing/isNarration';
|
||||
import { shouldForwardGutterWheel } from './gutterWheel';
|
||||
import { openMarketplace } from '@/app/pages/Directory/openMarketplace';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
// The docked browser slot used to land at its full height in one frame and re-pin the view 287 px (ENG-468); it grows in over one reveal beat instead.
|
||||
const BROWSER_SLOT_REVEAL_MS = 200;
|
||||
|
||||
const CONTEXT_WINDOWS: Record<string, number> = {
|
||||
'opus-4-8': 1_000_000,
|
||||
@@ -2119,7 +2122,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
return (
|
||||
<React.Fragment key={`${item.id}-with-browser`}>
|
||||
{rendered}
|
||||
<Box data-browser-slot={id} ref={announceBrowserSlot} sx={browserSlotSx}>{browserSlotBody}</Box>
|
||||
<Collapse in appear timeout={BROWSER_SLOT_REVEAL_MS} easing="cubic-bezier(0.32, 0.72, 0, 1)"><Box data-browser-slot={id} ref={announceBrowserSlot} sx={browserSlotSx}>{browserSlotBody}</Box></Collapse>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -2194,7 +2197,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
)}
|
||||
{/* Fallback dock slot for a browser that docked before any browser tool row exists (or whose row was compacted away); once a row appears the slot anchors at it instead (see browserAnchorItemId). The real card overlays this rect geometrically, so the webview never remounts; the mini hides itself when its slot scrolls mostly out of view, since a live webview can't be clipped by the scroller. */}
|
||||
{hasDockedBrowser && !browserAnchorItemId && (
|
||||
<Box data-browser-slot={id} ref={announceBrowserSlot} sx={browserSlotSx}>{browserSlotBody}</Box>
|
||||
<Collapse in appear timeout={BROWSER_SLOT_REVEAL_MS} easing="cubic-bezier(0.32, 0.72, 0, 1)"><Box data-browser-slot={id} ref={announceBrowserSlot} sx={browserSlotSx}>{browserSlotBody}</Box></Collapse>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -6,7 +6,7 @@ import LinksWidget from './LinksWidget';
|
||||
import VendoredToolUi from '@toolui/VendoredToolUi';
|
||||
import type { ShowUiPayload } from './showUiPayload';
|
||||
import { useOpenUrlInBrowserCard } from './useOpenUrlInBrowserCard';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useClaudeTokens, useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
import { ambientShape } from './showUiAmbient';
|
||||
import { perfBaselineFor } from '@/shared/perfBaseline';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { perfBaselineFor } from '@/shared/perfBaseline';
|
||||
function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambient?: boolean }): React.ReactElement | null {
|
||||
const openUrl = useOpenUrlInBrowserCard();
|
||||
const c = useClaudeTokens();
|
||||
const { mode } = useThemeMode();
|
||||
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} />;
|
||||
@@ -37,10 +38,11 @@ function ShowUiWidgetView({ payload, ambient }: { payload: ShowUiPayload; ambien
|
||||
const shaped = ambient && !perfBaselineFor('ambient') ? ambientShape(payload.name, raw) : { props: payload.props, note: null, extraProps: {} };
|
||||
let widget = <VendoredToolUi name={payload.name} props={shaped.props} quietFail={ambient} extraProps={{ ...nav, ...shaped.extraProps }} />;
|
||||
if (shaped.note) {
|
||||
// The note sits on the widget's own surface (the tool-ui theme's card colour, tucked under the table's rounded bottom) so it reads as the artifact's footer, not stray canvas text (ENG-474).
|
||||
widget = (
|
||||
<div>
|
||||
<div className={`tool-ui-scope${mode === 'dark' ? ' dark' : ''}`}>
|
||||
{widget}
|
||||
<div style={{ fontSize: '0.75rem', color: c.text.secondary, padding: '6px 8px 2px' }}>{shaped.note}</div>
|
||||
<div className="bg-card text-muted-foreground border border-border border-t-0 rounded-b-xl text-xs" style={{ marginTop: -12, padding: '18px 12px 6px' }}>{shaped.note}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { coveredByTiledZones, rectsIntersect } from './spawnPillCover';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { coveredByTiledZones, healthToastAnchor, 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.
|
||||
@@ -28,3 +30,22 @@ 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);
|
||||
});
|
||||
|
||||
test('the reconnect pill moves to the top centre only while a tile covers its bottom-left home, and never hides', () => {
|
||||
const home = { x: 24, y: 840, w: 420, h: 48 };
|
||||
const rects: Record<string, { x: number; y: number; w: number; h: number }> = { bl: { x: 0, y: 450, w: 700, h: 450 }, tr: { x: 700, y: 0, w: 700, h: 450 } };
|
||||
const rectFor = (z: string) => rects[z] ?? null;
|
||||
assert.deepEqual(healthToastAnchor([], home, rectFor), { vertical: 'bottom', horizontal: 'left' });
|
||||
assert.deepEqual(healthToastAnchor(['tr'], home, rectFor), { vertical: 'bottom', horizontal: 'left' });
|
||||
assert.deepEqual(healthToastAnchor(['bl'], home, rectFor), { vertical: 'top', horizontal: 'center' });
|
||||
assert.deepEqual(healthToastAnchor(['bl'], null, rectFor), { vertical: 'bottom', horizontal: 'left' }, 'unmeasured stays home');
|
||||
});
|
||||
|
||||
test('the toast wires the anchor to the tiled zones, and the capped-table note lives on the widget surface', () => {
|
||||
const toast = fs.readFileSync(path.join(process.cwd(), 'src/app/components/overlays/ProviderHealthToast.tsx'), 'utf8');
|
||||
assert.ok(toast.includes('anchorOrigin={anchor}') && toast.includes('healthToastAnchor('));
|
||||
const view = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/tool-ui/ShowUiWidgetView.tsx'), 'utf8');
|
||||
assert.ok(view.includes('className="bg-card text-muted-foreground border border-border border-t-0 rounded-b-xl text-xs"'), 'ENG-474: the note is painted on the tool-ui card surface');
|
||||
const chat = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/AgentChat.tsx'), 'utf8');
|
||||
assert.equal((chat.match(/<Collapse in appear timeout=\{BROWSER_SLOT_REVEAL_MS\}/g) || []).length, 2, 'ENG-468: both browser-slot mounts grow in');
|
||||
});
|
||||
|
||||
@@ -12,3 +12,11 @@ export function coveredByTiledZones(zones: string[], pill: ZoneRect, rectFor: (z
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type HealthToastAnchor = { vertical: 'bottom' | 'top'; horizontal: 'left' | 'center' };
|
||||
/** The reconnect pill must never hide (a covered composer is row 7, a hidden reconnect is row 5), so when a tile covers its bottom-left spot it moves to the top centre, which no quarter or half tile owns together with the composer. */
|
||||
export function healthToastAnchor(zones: string[], toastRect: ZoneRect | null, rectFor: (zone: string) => ZoneRect | null = zoneRect): HealthToastAnchor {
|
||||
if (toastRect && coveredByTiledZones(zones, toastRect, rectFor)) return { vertical: 'top', horizontal: 'center' };
|
||||
return { vertical: 'bottom', horizontal: 'left' };
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ function StatCard({
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col gap-1",
|
||||
compact ? "min-h-16 px-3" : "min-h-28 px-6",
|
||||
compact ? "min-h-16 px-3" : "min-h-16 px-3 @[440px]:min-h-28 @[440px]:px-6",
|
||||
isSingle ? "justify-center" : "justify-end",
|
||||
)}
|
||||
>
|
||||
@@ -203,7 +203,7 @@ function StatCard({
|
||||
<span
|
||||
className={cn(
|
||||
"font-light tracking-normal",
|
||||
isSingle ? "text-5xl" : compact ? "text-xl" : "text-3xl",
|
||||
isSingle ? "text-5xl" : compact ? "text-xl" : "text-xl @[440px]:text-3xl",
|
||||
)}
|
||||
>
|
||||
<FormattedValue
|
||||
@@ -257,11 +257,13 @@ export function StatsDisplay({
|
||||
)}
|
||||
<CardContent className="@container overflow-hidden p-0">
|
||||
<div
|
||||
className="grid @[440px]:-ml-px @[440px]:-mt-px"
|
||||
style={{
|
||||
// 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))",
|
||||
}}
|
||||
// A chat column or a pill is narrower than the page these were drawn for: cells sit side by side from ~330 px (110 px each) and take the page's 220 px minimum only past 660 px, so a quarter-tiled chat no longer stacks three numbers into a 336 px column (ENG-469).
|
||||
className={cn(
|
||||
"grid @[440px]:-ml-px @[440px]:-mt-px",
|
||||
compact
|
||||
? "grid-cols-[repeat(auto-fit,minmax(110px,1fr))]"
|
||||
: "grid-cols-[repeat(auto-fit,minmax(110px,1fr))] @[660px]:grid-cols-[repeat(auto-fit,minmax(220px,1fr))]",
|
||||
)}
|
||||
>
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
|
||||
@@ -5,10 +5,13 @@ 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', () => {
|
||||
// A quarter-tiled chat column is under 440 px too, so the same density follows the CONTAINER width in the chat: dense
|
||||
// cells and a 110 px minimum by default, the page's 220 px minimum only past 660 px (ENG-469 item 3).
|
||||
test('density follows the container: dense by default, page density only in a wide container, compact forces dense', () => {
|
||||
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('? "grid-cols-[repeat(auto-fit,minmax(110px,1fr))]"'));
|
||||
assert.ok(src.includes(': "grid-cols-[repeat(auto-fit,minmax(110px,1fr))] @[660px]:grid-cols-[repeat(auto-fit,minmax(220px,1fr))]"'));
|
||||
assert.ok(src.includes('compact ? "min-h-16 px-3" : "min-h-16 px-3 @[440px]:min-h-28 @[440px]: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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user