[eric] browser: agent clicks stop hijacking last-interacted targeting (Ctrl+R, zoom, dictation fallback); .swarm app import refreshes the dock

This commit is contained in:
ciregenz
2026-08-05 15:21:03 -07:00
parent a3d952dc43
commit f60653174f
4 changed files with 24 additions and 9 deletions
@@ -10,6 +10,7 @@ import { useNavigate } from 'react-router-dom';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { fetchOutputs } from '@/shared/state/outputsSlice';
import { fetchWorkflows } from '@/shared/state/workflowsSlice';
import ImportDigest, { DigestHandle } from './ImportDigest';
@@ -62,6 +63,8 @@ const ImportEntryPoint: React.FC = () => {
setToast({ msg, sev: 'success' });
// A workflow has no route of its own, so nothing would pull it in: an open Workflows hub only fetches on mount and would keep showing a stale list. Import drops dashboard_id, and /list keeps unassigned workflows for every dashboard, so this surfaces it wherever the user is.
if (rootType === 'workflow') dispatch(fetchWorkflows(dashboardId));
// Same staleness for apps: the Apps dock reads the outputs slice, which nothing refetches on import.
if (rootType === 'app') dispatch(fetchOutputs());
const to = DEST[rootType]?.(rootId);
if (to) navigate(to);
},
@@ -62,6 +62,7 @@ import {
} from '@/shared/browserRegistry';
import { captureBrowserShot } from '@/shared/captureBrowserShot';
import { setLastInteractedBrowser } from '@/shared/browserFocus';
import { isAgentDrivenBrowser } from '@/shared/isAgentDrivenBrowser';
import { registerCapsuleForRestore } from '@/shared/browserStateCapsule';
import BrowserFindBar from './BrowserFindBar';
import { openCardContextMenu, isNativeMenuTarget } from '../desktop/openCardContextMenu';
@@ -562,8 +563,11 @@ const BrowserCard: React.FC<Props> = ({
);
} else if (e?.channel === 'app-clicked') {
// In-guest mousedown: a page click never reaches the host document, so this IPC is how a webview-content click marks this browser as last-interacted (drives Ctrl+R/zoom/tab targeting) and selected (spawn-beside anchor).
setLastInteractedBrowser(browserId);
window.dispatchEvent(new CustomEvent('openswarm:browser-guest-select', { detail: { browserId } }));
// The guest fires this for the AGENT's clicks too; those must not hijack targeting (or dictation's fallback types into the agent's page).
if (!isAgentDrivenBrowser(browserId)) {
setLastInteractedBrowser(browserId);
window.dispatchEvent(new CustomEvent('openswarm:browser-guest-select', { detail: { browserId } }));
}
}
};
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, type Dispatch, type SetStateActi
import { report } from '@/shared/serviceClient';
import { useAppDispatch } from '@/shared/hooks';
import { store } from '@/shared/state/store';
import { isAgentDrivenBrowser } from '@/shared/isAgentDrivenBrowser';
import { expandSession } from '@/shared/state/agentsSlice';
import { bringToFront } from '@/shared/state/dashboardLayoutSlice';
import { setScrollFocusedCard } from '@/shared/cardScrollFocus';
@@ -145,13 +146,7 @@ export function useDashboardInteractions({
// Mid-drag/marquee a selection change joins the card to the multi-drag (the browser visibly chased the cursor); the shield class is up for exactly that window.
if (document.body.classList.contains('dashboard-marquee-active')) return;
// The guest preload fires app-clicked for the AGENT's clicks too; a working agent driving its own page must not steal selection (it also re-anchored spawn-beside onto its browser).
const st = store.getState();
const working = (s?: { status?: string }) => !!s && (s.status === 'running' || s.status === 'waiting_approval');
const glow = st.dashboardLayout.glowingBrowserCards[browserId];
const agentDriven =
Object.values(st.agents.sessions).some((s) => s.browser_id === browserId && working(s)) ||
(!!glow && !glow.fading && working(st.agents.sessions[glow.sourceId]));
if (agentDriven) return;
if (isAgentDrivenBrowser(browserId)) return;
selection.selectCard(browserId, 'browser', false);
dispatch(bringToFront({ id: browserId, type: 'browser' }));
// In-guest clicks never reach the host capture handler, so mark the browser focused here, mainly to UN-focus any chat so scroll over other cards behaves right (the browser's own page scroll/zoom is native regardless).
@@ -0,0 +1,13 @@
import { store } from '@/shared/state/store';
// A browser an agent is actively working: its guest clicks are the AGENT's, not the user's, so they
// must not steal selection, last-interacted targeting (Ctrl+R, zoom, dictation fallback), or z-order.
export function isAgentDrivenBrowser(browserId: string): boolean {
const st = store.getState();
const working = (s?: { status?: string }) => !!s && (s.status === 'running' || s.status === 'waiting_approval');
const glow = st.dashboardLayout.glowingBrowserCards[browserId];
return (
Object.values(st.agents.sessions).some((s) => s.browser_id === browserId && working(s)) ||
(!!glow && !glow.fading && working(st.agents.sessions[glow.sourceId]))
);
}