diff --git a/frontend/src/app/components/share/ImportEntryPoint.tsx b/frontend/src/app/components/share/ImportEntryPoint.tsx index 8089d2b5..3d094a0d 100644 --- a/frontend/src/app/components/share/ImportEntryPoint.tsx +++ b/frontend/src/app/components/share/ImportEntryPoint.tsx @@ -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); }, diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index 361d45ab..7756b77d 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -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 = ({ ); } 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 } })); + } } }; diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts index f7102d46..e2c86f34 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts @@ -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). diff --git a/frontend/src/shared/isAgentDrivenBrowser.ts b/frontend/src/shared/isAgentDrivenBrowser.ts new file mode 100644 index 00000000..9d69fe90 --- /dev/null +++ b/frontend/src/shared/isAgentDrivenBrowser.ts @@ -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])) + ); +}