diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts index d42ca6f1..08d71662 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts @@ -13,7 +13,6 @@ import { import { placeCard, removeCard, - setCardPosition, setGlowingAgentCard, setGlowingBrowserCards, clearGlowingBrowserCards, @@ -176,9 +175,8 @@ export function useAgentSpawn({ const cardHeight = expandNewChats ? EXPANDED_CARD_MIN_H : DEFAULT_CARD_H; const anchorX = browserAnchor ? browserAnchor.x - DEFAULT_CARD_W - GRID_GAP * 12 : spawnPos.x; const anchorY = browserAnchor ? browserAnchor.y : spawnPos.y; - dispatch(placeCard({ sessionId: draftId, x: anchorX, y: anchorY, width: DEFAULT_CARD_W, height: cardHeight, expandedSessionIds })); - // placeCard grid-snaps + collision-dodges, drifting the card off the resolved spawn point. Pin it back for the viewport-center / beside-card cases so it lands dead-center as intended; the single-browser dock keeps the dodge so it cascades off an occupied slot. - if (!browserAnchor) dispatch(setCardPosition({ sessionId: draftId, x: anchorX, y: anchorY })); + // exact pins the resolved spawn point for the viewport-center / beside-card cases (dead-center, overlap allowed); the single-browser dock omits it so placeCard's collision-dodge cascades the chat off an occupied slot. + dispatch(placeCard({ sessionId: draftId, x: anchorX, y: anchorY, width: DEFAULT_CARD_W, height: cardHeight, expandedSessionIds, exact: !browserAnchor })); if (expandNewChats) { dispatch(expandSession(draftId)); setAutoFocusSessionId(draftId); diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index 605c785d..85364346 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -545,11 +545,12 @@ const dashboardLayoutSlice = createSlice({ height: number; // Optional: which existing sessions are currently expanded (showing their full chat history). Without this, the collision check uses each card's STORED height, which is the collapsed value, even when the card is currently rendering at the expanded ~620px. Result: new sub-agent cards spawn into the collapsed footprint but overlap the visually expanded one. Caller (Dashboard.tsx) passes the current expanded set so the collision math matches what the user actually sees. expandedSessionIds?: string[]; + // Honor the given x/y verbatim (dead-center "in front of you", overlap allowed) instead of collision-dodging to a free grid cell. Set when the caller already resolved the spot (viewport center / beside a selected card) and dodging would defeat that; tidyLayout cleans up any overlap on demand. + exact?: boolean; }> ) { - const { sessionId, x, y, width, height, expandedSessionIds } = action.payload; - const rects = collectOccupiedRects(state, expandedSessionIds); - const pos = findOpenSpotNear(x, y, rects, width, height); + const { sessionId, x, y, width, height, expandedSessionIds, exact } = action.payload; + const pos = exact ? { x, y } : findOpenSpotNear(x, y, collectOccupiedRects(state, expandedSessionIds), width, height); state.cards[sessionId] = { session_id: sessionId, x: pos.x,