From 82d4acd6dd348769ce7a03ab3053f901e7bc8077 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 15 Jul 2026 17:30:22 -0700 Subject: [PATCH] [eric] dashboard: agent clicks in its own browser no longer steal selection (browser chased the cursor mid-drag, spawn-beside re-anchored); also block Task, the 2.1.122 name of the builtin sub-agent tool --- backend/apps/agents/manager/run/RunOptions.py | 3 ++- backend/tests/test_spawn_agent.py | 8 +++++--- .../hooks/interaction/useDashboardInteractions.ts | 11 +++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/backend/apps/agents/manager/run/RunOptions.py b/backend/apps/agents/manager/run/RunOptions.py index fce7e8e0..c9266625 100644 --- a/backend/apps/agents/manager/run/RunOptions.py +++ b/backend/apps/agents/manager/run/RunOptions.py @@ -229,8 +229,9 @@ class RunOptions(AgentManagerProtocol): # The claude_code preset auto-attaches the user's claude.ai- connected partner MCPs (`mcp__claude_ai_*`). Those bypass our MCPActivate gate, don't share OAuth state with the OpenSwarm Gmail/Calendar/Drive connectors the user actually configured here, and confuse the model into picking the partner shim instead of our vetted server. Hard-block them at the SDK layer so the model can't even attempt the call. options_kwargs["disallowed_tools"] = [ "mcp__claude_ai_*", - # The CLI's built-in Agent tool is replaced by our SpawnAgent MCP (prompt + run_in_background only); its subagent types resolve to models router setups can't serve. + # The CLI's built-in sub-agent tool is replaced by our SpawnAgent MCP (prompt + run_in_background only); it is named Task on CLI 2.1.122 (Agent on older builds, kept for safety), and its subagent types resolve to models router setups can't serve. "Agent", + "Task", ] if session.cwd: diff --git a/backend/tests/test_spawn_agent.py b/backend/tests/test_spawn_agent.py index d66b85b0..757fbd1c 100644 --- a/backend/tests/test_spawn_agent.py +++ b/backend/tests/test_spawn_agent.py @@ -70,14 +70,16 @@ def test_spawn_agent_unknown_parent_raises() -> None: asyncio.run(agent_manager.spawn_agent(prompt="x", parent_session_id="nope-" + "0" * 28)) -def test_builtin_agent_tool_stays_blocked() -> None: - # The CLI's Agent tool must not be offered: out of the catalog AND hard-blocked at the SDK layer. +def test_builtin_subagent_tools_stay_blocked() -> None: + # The CLI's built-in sub-agent tool (Task on 2.1.122, Agent on older builds) must not be offered: out of the catalog AND hard-blocked at the SDK layer. from backend.apps.agents.manager.prompt.tool_catalog import FULL_TOOLS assert "Agent" not in FULL_TOOLS + assert "Task" not in FULL_TOOLS import inspect from backend.apps.agents.manager.run import RunOptions src = inspect.getsource(RunOptions) - assert '"Agent",' in src.split('disallowed_tools"] = [')[1][:300] + block = src.split('disallowed_tools"] = [')[1][:400] + assert '"Agent",' in block and '"Task",' in block def test_spawn_server_schema_is_prompt_plus_background_only() -> None: diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts index 91ca5fe7..1c66a144 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } from 'react'; import { report } from '@/shared/serviceClient'; import { useAppDispatch } from '@/shared/hooks'; +import { store } from '@/shared/state/store'; import { collapseSession, expandSession } from '@/shared/state/agentsSlice'; import { bringToFront } from '@/shared/state/dashboardLayoutSlice'; import type { CardType, useDashboardSelection } from '../state/useDashboardSelection'; @@ -96,6 +97,16 @@ export function useDashboardInteractions({ const onGuestSelect = (e: Event) => { const browserId = (e as CustomEvent).detail?.browserId; if (typeof browserId !== 'string' || !browserId) return; + // 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; selection.selectCard(browserId, 'browser', false); dispatch(bringToFront({ id: browserId, type: 'browser' })); };