[aidan] fix: remove duplicate logic

This commit is contained in:
abccodes
2026-06-15 18:13:51 -07:00
parent f70bdfc1a6
commit c4bc7cf616
3 changed files with 88 additions and 127 deletions
@@ -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 {
+12 -13
View File
@@ -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],