diff --git a/.github/workflows/claude-dependabot.yml b/.github/workflows/claude-dependabot.yml index c39fdb0e35..6dbe068c3d 100644 --- a/.github/workflows/claude-dependabot.yml +++ b/.github/workflows/claude-dependabot.yml @@ -78,7 +78,7 @@ jobs: # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 805f3d78bb..8e165b823e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -94,7 +94,7 @@ jobs: # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f70fe36572..eae6eea5d2 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -76,7 +76,7 @@ jobs: # Frontend Node.js/pnpm setup (mirrors platform-frontend-ci.yml) - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 01e057207d..669a775934 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -42,7 +42,7 @@ jobs: - 'autogpt_platform/frontend/src/components/**' - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -74,7 +74,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -112,7 +112,7 @@ jobs: fetch-depth: 0 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -153,7 +153,7 @@ jobs: submodules: recursive - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -282,7 +282,7 @@ jobs: submodules: recursive - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" diff --git a/.github/workflows/platform-fullstack-ci.yml b/.github/workflows/platform-fullstack-ci.yml index f64d5e33c9..67be0ae939 100644 --- a/.github/workflows/platform-fullstack-ci.yml +++ b/.github/workflows/platform-fullstack-ci.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" @@ -68,7 +68,7 @@ jobs: submodules: recursive - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22.18.0" diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py b/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py index 7ca85961f9..f55cd567e8 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/find_block.py @@ -13,10 +13,32 @@ from backend.api.features.chat.tools.models import ( NoResultsResponse, ) from backend.api.features.store.hybrid_search import unified_hybrid_search -from backend.data.block import get_block +from backend.data.block import BlockType, get_block logger = logging.getLogger(__name__) +_TARGET_RESULTS = 10 +# Over-fetch to compensate for post-hoc filtering of graph-only blocks. +# 40 is 2x current removed; speed of query 10 vs 40 is minimial +_OVERFETCH_PAGE_SIZE = 40 + +# Block types that only work within graphs and cannot run standalone in CoPilot. +COPILOT_EXCLUDED_BLOCK_TYPES = { + BlockType.INPUT, # Graph interface definition - data enters via chat, not graph inputs + BlockType.OUTPUT, # Graph interface definition - data exits via chat, not graph outputs + BlockType.WEBHOOK, # Wait for external events - would hang forever in CoPilot + BlockType.WEBHOOK_MANUAL, # Same as WEBHOOK + BlockType.NOTE, # Visual annotation only - no runtime behavior + BlockType.HUMAN_IN_THE_LOOP, # Pauses for human approval - CoPilot IS human-in-the-loop + BlockType.AGENT, # AgentExecutorBlock requires execution_context - use run_agent tool +} + +# Specific block IDs excluded from CoPilot (STANDARD type but still require graph context) +COPILOT_EXCLUDED_BLOCK_IDS = { + # SmartDecisionMakerBlock - dynamically discovers downstream blocks via graph topology + "3b191d9f-356f-482d-8238-ba04b6d18381", +} + class FindBlockTool(BaseTool): """Tool for searching available blocks.""" @@ -88,7 +110,7 @@ class FindBlockTool(BaseTool): query=query, content_types=[ContentType.BLOCK], page=1, - page_size=10, + page_size=_OVERFETCH_PAGE_SIZE, ) if not results: @@ -108,60 +130,90 @@ class FindBlockTool(BaseTool): block = get_block(block_id) # Skip disabled blocks - if block and not block.disabled: - # Get input/output schemas - input_schema = {} - output_schema = {} - try: - input_schema = block.input_schema.jsonschema() - except Exception: - pass - try: - output_schema = block.output_schema.jsonschema() - except Exception: - pass + if not block or block.disabled: + continue - # Get categories from block instance - categories = [] - if hasattr(block, "categories") and block.categories: - categories = [cat.value for cat in block.categories] + # Skip blocks excluded from CoPilot (graph-only blocks) + if ( + block.block_type in COPILOT_EXCLUDED_BLOCK_TYPES + or block.id in COPILOT_EXCLUDED_BLOCK_IDS + ): + continue - # Extract required inputs for easier use - required_inputs: list[BlockInputFieldInfo] = [] - if input_schema: - properties = input_schema.get("properties", {}) - required_fields = set(input_schema.get("required", [])) - # Get credential field names to exclude from required inputs - credentials_fields = set( - block.input_schema.get_credentials_fields().keys() - ) - - for field_name, field_schema in properties.items(): - # Skip credential fields - they're handled separately - if field_name in credentials_fields: - continue - - required_inputs.append( - BlockInputFieldInfo( - name=field_name, - type=field_schema.get("type", "string"), - description=field_schema.get("description", ""), - required=field_name in required_fields, - default=field_schema.get("default"), - ) - ) - - blocks.append( - BlockInfoSummary( - id=block_id, - name=block.name, - description=block.description or "", - categories=categories, - input_schema=input_schema, - output_schema=output_schema, - required_inputs=required_inputs, - ) + # Get input/output schemas + input_schema = {} + output_schema = {} + try: + input_schema = block.input_schema.jsonschema() + except Exception as e: + logger.debug( + "Failed to generate input schema for block %s: %s", + block_id, + e, ) + try: + output_schema = block.output_schema.jsonschema() + except Exception as e: + logger.debug( + "Failed to generate output schema for block %s: %s", + block_id, + e, + ) + + # Get categories from block instance + categories = [] + if hasattr(block, "categories") and block.categories: + categories = [cat.value for cat in block.categories] + + # Extract required inputs for easier use + required_inputs: list[BlockInputFieldInfo] = [] + if input_schema: + properties = input_schema.get("properties", {}) + required_fields = set(input_schema.get("required", [])) + # Get credential field names to exclude from required inputs + credentials_fields = set( + block.input_schema.get_credentials_fields().keys() + ) + + for field_name, field_schema in properties.items(): + # Skip credential fields - they're handled separately + if field_name in credentials_fields: + continue + + required_inputs.append( + BlockInputFieldInfo( + name=field_name, + type=field_schema.get("type", "string"), + description=field_schema.get("description", ""), + required=field_name in required_fields, + default=field_schema.get("default"), + ) + ) + + blocks.append( + BlockInfoSummary( + id=block_id, + name=block.name, + description=block.description or "", + categories=categories, + input_schema=input_schema, + output_schema=output_schema, + required_inputs=required_inputs, + ) + ) + + if len(blocks) >= _TARGET_RESULTS: + break + + if blocks and len(blocks) < _TARGET_RESULTS: + logger.debug( + "find_block returned %d/%d results for query '%s' " + "(filtered %d excluded/disabled blocks)", + len(blocks), + _TARGET_RESULTS, + query, + len(results) - len(blocks), + ) if not blocks: return NoResultsResponse( diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py b/autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py new file mode 100644 index 0000000000..0f3d4cbfa5 --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/chat/tools/find_block_test.py @@ -0,0 +1,139 @@ +"""Tests for block filtering in FindBlockTool.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from backend.api.features.chat.tools.find_block import ( + COPILOT_EXCLUDED_BLOCK_IDS, + COPILOT_EXCLUDED_BLOCK_TYPES, + FindBlockTool, +) +from backend.api.features.chat.tools.models import BlockListResponse +from backend.data.block import BlockType + +from ._test_data import make_session + +_TEST_USER_ID = "test-user-find-block" + + +def make_mock_block( + block_id: str, name: str, block_type: BlockType, disabled: bool = False +): + """Create a mock block for testing.""" + mock = MagicMock() + mock.id = block_id + mock.name = name + mock.description = f"{name} description" + mock.block_type = block_type + mock.disabled = disabled + mock.input_schema = MagicMock() + mock.input_schema.jsonschema.return_value = {"properties": {}, "required": []} + mock.input_schema.get_credentials_fields.return_value = {} + mock.output_schema = MagicMock() + mock.output_schema.jsonschema.return_value = {} + mock.categories = [] + return mock + + +class TestFindBlockFiltering: + """Tests for block filtering in FindBlockTool.""" + + def test_excluded_block_types_contains_expected_types(self): + """Verify COPILOT_EXCLUDED_BLOCK_TYPES contains all graph-only types.""" + assert BlockType.INPUT in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.OUTPUT in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.WEBHOOK in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.WEBHOOK_MANUAL in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.NOTE in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.HUMAN_IN_THE_LOOP in COPILOT_EXCLUDED_BLOCK_TYPES + assert BlockType.AGENT in COPILOT_EXCLUDED_BLOCK_TYPES + + def test_excluded_block_ids_contains_smart_decision_maker(self): + """Verify SmartDecisionMakerBlock is in COPILOT_EXCLUDED_BLOCK_IDS.""" + assert "3b191d9f-356f-482d-8238-ba04b6d18381" in COPILOT_EXCLUDED_BLOCK_IDS + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_type_filtered_from_results(self): + """Verify blocks with excluded BlockTypes are filtered from search results.""" + session = make_session(user_id=_TEST_USER_ID) + + # Mock search returns an INPUT block (excluded) and a STANDARD block (included) + search_results = [ + {"content_id": "input-block-id", "score": 0.9}, + {"content_id": "standard-block-id", "score": 0.8}, + ] + + input_block = make_mock_block("input-block-id", "Input Block", BlockType.INPUT) + standard_block = make_mock_block( + "standard-block-id", "HTTP Request", BlockType.STANDARD + ) + + def mock_get_block(block_id): + return { + "input-block-id": input_block, + "standard-block-id": standard_block, + }.get(block_id) + + with patch( + "backend.api.features.chat.tools.find_block.unified_hybrid_search", + new_callable=AsyncMock, + return_value=(search_results, 2), + ): + with patch( + "backend.api.features.chat.tools.find_block.get_block", + side_effect=mock_get_block, + ): + tool = FindBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, session=session, query="test" + ) + + # Should only return the standard block, not the INPUT block + assert isinstance(response, BlockListResponse) + assert len(response.blocks) == 1 + assert response.blocks[0].id == "standard-block-id" + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_id_filtered_from_results(self): + """Verify SmartDecisionMakerBlock is filtered from search results.""" + session = make_session(user_id=_TEST_USER_ID) + + smart_decision_id = "3b191d9f-356f-482d-8238-ba04b6d18381" + search_results = [ + {"content_id": smart_decision_id, "score": 0.9}, + {"content_id": "normal-block-id", "score": 0.8}, + ] + + # SmartDecisionMakerBlock has STANDARD type but is excluded by ID + smart_block = make_mock_block( + smart_decision_id, "Smart Decision Maker", BlockType.STANDARD + ) + normal_block = make_mock_block( + "normal-block-id", "Normal Block", BlockType.STANDARD + ) + + def mock_get_block(block_id): + return { + smart_decision_id: smart_block, + "normal-block-id": normal_block, + }.get(block_id) + + with patch( + "backend.api.features.chat.tools.find_block.unified_hybrid_search", + new_callable=AsyncMock, + return_value=(search_results, 2), + ): + with patch( + "backend.api.features.chat.tools.find_block.get_block", + side_effect=mock_get_block, + ): + tool = FindBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, session=session, query="decision" + ) + + # Should only return normal block, not SmartDecisionMakerBlock + assert isinstance(response, BlockListResponse) + assert len(response.blocks) == 1 + assert response.blocks[0].id == "normal-block-id" diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py b/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py index 51bb2c0575..590f81ff23 100644 --- a/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py +++ b/autogpt_platform/backend/backend/api/features/chat/tools/run_block.py @@ -8,6 +8,10 @@ from typing import Any from pydantic_core import PydanticUndefined from backend.api.features.chat.model import ChatSession +from backend.api.features.chat.tools.find_block import ( + COPILOT_EXCLUDED_BLOCK_IDS, + COPILOT_EXCLUDED_BLOCK_TYPES, +) from backend.data.block import get_block from backend.data.execution import ExecutionContext from backend.data.model import CredentialsMetaInput @@ -212,6 +216,19 @@ class RunBlockTool(BaseTool): session_id=session_id, ) + # Check if block is excluded from CoPilot (graph-only blocks) + if ( + block.block_type in COPILOT_EXCLUDED_BLOCK_TYPES + or block.id in COPILOT_EXCLUDED_BLOCK_IDS + ): + return ErrorResponse( + message=( + f"Block '{block.name}' cannot be run directly in CoPilot. " + "This block is designed for use within graphs only." + ), + session_id=session_id, + ) + logger.info(f"Executing block {block.name} ({block_id}) for user {user_id}") creds_manager = IntegrationCredentialsManager() diff --git a/autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py b/autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py new file mode 100644 index 0000000000..2aae45e875 --- /dev/null +++ b/autogpt_platform/backend/backend/api/features/chat/tools/run_block_test.py @@ -0,0 +1,106 @@ +"""Tests for block execution guards in RunBlockTool.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from backend.api.features.chat.tools.models import ErrorResponse +from backend.api.features.chat.tools.run_block import RunBlockTool +from backend.data.block import BlockType + +from ._test_data import make_session + +_TEST_USER_ID = "test-user-run-block" + + +def make_mock_block( + block_id: str, name: str, block_type: BlockType, disabled: bool = False +): + """Create a mock block for testing.""" + mock = MagicMock() + mock.id = block_id + mock.name = name + mock.block_type = block_type + mock.disabled = disabled + mock.input_schema = MagicMock() + mock.input_schema.jsonschema.return_value = {"properties": {}, "required": []} + mock.input_schema.get_credentials_fields_info.return_value = [] + return mock + + +class TestRunBlockFiltering: + """Tests for block execution guards in RunBlockTool.""" + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_type_returns_error(self): + """Attempting to execute a block with excluded BlockType returns error.""" + session = make_session(user_id=_TEST_USER_ID) + + input_block = make_mock_block("input-block-id", "Input Block", BlockType.INPUT) + + with patch( + "backend.api.features.chat.tools.run_block.get_block", + return_value=input_block, + ): + tool = RunBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, + session=session, + block_id="input-block-id", + input_data={}, + ) + + assert isinstance(response, ErrorResponse) + assert "cannot be run directly in CoPilot" in response.message + assert "designed for use within graphs only" in response.message + + @pytest.mark.asyncio(loop_scope="session") + async def test_excluded_block_id_returns_error(self): + """Attempting to execute SmartDecisionMakerBlock returns error.""" + session = make_session(user_id=_TEST_USER_ID) + + smart_decision_id = "3b191d9f-356f-482d-8238-ba04b6d18381" + smart_block = make_mock_block( + smart_decision_id, "Smart Decision Maker", BlockType.STANDARD + ) + + with patch( + "backend.api.features.chat.tools.run_block.get_block", + return_value=smart_block, + ): + tool = RunBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, + session=session, + block_id=smart_decision_id, + input_data={}, + ) + + assert isinstance(response, ErrorResponse) + assert "cannot be run directly in CoPilot" in response.message + + @pytest.mark.asyncio(loop_scope="session") + async def test_non_excluded_block_passes_guard(self): + """Non-excluded blocks pass the filtering guard (may fail later for other reasons).""" + session = make_session(user_id=_TEST_USER_ID) + + standard_block = make_mock_block( + "standard-id", "HTTP Request", BlockType.STANDARD + ) + + with patch( + "backend.api.features.chat.tools.run_block.get_block", + return_value=standard_block, + ): + tool = RunBlockTool() + response = await tool._execute( + user_id=_TEST_USER_ID, + session=session, + block_id="standard-id", + input_data={}, + ) + + # Should NOT be an ErrorResponse about CoPilot exclusion + # (may be other errors like missing credentials, but not the exclusion guard) + if isinstance(response, ErrorResponse): + assert "cannot be run directly in CoPilot" not in response.message diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index f22a182d20..e8c9871a72 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -102,7 +102,7 @@ "react-markdown": "9.0.3", "react-modal": "3.16.3", "react-shepherd": "6.1.9", - "react-window": "1.8.11", + "react-window": "2.2.0", "recharts": "3.3.0", "rehype-autolink-headings": "7.1.0", "rehype-highlight": "7.0.2", @@ -140,7 +140,7 @@ "@types/react": "18.3.17", "@types/react-dom": "18.3.5", "@types/react-modal": "3.16.3", - "@types/react-window": "1.8.8", + "@types/react-window": "2.0.0", "@vitejs/plugin-react": "5.1.2", "axe-playwright": "2.2.2", "chromatic": "13.3.3", diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index db891ccf3f..377a298564 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -228,8 +228,8 @@ importers: specifier: 6.1.9 version: 6.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-window: - specifier: 1.8.11 - version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 2.2.0 + version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: 3.3.0 version: 3.3.0(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1) @@ -337,8 +337,8 @@ importers: specifier: 3.16.3 version: 3.16.3 '@types/react-window': - specifier: 1.8.8 - version: 1.8.8 + specifier: 2.0.0 + version: 2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@vitejs/plugin-react': specifier: 5.1.2 version: 5.1.2(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) @@ -3469,8 +3469,9 @@ packages: '@types/react-modal@3.16.3': resolution: {integrity: sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==} - '@types/react-window@1.8.8': - resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + '@types/react-window@2.0.0': + resolution: {integrity: sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ==} + deprecated: This is a stub types definition. react-window provides its own type definitions, so you do not need this installed. '@types/react@18.3.17': resolution: {integrity: sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==} @@ -5976,9 +5977,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6891,12 +6889,11 @@ packages: '@types/react': optional: true - react-window@1.8.11: - resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} - engines: {node: '>8.0.0'} + react-window@2.2.0: + resolution: {integrity: sha512-Y2L7yonHq6K1pQA2P98wT5QdIsEcjBTB7T8o6Mub12hH9eYppXoYu6vgClmcjlh3zfNcW2UrXiJJJqDxUY7GVw==} peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} @@ -11603,9 +11600,12 @@ snapshots: dependencies: '@types/react': 18.3.17 - '@types/react-window@1.8.8': + '@types/react-window@2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@types/react': 18.3.17 + react-window: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom '@types/react@18.3.17': dependencies: @@ -14545,8 +14545,6 @@ snapshots: dependencies: fs-monkey: 1.1.0 - memoize-one@5.2.1: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -15592,10 +15590,8 @@ snapshots: optionalDependencies: '@types/react': 18.3.17 - react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-window@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 - memoize-one: 5.2.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx index 263453b327..885877786f 100644 --- a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/components/ActivityDropdown/ActivityDropdown.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/atoms/Button/Button"; import { Input } from "@/components/atoms/Input/Input"; import { Text } from "@/components/atoms/Text/Text"; import { Bell, MagnifyingGlass, X } from "@phosphor-icons/react"; -import { FixedSizeList as List } from "react-window"; +import { List, type RowComponentProps } from "react-window"; import { AgentExecutionWithInfo } from "../../helpers"; import { ActivityItem } from "../ActivityItem"; import styles from "./styles.module.css"; @@ -19,14 +19,16 @@ interface Props { recentFailures: AgentExecutionWithInfo[]; } -interface VirtualizedItemProps { - index: number; - style: React.CSSProperties; - data: AgentExecutionWithInfo[]; +interface ActivityRowProps { + executions: AgentExecutionWithInfo[]; } -function VirtualizedActivityItem({ index, style, data }: VirtualizedItemProps) { - const execution = data[index]; +function VirtualizedActivityItem({ + index, + style, + executions, +}: RowComponentProps) { + const execution = executions[index]; return (
@@ -129,14 +131,13 @@ export function ActivityDropdown({ > {filteredExecutions.length > 0 ? ( - {VirtualizedActivityItem} - + defaultHeight={listHeight} + rowCount={filteredExecutions.length} + rowHeight={itemHeight} + rowProps={{ executions: filteredExecutions }} + rowComponent={VirtualizedActivityItem} + style={{ width: 320, height: listHeight }} + /> ) : (