mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
performance updates
This commit is contained in:
@@ -170,7 +170,7 @@ const AppShell: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispatch(addBrowserCard({ url }));
|
||||
dispatch(addBrowserCard({ url, viewportWidth: window.innerWidth }));
|
||||
} else {
|
||||
dispatch(setPendingBrowserUrl(url));
|
||||
const lastId = (window as any).__openswarm_last_dashboard_id as string | undefined;
|
||||
|
||||
@@ -45,7 +45,7 @@ const SelectionOverlay: React.FC<Props> = ({ overlay, dragRect, dragPreview = []
|
||||
return;
|
||||
}
|
||||
|
||||
const updateRects = () => {
|
||||
const measureRects = () => {
|
||||
const rects: PersistentRect[] = [];
|
||||
for (const sel of semanticEls) {
|
||||
try {
|
||||
@@ -65,13 +65,47 @@ const SelectionOverlay: React.FC<Props> = ({ overlay, dragRect, dragPreview = []
|
||||
// selector might be invalid
|
||||
}
|
||||
}
|
||||
setPersistentRects(rects);
|
||||
rafRef.current = requestAnimationFrame(updateRects);
|
||||
|
||||
setPersistentRects((prev) => {
|
||||
if (prev.length !== rects.length) return rects;
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
const a = prev[i], b = rects[i];
|
||||
if (a.id !== b.id || a.top !== b.top || a.left !== b.left ||
|
||||
a.width !== b.width || a.height !== b.height) return rects;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
rafRef.current = requestAnimationFrame(updateRects);
|
||||
measureRects();
|
||||
|
||||
const scheduleUpdate = () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = requestAnimationFrame(measureRects);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', scheduleUpdate, true);
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
|
||||
// Observe size changes on the selected elements specifically. We
|
||||
// deliberately avoid a document-wide MutationObserver here — that would
|
||||
// fire on every streamed chat token (and many other unrelated mutations)
|
||||
// and effectively re-create the runaway loop we're replacing.
|
||||
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleUpdate) : null;
|
||||
if (ro) {
|
||||
for (const sel of semanticEls) {
|
||||
try {
|
||||
const domEl = document.querySelector(sel.selectorPath);
|
||||
if (domEl) ro.observe(domEl);
|
||||
} catch { /* selector might be invalid */ }
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
window.removeEventListener('scroll', scheduleUpdate, true);
|
||||
window.removeEventListener('resize', scheduleUpdate);
|
||||
if (ro) ro.disconnect();
|
||||
};
|
||||
}, [ctx?.selectedElements]);
|
||||
|
||||
|
||||
@@ -109,6 +109,27 @@ function getAgentWorkTime(messages: Array<{ role: string; timestamp: string }>,
|
||||
return { total, last };
|
||||
}
|
||||
|
||||
const ElapsedTime: React.FC<{
|
||||
messages: Array<{ role: string; timestamp: string }>;
|
||||
status: string;
|
||||
sx?: Record<string, any>;
|
||||
}> = React.memo(({ messages, status, sx }) => {
|
||||
const [, setTick] = useState(0);
|
||||
const isLive = status === 'running' || status === 'waiting_approval';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) return;
|
||||
const interval = setInterval(() => setTick((t) => t + 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isLive]);
|
||||
|
||||
return (
|
||||
<Typography variant="caption" sx={sx}>
|
||||
{fmtSeconds(getAgentWorkTime(messages, status).last)}
|
||||
</Typography>
|
||||
);
|
||||
});
|
||||
|
||||
function summarizeToolInput(toolName: string, toolInput: Record<string, any>): string {
|
||||
const mcp = parseMcpToolName(toolName);
|
||||
if (mcp.isMcp) {
|
||||
@@ -295,7 +316,6 @@ const AgentCard: React.FC<Props> = ({
|
||||
draft: { color: c.accent.primary, bg: c.bg.secondary },
|
||||
};
|
||||
|
||||
const [, setTick] = useState(0);
|
||||
const isDraft = session.status === 'draft';
|
||||
|
||||
// ---- Drag via header (pointer events) ----
|
||||
@@ -485,13 +505,6 @@ const AgentCard: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (session.status === 'running' || session.status === 'waiting_approval') {
|
||||
const interval = setInterval(() => setTick((t) => t + 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [session.status]);
|
||||
|
||||
const lastMessage = session.messages[session.messages.length - 1];
|
||||
const isStreaming = !!session.streamingMessage;
|
||||
const previewContent = isStreaming
|
||||
@@ -881,9 +894,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
<Typography variant="caption" sx={{ color: c.text.tertiary }}>
|
||||
{session.mode}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: c.text.tertiary }}>
|
||||
{fmtSeconds(getAgentWorkTime(session.messages, session.status).last)}
|
||||
</Typography>
|
||||
<ElapsedTime messages={session.messages} status={session.status} sx={{ color: c.text.tertiary }} />
|
||||
{session.cost_usd > 0 && hasApiKey && (
|
||||
<Typography variant="caption" sx={{ color: c.accent.primary }}>
|
||||
${session.cost_usd.toFixed(4)}
|
||||
|
||||
@@ -115,6 +115,38 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
const glowingBrowserCards = useAppSelector((state) => state.dashboardLayout.glowingBrowserCards);
|
||||
const sessionList = Object.values(sessions);
|
||||
|
||||
// Reference-stable list of browser-agent sessions that need tethers.
|
||||
// Without this, the tether useMemo below recomputes ~190 lines of SVG
|
||||
// path math on every streamed token in any chat, because `sessionList`
|
||||
// is a fresh array on every Dashboard re-render.
|
||||
const browserAgentTetherSeedsRef = useRef<
|
||||
Array<{ id: string; browser_id: string; parent_session_id: string }>
|
||||
>([]);
|
||||
const browserAgentTetherSeeds = useMemo(() => {
|
||||
const next: Array<{ id: string; browser_id: string; parent_session_id: string }> = [];
|
||||
for (const s of Object.values(sessions)) {
|
||||
if (s.mode !== 'browser-agent') continue;
|
||||
if (s.status !== 'running' && s.status !== 'waiting_approval') continue;
|
||||
if (!s.browser_id || !s.parent_session_id) continue;
|
||||
next.push({ id: s.id, browser_id: s.browser_id, parent_session_id: s.parent_session_id });
|
||||
}
|
||||
const prev = browserAgentTetherSeedsRef.current;
|
||||
if (prev.length === next.length) {
|
||||
let same = true;
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (prev[i].id !== next[i].id
|
||||
|| prev[i].browser_id !== next[i].browser_id
|
||||
|| prev[i].parent_session_id !== next[i].parent_session_id) {
|
||||
same = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (same) return prev;
|
||||
}
|
||||
browserAgentTetherSeedsRef.current = next;
|
||||
return next;
|
||||
}, [sessions]);
|
||||
|
||||
const contentBounds = useMemo(() => {
|
||||
const allRects = [
|
||||
...Object.values(cards).map((c) => ({ x: c.x, y: c.y, w: c.width, h: c.height })),
|
||||
@@ -483,7 +515,7 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingBrowserUrl || !layoutInitialized) return;
|
||||
dispatch(addBrowserCard({ url: pendingBrowserUrl, expandedSessionIds }));
|
||||
dispatch(addBrowserCard({ url: pendingBrowserUrl, expandedSessionIds, viewportWidth: window.innerWidth }));
|
||||
dispatch(clearPendingBrowserUrl());
|
||||
}, [pendingBrowserUrl, layoutInitialized, dispatch, expandedSessionIds]);
|
||||
|
||||
@@ -605,7 +637,7 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
const liveIds = dashboardSessionIds.sort().join(',');
|
||||
if (liveIds === prevSessionIdsRef.current) return;
|
||||
prevSessionIdsRef.current = liveIds;
|
||||
dispatch(reconcileSessions({ sessionIds: dashboardSessionIds, expandedSessionIds }));
|
||||
dispatch(reconcileSessions({ sessionIds: dashboardSessionIds, expandedSessionIds, viewportWidth: window.innerWidth }));
|
||||
}, [sessions, layoutInitialized, dispatch, dashboardId, expandedSessionIds]);
|
||||
|
||||
// ---- Auto-reveal / collapse / unreveal sub-agent cards ----
|
||||
@@ -1241,7 +1273,7 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
);
|
||||
|
||||
const handleAddView = useCallback((outputId: string) => {
|
||||
dispatch(addViewCard({ outputId, expandedSessionIds }));
|
||||
dispatch(addViewCard({ outputId, expandedSessionIds, viewportWidth: window.innerWidth }));
|
||||
setTimeout(() => {
|
||||
const card = store.getState().dashboardLayout.viewCards[outputId];
|
||||
if (card) {
|
||||
@@ -1254,7 +1286,7 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
const handleAddBrowser = useCallback(() => {
|
||||
trackEvent('dashboard.browser_added');
|
||||
const prevIds = new Set(Object.keys(store.getState().dashboardLayout.browserCards));
|
||||
dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds }));
|
||||
dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds, viewportWidth: window.innerWidth }));
|
||||
setTimeout(() => {
|
||||
const allBrowserCards = store.getState().dashboardLayout.browserCards;
|
||||
const newId = Object.keys(allBrowserCards).find((id) => !prevIds.has(id));
|
||||
@@ -1299,7 +1331,7 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
const handleTidy = useCallback(() => {
|
||||
trackEvent('dashboard.tidy_layout');
|
||||
const currentExpanded = store.getState().agents.expandedSessionIds;
|
||||
dispatch(tidyLayout({ expandedSessionIds: currentExpanded }));
|
||||
dispatch(tidyLayout({ expandedSessionIds: currentExpanded, viewportWidth: window.innerWidth }));
|
||||
|
||||
const expandedSet = new Set(currentExpanded);
|
||||
const { cards: tidied, viewCards: tidiedViews, browserCards: tidiedBrowsers } = store.getState().dashboardLayout;
|
||||
@@ -1563,20 +1595,17 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
|
||||
}
|
||||
|
||||
// Source 2: active browser-agent sessions (persistent — survives parent turn completion)
|
||||
for (const s of sessionList) {
|
||||
if (s.mode !== 'browser-agent') continue;
|
||||
if (s.status !== 'running' && s.status !== 'waiting_approval') continue;
|
||||
if (!s.browser_id || !s.parent_session_id) continue;
|
||||
if (glowTethers.has(s.browser_id)) continue; // glow already covers this one
|
||||
const t = browserTether(s.browser_id, s.parent_session_id, false, '');
|
||||
if (t) glowTethers.set(s.browser_id, t);
|
||||
for (const seed of browserAgentTetherSeeds) {
|
||||
if (glowTethers.has(seed.browser_id)) continue; // glow already covers this one
|
||||
const t = browserTether(seed.browser_id, seed.parent_session_id, false, '');
|
||||
if (t) glowTethers.set(seed.browser_id, t);
|
||||
}
|
||||
|
||||
const browserTethers = Array.from(glowTethers.values()).filter(Boolean) as Array<{ key: string; path: string; labelX: number; labelY: number; label: string; fading: boolean }>;
|
||||
|
||||
return [...agentTethers, ...browserTethers];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [glowingAgentCards, glowingBrowserCards, cards, browserCards, expandedSessionIds, liveDragInfo, measuredHeightsTick, sessionList]);
|
||||
}, [glowingAgentCards, glowingBrowserCards, cards, browserCards, expandedSessionIds, liveDragInfo, measuredHeightsTick, browserAgentTetherSeeds]);
|
||||
|
||||
const dotSize = Math.max(1, 1.5 * canvas.zoom);
|
||||
const dotSpacing = 24 * canvas.zoom;
|
||||
|
||||
@@ -185,12 +185,14 @@ export function findOpenGridCell(
|
||||
occupiedRects: Rect[],
|
||||
newW: number,
|
||||
newH: number,
|
||||
viewportWidth?: number,
|
||||
): { x: number; y: number } {
|
||||
const cellW = DEFAULT_CARD_W + GRID_GAP;
|
||||
const cellH = DEFAULT_CARD_H + GRID_GAP;
|
||||
const vw = viewportWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 0);
|
||||
const maxCols = Math.max(
|
||||
1,
|
||||
Math.floor((window.innerWidth - GRID_ORIGIN.x) / cellW) || GRID_COLS_FALLBACK,
|
||||
Math.floor((vw - GRID_ORIGIN.x) / cellW) || GRID_COLS_FALLBACK,
|
||||
);
|
||||
|
||||
for (let row = 0; ; row++) {
|
||||
@@ -265,9 +267,9 @@ const dashboardLayoutSlice = createSlice({
|
||||
|
||||
reconcileSessions(
|
||||
state,
|
||||
action: PayloadAction<{ sessionIds: string[]; expandedSessionIds: string[] }>,
|
||||
action: PayloadAction<{ sessionIds: string[]; expandedSessionIds: string[]; viewportWidth?: number }>,
|
||||
) {
|
||||
const { sessionIds, expandedSessionIds } = action.payload;
|
||||
const { sessionIds, expandedSessionIds, viewportWidth } = action.payload;
|
||||
const liveIds = new Set(sessionIds);
|
||||
|
||||
for (const id of Object.keys(state.cards)) {
|
||||
@@ -287,7 +289,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
delete state.closedCardPositions[id];
|
||||
} else {
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_CARD_W, DEFAULT_CARD_H);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_CARD_W, DEFAULT_CARD_H, viewportWidth);
|
||||
state.cards[id] = {
|
||||
session_id: id,
|
||||
x: pos.x,
|
||||
@@ -302,8 +304,9 @@ const dashboardLayoutSlice = createSlice({
|
||||
|
||||
tidyLayout(
|
||||
state,
|
||||
action: PayloadAction<{ expandedSessionIds: string[] }>,
|
||||
action: PayloadAction<{ expandedSessionIds: string[]; viewportWidth?: number }>,
|
||||
) {
|
||||
const { viewportWidth } = action.payload;
|
||||
const expanded = new Set(action.payload.expandedSessionIds);
|
||||
const agentCards = Object.values(state.cards);
|
||||
const viewCards = Object.values(state.viewCards);
|
||||
@@ -330,7 +333,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
h = item.storedH;
|
||||
}
|
||||
|
||||
const pos = findOpenGridCell(placedRects, w, h);
|
||||
const pos = findOpenGridCell(placedRects, w, h, viewportWidth);
|
||||
placedRects.push({ x: pos.x, y: pos.y, w, h });
|
||||
|
||||
if (item.kind === 'agent') {
|
||||
@@ -349,8 +352,9 @@ const dashboardLayoutSlice = createSlice({
|
||||
addViewCard(state, action: PayloadAction<{
|
||||
outputId: string; expandedSessionIds?: string[];
|
||||
x?: number; y?: number; width?: number; height?: number;
|
||||
viewportWidth?: number;
|
||||
}>) {
|
||||
const { outputId, expandedSessionIds, x, y, width, height } = action.payload;
|
||||
const { outputId, expandedSessionIds, x, y, width, height, viewportWidth } = action.payload;
|
||||
if (state.viewCards[outputId]) return;
|
||||
let posX: number, posY: number;
|
||||
if (x != null && y != null) {
|
||||
@@ -358,7 +362,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
posY = y;
|
||||
} else {
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H, viewportWidth);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
}
|
||||
@@ -397,11 +401,11 @@ const dashboardLayoutSlice = createSlice({
|
||||
delete state.viewCards[action.payload];
|
||||
},
|
||||
|
||||
addBrowserCard(state, action: PayloadAction<{ url: string; expandedSessionIds?: string[] }>) {
|
||||
addBrowserCard(state, action: PayloadAction<{ url: string; expandedSessionIds?: string[]; viewportWidth?: number }>) {
|
||||
const id = `browser-${Date.now().toString(36)}`;
|
||||
const tabId = generateTabId();
|
||||
const rects = collectOccupiedRects(state, action.payload.expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H, action.payload.viewportWidth);
|
||||
state.browserCards[id] = {
|
||||
browser_id: id,
|
||||
url: action.payload.url,
|
||||
@@ -462,9 +466,10 @@ const dashboardLayoutSlice = createSlice({
|
||||
action: PayloadAction<{
|
||||
tabs: BrowserTab[]; url: string; expandedSessionIds?: string[];
|
||||
id?: string; x?: number; y?: number; width?: number; height?: number;
|
||||
viewportWidth?: number;
|
||||
}>
|
||||
) {
|
||||
const { x, y, width, height } = action.payload;
|
||||
const { x, y, width, height, viewportWidth } = action.payload;
|
||||
const id = action.payload.id || `browser-${Date.now().toString(36)}`;
|
||||
const newTabs = action.payload.tabs.map((t) => ({
|
||||
id: generateTabId(),
|
||||
@@ -479,7 +484,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
posY = y;
|
||||
} else {
|
||||
const rects = collectOccupiedRects(state, action.payload.expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H, viewportWidth);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user