[eric] browser: fix the REAL bleed (fit-to-view chased the off-screen keep-alive card); restore full keep-alive so Discord stays logged in across dashboard switches

This commit is contained in:
ciregenz
2026-06-26 06:25:44 -07:00
parent f8ab34efc5
commit 36268d2448
5 changed files with 10 additions and 30 deletions
@@ -729,6 +729,8 @@ const BrowserCard: React.FC<Props> = ({
data-select-type="browser-card"
data-select-id={browserId}
data-select-meta={JSON.stringify({ name: activeTitle || 'Browser', url: activeUrl })}
// Marks a kept-alive card parked off-screen (it belongs to another dashboard); fit-to-view must skip it or it pans the canvas to chase it and the card bleeds onto the dashboard you're viewing.
data-keepalive-hidden={keepAliveHidden ? '1' : undefined}
onPointerDownCapture={() => onBringToFront?.(browserId, 'browser')}
onClick={(e: React.MouseEvent) => {
if (justDraggedRef.current) return;
@@ -543,6 +543,9 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
const prev = stateRef.current;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (let i = 0; i < children.length; i++) {
const child = children[i] as HTMLElement;
// Skip a kept-alive browser card from another dashboard (parked off-screen): fitting to it pans the canvas right onto it, which is the cross-dashboard bleed. On an empty dashboard this leaves nothing to fit, so the !isFinite reset below restores an identity transform and the off-screen card stays off-screen.
if (child.getAttribute?.('data-keepalive-hidden') === '1' || child.querySelector?.('[data-keepalive-hidden="1"]')) continue;
const r = children[i].getBoundingClientRect();
if (r.width === 0 && r.height === 0) continue;
const sx = (r.left - vRect.left - prev.panX) / prev.zoom;
@@ -26,7 +26,7 @@ import { fetchWorkflows, fetchAllRuns, fetchActiveRuns } from '@/shared/state/wo
import { fetchMissedRuns } from '@/shared/state/missedRunsSlice';
import { dashboardWs } from '@/shared/ws/WebSocketManager';
import { initBrowserCommandHandler } from '@/shared/browserCommandHandler';
import { isAgentDrivingBrowser } from '@/shared/isAgentDrivingBrowser';
import { getKeepAliveBrowserIds } from '@/shared/browserFocus';
import { clearPendingBrowserUrl, clearPendingFocusAgentId } from '@/shared/state/tempStateSlice';
import { API_BASE } from '@/shared/config';
import type { CanvasActions } from '../interaction/useCanvasControls';
@@ -99,12 +99,7 @@ export function useDashboardLifecycle({
hasFittedRef.current = false;
restoredExpandedRef.current = false;
setOutputsRefetched(false);
// Only keep AGENT-driven browsers alive across the switch; a manual browser is dropped here (reloads when you return) so its kept-alive surface can't bleed onto the dashboard you land on.
const st = store.getState();
const agentLiveIds = Object.keys(st.dashboardLayout.browserCards).filter(
(id) => isAgentDrivingBrowser(st.agents.sessions, id, st.dashboardLayout.browserCards[id]?.spawned_by),
);
dispatch(resetLayout({ keepBrowserIds: agentLiveIds }));
dispatch(resetLayout({ keepBrowserIds: getKeepAliveBrowserIds() }));
// CRITICAL path: these populate the cards the user expects to see on first paint. Don't defer.
dispatch(fetchSessions({ dashboardId }));
dispatch(fetchLayout({ dashboardId }));
@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { useAppSelector } from '@/shared/hooks';
import { isAgentDrivingBrowser } from '@/shared/isAgentDrivingBrowser';
// All of the dashboard's Redux reads in one place. Keeps Dashboard.tsx a thin composition layer instead of a 25-line selector wall.
export function useDashboardSelectors(dashboardId: string) {
@@ -20,14 +19,14 @@ export function useDashboardSelectors(dashboardId: string) {
}
return out;
}, [allBrowserCards, dashboardId]);
// Only an AGENT-driven browser from another dashboard stays mounted-but-hidden here so its run keeps going in the background; a MANUAL browser is deliberately NOT rendered off its own dashboard (it reloads on return) because a kept-alive heavy page bleeds its webview surface onto whatever dashboard you're viewing. Kept OUT of `browserCards` so save/bounds/keyboard-nav only ever see THIS dashboard's cards.
// Browser cards from OTHER dashboards stay mounted (so their webContents + session survive a switch, no Discord logout) but get rendered parked far off-screen by the card layer; that off-screen park reliably hides even a heavy live page (Discord), CDP-verified. Kept OUT of `browserCards` so save/bounds/keyboard-nav only ever see THIS dashboard's cards (no cross-dashboard leak), and tagging every card's home dashboard is what stops the real bleed (an untagged card renders as home everywhere).
const keepAliveBrowserCards = useMemo(() => {
const out: typeof allBrowserCards = {};
for (const [id, bc] of Object.entries(allBrowserCards)) {
if (bc.dashboard_id && bc.dashboard_id !== dashboardId && isAgentDrivingBrowser(sessions, id, bc.spawned_by)) out[id] = bc;
if (bc.dashboard_id && bc.dashboard_id !== dashboardId) out[id] = bc;
}
return out;
}, [allBrowserCards, dashboardId, sessions]);
}, [allBrowserCards, dashboardId]);
const workflowCards = useAppSelector((state) => state.dashboardLayout.workflowCards);
const workflowsHub = useAppSelector((state) => state.dashboardLayout.workflowsHub);
const pendingFocusWorkflowId = useAppSelector((state) => state.dashboardLayout.pendingFocusWorkflowId);
@@ -1,19 +0,0 @@
import type { AgentSession } from '@/shared/state/agentsSlice';
const ACTIVE_STATUSES = new Set<AgentSession['status']>(['running', 'waiting_approval']);
// True while an agent is actively running against this browser, so its webContents must survive a dashboard switch (the run keeps going in the background and the agent reaches it over CDP). A MANUAL browser is false: it stops rendering the moment you leave its dashboard, so it can't bleed onto another, and it reloads when you come back.
export function isAgentDrivingBrowser(
sessions: Record<string, AgentSession>,
browserId: string,
spawnedBy?: string | null,
): boolean {
for (const s of Object.values(sessions)) {
if (s.browser_id === browserId && ACTIVE_STATUSES.has(s.status)) return true;
}
if (spawnedBy) {
const parent = sessions[spawnedBy];
if (parent && ACTIVE_STATUSES.has(parent.status)) return true;
}
return false;
}