From 4e9bd406a71a1b7928f72e099efc9079a1f986fc Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 15 Jul 2026 16:14:14 -0700 Subject: [PATCH] [eric] dashboard: spawn lands in the closest open gap to viewport center; spawn-framing zoom capped 1.15 -> 0.75 --- .../pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts | 3 ++- .../hooks/lifecycle/useDashboardCardActions.ts | 7 ++++--- .../hooks/lifecycle/useDashboardLifecycle.ts | 9 +++++---- frontend/src/shared/state/dashboardLayoutSlice.ts | 12 ++++++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts index 08d71662..80cd39d1 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts @@ -21,6 +21,7 @@ import { EXPANDED_CARD_MIN_H, GRID_GAP, type CardPosition, + SPAWN_FOCUS_MAX_ZOOM, } from '@/shared/state/dashboardLayoutSlice'; import { generateDashboardName } from '@/shared/state/dashboardsSlice'; import type { ContextPath } from '@/app/components/editor/DirectoryBrowser'; @@ -198,7 +199,7 @@ export function useAgentSpawn({ if (bc) rects.push({ x: bc.x, y: bc.y, width: bc.width, height: bc.height }); } } - canvasActions.fitToCards(rects, 1.15, true, undefined, true); + canvasActions.fitToCards(rects, SPAWN_FOCUS_MAX_ZOOM, true, undefined, true); handleHighlightCard(draftId); } diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts index 89d743e4..40650069 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts @@ -16,6 +16,7 @@ import { DEFAULT_NOTE_W, DEFAULT_NOTE_H, EXPANDED_CARD_MIN_H, + SPAWN_FOCUS_MAX_ZOOM, } from '@/shared/state/dashboardLayoutSlice'; import type { CardType, useDashboardSelection } from '../state/useDashboardSelection'; import type { CanvasActions } from '../interaction/useCanvasControls'; @@ -65,7 +66,7 @@ export function useDashboardCardActions({ } const card = viewCards[focusKey]; if (card) { - canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true, undefined, true); + canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true, undefined, true); handleHighlightCard(focusKey); } }, 200); @@ -88,7 +89,7 @@ export function useDashboardCardActions({ const newId = Object.keys(allNotes).find((id) => !prevIds.has(id)); if (newId) { const note = allNotes[newId]; - canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], 1.15, true, undefined, true); + canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], SPAWN_FOCUS_MAX_ZOOM, true, undefined, true); handleHighlightCard(newId); } }, 200); @@ -109,7 +110,7 @@ export function useDashboardCardActions({ setTimeout(() => { const card = store.getState().dashboardLayout.cards[sessionId]; if (card) { - canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true); + canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true); handleHighlightCard(sessionId); } }, 200); diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts index 1db5dfd4..3e913849 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts @@ -20,6 +20,7 @@ import { clearPendingFocusWorkflowId, clearPendingFocusWorkflowsHub, type ViewCardPosition, + SPAWN_FOCUS_MAX_ZOOM, } from '@/shared/state/dashboardLayoutSlice'; import { fetchOutputs, type Output } from '@/shared/state/outputsSlice'; import { generateDashboardName } from '@/shared/state/dashboardsSlice'; @@ -215,7 +216,7 @@ export function useDashboardLifecycle({ setTimeout(() => { const card = store.getState().dashboardLayout.cards[agentId]; if (card) { - canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true); + canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true); handleHighlightCard(agentId); } }, 350); @@ -253,7 +254,7 @@ export function useDashboardLifecycle({ setTimeout(() => { const card = store.getState().dashboardLayout.viewCards[cardKey]; if (card) { - canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true); + canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true); handleHighlightCard(cardKey); } }, 200); @@ -270,7 +271,7 @@ export function useDashboardLifecycle({ if (card) { canvasActions.fitToCards( [{ x: card.x, y: card.y, width: card.width, height: card.height }], - 1.15, + SPAWN_FOCUS_MAX_ZOOM, true, ); handleHighlightCard(workflowId); @@ -352,7 +353,7 @@ export function useDashboardLifecycle({ const rects = [{ x: vc.x, y: vc.y, width: vc.width, height: vc.height }]; const ac = store.getState().dashboardLayout.cards[sid]; if (ac) rects.push({ x: ac.x, y: ac.y, width: ac.width, height: ac.height }); - canvasActions.fitToCards(rects, 1.15, true); + canvasActions.fitToCards(rects, SPAWN_FOCUS_MAX_ZOOM, true); handleHighlightCard(outputId); }, 200); } diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index 18ff1735..d72f9370 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -26,6 +26,8 @@ export const DEFAULT_WORKFLOWS_HUB_W = DEFAULT_BROWSER_CARD_W; export const DEFAULT_WORKFLOWS_HUB_H = DEFAULT_BROWSER_CARD_H; export const EXPANDED_CARD_MIN_H = 620; export const GRID_GAP = 24; +// Zoom ceiling when the camera frames a freshly spawned card. 1.15 slammed the user to 115% on every spawn; 0.75 keeps the new card readable without the lurch-into-your-face zoom. +export const SPAWN_FOCUS_MAX_ZOOM = 0.75; // Gap between the Workflows window and the cards it spawns (run monitor, that monitor's browser). Keeps the hub -> monitor -> browser row evenly spaced. export const WORKFLOW_CARD_GAP = 140; const GRID_ORIGIN = { x: 40, y: 100 }; @@ -505,8 +507,14 @@ export function computeSpawnPosition( return placeBesideCard(state, anchor.beside, newW, newH, expandedSessionIds); } if (anchor.viewportCenter) { - // Land dead-center, "in front of you", even if a card is already there. Overlap is intentional (new card sits on top via its higher zOrder); dodging to free space is exactly the "spawned off to the side" behavior we're removing. - return { x: anchor.viewportCenter.x - newW / 2, y: anchor.viewportCenter.y - newH / 2 }; + // Closest open gap to the viewport center: dead-center-with-overlap stacked spawns invisibly on top of each other (two center spawns in a row = the second fully covers the first). The spiral stays center-biased so it still reads as "in front of you". + return findOpenSpotNear( + anchor.viewportCenter.x - newW / 2, + anchor.viewportCenter.y - newH / 2, + collectOccupiedRects(state, expandedSessionIds), + newW, + newH, + ); } return findOpenGridCell(collectOccupiedRects(state, expandedSessionIds), newW, newH); }