mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[aidan] fix: remove duplicate logic
This commit is contained in:
@@ -123,21 +123,25 @@ export function useTethers({
|
||||
};
|
||||
}).filter(Boolean) as Tether[];
|
||||
|
||||
function browserTether(
|
||||
browserId: string,
|
||||
// One tether builder for both browser and view cards: the anchor-pairing
|
||||
// and elbow/vertical path are identical; only the destination card map and
|
||||
// the key prefix differ, so the resolved dst card is passed in.
|
||||
function cardTether(
|
||||
dst: { x: number; y: number; width: number; height: number } | undefined,
|
||||
dstId: string,
|
||||
sourceId: string,
|
||||
fading: boolean,
|
||||
key: string,
|
||||
label: string,
|
||||
fading: boolean,
|
||||
): Tether | null {
|
||||
const src = cards[sourceId];
|
||||
const dst = browserCards[browserId];
|
||||
if (!src || !dst) return null;
|
||||
|
||||
let srcX = src.x, srcY = src.y;
|
||||
let dstX = dst.x, dstY = dst.y;
|
||||
if (liveDragInfo) {
|
||||
if (liveDragInfo.cardId === sourceId) { srcX += liveDragInfo.dx; srcY += liveDragInfo.dy; }
|
||||
if (liveDragInfo.cardId === browserId) { dstX += liveDragInfo.dx; dstY += liveDragInfo.dy; }
|
||||
if (liveDragInfo.cardId === dstId) { dstX += liveDragInfo.dx; dstY += liveDragInfo.dy; }
|
||||
}
|
||||
|
||||
const srcMeasured = measuredHeightsRef.current![sourceId];
|
||||
@@ -205,7 +209,7 @@ export function useTethers({
|
||||
const labelY = isVertical ? midY + (y2 - midY) * 0.15 : y2;
|
||||
|
||||
return {
|
||||
key: `browser-${browserId}`,
|
||||
key,
|
||||
path: pathD,
|
||||
labelX,
|
||||
labelY,
|
||||
@@ -214,9 +218,16 @@ export function useTethers({
|
||||
};
|
||||
}
|
||||
|
||||
const glowTethers = new Map<string, ReturnType<typeof browserTether>>();
|
||||
const glowTethers = new Map<string, ReturnType<typeof cardTether>>();
|
||||
for (const [browserId, { sourceId, fading, label }] of Object.entries(glowingBrowserCards)) {
|
||||
const t = browserTether(browserId, sourceId, fading, label || '');
|
||||
const t = cardTether(
|
||||
browserCards[browserId],
|
||||
browserId,
|
||||
sourceId,
|
||||
`browser-${browserId}`,
|
||||
label || '',
|
||||
fading,
|
||||
);
|
||||
if (t) glowTethers.set(browserId, t);
|
||||
}
|
||||
|
||||
@@ -225,105 +236,19 @@ export function useTethers({
|
||||
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;
|
||||
const t = browserTether(s.browser_id, s.parent_session_id, false, '');
|
||||
const t = cardTether(
|
||||
browserCards[s.browser_id],
|
||||
s.browser_id,
|
||||
s.parent_session_id,
|
||||
`browser-${s.browser_id}`,
|
||||
'',
|
||||
false,
|
||||
);
|
||||
if (t) glowTethers.set(s.browser_id, t);
|
||||
}
|
||||
|
||||
const browserTethers = Array.from(glowTethers.values()).filter(Boolean) as Tether[];
|
||||
|
||||
// Edit tether: same anchor-pairing as browserTether (chat <-> view card).
|
||||
// Used to visually link an App Builder chat to the running app while the
|
||||
// session is actively editing.
|
||||
function viewTether(
|
||||
outputId: string,
|
||||
sourceId: string,
|
||||
label: string,
|
||||
): Tether | null {
|
||||
const src = cards[sourceId];
|
||||
const dst = viewCards[outputId];
|
||||
if (!src || !dst) return null;
|
||||
|
||||
let srcX = src.x, srcY = src.y;
|
||||
let dstX = dst.x, dstY = dst.y;
|
||||
if (liveDragInfo) {
|
||||
if (liveDragInfo.cardId === sourceId) { srcX += liveDragInfo.dx; srcY += liveDragInfo.dy; }
|
||||
if (liveDragInfo.cardId === outputId) { dstX += liveDragInfo.dx; dstY += liveDragInfo.dy; }
|
||||
}
|
||||
|
||||
const srcMeasured = measuredHeightsRef.current![sourceId];
|
||||
const srcH = srcMeasured ?? (expandedSessionIds.includes(sourceId)
|
||||
? Math.max(EXPANDED_CARD_MIN_H, src.height)
|
||||
: src.height);
|
||||
const dstH = dst.height;
|
||||
|
||||
const srcCx = srcX + src.width / 2;
|
||||
const dstCx = dstX + dst.width / 2;
|
||||
|
||||
const srcAnchors: Anchor[] = [
|
||||
{ x: srcX + src.width, y: srcY + srcH * 0.54, side: 'right' },
|
||||
{ x: srcX, y: srcY + srcH * 0.54, side: 'left' },
|
||||
{ x: srcCx, y: srcY, side: 'top' },
|
||||
{ x: srcCx, y: srcY + srcH, side: 'bottom' },
|
||||
];
|
||||
const dstAnchors: Anchor[] = [
|
||||
{ x: dstX, y: dstY + dstH * 0.54, side: 'left' },
|
||||
{ x: dstX + dst.width, y: dstY + dstH * 0.54, side: 'right' },
|
||||
{ x: dstCx, y: dstY, side: 'top' },
|
||||
{ x: dstCx, y: dstY + dstH, side: 'bottom' },
|
||||
];
|
||||
|
||||
let bestSrc = srcAnchors[0], bestDst = dstAnchors[0];
|
||||
let bestDist = Infinity;
|
||||
for (const sa of srcAnchors) {
|
||||
for (const da of dstAnchors) {
|
||||
const d = Math.hypot(sa.x - da.x, sa.y - da.y);
|
||||
if (d < bestDist) { bestDist = d; bestSrc = sa; bestDst = da; }
|
||||
}
|
||||
}
|
||||
|
||||
const x1 = bestSrc.x, y1 = bestSrc.y;
|
||||
const x2 = bestDst.x, y2 = bestDst.y;
|
||||
|
||||
const isVertical = (bestSrc.side === 'top' || bestSrc.side === 'bottom')
|
||||
&& (bestDst.side === 'top' || bestDst.side === 'bottom');
|
||||
|
||||
let pathD: string;
|
||||
if (isVertical) {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const midY = y1 + dy / 2;
|
||||
const r = (Math.abs(dx) < 1 || Math.abs(dy) < ELBOW_RADIUS * 2)
|
||||
? 0
|
||||
: Math.min(ELBOW_RADIUS, Math.abs(dx) / 2, Math.abs(dy) / 4);
|
||||
const sx = dx >= 0 ? 1 : -1;
|
||||
const sy = dy >= 0 ? 1 : -1;
|
||||
pathD = [
|
||||
`M ${x1},${y1}`,
|
||||
`V ${midY - sy * r}`,
|
||||
`Q ${x1},${midY} ${x1 + sx * r},${midY}`,
|
||||
`H ${x2 - sx * r}`,
|
||||
`Q ${x2},${midY} ${x2},${midY + sy * r}`,
|
||||
`V ${y2}`,
|
||||
].join(' ');
|
||||
} else {
|
||||
pathD = elbowPath(x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
const midX = x1 + (x2 - x1) / 2;
|
||||
const midY = y1 + (y2 - y1) / 2;
|
||||
const labelX = isVertical ? midX : midX + (x2 - midX) * 0.15;
|
||||
const labelY = isVertical ? midY + (y2 - midY) * 0.15 : y2;
|
||||
|
||||
return {
|
||||
key: `view-${outputId}`,
|
||||
path: pathD,
|
||||
labelX,
|
||||
labelY,
|
||||
label,
|
||||
fading: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Index outputs by their owning session so the per-session lookup below
|
||||
// doesn't scan the whole outputs map for every view-builder chat.
|
||||
const outputsBySession = new Map<string, string[]>();
|
||||
@@ -341,7 +266,14 @@ export function useTethers({
|
||||
if (!outIds) continue;
|
||||
for (const outputId of outIds) {
|
||||
if (!viewCards[outputId]) continue;
|
||||
const t = viewTether(outputId, s.id, 'Editing');
|
||||
const t = cardTether(
|
||||
viewCards[outputId],
|
||||
outputId,
|
||||
s.id,
|
||||
`view-${outputId}`,
|
||||
'Editing',
|
||||
false,
|
||||
);
|
||||
if (t) viewTethers.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,11 @@ interface Rect {
|
||||
h: number;
|
||||
}
|
||||
|
||||
interface CardPlacementExclusion {
|
||||
type: CardType;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function rectsOverlap(a: Rect, b: Rect): boolean {
|
||||
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
||||
}
|
||||
@@ -203,20 +208,25 @@ function rectsOverlap(a: Rect, b: Rect): boolean {
|
||||
function collectOccupiedRects(
|
||||
state: DashboardLayoutState,
|
||||
expandedSessionIds?: string[],
|
||||
exclude?: CardPlacementExclusion,
|
||||
): Rect[] {
|
||||
const expanded = new Set(expandedSessionIds);
|
||||
const rects: Rect[] = [];
|
||||
for (const c of Object.values(state.cards)) {
|
||||
if (exclude?.type === 'agent' && exclude.id === c.session_id) continue;
|
||||
const h = expanded.has(c.session_id) ? Math.max(EXPANDED_CARD_MIN_H, c.height) : c.height;
|
||||
rects.push({ x: c.x, y: c.y, w: c.width, h });
|
||||
}
|
||||
for (const c of Object.values(state.viewCards)) {
|
||||
if (exclude?.type === 'view' && exclude.id === c.output_id) continue;
|
||||
rects.push({ x: c.x, y: c.y, w: c.width, h: c.height });
|
||||
}
|
||||
for (const c of Object.values(state.browserCards)) {
|
||||
if (exclude?.type === 'browser' && exclude.id === c.browser_id) continue;
|
||||
rects.push({ x: c.x, y: c.y, w: c.width, h: c.height });
|
||||
}
|
||||
for (const n of Object.values(state.notes)) {
|
||||
if (exclude?.type === 'note' && exclude.id === n.note_id) continue;
|
||||
rects.push({ x: n.x, y: n.y, w: n.width, h: n.height });
|
||||
}
|
||||
return rects;
|
||||
@@ -309,6 +319,36 @@ export function findOpenSpotNear(
|
||||
return findOpenGridCell(occupiedRects, newW, newH);
|
||||
}
|
||||
|
||||
export function placeInParentColumn(
|
||||
state: DashboardLayoutState,
|
||||
parentSessionId: string | null | undefined,
|
||||
newW: number,
|
||||
newH: number,
|
||||
expandedSessionIds?: string[],
|
||||
exclude?: CardPlacementExclusion,
|
||||
): { x: number; y: number } {
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds, exclude);
|
||||
const parentCard = parentSessionId ? state.cards[parentSessionId] : null;
|
||||
if (!parentCard) {
|
||||
return findOpenGridCell(rects, newW, newH);
|
||||
}
|
||||
|
||||
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
|
||||
const columnCards = [
|
||||
...Object.values(state.browserCards).filter(
|
||||
(c) => !(exclude?.type === 'browser' && exclude.id === c.browser_id),
|
||||
),
|
||||
...Object.values(state.viewCards).filter(
|
||||
(c) => !(exclude?.type === 'view' && exclude.id === c.output_id),
|
||||
),
|
||||
].filter((c) => Math.abs(c.x - targetX) < 50);
|
||||
const targetY = columnCards.length > 0
|
||||
? Math.max(...columnCards.map((c) => c.y + c.height)) + GRID_GAP
|
||||
: parentCard.y;
|
||||
|
||||
return findOpenSpotNear(targetX, targetY, rects, newW, newH);
|
||||
}
|
||||
|
||||
// Reconnect-refetch merge: ADD only the cards the snapshot carries that the
|
||||
// client is missing (e.g. a spawned browser whose broadcast was lost in a
|
||||
// socket gap), collision-resolving each against the live layout so a recovered
|
||||
@@ -533,17 +573,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
} else {
|
||||
const parentCard = parentSessionId ? state.cards[parentSessionId] : null;
|
||||
if (parentCard) {
|
||||
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
|
||||
let targetY = parentCard.y;
|
||||
const siblings = [
|
||||
...Object.values(state.browserCards),
|
||||
...Object.values(state.viewCards),
|
||||
].filter((c) => Math.abs(c.x - targetX) < 50);
|
||||
if (siblings.length > 0) {
|
||||
targetY = Math.max(...siblings.map((c) => c.y + c.height)) + GRID_GAP;
|
||||
}
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds);
|
||||
const pos = findOpenSpotNear(targetX, targetY, rects, w, h);
|
||||
const pos = placeInParentColumn(state, parentSessionId, w, h, expandedSessionIds);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
clearTurnLabel,
|
||||
} from '../state/agentsSlice';
|
||||
import { streamStart, streamDelta, streamEnd, clearStreamingForSession } from '../state/streamingSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, setBrowserCardPosition, setGlowingBrowserCards, GRID_GAP } from '../state/dashboardLayoutSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, placeInParentColumn, setBrowserCardPosition, setGlowingBrowserCards } from '../state/dashboardLayoutSlice';
|
||||
import { upsertOutput } from '../state/outputsSlice';
|
||||
import { displaySessionName } from '../state/sessionDisplay';
|
||||
import { getAuthToken } from '../config';
|
||||
@@ -756,21 +756,20 @@ class WebSocketManager {
|
||||
const parentId = data.parent_session_id;
|
||||
if (parentId) {
|
||||
const layoutState = store.getState().dashboardLayout;
|
||||
const parentCard = layoutState.cards[parentId];
|
||||
if (parentCard) {
|
||||
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
|
||||
let targetY = parentCard.y;
|
||||
const columnCards = Object.values(layoutState.browserCards).filter(
|
||||
(c) => Math.abs(c.x - targetX) < 50 && c.browser_id !== data.browser_card.browser_id,
|
||||
const browserCard = layoutState.browserCards[data.browser_card.browser_id];
|
||||
if (layoutState.cards[parentId] && browserCard) {
|
||||
const pos = placeInParentColumn(
|
||||
layoutState,
|
||||
parentId,
|
||||
browserCard.width,
|
||||
browserCard.height,
|
||||
undefined,
|
||||
{ type: 'browser', id: browserCard.browser_id },
|
||||
);
|
||||
if (columnCards.length > 0) {
|
||||
const lowestBottom = Math.max(...columnCards.map((c) => c.y + c.height));
|
||||
targetY = lowestBottom + GRID_GAP;
|
||||
}
|
||||
store.dispatch(setBrowserCardPosition({
|
||||
browserId: data.browser_card.browser_id,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
}));
|
||||
store.dispatch(setGlowingBrowserCards({
|
||||
browserIds: [data.browser_card.browser_id],
|
||||
|
||||
Reference in New Issue
Block a user