diff --git a/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts b/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts index ec5a1e22..9b5c9624 100644 --- a/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts +++ b/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts @@ -55,6 +55,7 @@ export function elbowPath(x1: number, y1: number, x2: number, y2: number): strin } type Anchor = { x: number; y: number; side: 'left' | 'right' | 'top' | 'bottom' }; +type CanvasRect = { x: number; y: number; width: number; height: number }; // Where the ray from a rect's center toward (tx,ty) crosses the rect border. // Pins a tether endpoint to the card edge facing the other card, so it can @@ -69,6 +70,39 @@ function borderPoint(x: number, y: number, w: number, h: number, tx: number, ty: return { x: cx + dx * scale, y: cy + dy * scale }; } +function rectCenter(r: CanvasRect): { x: number; y: number } { + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; +} + +function selectCardElement(content: HTMLElement, type: 'agent-card' | 'workflow-card', id: string): HTMLElement | null { + const candidates = content.querySelectorAll(`[data-select-type="${type}"]`); + for (const el of Array.from(candidates)) { + if (el.dataset.selectId === id) return el; + } + return null; +} + +function measuredCanvasRect( + contentRef: RefObject, + zoom: number, + type: 'agent-card' | 'workflow-card', + id: string, +): CanvasRect | null { + const content = contentRef.current; + if (!content) return null; + const el = selectCardElement(content, type, id); + if (!el) return null; + const contentRect = content.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + const z = zoom || 1; + return { + x: (elRect.left - contentRect.left) / z, + y: (elRect.top - contentRect.top) / z, + width: elRect.width / z, + height: elRect.height / z, + }; +} + interface UseTethersArgs { glowingAgentCards: Record; glowingBrowserCards: Record; @@ -82,6 +116,8 @@ interface UseTethersArgs { liveDragInfo: LiveDragInfo | null; measuredHeightsRef: RefObject>; measuredHeightsTick: number; + contentRef: RefObject; + zoom: number; sessionList: AgentSession[]; } @@ -98,6 +134,8 @@ export function useTethers({ liveDragInfo, measuredHeightsRef, measuredHeightsTick, + contentRef, + zoom, sessionList, }: UseTethersArgs): Tether[] { return useMemo(() => { @@ -357,10 +395,14 @@ export function useTethers({ ? Math.max(EXPANDED_CARD_MIN_H, sidecar.height) : sidecar.height); const wcH = wfHeight(wc); - const srcCx = srcX + wc.width / 2, srcCy = srcY + wcH / 2; - const dstCx = dstX + sidecar.width / 2, dstCy = dstY + dstH / 2; - const a = borderPoint(srcX, srcY, wc.width, wcH, dstCx, dstCy); - const b = borderPoint(dstX, dstY, sidecar.width, dstH, srcCx, srcCy); + const measuredWorkflow = measuredCanvasRect(contentRef, zoom, 'workflow-card', wc.workflow_id); + const measuredSidecar = measuredCanvasRect(contentRef, zoom, 'agent-card', sidecarId); + const workflowRect = measuredWorkflow ?? { x: srcX, y: srcY, width: wc.width, height: wcH }; + const sidecarRect = measuredSidecar ?? { x: dstX, y: dstY, width: sidecar.width, height: dstH }; + const srcCenter = rectCenter(workflowRect); + const dstCenter = rectCenter(sidecarRect); + const a = borderPoint(workflowRect.x, workflowRect.y, workflowRect.width, workflowRect.height, dstCenter.x, dstCenter.y); + const b = borderPoint(sidecarRect.x, sidecarRect.y, sidecarRect.width, sidecarRect.height, srcCenter.x, srcCenter.y); const x1 = a.x, y1 = a.y; const x2 = b.x, y2 = b.y; const pathD = elbowPath(x1, y1, x2, y2); @@ -429,5 +471,5 @@ export function useTethers({ // measuredHeightsTick re-runs the memo once ResizeObserver reports a new // height after a collapse (the ref read is invisible to the dep checker). // eslint-disable-next-line react-hooks/exhaustive-deps - }, [glowingAgentCards, glowingBrowserCards, cards, browserCards, workflowCards, workflowItems, workflowOpenCards, configurePanels, expandedSessionIds, liveDragInfo, measuredHeightsTick, sessionList]); + }, [glowingAgentCards, glowingBrowserCards, cards, browserCards, workflowCards, workflowItems, workflowOpenCards, configurePanels, expandedSessionIds, liveDragInfo, measuredHeightsTick, contentRef, zoom, sessionList]); } diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index e37e4233..33e57949 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -288,6 +288,8 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { liveDragInfo, measuredHeightsRef, measuredHeightsTick, + contentRef: canvas.contentRef, + zoom: canvas.zoom, sessionList, });