mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 04:07:44 +02:00
[eric] canvas: docked-mini and app-card camera listeners read layout once per frame, not inside the per-frame fan-out
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b645d4e03f
commit
fb17809682
@@ -311,7 +311,8 @@ const BrowserCard: React.FC<Props> = ({
|
||||
window.addEventListener('resize', measure);
|
||||
// A RO only fires on slot RESIZE; the chat tiling/untiling MOVES the slot without resizing the
|
||||
// window, so re-measure on camera writes + settle timers or the docked card lags behind.
|
||||
window.addEventListener('openswarm:canvas-pan-changed', measure);
|
||||
// Through the rAF coalescer, not directly: the camera event fans out per frame between style writes, and a layout read here forced a whole-board layout per docked mini per frame (6.5 s in one edge-pan frame on a 44-card board).
|
||||
window.addEventListener('openswarm:canvas-pan-changed', onScroll);
|
||||
document.addEventListener('visibilitychange', measure);
|
||||
const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms));
|
||||
// The slot lives in the WINDOWED transcript and remounts without firing any of the events
|
||||
@@ -328,7 +329,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
ro.disconnect();
|
||||
mo.disconnect();
|
||||
window.removeEventListener('resize', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', onScroll);
|
||||
document.removeEventListener('visibilitychange', measure);
|
||||
window.removeEventListener('openswarm:browser-slot-mounted', onSlotMounted);
|
||||
scrollHost?.removeEventListener('load', measure, true);
|
||||
|
||||
@@ -200,13 +200,17 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
window.addEventListener('resize', measure);
|
||||
// A RO only fires on slot RESIZE; the chat tiling/untiling MOVES the slot without resizing the
|
||||
// window, so re-measure on camera writes + settle timers or the docked card lags behind.
|
||||
window.addEventListener('openswarm:canvas-pan-changed', measure);
|
||||
// One read per frame, after the frame's own layout: a direct read inside the per-frame camera fan-out forced a whole-board layout per card per frame.
|
||||
let panRaf = 0;
|
||||
const onPanChanged = (): void => { if (!panRaf) panRaf = requestAnimationFrame(() => { panRaf = 0; measure(); }); };
|
||||
window.addEventListener('openswarm:canvas-pan-changed', onPanChanged);
|
||||
document.addEventListener('visibilitychange', measure);
|
||||
const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms));
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener('resize', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', onPanChanged);
|
||||
if (panRaf) cancelAnimationFrame(panRaf);
|
||||
document.removeEventListener('visibilitychange', measure);
|
||||
timers.forEach((tm) => window.clearTimeout(tm));
|
||||
};
|
||||
@@ -287,11 +291,15 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
};
|
||||
evaluate();
|
||||
const unsubBudget = subscribeAppBudget(evaluate); // an eviction or a freed slot re-runs this card's decision
|
||||
window.addEventListener('openswarm:canvas-pan-changed', evaluate);
|
||||
// Same rule as the dock measure above: the viewport size read must not force a layout inside the per-frame camera fan-out.
|
||||
let panRaf = 0;
|
||||
const onPanChanged = (): void => { if (!panRaf) panRaf = requestAnimationFrame(() => { panRaf = 0; evaluate(); }); };
|
||||
window.addEventListener('openswarm:canvas-pan-changed', onPanChanged);
|
||||
window.addEventListener('resize', evaluate);
|
||||
return () => {
|
||||
unsubBudget();
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', evaluate);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', onPanChanged);
|
||||
if (panRaf) cancelAnimationFrame(panRaf);
|
||||
window.removeEventListener('resize', evaluate);
|
||||
if (suspendTimerRef.current) { clearTimeout(suspendTimerRef.current); suspendTimerRef.current = null; }
|
||||
};
|
||||
|
||||
@@ -43,3 +43,13 @@ test('the vendored widgets\' group-hover variant is keyed on .group, never on a
|
||||
const widgets = execSync('grep -rl "group-hover/" src/toolui --include=*.tsx || true', { cwd: process.cwd() }).toString().trim();
|
||||
assert.equal(widgets, '', 'a named group-hover/<name> falls back to Tailwind\'s :is(:where(.group):hover *) shape: ' + widgets);
|
||||
});
|
||||
|
||||
test('no card reads layout directly inside the per-frame camera fan-out', () => {
|
||||
// Every openswarm:canvas-pan-changed listener that READS layout goes through a rAF coalescer; the direct listeners left are the drag re-pins, which only write.
|
||||
for (const [file, allowed] of [['../../cards/BrowserCard.tsx', ['onScroll', 'onPanChange']], ['../../cards/DashboardViewCard.tsx', ['onPanChanged', 'onPanChange']], ['../../cards/AgentCard.tsx', ['onPanChange']]] as const) {
|
||||
const src = read(file);
|
||||
const listeners = [...src.matchAll(/addEventListener\('openswarm:canvas-pan-changed', (\w+)\)/g)].map((m) => m[1]);
|
||||
assert.ok(listeners.length > 0, `${file} lost its camera listeners`);
|
||||
for (const name of listeners) assert.ok(allowed.includes(name), `${file}: ${name} listens to the per-frame camera event directly (measure/evaluate must ride a rAF)`);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user