mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-22 01:24:52 +02:00
[eric] canvas: drag frames stop re-rendering the page, only the tether layer follows the pointer
This commit is contained in:
@@ -3,7 +3,7 @@ import Box from '@mui/material/Box';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { addViewCard, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
|
||||
import DashboardHeader from './DashboardHeader';
|
||||
import TetherLayer from './TetherLayer';
|
||||
import TetherLayerHost from './TetherLayerHost';
|
||||
import DashboardCardLayer from './DashboardCardLayer';
|
||||
import DashboardOverlays from './DashboardOverlays';
|
||||
import CardContextMenu from '../desktop/CardContextMenu';
|
||||
@@ -30,7 +30,7 @@ import type { CardType, useDashboardSelection } from '../hooks/state/useDashboar
|
||||
import type { useCanvasControls } from '../hooks/interaction/useCanvasControls';
|
||||
import { useWebviewSuspend } from '../hooks/interaction/useWebviewSuspend';
|
||||
import { deleteSelectedCards } from '../hooks/interaction/deleteSelectedCards';
|
||||
import type { Tether } from '../geometry/dashboardTethers';
|
||||
import type { TetherInputs } from '../geometry/dashboardTethers';
|
||||
|
||||
type Selection = ReturnType<typeof useDashboardSelection>;
|
||||
type Canvas = ReturnType<typeof useCanvasControls>;
|
||||
@@ -56,7 +56,7 @@ interface DashboardCanvasProps {
|
||||
outputs: Record<string, Output>;
|
||||
glowingAgentCards: Record<string, GlowingAgentCard>;
|
||||
expandedSessionIds: string[];
|
||||
tethers: Tether[];
|
||||
tetherInputs: TetherInputs;
|
||||
highlightedCardId: string | null;
|
||||
autoFocusSessionId: string | null;
|
||||
focusedCardId: string | null;
|
||||
@@ -118,7 +118,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
outputs,
|
||||
glowingAgentCards,
|
||||
expandedSessionIds,
|
||||
tethers,
|
||||
tetherInputs,
|
||||
highlightedCardId,
|
||||
autoFocusSessionId,
|
||||
focusedCardId,
|
||||
@@ -407,8 +407,8 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Tether lines between branched cards */}
|
||||
<TetherLayer tethers={tethers} c={c} />
|
||||
{/* Tether lines between branched cards; the host alone re-renders on drag frames */}
|
||||
<TetherLayerHost inputs={tetherInputs} c={c} />
|
||||
<DashboardCardLayer
|
||||
dashboardId={dashboardId}
|
||||
cards={cards}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import { useTethers, type TetherInputs, type LiveDragInfo } from '../geometry/dashboardTethers';
|
||||
import { subscribeLiveDrag } from '../hooks/interaction/liveDragChannel';
|
||||
import TetherLayer from './TetherLayer';
|
||||
|
||||
// The ONE component that re-renders per drag frame: it subscribes to the live-drag channel and
|
||||
// recomputes just the tether SVG, so a 120Hz card drag costs a handful of paths instead of the
|
||||
// whole dashboard tree (the ENG-88 input delay).
|
||||
const TetherLayerHost: React.FC<{ inputs: TetherInputs; c: ClaudeTokens }> = ({ inputs, c }) => {
|
||||
const [liveDrag, setLiveDrag] = useState<LiveDragInfo | null>(null);
|
||||
useEffect(() => subscribeLiveDrag(setLiveDrag), []);
|
||||
const tethers = useTethers(inputs, liveDrag);
|
||||
return <TetherLayer tethers={tethers} c={c} />;
|
||||
};
|
||||
|
||||
export default React.memo(TetherLayerHost);
|
||||
@@ -29,7 +29,7 @@ interface GlowingBrowserCard {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface LiveDragInfo {
|
||||
export interface LiveDragInfo {
|
||||
cardId: string;
|
||||
dx: number;
|
||||
dy: number;
|
||||
@@ -73,7 +73,7 @@ function rectCenter(r: CanvasRect): { x: number; y: number } {
|
||||
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
||||
}
|
||||
|
||||
interface UseTethersArgs {
|
||||
export interface TetherInputs {
|
||||
glowingAgentCards: Record<string, GlowingAgentCard>;
|
||||
glowingBrowserCards: Record<string, GlowingBrowserCard>;
|
||||
cards: Record<string, CardPosition>;
|
||||
@@ -84,7 +84,6 @@ interface UseTethersArgs {
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
outputs: Record<string, Output>;
|
||||
expandedSessionIds: string[];
|
||||
liveDragInfo: LiveDragInfo | null;
|
||||
measuredHeightsRef: RefObject<Record<string, number>>;
|
||||
measuredHeightsTick: number;
|
||||
sessionList: AgentSession[];
|
||||
@@ -106,7 +105,6 @@ export function useTethers({
|
||||
viewCards,
|
||||
outputs,
|
||||
expandedSessionIds,
|
||||
liveDragInfo,
|
||||
measuredHeightsRef,
|
||||
measuredHeightsTick,
|
||||
sessionList,
|
||||
@@ -114,7 +112,7 @@ export function useTethers({
|
||||
workflowsMonitorCard,
|
||||
workflowsMonitorLabel,
|
||||
monitorRunSessionId,
|
||||
}: UseTethersArgs): Tether[] {
|
||||
}: TetherInputs, liveDragInfo: LiveDragInfo | null): Tether[] {
|
||||
return useMemo(() => {
|
||||
const sessionById = new Map(sessionList.map((s) => [s.id, s]));
|
||||
const expandedSet = new Set(expandedSessionIds);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Per-frame drag deltas travel OUTSIDE React: a drag move fires at pointer rate (120Hz on ProMotion),
|
||||
// and holding this in page-level state re-rendered the whole dashboard per frame (the ENG-88 input
|
||||
// delay). Publishers write here; the one consumer that must follow live (the tether layer) subscribes
|
||||
// and re-renders alone.
|
||||
|
||||
import type { LiveDragInfo } from '../../geometry/dashboardTethers';
|
||||
|
||||
type Listener = (info: LiveDragInfo | null) => void;
|
||||
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
export function publishLiveDrag(info: LiveDragInfo | null): void {
|
||||
for (const listener of listeners) listener(info);
|
||||
}
|
||||
|
||||
export function subscribeLiveDrag(listener: Listener): () => void {
|
||||
listeners.add(listener);
|
||||
return () => { listeners.delete(listener); };
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useAppDispatch } from '@/shared/hooks';
|
||||
import { moveCards } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
|
||||
import type { CanvasActions } from './useCanvasControls';
|
||||
import { publishLiveDrag } from './liveDragChannel';
|
||||
|
||||
type Selection = ReturnType<typeof useDashboardSelection>;
|
||||
|
||||
@@ -33,7 +34,6 @@ export function useCardDrag({
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [multiDragDelta, setMultiDragDelta] = useState<{ dx: number; dy: number } | null>(null);
|
||||
const [liveDragInfo, setLiveDragInfo] = useState<{ cardId: string; dx: number; dy: number } | null>(null);
|
||||
const activeDragCardRef = useRef<string | null>(null);
|
||||
const isMultiDragRef = useRef(false);
|
||||
|
||||
@@ -100,7 +100,7 @@ export function useCardDrag({
|
||||
setMultiDragDelta({ dx, dy });
|
||||
}
|
||||
if (activeDragCardRef.current) {
|
||||
setLiveDragInfo({ cardId: activeDragCardRef.current, dx, dy });
|
||||
publishLiveDrag({ cardId: activeDragCardRef.current, dx, dy });
|
||||
}
|
||||
}, [tickEdgePan]);
|
||||
|
||||
@@ -112,7 +112,7 @@ export function useCardDrag({
|
||||
document.body.classList.remove('dashboard-marquee-active');
|
||||
isMultiDragRef.current = false;
|
||||
setMultiDragDelta(null);
|
||||
setLiveDragInfo(null);
|
||||
publishLiveDrag(null);
|
||||
}, [stopEdgePan, canvasActions]);
|
||||
|
||||
const handleCardDragEnd = useCallback((dx: number, dy: number, didDrag: boolean) => {
|
||||
@@ -143,7 +143,6 @@ export function useCardDrag({
|
||||
|
||||
return {
|
||||
multiDragDelta,
|
||||
liveDragInfo,
|
||||
handleCardDragStart,
|
||||
handleCardDragMove,
|
||||
handleCardDragEnd,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getCardRect } from '../../geometry/getCardRect';
|
||||
import { computeContentBounds } from '../../geometry/contentBounds';
|
||||
import { useDashboardUiState } from './useDashboardUiState';
|
||||
import { useLayoutSave } from './useLayoutSave';
|
||||
import { useTethers } from '../../geometry/dashboardTethers';
|
||||
import type { TetherInputs } from '../../geometry/dashboardTethers';
|
||||
import { useArrowNav } from '../interaction/useArrowNav';
|
||||
import { useDashboardShortcuts } from '../interaction/useDashboardShortcuts';
|
||||
import { useDashboardClipboard } from '../interaction/useDashboardClipboard';
|
||||
@@ -105,7 +105,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
|
||||
const {
|
||||
multiDragDelta,
|
||||
liveDragInfo,
|
||||
handleCardDragStart,
|
||||
handleCardDragMove,
|
||||
handleCardDragEnd,
|
||||
@@ -305,7 +304,9 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
measuredHeightsTick,
|
||||
});
|
||||
|
||||
const tethers = useTethers({
|
||||
// Bundled for the canvas's TetherLayerHost, which re-renders ALONE on drag frames; holding drag
|
||||
// state here re-rendered the whole page per pointer move (the ENG-88 input delay).
|
||||
const tetherInputs = useMemo<TetherInputs>(() => ({
|
||||
glowingAgentCards,
|
||||
glowingBrowserCards,
|
||||
cards,
|
||||
@@ -316,7 +317,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
viewCards,
|
||||
outputs,
|
||||
expandedSessionIds,
|
||||
liveDragInfo,
|
||||
measuredHeightsRef,
|
||||
measuredHeightsTick,
|
||||
sessionList,
|
||||
@@ -324,13 +324,13 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
workflowsMonitorCard,
|
||||
workflowsMonitorLabel,
|
||||
monitorRunSessionId,
|
||||
});
|
||||
}), [glowingAgentCards, glowingBrowserCards, cards, browserCards, workflowCards, workflowItems, workflowOpenCards, viewCards, outputs, expandedSessionIds, measuredHeightsRef, measuredHeightsTick, sessionList, workflowsHub, workflowsMonitorCard, workflowsMonitorLabel, monitorRunSessionId]);
|
||||
|
||||
return {
|
||||
c, dashboardId, dashboardName, canvas, selection, sessions, sessionList,
|
||||
cards, viewCards, browserCards, keepAliveBrowserCards, outputs, glowingAgentCards,
|
||||
workflowCards, workflowsHub,
|
||||
expandedSessionIds, tethers, highlightedCardId, autoFocusSessionId,
|
||||
expandedSessionIds, tetherInputs, highlightedCardId, autoFocusSessionId,
|
||||
focusedCardId, multiDragDelta, shakeDirection,
|
||||
neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce, canvasEmpty,
|
||||
toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState,
|
||||
|
||||
Reference in New Issue
Block a user